From ce63c3c1daa54d2d7096c843e13496ae7b38a0e1 Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Wed, 20 Nov 2024 14:56:40 -0500 Subject: [PATCH] feat(storybook): add support for TS solutions file --- .../src/utils/typescript/ts-solution-setup.ts | 11 ++ .../component-story/component-story.ts | 6 +- .../react/src/generators/stories/stories.ts | 9 +- .../storybook-configuration/configuration.ts | 4 +- .../__snapshots__/configuration.spec.ts.snap | 10 +- .../configuration/configuration.spec.ts | 111 ++++++++++++++++++ .../generators/configuration/configuration.ts | 3 - .../lib/interaction-testing.utils.ts | 12 ++ .../configuration/lib/util-functions.ts | 106 ++++++++++------- .../cypress-project/cypress-project.ts | 3 - .../storybook/src/generators/init/init.ts | 3 - 11 files changed, 217 insertions(+), 61 deletions(-) diff --git a/packages/js/src/utils/typescript/ts-solution-setup.ts b/packages/js/src/utils/typescript/ts-solution-setup.ts index d50d9fdb7c724b..c7265560043378 100644 --- a/packages/js/src/utils/typescript/ts-solution-setup.ts +++ b/packages/js/src/utils/typescript/ts-solution-setup.ts @@ -101,6 +101,17 @@ export function assertNotUsingTsSolutionSetup( process.exit(1); } +export function findRuntimeTsConfigName( + tree: Tree, + projectRoot: string +): string | null { + if (tree.exists(joinPathFragments(projectRoot, 'tsconfig.app.json'))) + return 'tsconfig.app.json'; + if (tree.exists(joinPathFragments(projectRoot, 'tsconfig.lib.json'))) + return 'tsconfig.lib.json'; + return null; +} + export function updateTsconfigFiles( tree: Tree, projectRoot: string, diff --git a/packages/react/src/generators/component-story/component-story.ts b/packages/react/src/generators/component-story/component-story.ts index d4b913d4490b38..ee36ede2a14e3b 100644 --- a/packages/react/src/generators/component-story/component-story.ts +++ b/packages/react/src/generators/component-story/component-story.ts @@ -31,9 +31,11 @@ export function createComponentStoriesFile( tsModule = ensureTypescript(); } const proj = getProjects(host).get(project); - const sourceRoot = proj.sourceRoot; - const componentFilePath = joinPathFragments(sourceRoot, componentPath); + const componentFilePath = joinPathFragments( + proj.sourceRoot ?? proj.root, + componentPath + ); const componentDirectory = componentFilePath.replace( componentFilePath.slice(componentFilePath.lastIndexOf('/')), diff --git a/packages/react/src/generators/stories/stories.ts b/packages/react/src/generators/stories/stories.ts index b21a78c0f870f1..f5e23e6f90f05b 100644 --- a/packages/react/src/generators/stories/stories.ts +++ b/packages/react/src/generators/stories/stories.ts @@ -47,8 +47,10 @@ export async function projectRootPath( } else if (config.projectType == 'library') { // libs/test-lib/src/lib projectDir = 'lib'; + } else { + projectDir = '.'; } - return joinPathFragments(config.sourceRoot, projectDir); + return joinPathFragments(config.sourceRoot ?? config.root, projectDir); } export function containsComponentDeclaration( @@ -119,7 +121,10 @@ export async function createAllStories( await Promise.all( componentPaths.map(async (componentPath) => { - const relativeCmpDir = componentPath.replace(join(sourceRoot, '/'), ''); + const relativeCmpDir = componentPath.replace( + join(sourceRoot ?? root, '/'), + '' + ); if (!containsComponentDeclaration(tree, componentPath)) { return; diff --git a/packages/react/src/generators/storybook-configuration/configuration.ts b/packages/react/src/generators/storybook-configuration/configuration.ts index 1c65f589557957..ce4fd568de9152 100644 --- a/packages/react/src/generators/storybook-configuration/configuration.ts +++ b/packages/react/src/generators/storybook-configuration/configuration.ts @@ -54,8 +54,8 @@ export async function storybookConfigurationGeneratorInternal( if ( findWebpackConfig(host, projectConfig.root) || - projectConfig.targets['build']?.executor === '@nx/rollup:rollup' || - projectConfig.targets['build']?.executor === '@nx/expo:build' + projectConfig.targets?.['build']?.executor === '@nx/rollup:rollup' || + projectConfig.targets?.['build']?.executor === '@nx/expo:build' ) { uiFramework = '@storybook/react-webpack5'; } diff --git a/packages/storybook/src/generators/configuration/__snapshots__/configuration.spec.ts.snap b/packages/storybook/src/generators/configuration/__snapshots__/configuration.spec.ts.snap index 4bfa0bdcd4db79..a8127944fd3ca3 100644 --- a/packages/storybook/src/generators/configuration/__snapshots__/configuration.spec.ts.snap +++ b/packages/storybook/src/generators/configuration/__snapshots__/configuration.spec.ts.snap @@ -47,11 +47,6 @@ exports[`@nx/storybook:configuration for Storybook v7 basic functionalities shou "emitDecoratorMetadata": true, "outDir": "" }, - "files": [ - "../node_modules/@nx/react/typings/styled-jsx.d.ts", - "../node_modules/@nx/react/typings/cssmodule.d.ts", - "../node_modules/@nx/react/typings/image.d.ts" - ], "exclude": [ "src/**/*.spec.ts", "src/**/*.test.ts", @@ -70,6 +65,11 @@ exports[`@nx/storybook:configuration for Storybook v7 basic functionalities shou "src/**/*.stories.mdx", ".storybook/*.js", ".storybook/*.ts" + ], + "files": [ + "../node_modules/@nx/react/typings/styled-jsx.d.ts", + "../node_modules/@nx/react/typings/cssmodule.d.ts", + "../node_modules/@nx/react/typings/image.d.ts" ] } " diff --git a/packages/storybook/src/generators/configuration/configuration.spec.ts b/packages/storybook/src/generators/configuration/configuration.spec.ts index cff1ca36696d43..63ebd8247ba952 100644 --- a/packages/storybook/src/generators/configuration/configuration.spec.ts +++ b/packages/storybook/src/generators/configuration/configuration.spec.ts @@ -794,4 +794,115 @@ describe('@nx/storybook:configuration for Storybook v7', () => { } ); }); + + describe('TS solution setup', () => { + let tree: Tree; + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + updateJson(tree, 'package.json', (json) => { + json.workspaces = ['packages/*', 'apps/*']; + return json; + }); + writeJson(tree, 'tsconfig.base.json', { + compilerOptions: { + composite: true, + declaration: true, + }, + }); + writeJson(tree, 'tsconfig.json', { + extends: './tsconfig.base.json', + files: [], + references: [], + }); + }); + + it('should add project references when using TS solution', async () => { + await libraryGenerator(tree, { + directory: 'mylib', + bundler: 'none', + skipFormat: true, + addPlugin: true, + }); + + await configurationGenerator(tree, { + project: 'mylib', + standaloneConfig: false, + uiFramework: '@storybook/react-vite', + addPlugin: true, + }); + + expect(readJson(tree, 'tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "./tsconfig.base.json", + "files": [], + "references": [ + { + "path": "./mylib", + }, + ], + "ts-node": { + "compilerOptions": { + "module": "commonjs", + }, + }, + } + `); + expect(readJson(tree, 'mylib/tsconfig.json')).toMatchInlineSnapshot(` + { + "extends": "../tsconfig.base.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + { + "path": "./tsconfig.storybook.json", + }, + ], + } + `); + expect(readJson(tree, 'mylib/tsconfig.storybook.json')) + .toMatchInlineSnapshot(` + { + "compilerOptions": { + "jsx": "preserve", + "module": "esnext", + "moduleResolution": "bundler", + "outDir": "out-tsc/storybook", + }, + "exclude": [ + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/**/*.spec.js", + "src/**/*.test.js", + "src/**/*.spec.tsx", + "src/**/*.test.tsx", + "src/**/*.spec.jsx", + "src/**/*.test.js", + ], + "extends": "../tsconfig.base.json", + "files": [ + "../node_modules/@nx/react/typings/styled-jsx.d.ts", + "../node_modules/@nx/react/typings/cssmodule.d.ts", + "../node_modules/@nx/react/typings/image.d.ts", + ], + "include": [ + "src/**/*.stories.ts", + "src/**/*.stories.js", + "src/**/*.stories.jsx", + "src/**/*.stories.tsx", + "src/**/*.stories.mdx", + ".storybook/*.js", + ".storybook/*.ts", + ], + "references": [ + { + "path": "./tsconfig.lib.json", + }, + ], + } + `); + }); + }); }); diff --git a/packages/storybook/src/generators/configuration/configuration.ts b/packages/storybook/src/generators/configuration/configuration.ts index 43d2233dc11e9c..8fd73406d42d0d 100644 --- a/packages/storybook/src/generators/configuration/configuration.ts +++ b/packages/storybook/src/generators/configuration/configuration.ts @@ -9,7 +9,6 @@ import { Tree, } from '@nx/devkit'; import { initGenerator as jsInitGenerator } from '@nx/js'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { StorybookConfigureSchema } from './schema'; import { initGenerator } from '../init/init'; @@ -60,8 +59,6 @@ export async function configurationGeneratorInternal( tree: Tree, rawSchema: StorybookConfigureSchema ) { - assertNotUsingTsSolutionSetup(tree, 'storybook', 'configuration'); - const storybookMajor = storybookMajorVersion(); if (storybookMajor > 0 && storybookMajor === 6) { throw new Error(pleaseUpgrade()); diff --git a/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.ts b/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.ts index 9ebd37fc51ec85..f877b0db6ce9b8 100644 --- a/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.ts +++ b/packages/storybook/src/generators/configuration/lib/interaction-testing.utils.ts @@ -67,6 +67,18 @@ function getMainTsJsPath( host: Tree, projectConfig: ProjectConfiguration ): string | undefined { + // Inferred targets from `@nx/storybook/plugin` are inferred from `.storybook/main.{js,ts,mjs,mts,cjs,cts}` so we can assume the directory. + if (!projectConfig.targets) { + const exts = ['js', 'ts', 'mjs', 'mts', 'cjs', 'cts']; + for (const ext of exts) { + const candidate = `${projectConfig.root}/.storybook/main.${ext}`; + if (host.exists(candidate)) return candidate; + } + throw new Error( + `Cannot find main Storybook file. Does this file exist? e.g. ${projectConfig.root}/.storybook/main.ts` + ); + } + let mainJsTsPath: string | undefined = undefined; Object.entries(projectConfig.targets).forEach( ([_targetName, targetConfig]) => { diff --git a/packages/storybook/src/generators/configuration/lib/util-functions.ts b/packages/storybook/src/generators/configuration/lib/util-functions.ts index 9c6e25c95234d5..b19b557267fb13 100644 --- a/packages/storybook/src/generators/configuration/lib/util-functions.ts +++ b/packages/storybook/src/generators/configuration/lib/util-functions.ts @@ -1,11 +1,9 @@ import { - createProjectGraphAsync, ensurePackage, generateFiles, joinPathFragments, logger, offsetFromRoot, - parseTargetString, readJson, readNxJson, readProjectConfiguration, @@ -17,7 +15,6 @@ import { workspaceRoot, writeJson, } from '@nx/devkit'; -import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils'; import { Linter } from '@nx/eslint'; import { join, relative } from 'path'; import { @@ -30,6 +27,10 @@ import { UiFramework } from '../../../utils/models'; import { nxVersion } from '../../../utils/versions'; import { findEslintFile } from '@nx/eslint/src/generators/utils/eslint-file'; import { useFlatConfig } from '@nx/eslint/src/utils/flat-config'; +import { + findRuntimeTsConfigName, + isUsingTsSolutionSetup, +} from '@nx/js/src/utils/typescript/ts-solution-setup'; const DEFAULT_PORT = 4400; @@ -167,6 +168,9 @@ export function createStorybookTsconfigFile( isRootProject: boolean, mainDir: 'components' | 'src' ) { + const offset = offsetFromRoot(projectRoot); + const useTsSolution = 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 = joinPathFragments( @@ -186,9 +190,10 @@ export function createStorybookTsconfigFile( return; } + const storybookTsConfigName = 'tsconfig.storybook.json'; const storybookTsConfigPath = joinPathFragments( projectRoot, - 'tsconfig.storybook.json' + storybookTsConfigName ); if (tree.exists(storybookTsConfigPath)) { @@ -196,12 +201,52 @@ export function createStorybookTsconfigFile( return; } - const exclude = [`${mainDir}/**/*.spec.ts`, `${mainDir}/**/*.test.ts`]; + const storybookTsConfig: any = { + extends: useTsSolution + ? 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 = findRuntimeTsConfigName(tree, projectRoot); + if (runtimeConfig) { + storybookTsConfig.references ??= []; + storybookTsConfig.references.push({ + path: `./${runtimeConfig}`, + }); + } + } + if ( uiFramework === '@storybook/react-webpack5' || uiFramework === '@storybook/react-vite' ) { - exclude.push( + storybookTsConfig.exclude.push( `${mainDir}/**/*.spec.js`, `${mainDir}/**/*.test.js`, `${mainDir}/**/*.spec.tsx`, @@ -209,17 +254,7 @@ export function createStorybookTsconfigFile( `${mainDir}/**/*.spec.jsx`, `${mainDir}/**/*.test.js` ); - } - - let files: string[]; - - if ( - uiFramework === '@storybook/react-webpack5' || - uiFramework === '@storybook/react-vite' - ) { - const offset = offsetFromRoot(projectRoot); - - files = [ + storybookTsConfig.files = [ `${ !isRootProject ? offset : '' }node_modules/@nx/react/typings/styled-jsx.d.ts`, @@ -232,30 +267,19 @@ export function createStorybookTsconfigFile( ]; } - const include: string[] = [ - `${mainDir}/**/*.stories.ts`, - `${mainDir}/**/*.stories.js`, - `${mainDir}/**/*.stories.jsx`, - `${mainDir}/**/*.stories.tsx`, - `${mainDir}/**/*.stories.mdx`, - '.storybook/*.js', - '.storybook/*.ts', - ]; - - const storybookTsConfig: TsConfig = { - extends: './tsconfig.json', - compilerOptions: { - emitDecoratorMetadata: true, - outDir: - uiFramework === '@storybook/react-webpack5' || - uiFramework === '@storybook/react-vite' - ? '' - : undefined, - }, - files, - exclude, - include, - }; + if (useTsSolution) { + updateJson( + tree, + joinPathFragments(projectRoot, 'tsconfig.json'), + (json) => { + json.references ??= []; + json.references.push({ + path: `./${storybookTsConfigName}`, + }); + return json; + } + ); + } writeJson(tree, storybookTsConfigPath, storybookTsConfig); } diff --git a/packages/storybook/src/generators/cypress-project/cypress-project.ts b/packages/storybook/src/generators/cypress-project/cypress-project.ts index a05185f061d7aa..ee2af4b0cdaa57 100644 --- a/packages/storybook/src/generators/cypress-project/cypress-project.ts +++ b/packages/storybook/src/generators/cypress-project/cypress-project.ts @@ -15,7 +15,6 @@ import { } from '@nx/devkit'; import { Linter, LinterType } from '@nx/eslint'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { join } from 'path'; import { safeFileDelete } from '../../utils/utilities'; @@ -35,8 +34,6 @@ export async function cypressProjectGenerator( tree: Tree, schema: CypressConfigureSchema ) { - assertNotUsingTsSolutionSetup(tree, 'cypress', 'cypress-project'); - logger.warn( `Use 'interactionTests' instead when running '@nx/storybook:configuration'. This generator will be removed in v21.` ); diff --git a/packages/storybook/src/generators/init/init.ts b/packages/storybook/src/generators/init/init.ts index f6c77b871c2df4..452031928ea725 100644 --- a/packages/storybook/src/generators/init/init.ts +++ b/packages/storybook/src/generators/init/init.ts @@ -11,7 +11,6 @@ import { updateNxJson, } from '@nx/devkit'; import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin'; -import { assertNotUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { gte } from 'semver'; import { createNodes } from '../../plugins/plugin'; import { @@ -96,8 +95,6 @@ export function initGenerator(tree: Tree, schema: Schema) { } export async function initGeneratorInternal(tree: Tree, schema: Schema) { - assertNotUsingTsSolutionSetup(tree, 'storybook', 'init'); - const nxJson = readNxJson(tree); const addPluginDefault = process.env.NX_ADD_PLUGINS !== 'false' &&