From 9a48b69cac036efa473970dc95f677374cbcac3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leosvel=20P=C3=A9rez=20Espinosa?= Date: Tue, 23 Apr 2024 21:32:04 +0200 Subject: [PATCH] fix(storybook): handle inherited config correctly when identifying the framework used for inferred tasks (#22953) (cherry picked from commit 6cab4c9a1bfbf21d425be98640bf1a24df8ebbd8) --- packages/storybook/src/plugins/plugin.spec.ts | 117 ++++++++---------- packages/storybook/src/plugins/plugin.ts | 71 +++-------- 2 files changed, 68 insertions(+), 120 deletions(-) diff --git a/packages/storybook/src/plugins/plugin.spec.ts b/packages/storybook/src/plugins/plugin.spec.ts index e5cf37569f009..c81f5d4e930c4 100644 --- a/packages/storybook/src/plugins/plugin.spec.ts +++ b/packages/storybook/src/plugins/plugin.spec.ts @@ -1,14 +1,16 @@ import { CreateNodesContext } from '@nx/devkit'; - -import { createNodes } from './plugin'; import { TempFs } from '@nx/devkit/internal-testing-utils'; +import type { StorybookConfig } from '@storybook/types'; +import { join } from 'node:path'; +import { createNodes } from './plugin'; describe('@nx/storybook/plugin', () => { let createNodesFunction = createNodes[1]; let context: CreateNodesContext; - let tempFs = new TempFs('test'); + let tempFs: TempFs; beforeEach(async () => { + tempFs = new TempFs('storybook-plugin'); context = { nxJsonConfiguration: { namedInputs: { @@ -35,34 +37,22 @@ describe('@nx/storybook/plugin', () => { afterEach(() => { jest.resetModules(); - tempFs = new TempFs('test'); + tempFs.cleanup(); + tempFs = null; }); - it('should create nodes', () => { - tempFs.createFileSync( - 'my-app/.storybook/main.ts', - `import type { StorybookConfig } from '@storybook/react-vite'; + it('should create nodes', async () => { + tempFs.createFileSync('my-app/.storybook/main.ts', ''); + mockStorybookMainConfig('my-app/.storybook/main.ts', { + stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/react-vite', + options: {}, + }, + }); - import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; - import { mergeConfig } from 'vite'; - - const config: StorybookConfig = { - stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'], - addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/react-vite', - options: {}, - }, - - viteFinal: async (config) => - mergeConfig(config, { - plugins: [nxViteTsPaths()], - }), - }; - - export default config;` - ); - const nodes = createNodesFunction( + const nodes = await createNodesFunction( 'my-app/.storybook/main.ts', { buildStorybookTargetName: 'build-storybook', @@ -94,7 +84,6 @@ describe('@nx/storybook/plugin', () => { { externalDependencies: ['storybook', '@storybook/test-runner'] }, ], }); - expect( nodes?.['projects']?.['my-app']?.targets?.['storybook'] ).toMatchObject({ @@ -107,23 +96,18 @@ describe('@nx/storybook/plugin', () => { }); }); - it('should create angular nodes', () => { - tempFs.createFileSync( - 'my-ng-app/.storybook/main.ts', - `import type { StorybookConfig } from '@storybook/angular'; + it('should create angular nodes', async () => { + tempFs.createFileSync('my-ng-app/.storybook/main.ts', ''); + mockStorybookMainConfig('my-ng-app/.storybook/main.ts', { + stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: { + name: '@storybook/angular', + options: {}, + }, + }); - const config: StorybookConfig = { - stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'], - addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'], - framework: { - name: '@storybook/angular', - options: {}, - }, - }; - - export default config;` - ); - const nodes = createNodesFunction( + const nodes = await createNodesFunction( 'my-ng-app/.storybook/main.ts', { buildStorybookTargetName: 'build-storybook', @@ -164,7 +148,6 @@ describe('@nx/storybook/plugin', () => { }, ], }); - expect( nodes?.['projects']?.['my-ng-app']?.targets?.['storybook'] ).toMatchObject({ @@ -182,24 +165,22 @@ describe('@nx/storybook/plugin', () => { }); }); - it('should support main.js', () => { - tempFs.createFileSync( - 'my-react-lib/.storybook/main.js', - `const config = { - stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'], - addons: ['@storybook/addon-essentials'], - framework: { - name: '@storybook/react-vite', - options: { - builder: { - viteConfigPath: 'vite.config.js', - }, + it('should support main.js', async () => { + tempFs.createFileSync('my-react-lib/.storybook/main.js', ''); + mockStorybookMainConfig('my-react-lib/.storybook/main.js', { + stories: ['../src/lib/**/*.stories.@(js|jsx|ts|tsx|mdx)'], + addons: ['@storybook/addon-essentials'], + framework: { + name: '@storybook/react-vite', + options: { + builder: { + viteConfigPath: 'vite.config.js', }, }, - }; - export default config;` - ); - const nodes = createNodesFunction( + }, + }); + + const nodes = await createNodesFunction( 'my-react-lib/.storybook/main.js', { buildStorybookTargetName: 'build-storybook', @@ -231,7 +212,6 @@ describe('@nx/storybook/plugin', () => { { externalDependencies: ['storybook', '@storybook/test-runner'] }, ], }); - expect( nodes?.['projects']?.['my-react-lib']?.targets?.['storybook'] ).toMatchObject({ @@ -243,4 +223,15 @@ describe('@nx/storybook/plugin', () => { command: 'test-storybook', }); }); + + function mockStorybookMainConfig( + mainTsPath: string, + mainTsConfig: StorybookConfig + ) { + jest.mock( + join(tempFs.tempDir, mainTsPath), + () => ({ default: mainTsConfig }), + { virtual: true } + ); + } }); diff --git a/packages/storybook/src/plugins/plugin.ts b/packages/storybook/src/plugins/plugin.ts index e2d44ebaccff9..440f0a8386fcc 100644 --- a/packages/storybook/src/plugins/plugin.ts +++ b/packages/storybook/src/plugins/plugin.ts @@ -15,7 +15,8 @@ import { existsSync, readFileSync, readdirSync } from 'fs'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; import { getLockFileName } from '@nx/js'; -import { tsquery } from '@phenomnomnominal/tsquery'; +import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; +import type { StorybookConfig } from '@storybook/types'; export interface StorybookPluginOptions { buildStorybookTargetName?: string; @@ -52,7 +53,7 @@ export const createDependencies: CreateDependencies = () => { export const createNodes: CreateNodes = [ '**/.storybook/main.{js,ts,mjs,mts,cjs,cts}', - (configFilePath, options, context) => { + async (configFilePath, options, context) => { let projectRoot = ''; if (configFilePath.includes('/.storybook')) { projectRoot = dirname(configFilePath).replace('/.storybook', ''); @@ -82,7 +83,7 @@ export const createNodes: CreateNodes = [ const targets = targetsCache[hash] ? targetsCache[hash] - : buildStorybookTargets( + : await buildStorybookTargets( configFilePath, projectRoot, options, @@ -105,7 +106,7 @@ export const createNodes: CreateNodes = [ }, ]; -function buildStorybookTargets( +async function buildStorybookTargets( configFilePath: string, projectRoot: string, options: StorybookPluginOptions, @@ -116,9 +117,12 @@ function buildStorybookTargets( const namedInputs = getNamedInputs(projectRoot, context); - const storybookFramework = getStorybookFramework(configFilePath, context); + const storybookFramework = await getStorybookFramework( + configFilePath, + context + ); - const frameworkIsAngular = storybookFramework === "'@storybook/angular'"; + const frameworkIsAngular = storybookFramework === '@storybook/angular'; if (frameworkIsAngular && !projectName) { throw new Error( @@ -262,61 +266,14 @@ function serveStaticTarget( return targetConfig; } -function getStorybookFramework( +async function getStorybookFramework( configFilePath: string, context: CreateNodesContext -): string { +): Promise { const resolvedPath = join(context.workspaceRoot, configFilePath); - const mainTsJs = readFileSync(resolvedPath, 'utf-8'); - const importDeclarations = tsquery.query( - mainTsJs, - 'ImportDeclaration:has(ImportSpecifier:has([text="StorybookConfig"]))' - )?.[0]; - - if (!importDeclarations) { - return parseFrameworkName(mainTsJs); - } - - const storybookConfigImportPackage = tsquery.query( - importDeclarations, - 'StringLiteral' - )?.[0]; - - if (storybookConfigImportPackage?.getText() === `'@storybook/core-common'`) { - return parseFrameworkName(mainTsJs); - } - - return storybookConfigImportPackage?.getText(); -} - -function parseFrameworkName(mainTsJs: string) { - const frameworkPropertyAssignment = tsquery.query( - mainTsJs, - `PropertyAssignment:has(Identifier:has([text="framework"]))` - )?.[0]; - - if (!frameworkPropertyAssignment) { - return undefined; - } - - const propertyAssignments = tsquery.query( - frameworkPropertyAssignment, - `PropertyAssignment:has(Identifier:has([text="name"]))` - ); - - const namePropertyAssignment = propertyAssignments?.find((expression) => { - return expression.getText().startsWith('name'); - }); - - if (!namePropertyAssignment) { - const storybookConfigImportPackage = tsquery.query( - frameworkPropertyAssignment, - 'StringLiteral' - )?.[0]; - return storybookConfigImportPackage?.getText(); - } + const { framework } = await loadConfigFile(resolvedPath); - return tsquery.query(namePropertyAssignment, `StringLiteral`)?.[0]?.getText(); + return typeof framework === 'string' ? framework : framework.name; } function getOutputs(projectRoot: string): string[] {