diff --git a/.storybook/main.js b/.storybook/main.js index 6d51fefc2c922c..30c501933e30d5 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -9,18 +9,30 @@ const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); */ /** - * @typedef {{check:boolean; checkOptions: Record; reactDocgen: string | boolean; reactDocgenTypescriptOptions: Record}} StorybookTsConfig + * @typedef {{ + * check:boolean; + * checkOptions: Record; + * reactDocgen: string | boolean; + * reactDocgenTypescriptOptions: Record + * }} StorybookTsConfig */ /** - * @typedef {{stories: string[] ; addons: string[]; typescript: StorybookTsConfig; babel: (options:Record)=>Promise>; webpackFinal: StorybookWebpackConfig}} StorybookConfig + * @typedef {{ + * stories: string[]; + * addons: string[]; + * typescript: StorybookTsConfig; + * babel: (options:Record)=>Promise>; + * webpackFinal: StorybookWebpackConfig; + * core: {builder:'webpack5'}; + * }} StorybookConfig */ /** * @typedef {{loader: string; options: { [index: string]: any }}} LoaderObjectDef */ -module.exports = /** @type {Pick} */ ({ +module.exports = /** @type {Omit} */ ({ stories: [], addons: [ '@storybook/addon-essentials', diff --git a/tools/generators/migrate-converged-pkg/index.spec.ts b/tools/generators/migrate-converged-pkg/index.spec.ts index 4b4d386becff8d..ac9889be37c900 100644 --- a/tools/generators/migrate-converged-pkg/index.spec.ts +++ b/tools/generators/migrate-converged-pkg/index.spec.ts @@ -114,10 +114,11 @@ describe('migrate-converged-pkg generator', () => { return json; }); + /* eslint-disable @fluentui/max-len */ await expect(generator(tree, options)).rejects.toMatchInlineSnapshot( - // eslint-disable-next-line @fluentui/max-len `[Error: @proj/react-dummy is not converged package. Make sure to run the migration on packages with version 9.x.x]`, ); + /* eslint-enable @fluentui/max-len */ }); }); @@ -188,7 +189,7 @@ describe('migrate-converged-pkg generator', () => { outDir: 'dist', preserveConstEnums: true, target: 'ES2020', - types: ['jest', 'custom-global', 'inline-style-expand-shorthand', 'storybook__addons'], + types: ['jest', 'custom-global', 'inline-style-expand-shorthand'], }, extends: '../../tsconfig.base.json', include: ['src'], @@ -211,7 +212,6 @@ describe('migrate-converged-pkg generator', () => { 'jest', 'custom-global', 'inline-style-expand-shorthand', - 'storybook__addons', '@testing-library/jest-dom', 'foo-bar', ]); @@ -392,68 +392,16 @@ describe('migrate-converged-pkg generator', () => { }); describe(`storybook updates`, () => { - it(`should setup local storybook`, async () => { - const projectConfig = readProjectConfiguration(tree, options.name); - const projectStorybookConfigPath = `${projectConfig.root}/.storybook`; - - expect(tree.exists(projectStorybookConfigPath)).toBeFalsy(); - - await generator(tree, options); - - expect(tree.exists(projectStorybookConfigPath)).toBeTruthy(); - expect(readJson(tree, `${projectStorybookConfigPath}/tsconfig.json`)).toMatchInlineSnapshot(` - Object { - "compilerOptions": Object { - "allowJs": true, - "checkJs": true, - }, - "exclude": Array [ - "../**/*.test.ts", - "../**/*.test.js", - "../**/*.test.tsx", - "../**/*.test.jsx", - ], - "extends": "../tsconfig.json", - "include": Array [ - "../src/**/*", - "*.js", - ], - } - `); - - /* eslint-disable @fluentui/max-len */ - expect(tree.read(`${projectStorybookConfigPath}/main.js`)?.toString('utf-8')).toMatchInlineSnapshot(` - "const rootMain = require('../../../.storybook/main'); + function setup(_config?: Partial<{ createDummyStories: boolean }>) { + const defaults = { createDummyStories: true }; + const config = { ...defaults, ..._config }; - module.exports = /** @type {Pick} */ ({ - stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/*.stories.@(ts|tsx)'], - addons: [...rootMain.addons], - webpackFinal: (config, options) => { - const localConfig = { ...rootMain.webpackFinal(config, options) }; - - return localConfig; - }, - });" - `); - /* eslint-enable @fluentui/max-len */ - - expect(tree.read(`${projectStorybookConfigPath}/preview.js`)?.toString('utf-8')).toMatchInlineSnapshot(` - "import * as rootPreview from '../../../.storybook/preview'; - - /** @type {typeof rootPreview.decorators} */ - export const decorators = [...rootPreview.decorators]; - - /** @type {typeof rootPreview.parameters} */ - export const parameters = { ...rootPreview.parameters };" - `); - }); - - function setup() { const workspaceConfig = readWorkspaceConfiguration(tree); const projectConfig = readProjectConfiguration(tree, options.name); const normalizedProjectName = options.name.replace(`@${workspaceConfig.npmScope}/`, ''); const reactExamplesConfig = readProjectConfiguration(tree, '@proj/react-examples'); const pathToStoriesWithinReactExamples = `${reactExamplesConfig.root}/src/${normalizedProjectName}`; + const projectStorybookConfigPath = `${projectConfig.root}/.storybook`; const paths = { reactExamples: { @@ -470,21 +418,23 @@ describe('migrate-converged-pkg generator', () => { }, }; - tree.write( - paths.reactExamples.storyFileOne, - stripIndents` + if (config.createDummyStories) { + tree.write( + paths.reactExamples.storyFileOne, + stripIndents` import * as Implementation from '${options.name}'; export const Foo = (props: FooProps) => { return
Foo
; } `, - ); + ); - tree.write( - paths.reactExamples.storyFileTwo, - stripIndents` + tree.write( + paths.reactExamples.storyFileTwo, + stripIndents` import * as Implementation from '${options.name}'; export const FooOther = (props: FooPropsOther) => { return
FooOther
; } `, - ); + ); + } function getMovedStoriesData() { const movedStoriesExportNames = { @@ -515,8 +465,104 @@ describe('migrate-converged-pkg generator', () => { normalizedProjectName, pathToStoriesWithinReactExamples, getMovedStoriesData, + projectStorybookConfigPath, }; } + it(`should setup package storybook when needed`, async () => { + const { projectStorybookConfigPath, projectConfig } = setup(); + + expect(tree.exists(projectStorybookConfigPath)).toBeFalsy(); + + await generator(tree, options); + + expect(tree.exists(projectStorybookConfigPath)).toBeTruthy(); + expect(readJson(tree, `${projectStorybookConfigPath}/tsconfig.json`)).toMatchInlineSnapshot(` + Object { + "compilerOptions": Object { + "allowJs": true, + "checkJs": true, + }, + "exclude": Array [ + "../**/*.test.ts", + "../**/*.test.js", + "../**/*.test.tsx", + "../**/*.test.jsx", + ], + "extends": "../tsconfig.json", + "include": Array [ + "../src/**/*", + "*.js", + ], + } + `); + + expect(tree.read(`${projectStorybookConfigPath}/main.js`)?.toString('utf-8')).toMatchInlineSnapshot(` + "const rootMain = require('../../../.storybook/main'); + + module.exports = /** @type {Omit} */ ({ + ...rootMain, + stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/*.stories.@(ts|tsx)'], + addons: [...rootMain.addons], + webpackFinal: (config, options) => { + const localConfig = { ...rootMain.webpackFinal(config, options) }; + + // add your own webpack tweaks if needed + + return localConfig; + }, + });" + `); + + expect(tree.read(`${projectStorybookConfigPath}/preview.js`)?.toString('utf-8')).toMatchInlineSnapshot(` + "import * as rootPreview from '../../../.storybook/preview'; + + /** @type {typeof rootPreview.decorators} */ + export const decorators = [...rootPreview.decorators]; + + /** @type {typeof rootPreview.parameters} */ + export const parameters = { ...rootPreview.parameters };" + `); + + expect(readJson(tree, `${projectConfig.root}/tsconfig.json`).compilerOptions.types).toContain( + 'storybook__addons', + ); + }); + + it(`should remove unused existing storybook setup`, async () => { + const { projectStorybookConfigPath, projectConfig } = setup({ createDummyStories: false }); + + const mainJsFilePath = `${projectStorybookConfigPath}/main.js`; + const packageJsonPath = `${projectConfig.root}/package.json`; + + let pkgJson: PackageJson = readJson(tree, packageJsonPath); + + tree.write(mainJsFilePath, 'module.exports = {}'); + + updateJson(tree, packageJsonPath, (json: PackageJson) => { + json.scripts = json.scripts || {}; + + Object.assign(json.scripts, { + start: 'echo "hello"', + storybook: 'echo "hello"', + 'build-storybook': 'echo "hello"', + }); + return json; + }); + + pkgJson = readJson(tree, packageJsonPath); + + expect(tree.exists(projectStorybookConfigPath)).toBeTruthy(); + expect(tree.exists(mainJsFilePath)).toBeTruthy(); + + await generator(tree, options); + + expect(tree.exists(mainJsFilePath)).toBeFalsy(); + expect(Object.keys(pkgJson.scripts || [])).not.toContain(['start', 'storybook', 'build-storybook']); + expect(tree.exists(projectStorybookConfigPath)).toBeFalsy(); + expect(readJson(tree, `${projectConfig.root}/tsconfig.json`).compilerOptions.types).not.toContain( + 'storybook__addons', + ); + }); it(`should work if there are no package stories in react-examples`, async () => { const reactExamplesConfig = readProjectConfiguration(tree, '@proj/react-examples'); diff --git a/tools/generators/migrate-converged-pkg/index.ts b/tools/generators/migrate-converged-pkg/index.ts index f61bf631174bdd..db2e2048a606df 100644 --- a/tools/generators/migrate-converged-pkg/index.ts +++ b/tools/generators/migrate-converged-pkg/index.ts @@ -91,17 +91,17 @@ function runMigrationOnProject(tree: Tree, schema: AssertedSchema, userLog: User updateLocalJestConfig(tree, options); updateRootJestConfig(tree, options); - // 3. setup storybook - setupStorybook(tree, options); - - // 4. move stories to package + // move stories to package moveStorybookFromReactExamples(tree, options, userLog); removeMigratedPackageFromReactExamples(tree, options, userLog); - // 5. update package npm scripts + // update package npm scripts updateNpmScripts(tree, options); updateApiExtractorForLocalBuilds(tree, options); + // setup storybook + setupStorybook(tree, options); + setupNpmIgnoreConfig(tree, options); setupBabel(tree, options); @@ -134,7 +134,7 @@ const templates = { importHelpers: true, noUnusedLocals: true, preserveConstEnums: true, - types: ['jest', 'custom-global', 'inline-style-expand-shorthand', 'storybook__addons'], + types: ['jest', 'custom-global', 'inline-style-expand-shorthand'], } as TsConfig['compilerOptions'], }, babelConfig: (options: { extraPlugins: Array }) => { @@ -169,21 +169,22 @@ const templates = { }; `, storybook: { - /* eslint-disable @fluentui/max-len */ main: stripIndents` const rootMain = require('../../../.storybook/main'); - module.exports = /** @type {Pick} */ ({ + module.exports = /** @type {Omit} */ ({ + ...rootMain, stories: [...rootMain.stories, '../src/**/*.stories.mdx', '../src/**/*.stories.@(ts|tsx)'], addons: [...rootMain.addons], webpackFinal: (config, options) => { const localConfig = { ...rootMain.webpackFinal(config, options) }; + // add your own webpack tweaks if needed + return localConfig; }, }); `, - /* eslint-enable @fluentui/max-len */ preview: stripIndents` import * as rootPreview from '../../../.storybook/preview'; @@ -261,6 +262,7 @@ function normalizeOptions(host: Tree, options: AssertedSchema) { rootJestConfig: '/jest.config.js', npmConfig: joinPathFragments(projectConfig.root, '.npmignore'), storybook: { + rootFolder: joinPathFragments(projectConfig.root, '.storybook'), tsconfig: joinPathFragments(projectConfig.root, '.storybook/tsconfig.json'), main: joinPathFragments(projectConfig.root, '.storybook/main.js'), preview: joinPathFragments(projectConfig.root, '.storybook/preview.js'), @@ -431,13 +433,75 @@ function updateApiExtractorForLocalBuilds(tree: Tree, options: NormalizedSchema) } function setupStorybook(tree: Tree, options: NormalizedSchema) { - tree.write(options.paths.storybook.tsconfig, serializeJson(templates.storybook.tsconfig)); - tree.write(options.paths.storybook.main, templates.storybook.main); - tree.write(options.paths.storybook.preview, templates.storybook.preview); + const sbAction = shouldSetupStorybook(tree, options); + + if (sbAction === 'init') { + tree.write(options.paths.storybook.tsconfig, serializeJson(templates.storybook.tsconfig)); + tree.write(options.paths.storybook.main, templates.storybook.main); + tree.write(options.paths.storybook.preview, templates.storybook.preview); + + updateJson(tree, options.paths.tsconfig, (json: TsConfig) => { + json.compilerOptions.types = json.compilerOptions.types || []; + + json.compilerOptions.types.push('storybook__addons'); + json.compilerOptions.types = uniqueArray(json.compilerOptions.types); + + return json; + }); + } + + if (sbAction === 'remove') { + tree.delete(options.paths.storybook.rootFolder); + updateJson(tree, options.paths.packageJson, (json: PackageJson) => { + json.scripts = json.scripts || {}; + + delete json.scripts.start; + delete json.scripts.storybook; + delete json.scripts['build-storybook']; + + return json; + }); + + updateJson(tree, options.paths.tsconfig, (json: TsConfig) => { + json.compilerOptions.types = json.compilerOptions.types || []; + + json.compilerOptions.types = json.compilerOptions.types.filter( + typeReference => typeReference !== 'storybook__addons', + ); + + return json; + }); + } return tree; } +function shouldSetupStorybook(tree: Tree, options: NormalizedSchema) { + const hasStorybookConfig = tree.exists(options.paths.storybook.main); + let hasStories = false; + + visitNotIgnoredFiles(tree, options.projectConfig.root, treePath => { + if (treePath.includes('.stories.')) { + hasStories = true; + return; + } + }); + + const tags = options.projectConfig.tags || []; + const hasTags = tags.includes('vNext') && tags.includes('platform:web'); + + const shouldInit = hasStories || hasTags; + const shouldDelete = !shouldInit && hasStorybookConfig; + + if (shouldInit) { + return 'init'; + } + + if (shouldDelete) { + return 'remove'; + } +} + function moveStorybookFromReactExamples(tree: Tree, options: NormalizedSchema, userLog: UserLog) { const reactExamplesConfig = getReactExamplesProjectConfig(tree, options); const pathToStoriesWithinReactExamples = `${reactExamplesConfig.root}/src/${options.normalizedPkgName}`;