UNPKG

@nx/storybook

Version:

The Nx Plugin for Storybook contains executors and generators for allowing your workspace to use the powerful Storybook integration testing & documenting capabilities.

572 lines (571 loc) • 24.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.addStorybookTarget = addStorybookTarget; exports.addAngularStorybookTarget = addAngularStorybookTarget; exports.addStaticTarget = addStaticTarget; exports.createStorybookTsconfigFile = createStorybookTsconfigFile; exports.editTsconfigBaseJson = editTsconfigBaseJson; exports.configureTsProjectConfig = configureTsProjectConfig; exports.configureTsSolutionConfig = configureTsSolutionConfig; exports.updateLintConfig = updateLintConfig; exports.normalizeSchema = normalizeSchema; exports.addStorybookToNamedInputs = addStorybookToNamedInputs; exports.addStorybookToTargetDefaults = addStorybookToTargetDefaults; exports.createProjectStorybookDir = createProjectStorybookDir; exports.getTsConfigPath = getTsConfigPath; exports.addBuildStorybookToCacheableOperations = addBuildStorybookToCacheableOperations; exports.projectIsRootProjectInStandaloneWorkspace = projectIsRootProjectInStandaloneWorkspace; exports.workspaceHasRootProject = workspaceHasRootProject; exports.rootFileIsTs = rootFileIsTs; exports.findViteConfig = findViteConfig; exports.findNextConfig = findNextConfig; exports.isUsingReactNative = isUsingReactNative; exports.renameAndMoveOldTsConfig = renameAndMoveOldTsConfig; const devkit_1 = require("@nx/devkit"); const path_1 = require("path"); const utilities_1 = require("../../../utils/utilities"); const versions_1 = require("../../../utils/versions"); const eslint_file_1 = require("@nx/eslint/src/generators/utils/eslint-file"); const flat_config_1 = require("@nx/eslint/src/utils/flat-config"); const ts_solution_setup_1 = require("@nx/js/src/utils/typescript/ts-solution-setup"); const DEFAULT_PORT = 4400; function addStorybookTarget(tree, projectName, uiFramework, interactionTests) { const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, projectName); projectConfig.targets['storybook'] = { executor: '@nx/storybook:storybook', options: { port: DEFAULT_PORT, configDir: `${projectConfig.root}/.storybook`, }, configurations: { ci: { quiet: true, }, }, }; projectConfig.targets['build-storybook'] = { executor: '@nx/storybook:build', outputs: ['{options.outputDir}'], options: { outputDir: (0, devkit_1.joinPathFragments)('dist/storybook', projectName), configDir: `${projectConfig.root}/.storybook`, }, configurations: { ci: { quiet: true, }, }, }; if (interactionTests === true) { projectConfig.targets['test-storybook'] = { executor: 'nx:run-commands', options: { command: `test-storybook -c ${projectConfig.root}/.storybook --url=http://localhost:${DEFAULT_PORT}`, }, }; } (0, devkit_1.updateProjectConfiguration)(tree, projectName, projectConfig); } function addAngularStorybookTarget(tree, projectName, interactionTests) { const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, projectName); const { ngBuildTarget } = (0, utilities_1.findStorybookAndBuildTargetsAndCompiler)(projectConfig.targets); projectConfig.targets['storybook'] = { executor: '@storybook/angular:start-storybook', options: { port: 4400, configDir: `${projectConfig.root}/.storybook`, browserTarget: `${projectName}:${ngBuildTarget ? 'build' : 'build-storybook'}`, compodoc: false, }, configurations: { ci: { quiet: true, }, }, }; projectConfig.targets['build-storybook'] = { executor: '@storybook/angular:build-storybook', outputs: ['{options.outputDir}'], options: { outputDir: (0, devkit_1.joinPathFragments)('dist/storybook', projectName), configDir: `${projectConfig.root}/.storybook`, browserTarget: `${projectName}:${ngBuildTarget ? 'build' : 'build-storybook'}`, compodoc: false, }, configurations: { ci: { quiet: true, }, }, }; if (interactionTests === true) { projectConfig.targets['test-storybook'] = { executor: 'nx:run-commands', options: { command: `test-storybook -c ${projectConfig.root}/.storybook --url=http://localhost:${DEFAULT_PORT}`, }, }; } (0, devkit_1.updateProjectConfiguration)(tree, projectName, projectConfig); } async function addStaticTarget(tree, opts) { const { webStaticServeGenerator } = (0, devkit_1.ensurePackage)('@nx/web', versions_1.nxVersion); await webStaticServeGenerator(tree, { buildTarget: `${opts.project}:build-storybook`, outputPath: (0, devkit_1.joinPathFragments)('dist/storybook', opts.project), targetName: 'static-storybook', }); const projectConfig = (0, devkit_1.readProjectConfiguration)(tree, opts.project); projectConfig.targets['static-storybook'].configurations = { ci: { buildTarget: `${opts.project}:build-storybook:ci`, }, }; (0, devkit_1.updateProjectConfiguration)(tree, opts.project, projectConfig); } function createStorybookTsconfigFile(tree, projectRoot, uiFramework, isRootProject, mainDir) { const offset = (0, devkit_1.offsetFromRoot)(projectRoot); const useTsSolution = (0, ts_solution_setup_1.isUsingTsSolutionSetup)(tree); // First let's check if old configuration file exists // If it exists, let's rename it and move it to the new location const oldStorybookTsConfigPath = (0, devkit_1.joinPathFragments)(projectRoot, '.storybook/tsconfig.json'); if (tree.exists(oldStorybookTsConfigPath)) { devkit_1.logger.warn(`.storybook/tsconfig.json already exists for this project`); devkit_1.logger.warn(`It will be renamed and moved to tsconfig.storybook.json. Please make sure all settings look correct after this change. Also, please make sure to use "nx migrate" to move from one version of Nx to another. `); renameAndMoveOldTsConfig(projectRoot, oldStorybookTsConfigPath, tree); return; } const storybookTsConfigName = 'tsconfig.storybook.json'; const storybookTsConfigPath = (0, devkit_1.joinPathFragments)(projectRoot, storybookTsConfigName); if (tree.exists(storybookTsConfigPath)) { devkit_1.logger.info(`tsconfig.storybook.json already exists for this project`); return; } const storybookTsConfig = { extends: useTsSolution ? (0, devkit_1.joinPathFragments)(offset, 'tsconfig.base.json') : './tsconfig.json', compilerOptions: { emitDecoratorMetadata: useTsSolution ? undefined : true, outDir: useTsSolution ? 'out-tsc/storybook' : uiFramework === '@storybook/react-webpack5' || uiFramework === '@storybook/react-vite' ? '' : undefined, module: useTsSolution ? 'esnext' : undefined, moduleResolution: useTsSolution ? 'bundler' : undefined, jsx: useTsSolution && uiFramework !== '@storybook/angular' ? 'preserve' : undefined, }, exclude: [`${mainDir}/**/*.spec.ts`, `${mainDir}/**/*.test.ts`], include: [ `${mainDir}/**/*.stories.ts`, `${mainDir}/**/*.stories.js`, `${mainDir}/**/*.stories.jsx`, `${mainDir}/**/*.stories.tsx`, `${mainDir}/**/*.stories.mdx`, '.storybook/*.js', '.storybook/*.ts', ], }; if (useTsSolution) { const runtimeConfig = (0, ts_solution_setup_1.findRuntimeTsConfigName)(projectRoot, tree); if (runtimeConfig) { storybookTsConfig.references ??= []; storybookTsConfig.references.push({ path: `./${runtimeConfig}`, }); } } if (uiFramework === '@storybook/react-webpack5' || uiFramework === '@storybook/react-vite') { storybookTsConfig.exclude.push(`${mainDir}/**/*.spec.js`, `${mainDir}/**/*.test.js`, `${mainDir}/**/*.spec.tsx`, `${mainDir}/**/*.test.tsx`, `${mainDir}/**/*.spec.jsx`, `${mainDir}/**/*.test.js`); storybookTsConfig.files = [ `${!isRootProject ? offset : ''}node_modules/@nx/react/typings/styled-jsx.d.ts`, `${!isRootProject ? offset : ''}node_modules/@nx/react/typings/cssmodule.d.ts`, `${!isRootProject ? offset : ''}node_modules/@nx/react/typings/image.d.ts`, ]; } if (useTsSolution) { (0, devkit_1.updateJson)(tree, (0, devkit_1.joinPathFragments)(projectRoot, 'tsconfig.json'), (json) => { json.references ??= []; json.references.push({ path: `./${storybookTsConfigName}`, }); return json; }); } (0, devkit_1.writeJson)(tree, storybookTsConfigPath, storybookTsConfig); } function editTsconfigBaseJson(tree) { let tsconfigBasePath = 'tsconfig.base.json'; // standalone workspace maybe if (!tree.exists(tsconfigBasePath)) tsconfigBasePath = 'tsconfig.json'; if (!tree.exists(tsconfigBasePath)) return; const tsconfigBaseContent = (0, devkit_1.readJson)(tree, tsconfigBasePath); if (!tsconfigBaseContent.compilerOptions) tsconfigBaseContent.compilerOptions = {}; tsconfigBaseContent.compilerOptions.skipLibCheck = true; (0, devkit_1.writeJson)(tree, tsconfigBasePath, tsconfigBaseContent); } function configureTsProjectConfig(tree, schema) { const { project: projectName } = schema; let tsConfigPath; let tsConfigContent; try { tsConfigPath = getTsConfigPath(tree, projectName); tsConfigContent = (0, devkit_1.readJson)(tree, tsConfigPath); } catch { /** * Custom app configurations * may contain a tsconfig.json * instead of a tsconfig.app.json. */ tsConfigPath = getTsConfigPath(tree, projectName, 'tsconfig.json'); tsConfigContent = (0, devkit_1.readJson)(tree, tsConfigPath); } if (!tsConfigContent?.exclude?.includes('**/*.stories.ts') && !tsConfigContent?.exclude?.includes('**/*.stories.js')) { tsConfigContent.exclude = [ ...(tsConfigContent.exclude || []), '**/*.stories.ts', '**/*.stories.js', ...(schema.uiFramework?.startsWith('@storybook/react') ? ['**/*.stories.jsx', '**/*.stories.tsx'] : []), ]; } (0, devkit_1.writeJson)(tree, tsConfigPath, tsConfigContent); } function configureTsSolutionConfig(tree, schema) { const { project: projectName } = schema; const { root } = (0, devkit_1.readProjectConfiguration)(tree, projectName); const tsConfigPath = (0, path_1.join)(root, 'tsconfig.json'); const tsConfigContent = (0, devkit_1.readJson)(tree, tsConfigPath); if (schema.uiFramework === '@storybook/angular') { if (!tsConfigContent.references ?.map((reference) => reference.path) ?.includes('./.storybook/tsconfig.json')) { tsConfigContent.references = [ ...(tsConfigContent.references || []), { path: './.storybook/tsconfig.json', }, ]; } } else { if (!tsConfigContent.references ?.map((reference) => reference.path) ?.includes('./tsconfig.storybook.json')) { tsConfigContent.references = [ ...(tsConfigContent.references || []), { path: './tsconfig.storybook.json', }, ]; } } (0, devkit_1.writeJson)(tree, tsConfigPath, tsConfigContent); } /** * When adding storybook we need to inform ESLint * of the additional tsconfig.json file which will be the only tsconfig * which includes *.stories files. * * This is done within the eslint config file. */ function updateLintConfig(tree, schema) { const { project: projectName } = schema; const { root } = (0, devkit_1.readProjectConfiguration)(tree, projectName); const eslintFile = (0, eslint_file_1.findEslintFile)(tree, root); if (!eslintFile) { return; } const parserConfigPath = (0, path_1.join)(root, schema.uiFramework === '@storybook/angular' ? '.storybook/tsconfig.json' : 'tsconfig.storybook.json'); if ((0, flat_config_1.useFlatConfig)(tree)) { let config = tree.read(eslintFile, 'utf-8'); const projectRegex = RegExp(/project:\s?\[?['"](.*)['"]\]?/g); let match; while ((match = projectRegex.exec(config)) !== null) { const matchSet = new Set(match[1].split(',').map((p) => p.trim().replace(/['"]/g, ''))); matchSet.add(parserConfigPath); const insert = `project: [${Array.from(matchSet) .map((p) => `'${p}'`) .join(', ')}]`; config = config.slice(0, match.index) + insert + config.slice(match.index + match[0].length); } tree.write(eslintFile, config); } else { (0, devkit_1.updateJson)(tree, (0, path_1.join)(root, eslintFile), (json) => { if (typeof json.parserOptions?.project === 'string') { json.parserOptions.project = [json.parserOptions.project]; } if (json.parserOptions?.project) { json.parserOptions.project = (0, utilities_1.dedupe)([ ...json.parserOptions.project, parserConfigPath, ]); } const overrides = json.overrides || []; for (const o of overrides) { if (typeof o.parserOptions?.project === 'string') { o.parserOptions.project = [o.parserOptions.project]; } if (o.parserOptions?.project) { o.parserOptions.project = (0, utilities_1.dedupe)([ ...o.parserOptions.project, parserConfigPath, ]); } } const ignorePatterns = json.ignorePatterns || []; if (!ignorePatterns.includes('storybook-static')) { ignorePatterns.push('storybook-static'); } return json; }); } } function normalizeSchema(schema) { const defaults = { configureCypress: true, linter: 'eslint', js: false, }; return { ...defaults, ...schema, }; } function addStorybookToNamedInputs(tree) { const nxJson = (0, devkit_1.readNxJson)(tree); if (nxJson.namedInputs) { const hasProductionFileset = !!nxJson.namedInputs?.production; if (hasProductionFileset) { if (!nxJson.namedInputs.production.includes('!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)')) { nxJson.namedInputs.production.push('!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)'); } if (!nxJson.namedInputs.production.includes('!{projectRoot}/.storybook/**/*')) { nxJson.namedInputs.production.push('!{projectRoot}/.storybook/**/*'); } if (!nxJson.namedInputs.production.includes('!{projectRoot}/tsconfig.storybook.json')) { nxJson.namedInputs.production.push('!{projectRoot}/tsconfig.storybook.json'); } } (0, devkit_1.updateNxJson)(tree, nxJson); } } function addStorybookToTargetDefaults(tree, setCache = true) { const nxJson = (0, devkit_1.readNxJson)(tree); nxJson.targetDefaults ??= {}; nxJson.targetDefaults['build-storybook'] ??= {}; if (setCache) { nxJson.targetDefaults['build-storybook'].cache ??= true; } nxJson.targetDefaults['build-storybook'].inputs ??= [ 'default', nxJson.namedInputs && 'production' in nxJson.namedInputs ? '^production' : '^default', ]; if (!nxJson.targetDefaults['build-storybook'].inputs.includes('{projectRoot}/.storybook/**/*')) { nxJson.targetDefaults['build-storybook'].inputs.push('{projectRoot}/.storybook/**/*'); } // Delete the !{projectRoot}/.storybook/**/* glob from build-storybook // because we want to rebuild Storybook if the .storybook folder changes const index = nxJson.targetDefaults['build-storybook'].inputs.indexOf('!{projectRoot}/.storybook/**/*'); if (index !== -1) { nxJson.targetDefaults['build-storybook'].inputs.splice(index, 1); } if (!nxJson.targetDefaults['build-storybook'].inputs.includes('{projectRoot}/tsconfig.storybook.json')) { nxJson.targetDefaults['build-storybook'].inputs.push('{projectRoot}/tsconfig.storybook.json'); } (0, devkit_1.updateNxJson)(tree, nxJson); } function createProjectStorybookDir(tree, projectName, uiFramework, js, tsConfiguration, root, projectType, projectIsRootProjectInStandaloneWorkspace, interactionTests, mainDir, isNextJs, usesSwc, usesVite, viteConfigFilePath, hasPlugin, viteConfigFileName, usesReactNative) { let projectDirectory = (0, ts_solution_setup_1.getProjectType)(tree, root, projectType) === 'application' ? isNextJs ? 'components' : 'src/app' : 'src/lib'; if (uiFramework === '@storybook/vue3-vite') { projectDirectory = 'src'; } const storybookConfigExists = projectIsRootProjectInStandaloneWorkspace ? tree.exists('.storybook/main.js') || tree.exists('.storybook/main.ts') : tree.exists((0, path_1.join)(root, '.storybook/main.ts')) || tree.exists((0, path_1.join)(root, '.storybook/main.js')); if (storybookConfigExists) { devkit_1.logger.warn(`Storybook configuration files already exist for ${projectName}!`); return; } const templatePath = (0, path_1.join)(__dirname, `../project-files${tsConfiguration ? '-ts' : ''}`); (0, devkit_1.generateFiles)(tree, templatePath, root, { tmpl: '', uiFramework, offsetFromRoot: (0, devkit_1.offsetFromRoot)(root), projectDirectory, projectType, interactionTests, mainDir, isNextJs: isNextJs && (0, ts_solution_setup_1.getProjectType)(tree, root, projectType) === 'application', usesSwc, usesVite, isRootProject: projectIsRootProjectInStandaloneWorkspace, viteConfigFilePath, hasPlugin, viteConfigFileName, usesReactNative, }); if (js) { (0, devkit_1.toJS)(tree); } if (uiFramework !== '@storybook/angular') { // This file is only used for Angular // For non-Angular projects, we generate a file // called tsconfig.storybook.json at the root of the project // using the createStorybookTsconfigFile function // since Storybook is only taking into account .storybook/tsconfig.json // for Angular projects tree.delete((0, path_1.join)(root, '.storybook/tsconfig.json')); } } function getTsConfigPath(tree, projectName, path) { const { root, projectType } = (0, devkit_1.readProjectConfiguration)(tree, projectName); return (0, path_1.join)(root, path?.length > 0 ? path : (0, ts_solution_setup_1.getProjectType)(tree, root, projectType) === 'application' ? 'tsconfig.app.json' : 'tsconfig.lib.json'); } function addBuildStorybookToCacheableOperations(tree) { const nxJson = (0, devkit_1.readNxJson)(tree); if (nxJson.tasksRunnerOptions?.default?.options?.cacheableOperations && !nxJson.tasksRunnerOptions.default.options.cacheableOperations.includes('build-storybook')) { nxJson.tasksRunnerOptions.default.options.cacheableOperations.push('build-storybook'); (0, devkit_1.updateNxJson)(tree, nxJson); } } function projectIsRootProjectInStandaloneWorkspace(projectRoot) { return (0, path_1.relative)(devkit_1.workspaceRoot, projectRoot)?.length === 0; } function workspaceHasRootProject(tree) { return tree.exists('project.json'); } function rootFileIsTs(tree, rootFileName, tsConfiguration) { if (tree.exists(`.storybook/${rootFileName}.ts`) && !tsConfiguration) { devkit_1.logger.info(`The root Storybook configuration is in TypeScript, so Nx will generate TypeScript Storybook configuration files in this project's .storybook folder as well.`); return true; } else if (tree.exists(`.storybook/${rootFileName}.js`) && tsConfiguration) { devkit_1.logger.info(`The root Storybook configuration is in JavaScript, so Nx will generate JavaScript Storybook configuration files in this project's .storybook folder as well.`); return false; } else { return tsConfiguration; } } function findViteConfig(tree, projectRoot) { const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts']; for (const ext of allowsExt) { const viteConfigPath = (0, devkit_1.joinPathFragments)(projectRoot, `vite.config.${ext}`); if (tree.exists(viteConfigPath)) { return { fullConfigPath: viteConfigPath, viteConfigFileName: `vite.config.${ext}`, }; } } } function findNextConfig(tree, projectRoot) { const allowsExt = ['js', 'mjs', 'cjs']; for (const ext of allowsExt) { const nextConfigPath = (0, devkit_1.joinPathFragments)(projectRoot, `next.config.${ext}`); if (tree.exists(nextConfigPath)) { return nextConfigPath; } } } function isUsingReactNative(projectName) { try { const projectGraph = (0, devkit_1.readCachedProjectGraph)(); return projectGraph?.dependencies?.[projectName]?.some((dep) => dep.target === 'npm:react-native'); } catch { return false; } } function renameAndMoveOldTsConfig(projectRoot, pathToStorybookConfigFile, tree) { if (pathToStorybookConfigFile && tree.exists(pathToStorybookConfigFile)) { (0, devkit_1.updateJson)(tree, pathToStorybookConfigFile, (json) => { if (json.extends?.startsWith('../')) { // drop one level of nesting json.extends = json.extends.replace('../', './'); } for (let i = 0; i < json.files?.length; i++) { // drop one level of nesting if (json.files[i].startsWith('../../../')) { json.files[i] = json.files[i].replace('../../../', '../../'); } } for (let i = 0; i < json.include?.length; i++) { if (json.include[i].startsWith('../')) { json.include[i] = json.include[i].replace('../', ''); } if (json.include[i] === '*.js') { json.include[i] = '.storybook/*.js'; } if (json.include[i] === '*.ts') { json.include[i] = '.storybook/*.ts'; } } for (let i = 0; i < json.exclude?.length; i++) { if (json.exclude[i].startsWith('../')) { json.exclude[i] = json.exclude[i].replace('../', 'src/'); } } return json; }); tree.rename(pathToStorybookConfigFile, (0, devkit_1.joinPathFragments)(projectRoot, `tsconfig.storybook.json`)); } const projectTsConfig = (0, devkit_1.joinPathFragments)(projectRoot, 'tsconfig.json'); if (tree.exists(projectTsConfig)) { (0, devkit_1.updateJson)(tree, projectTsConfig, (json) => { for (let i = 0; i < json.references?.length; i++) { if (json.references[i].path === './.storybook/tsconfig.json') { json.references[i].path = './tsconfig.storybook.json'; break; } } return json; }); } const eslintFile = (0, eslint_file_1.findEslintFile)(tree, projectRoot); if (eslintFile) { const fileName = (0, devkit_1.joinPathFragments)(projectRoot, eslintFile); const config = tree.read(fileName, 'utf-8'); tree.write(fileName, config.replace(/\.storybook\/tsconfig\.json/g, 'tsconfig.storybook.json')); } }