diff --git a/packages/storybook/plugin.ts b/packages/storybook/plugin.ts index 8b8dbdde8ffcd..920c592a84acd 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/generators/convert-to-inferred/convert-to-inferred.ts b/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts index 161b579cb245f..e0dda2a19e6f4 100644 --- a/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -7,12 +7,12 @@ import { } from '@nx/devkit'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; import { - migrateProjectExecutorsToPluginV1, + migrateProjectExecutorsToPlugin, NoTargetsToMigrateError, } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; import { servePostTargetTransformer } from './lib/serve-post-target-transformer'; -import { createNodes } from '../../plugins/plugin'; +import { createNodesV2 } from '../../plugins/plugin'; import { storybookVersion } from '../../utils/versions'; interface Schema { @@ -23,11 +23,11 @@ interface Schema { export async function convertToInferred(tree: Tree, options: Schema) { const projectGraph = await createProjectGraphAsync(); const migrationLogs = new AggregatedLog(); - const migratedProjects = await migrateProjectExecutorsToPluginV1( + const migratedProjects = await migrateProjectExecutorsToPlugin( tree, projectGraph, '@nx/storybook/plugin', - createNodes, + createNodesV2, { buildStorybookTargetName: 'build-storybook', serveStorybookTargetName: 'storybook', diff --git a/packages/storybook/src/generators/init/init.ts b/packages/storybook/src/generators/init/init.ts index c5ed52fef7eab..88cbff0875a36 100644 --- a/packages/storybook/src/generators/init/init.ts +++ b/packages/storybook/src/generators/init/init.ts @@ -10,9 +10,9 @@ import { updateJson, updateNxJson, } from '@nx/devkit'; -import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin'; +import { addPlugin } from '@nx/devkit/src/utils/add-plugin'; import { gte } from 'semver'; -import { createNodes } from '../../plugins/plugin'; +import { createNodesV2 } from '../../plugins/plugin'; import { getInstalledStorybookVersion, storybookMajorVersion, @@ -99,11 +99,11 @@ export async function initGeneratorInternal(tree: Tree, schema: Schema) { schema.addPlugin ??= addPluginDefault; if (schema.addPlugin) { - await addPluginV1( + await addPlugin( tree, await createProjectGraphAsync(), '@nx/storybook/plugin', - createNodes, + createNodesV2, { serveStorybookTargetName: [ 'storybook', diff --git a/packages/storybook/src/plugins/plugin.spec.ts b/packages/storybook/src/plugins/plugin.spec.ts index a4cce98fe25ec..9729aaa75a7ac 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,32 +64,57 @@ describe('@nx/storybook/plugin', () => { context ); - expect(nodes?.['projects']?.['my-app']?.targets).toBeDefined(); - expect( - nodes?.['projects']?.['my-app']?.targets?.['build-storybook'] - ).toMatchObject({ - command: 'storybook build', - options: { - cwd: 'my-app', - }, - cache: true, - outputs: [ - '{projectRoot}/storybook-static', - '{options.output-dir}', - '{options.outputDir}', - '{options.o}', - ], - inputs: [ - 'production', - '^production', - { externalDependencies: ['storybook'] }, - ], - }); - expect( - nodes?.['projects']?.['my-app']?.targets?.['serve-storybook'] - ).toMatchObject({ - command: 'storybook dev', - }); + expect(nodes).toMatchInlineSnapshot(` + [ + [ + "my-app/.storybook/main.ts", + { + "projects": { + "my-app": { + "root": "my-app", + "targets": { + "build-storybook": { + "cache": true, + "command": "storybook build", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "storybook", + ], + }, + ], + "options": { + "cwd": "my-app", + }, + "outputs": [ + "{projectRoot}/storybook-static", + "{options.output-dir}", + "{options.outputDir}", + "{options.o}", + ], + }, + "serve-storybook": { + "command": "storybook dev", + "options": { + "cwd": "my-app", + }, + }, + "static-storybook": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build-storybook", + "staticFilePath": "my-app/storybook-static", + }, + }, + }, + }, + }, + }, + ], + ] + `); }); it('should create angular nodes', async () => { @@ -104,7 +129,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,42 +139,63 @@ describe('@nx/storybook/plugin', () => { context ); - expect(nodes?.['projects']?.['my-ng-app']?.targets).toBeDefined(); - expect( - nodes?.['projects']?.['my-ng-app']?.targets?.['build-storybook'] - ).toMatchObject({ - executor: '@storybook/angular:build-storybook', - options: { - outputDir: 'my-ng-app/storybook-static', - configDir: 'my-ng-app/.storybook', - browserTarget: 'my-ng-app:build-storybook', - compodoc: false, - }, - cache: true, - outputs: [ - '{projectRoot}/storybook-static', - '{options.output-dir}', - '{options.outputDir}', - '{options.o}', - ], - inputs: [ - 'production', - '^production', - { - externalDependencies: ['storybook', '@storybook/angular'], - }, - ], - }); - expect( - nodes?.['projects']?.['my-ng-app']?.targets?.['serve-storybook'] - ).toMatchObject({ - executor: '@storybook/angular:start-storybook', - options: { - browserTarget: 'my-ng-app:build-storybook', - configDir: 'my-ng-app/.storybook', - compodoc: false, - }, - }); + expect(nodes).toMatchInlineSnapshot(` + [ + [ + "my-ng-app/.storybook/main.ts", + { + "projects": { + "my-ng-app": { + "root": "my-ng-app", + "targets": { + "build-storybook": { + "cache": true, + "executor": "@storybook/angular:build-storybook", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "storybook", + "@storybook/angular", + ], + }, + ], + "options": { + "browserTarget": "my-ng-app:build-storybook", + "compodoc": false, + "configDir": "my-ng-app/.storybook", + "outputDir": "my-ng-app/storybook-static", + }, + "outputs": [ + "{projectRoot}/storybook-static", + "{options.output-dir}", + "{options.outputDir}", + "{options.o}", + ], + }, + "serve-storybook": { + "executor": "@storybook/angular:start-storybook", + "options": { + "browserTarget": "my-ng-app:build-storybook", + "compodoc": false, + "configDir": "my-ng-app/.storybook", + }, + }, + "static-storybook": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build-storybook", + "staticFilePath": "my-ng-app/storybook-static", + }, + }, + }, + }, + }, + }, + ], + ] + `); }); it('should support main.js', async () => { @@ -168,7 +214,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', @@ -178,32 +224,57 @@ describe('@nx/storybook/plugin', () => { context ); - expect(nodes?.['projects']?.['my-react-lib']?.targets).toBeDefined(); - expect( - nodes?.['projects']?.['my-react-lib']?.targets?.['build-storybook'] - ).toMatchObject({ - command: 'storybook build', - options: { - cwd: 'my-react-lib', - }, - cache: true, - outputs: [ - '{projectRoot}/storybook-static', - '{options.output-dir}', - '{options.outputDir}', - '{options.o}', - ], - inputs: [ - 'production', - '^production', - { externalDependencies: ['storybook'] }, - ], - }); - expect( - nodes?.['projects']?.['my-react-lib']?.targets?.['serve-storybook'] - ).toMatchObject({ - command: 'storybook dev', - }); + expect(nodes).toMatchInlineSnapshot(` + [ + [ + "my-react-lib/.storybook/main.js", + { + "projects": { + "my-react-lib": { + "root": "my-react-lib", + "targets": { + "build-storybook": { + "cache": true, + "command": "storybook build", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "storybook", + ], + }, + ], + "options": { + "cwd": "my-react-lib", + }, + "outputs": [ + "{projectRoot}/storybook-static", + "{options.output-dir}", + "{options.outputDir}", + "{options.o}", + ], + }, + "serve-storybook": { + "command": "storybook dev", + "options": { + "cwd": "my-react-lib", + }, + }, + "static-storybook": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build-storybook", + "staticFilePath": "my-react-lib/storybook-static", + }, + }, + }, + }, + }, + }, + ], + ] + `); }); function mockStorybookMainConfig( diff --git a/packages/storybook/src/plugins/plugin.ts b/packages/storybook/src/plugins/plugin.ts index 23cca1a5641d1..e4a03023de9d5 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,7 @@ 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'; export interface StorybookPluginOptions { buildStorybookTargetName?: string; @@ -25,82 +30,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', ''); - } - - if (projectRoot === '') { - projectRoot = '.'; - } +const storybookConfigGlob = '**/.storybook/main.{js,ts,mjs,mts,cjs,cts}'; - // 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, @@ -294,13 +344,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(