diff --git a/packages/storybook/plugin.ts b/packages/storybook/plugin.ts index 8b8dbdde8ffcde..920c592a84acd1 100644 --- a/packages/storybook/plugin.ts +++ b/packages/storybook/plugin.ts @@ -1,5 +1,6 @@ export { createNodes, + createNodesV2, StorybookPluginOptions, createDependencies, } from './src/plugins/plugin'; diff --git a/packages/storybook/src/plugins/plugin.spec.ts b/packages/storybook/src/plugins/plugin.spec.ts index 89be68b87a141a..bc5024c3e7f4c4 100644 --- a/packages/storybook/src/plugins/plugin.spec.ts +++ b/packages/storybook/src/plugins/plugin.spec.ts @@ -2,10 +2,10 @@ import { CreateNodesContext } from '@nx/devkit'; import { TempFs } from '@nx/devkit/internal-testing-utils'; import type { StorybookConfig } from '@storybook/types'; import { join } from 'node:path'; -import { createNodes } from './plugin'; +import { createNodesV2 } from './plugin'; describe('@nx/storybook/plugin', () => { - let createNodesFunction = createNodes[1]; + let createNodesFunction = createNodesV2[1]; let context: CreateNodesContext; let tempFs: TempFs; @@ -54,7 +54,7 @@ describe('@nx/storybook/plugin', () => { }); const nodes = await createNodesFunction( - 'my-app/.storybook/main.ts', + ['my-app/.storybook/main.ts'], { buildStorybookTargetName: 'build-storybook', staticStorybookTargetName: 'static-storybook', @@ -64,9 +64,10 @@ describe('@nx/storybook/plugin', () => { context ); - expect(nodes?.['projects']?.['my-app']?.targets).toBeDefined(); + expect(nodes.at(0)?.[0]).toStrictEqual('my-app/.storybook/main.ts'); + expect(nodes.at(0)?.[1]?.['projects']?.['my-app']?.targets).toBeDefined(); expect( - nodes?.['projects']?.['my-app']?.targets?.['build-storybook'] + nodes.at(0)?.[1]?.['projects']?.['my-app']?.targets['build-storybook'] ).toMatchObject({ command: 'storybook build', options: { @@ -86,7 +87,7 @@ describe('@nx/storybook/plugin', () => { ], }); expect( - nodes?.['projects']?.['my-app']?.targets?.['serve-storybook'] + nodes.at(0)?.[1]?.['projects']?.['my-app']?.targets['serve-storybook'] ).toMatchObject({ command: 'storybook dev', }); @@ -104,7 +105,7 @@ describe('@nx/storybook/plugin', () => { }); const nodes = await createNodesFunction( - 'my-ng-app/.storybook/main.ts', + ['my-ng-app/.storybook/main.ts'], { buildStorybookTargetName: 'build-storybook', staticStorybookTargetName: 'static-storybook', @@ -114,9 +115,14 @@ describe('@nx/storybook/plugin', () => { context ); - expect(nodes?.['projects']?.['my-ng-app']?.targets).toBeDefined(); + expect(nodes.at(0)?.[0]).toStrictEqual('my-ng-app/.storybook/main.ts'); expect( - nodes?.['projects']?.['my-ng-app']?.targets?.['build-storybook'] + nodes.at(0)?.[1]?.['projects']?.['my-ng-app']?.targets + ).toBeDefined(); + expect( + nodes.at(0)?.[1]?.['projects']?.['my-ng-app']?.targets?.[ + 'build-storybook' + ] ).toMatchObject({ executor: '@storybook/angular:build-storybook', options: { @@ -145,7 +151,9 @@ describe('@nx/storybook/plugin', () => { ], }); expect( - nodes?.['projects']?.['my-ng-app']?.targets?.['serve-storybook'] + nodes.at(0)?.[1]?.['projects']?.['my-ng-app']?.targets?.[ + 'serve-storybook' + ] ).toMatchObject({ executor: '@storybook/angular:start-storybook', options: { @@ -172,7 +180,7 @@ describe('@nx/storybook/plugin', () => { }); const nodes = await createNodesFunction( - 'my-react-lib/.storybook/main.js', + ['my-react-lib/.storybook/main.js'], { buildStorybookTargetName: 'build-storybook', staticStorybookTargetName: 'static-storybook', @@ -182,9 +190,14 @@ describe('@nx/storybook/plugin', () => { context ); - expect(nodes?.['projects']?.['my-react-lib']?.targets).toBeDefined(); + expect(nodes.at(0)?.[0]).toStrictEqual('my-react-lib/.storybook/main.js'); + expect( + nodes.at(0)?.[1]?.['projects']?.['my-react-lib']?.targets + ).toBeDefined(); expect( - nodes?.['projects']?.['my-react-lib']?.targets?.['build-storybook'] + nodes.at(0)?.[1]?.['projects']?.['my-react-lib']?.targets?.[ + 'build-storybook' + ] ).toMatchObject({ command: 'storybook build', options: { @@ -204,7 +217,9 @@ describe('@nx/storybook/plugin', () => { ], }); expect( - nodes?.['projects']?.['my-react-lib']?.targets?.['serve-storybook'] + nodes.at(0)?.[1]?.['projects']?.['my-react-lib']?.targets?.[ + 'serve-storybook' + ] ).toMatchObject({ command: 'storybook dev', }); diff --git a/packages/storybook/src/plugins/plugin.ts b/packages/storybook/src/plugins/plugin.ts index 7fc597cb24b925..3d6b892c878daf 100644 --- a/packages/storybook/src/plugins/plugin.ts +++ b/packages/storybook/src/plugins/plugin.ts @@ -2,8 +2,12 @@ import { CreateDependencies, CreateNodes, CreateNodesContext, + createNodesFromFiles, + CreateNodesV2, detectPackageManager, + getPackageManagerCommand, joinPathFragments, + logger, parseJson, readJsonFile, TargetConfiguration, @@ -17,6 +21,9 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory'; import { getLockFileName } from '@nx/js'; import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; import type { StorybookConfig } from '@storybook/types'; +import { hashObject } from 'nx/src/hasher/file-hasher'; + +const pmc = getPackageManagerCommand(); export interface StorybookPluginOptions { buildStorybookTargetName?: string; @@ -25,82 +32,127 @@ export interface StorybookPluginOptions { testStorybookTargetName?: string; } -const cachePath = join(workspaceDataDirectory, 'storybook.hash'); -const targetsCache = readTargetsCache(); - -function readTargetsCache(): Record< - string, - Record -> { +function readTargetsCache( + cachePath: string +): Record> { return existsSync(cachePath) ? readJsonFile(cachePath) : {}; } -function writeTargetsToCache() { - const oldCache = readTargetsCache(); - writeJsonFile(cachePath, { - ...oldCache, - ...targetsCache, - }); +function writeTargetsToCache( + cachePath: string, + results: Record> +) { + writeJsonFile(cachePath, results); } +/** + * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'. + */ export const createDependencies: CreateDependencies = () => { - writeTargetsToCache(); return []; }; -export const createNodes: CreateNodes = [ - '**/.storybook/main.{js,ts,mjs,mts,cjs,cts}', - async (configFilePath, options, context) => { - let projectRoot = ''; - if (configFilePath.includes('/.storybook')) { - projectRoot = dirname(configFilePath).replace('/.storybook', ''); - } else { - projectRoot = dirname(configFilePath).replace('.storybook', ''); - } +const storybookConfigGlob = '**/.storybook/main.{js,ts,mjs,mts,cjs,cts}'; - if (projectRoot === '') { - projectRoot = '.'; - } - - // Do not create a project if package.json and project.json isn't there. - const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); - if ( - !siblingFiles.includes('package.json') && - !siblingFiles.includes('project.json') - ) { - return {}; +export const createNodesV2: CreateNodesV2 = [ + storybookConfigGlob, + async (configFilePaths, options, context) => { + const normalizedOptions = normalizeOptions(options); + const optionsHash = hashObject(normalizedOptions); + const cachePath = join( + workspaceDataDirectory, + `storybook-${optionsHash}.hash` + ); + const targetsCache = readTargetsCache(cachePath); + + try { + return await createNodesFromFiles( + (configFile, _, context) => + createNodesInternal( + configFile, + normalizedOptions, + context, + targetsCache + ), + configFilePaths, + normalizedOptions, + context + ); + } finally { + writeTargetsToCache(cachePath, targetsCache); } + }, +]; - options = normalizeOptions(options); - const hash = await calculateHashForCreateNodes( - projectRoot, - options, - context, - [getLockFileName(detectPackageManager(context.workspaceRoot))] +export const createNodes: CreateNodes = [ + storybookConfigGlob, + (configFilePath, options, context) => { + logger.warn( + '`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.' ); - - const projectName = buildProjectName(projectRoot, context.workspaceRoot); - - targetsCache[hash] ??= await buildStorybookTargets( + return createNodesInternal( configFilePath, - projectRoot, - options, + normalizeOptions(options), context, - projectName + {} ); + }, +]; - const result = { - projects: { - [projectRoot]: { - root: projectRoot, - targets: targetsCache[hash], - }, +async function createNodesInternal( + configFilePath: string, + options: Required, + context: CreateNodesContext, + targetsCache: Record> +) { + let projectRoot = ''; + if (configFilePath.includes('/.storybook')) { + projectRoot = dirname(configFilePath).replace('/.storybook', ''); + } else { + projectRoot = dirname(configFilePath).replace('.storybook', ''); + } + + if (projectRoot === '') { + projectRoot = '.'; + } + + // Do not create a project if package.json and project.json isn't there. + const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); + if ( + !siblingFiles.includes('package.json') && + !siblingFiles.includes('project.json') + ) { + return {}; + } + + const hash = await calculateHashForCreateNodes( + projectRoot, + options, + context, + [getLockFileName(detectPackageManager(context.workspaceRoot))] + ); + + const projectName = buildProjectName(projectRoot, context.workspaceRoot); + + targetsCache[hash] ??= await buildStorybookTargets( + configFilePath, + projectRoot, + options, + context, + projectName + ); + + const result = { + projects: { + [projectRoot]: { + root: projectRoot, + targets: targetsCache[hash], }, - }; + }, + }; - return result; - }, -]; + return result; +} async function buildStorybookTargets( configFilePath: string, @@ -287,13 +339,16 @@ function getOutputs(): string[] { function normalizeOptions( options: StorybookPluginOptions -): StorybookPluginOptions { - options ??= {}; - options.buildStorybookTargetName ??= 'build-storybook'; - options.serveStorybookTargetName ??= 'storybook'; - options.testStorybookTargetName ??= 'test-storybook'; - options.staticStorybookTargetName ??= 'static-storybook'; - return options; +): Required { + return { + buildStorybookTargetName: + options.buildStorybookTargetName ?? 'build-storybook', + serveStorybookTargetName: options.serveStorybookTargetName ?? 'storybook', + testStorybookTargetName: + options.testStorybookTargetName ?? 'test-storybook', + staticStorybookTargetName: + options.staticStorybookTargetName ?? 'static-storybook', + }; } function buildProjectName(