Skip to content

Instantly share code, notes, and snippets.

@AdrianFahrbach
Created February 28, 2023 22:23
Show Gist options
  • Save AdrianFahrbach/9bccd4388be85253bf07435aeb57ddfe to your computer and use it in GitHub Desktop.
Save AdrianFahrbach/9bccd4388be85253bf07435aeb57ddfe to your computer and use it in GitHub Desktop.
Storybook stories generation
import { execSync } from 'child_process';
import fs from 'fs';
import { startCase } from 'lodash';
import path from 'path';
const warningMessage = `
/*****************************************************************************************************************
* *
* This file is autogenerated. If you want to make persistent changes, change the respective .legend.tsx file! *
* *
****************************************************************************************************************/`;
const srcDirectories = ['./libs/components/src/lib/blocks/', './libs/ui/src/lib/'];
let legendFiles: string[] = [];
const timerLabel = '\x1b[32minfo\x1b[0m => Stories generated in';
console.time(timerLabel);
(async () => {
// Merge all files in a single array and remove duplicates
await Promise.all(
srcDirectories.map(async srcDirectory => {
const files = fs
.readdirSync(srcDirectory)
.filter(filename => filename.endsWith('.legend.tsx'))
.map(filename => path.join(srcDirectory, filename));
legendFiles = legendFiles.concat(files);
})
);
legendFiles = [...new Set(legendFiles)];
// Loop through each legend file
console.log('\x1b[32minfo\x1b[0m => Generating stories...');
await Promise.all(
legendFiles.map(async legendFilePath => {
const storiesFilePath = legendFilePath.replace('.legend.tsx', '.generated.stories.tsx');
let tempFileContents = fs.readFileSync(legendFilePath, 'utf-8');
// We don't want the compiled React component. To prevent that we replace
// every instance of it with a placeholder string and revert that change later.
const spacesBefore = tempFileContents.match(/^(\s+)*render:/m)[1];
const findRenderFunctionRegex = new RegExp(`^${spacesBefore}render:([\\S\\s]*^${spacesBefore}[)}]?,|.+,)`, 'm');
const foundRenderFunction = tempFileContents.match(findRenderFunctionRegex);
let renderFunction = '';
if (foundRenderFunction && foundRenderFunction.length > 0) {
renderFunction = foundRenderFunction[0];
tempFileContents = tempFileContents.replace(findRenderFunctionRegex, 'render: "%RENDER_PLACEHOLDER%"');
}
const findComponentRegex = /component: .+,/;
const foundComponent = tempFileContents.match(findComponentRegex);
let component = '';
if (foundComponent && foundComponent.length > 0) {
component = foundComponent[0];
tempFileContents = tempFileContents.replace(findComponentRegex, 'component: "%COMPONENT_PLACEHOLDER%"');
}
fs.writeFileSync(storiesFilePath, tempFileContents);
// Get the adjusted file
const contents = fs.readFileSync(storiesFilePath, 'utf-8');
const { exportsToGenerate, meta } = await import(storiesFilePath);
// Get imports from file because we need those later
const foundImports = contents.match(/^import .+';/gm);
let imports = '';
if (foundImports && foundImports.length > 0) {
imports = foundImports.join('\n');
}
// Get meta object and use it as default export
let defaultExportString = `export default ${JSON.stringify(meta, null, 2).replace(
'"component": "%COMPONENT_PLACEHOLDER%"',
component
)}`;
// Convert exportsToGenerate object to sepatate exports
const generatedExports = exportsToGenerate.map((exportsEntry: { [key: string]: any }, index: number) => {
const key = startCase(Object.keys(exportsEntry)[0]).replace(/ /g, '');
const value = JSON.stringify(Object.values(exportsEntry)[0], null, 2);
let exportsString = `export const ${key} = ${value.endsWith(',') ? value.slice(0, -1) : value}`;
exportsString = exportsString.replace('"render": "%RENDER_PLACEHOLDER%"', renderFunction);
return exportsString;
});
// Construct new story file from different blocks and prettier it
const storyFile =
warningMessage + '\n\n' + imports + '\n\n' + defaultExportString + '\n\n' + generatedExports.join('\n\n');
// Write the new file contents
fs.writeFileSync(storiesFilePath, storyFile);
})
);
// Organize imports on all files
console.log('\x1b[32minfo\x1b[0m => Organizing imports...');
const storiesFiles = legendFiles.map(legendFile => legendFile.replace('.legend.tsx', '.generated.stories.tsx'));
execSync('organize-imports-cli ' + storiesFiles.join(' '));
console.log('\x1b[32minfo\x1b[0m => Prettifying stories...');
execSync('prettier --write --list-different ' + storiesFiles.join(' '));
console.timeEnd(timerLabel);
})();
import { schemaToArgTypes } from '@project/services/storybook.service';
import { HeroBlockComponent } from '@project/shared/blocks';
import { Schema } from '@project/shared/cms';
import heroSchema from '@project/shared/cms/schema/components/content/hero.json';
import { Color, colorToColorName } from '@project/shared/colors';
import Hero from './Hero';
import { heroSectionFakeData, HeroThemeColor, heroThemes } from './Hero.config';
export const meta = {
title: 'Blocks/Hero',
component: Hero,
};
export const exportsToGenerate = Object.keys(heroThemes).map(themeColor => ({
[colorToColorName[themeColor as Color]]: {
args: {
...heroSectionFakeData,
backgroundColor: themeColor as HeroThemeColor,
} satisfies HeroBlockComponent,
argTypes: schemaToArgTypes(heroSchema as Schema, heroSectionFakeData),
render: args => <Hero data={args} />,
},
}));
{
"name": "website",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/website",
"projectType": "application",
"targets": {
"generateStories": {
"command": "npx ts-node -O '{\"module\": \"commonjs\"}' -r tsconfig-paths/register apps/website/.storybook/generate-stories.ts"
},
"other": "entries...",
"storybook": {
"executor": "@nrwl/storybook:storybook",
"dependsOn": ["copyCmsSchemaFiles", "generateStories"],
"options": {
"port": 4400,
"configDir": "apps/website/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
},
"build-storybook": {
"executor": "@nrwl/storybook:build",
"dependsOn": ["copyCmsSchemaFiles", "generateStories"],
"outputs": ["{options.outputDir}"],
"options": {
"outputDir": "dist/storybook/website",
"configDir": "apps/website/.storybook"
},
"configurations": {
"ci": {
"quiet": true
}
}
}
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment