feat(storybook): use createNodesV2 for init and convert-to-inferred generators (#28092)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
createNodesV1 is used during init

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
createNodesV2 is used during init

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Based on PR #28091
This commit is contained in:
Phillip Barta 2024-12-10 18:41:23 +01:00 committed by GitHub
parent cfb67cf124
commit 098d8a64a1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 295 additions and 170 deletions

View File

@ -1,5 +1,6 @@
export { export {
createNodes, createNodes,
createNodesV2,
StorybookPluginOptions, StorybookPluginOptions,
createDependencies, createDependencies,
} from './src/plugins/plugin'; } from './src/plugins/plugin';

View File

@ -7,12 +7,12 @@ import {
} from '@nx/devkit'; } from '@nx/devkit';
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
import { import {
migrateProjectExecutorsToPluginV1, migrateProjectExecutorsToPlugin,
NoTargetsToMigrateError, NoTargetsToMigrateError,
} from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; import { buildPostTargetTransformer } from './lib/build-post-target-transformer';
import { servePostTargetTransformer } from './lib/serve-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'; import { storybookVersion } from '../../utils/versions';
interface Schema { interface Schema {
@ -23,11 +23,11 @@ interface Schema {
export async function convertToInferred(tree: Tree, options: Schema) { export async function convertToInferred(tree: Tree, options: Schema) {
const projectGraph = await createProjectGraphAsync(); const projectGraph = await createProjectGraphAsync();
const migrationLogs = new AggregatedLog(); const migrationLogs = new AggregatedLog();
const migratedProjects = await migrateProjectExecutorsToPluginV1( const migratedProjects = await migrateProjectExecutorsToPlugin(
tree, tree,
projectGraph, projectGraph,
'@nx/storybook/plugin', '@nx/storybook/plugin',
createNodes, createNodesV2,
{ {
buildStorybookTargetName: 'build-storybook', buildStorybookTargetName: 'build-storybook',
serveStorybookTargetName: 'storybook', serveStorybookTargetName: 'storybook',

View File

@ -10,9 +10,9 @@ import {
updateJson, updateJson,
updateNxJson, updateNxJson,
} from '@nx/devkit'; } 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 { gte } from 'semver';
import { createNodes } from '../../plugins/plugin'; import { createNodesV2 } from '../../plugins/plugin';
import { import {
getInstalledStorybookVersion, getInstalledStorybookVersion,
storybookMajorVersion, storybookMajorVersion,
@ -99,11 +99,11 @@ export async function initGeneratorInternal(tree: Tree, schema: Schema) {
schema.addPlugin ??= addPluginDefault; schema.addPlugin ??= addPluginDefault;
if (schema.addPlugin) { if (schema.addPlugin) {
await addPluginV1( await addPlugin(
tree, tree,
await createProjectGraphAsync(), await createProjectGraphAsync(),
'@nx/storybook/plugin', '@nx/storybook/plugin',
createNodes, createNodesV2,
{ {
serveStorybookTargetName: [ serveStorybookTargetName: [
'storybook', 'storybook',

View File

@ -2,10 +2,10 @@ import { CreateNodesContext } from '@nx/devkit';
import { TempFs } from '@nx/devkit/internal-testing-utils'; import { TempFs } from '@nx/devkit/internal-testing-utils';
import type { StorybookConfig } from '@storybook/types'; import type { StorybookConfig } from '@storybook/types';
import { join } from 'node:path'; import { join } from 'node:path';
import { createNodes } from './plugin'; import { createNodesV2 } from './plugin';
describe('@nx/storybook/plugin', () => { describe('@nx/storybook/plugin', () => {
let createNodesFunction = createNodes[1]; let createNodesFunction = createNodesV2[1];
let context: CreateNodesContext; let context: CreateNodesContext;
let tempFs: TempFs; let tempFs: TempFs;
@ -54,7 +54,7 @@ describe('@nx/storybook/plugin', () => {
}); });
const nodes = await createNodesFunction( const nodes = await createNodesFunction(
'my-app/.storybook/main.ts', ['my-app/.storybook/main.ts'],
{ {
buildStorybookTargetName: 'build-storybook', buildStorybookTargetName: 'build-storybook',
staticStorybookTargetName: 'static-storybook', staticStorybookTargetName: 'static-storybook',
@ -64,32 +64,57 @@ describe('@nx/storybook/plugin', () => {
context context
); );
expect(nodes?.['projects']?.['my-app']?.targets).toBeDefined(); expect(nodes).toMatchInlineSnapshot(`
expect( [
nodes?.['projects']?.['my-app']?.targets?.['build-storybook'] [
).toMatchObject({ "my-app/.storybook/main.ts",
command: 'storybook build', {
options: { "projects": {
cwd: 'my-app', "my-app": {
}, "root": "my-app",
cache: true, "targets": {
outputs: [ "build-storybook": {
'{projectRoot}/storybook-static', "cache": true,
'{options.output-dir}', "command": "storybook build",
'{options.outputDir}', "inputs": [
'{options.o}', "production",
], "^production",
inputs: [ {
'production', "externalDependencies": [
'^production', "storybook",
{ externalDependencies: ['storybook'] }, ],
], },
}); ],
expect( "options": {
nodes?.['projects']?.['my-app']?.targets?.['serve-storybook'] "cwd": "my-app",
).toMatchObject({ },
command: 'storybook dev', "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 () => { it('should create angular nodes', async () => {
@ -104,7 +129,7 @@ describe('@nx/storybook/plugin', () => {
}); });
const nodes = await createNodesFunction( const nodes = await createNodesFunction(
'my-ng-app/.storybook/main.ts', ['my-ng-app/.storybook/main.ts'],
{ {
buildStorybookTargetName: 'build-storybook', buildStorybookTargetName: 'build-storybook',
staticStorybookTargetName: 'static-storybook', staticStorybookTargetName: 'static-storybook',
@ -114,42 +139,63 @@ describe('@nx/storybook/plugin', () => {
context context
); );
expect(nodes?.['projects']?.['my-ng-app']?.targets).toBeDefined(); expect(nodes).toMatchInlineSnapshot(`
expect( [
nodes?.['projects']?.['my-ng-app']?.targets?.['build-storybook'] [
).toMatchObject({ "my-ng-app/.storybook/main.ts",
executor: '@storybook/angular:build-storybook', {
options: { "projects": {
outputDir: 'my-ng-app/storybook-static', "my-ng-app": {
configDir: 'my-ng-app/.storybook', "root": "my-ng-app",
browserTarget: 'my-ng-app:build-storybook', "targets": {
compodoc: false, "build-storybook": {
}, "cache": true,
cache: true, "executor": "@storybook/angular:build-storybook",
outputs: [ "inputs": [
'{projectRoot}/storybook-static', "production",
'{options.output-dir}', "^production",
'{options.outputDir}', {
'{options.o}', "externalDependencies": [
], "storybook",
inputs: [ "@storybook/angular",
'production', ],
'^production', },
{ ],
externalDependencies: ['storybook', '@storybook/angular'], "options": {
}, "browserTarget": "my-ng-app:build-storybook",
], "compodoc": false,
}); "configDir": "my-ng-app/.storybook",
expect( "outputDir": "my-ng-app/storybook-static",
nodes?.['projects']?.['my-ng-app']?.targets?.['serve-storybook'] },
).toMatchObject({ "outputs": [
executor: '@storybook/angular:start-storybook', "{projectRoot}/storybook-static",
options: { "{options.output-dir}",
browserTarget: 'my-ng-app:build-storybook', "{options.outputDir}",
configDir: 'my-ng-app/.storybook', "{options.o}",
compodoc: false, ],
}, },
}); "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 () => { it('should support main.js', async () => {
@ -168,7 +214,7 @@ describe('@nx/storybook/plugin', () => {
}); });
const nodes = await createNodesFunction( const nodes = await createNodesFunction(
'my-react-lib/.storybook/main.js', ['my-react-lib/.storybook/main.js'],
{ {
buildStorybookTargetName: 'build-storybook', buildStorybookTargetName: 'build-storybook',
staticStorybookTargetName: 'static-storybook', staticStorybookTargetName: 'static-storybook',
@ -178,32 +224,57 @@ describe('@nx/storybook/plugin', () => {
context context
); );
expect(nodes?.['projects']?.['my-react-lib']?.targets).toBeDefined(); expect(nodes).toMatchInlineSnapshot(`
expect( [
nodes?.['projects']?.['my-react-lib']?.targets?.['build-storybook'] [
).toMatchObject({ "my-react-lib/.storybook/main.js",
command: 'storybook build', {
options: { "projects": {
cwd: 'my-react-lib', "my-react-lib": {
}, "root": "my-react-lib",
cache: true, "targets": {
outputs: [ "build-storybook": {
'{projectRoot}/storybook-static', "cache": true,
'{options.output-dir}', "command": "storybook build",
'{options.outputDir}', "inputs": [
'{options.o}', "production",
], "^production",
inputs: [ {
'production', "externalDependencies": [
'^production', "storybook",
{ externalDependencies: ['storybook'] }, ],
], },
}); ],
expect( "options": {
nodes?.['projects']?.['my-react-lib']?.targets?.['serve-storybook'] "cwd": "my-react-lib",
).toMatchObject({ },
command: 'storybook dev', "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( function mockStorybookMainConfig(

View File

@ -2,8 +2,12 @@ import {
CreateDependencies, CreateDependencies,
CreateNodes, CreateNodes,
CreateNodesContext, CreateNodesContext,
createNodesFromFiles,
CreateNodesV2,
detectPackageManager, detectPackageManager,
getPackageManagerCommand,
joinPathFragments, joinPathFragments,
logger,
parseJson, parseJson,
readJsonFile, readJsonFile,
TargetConfiguration, TargetConfiguration,
@ -17,6 +21,7 @@ import { workspaceDataDirectory } from 'nx/src/utils/cache-directory';
import { getLockFileName } from '@nx/js'; import { getLockFileName } from '@nx/js';
import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; import { loadConfigFile } from '@nx/devkit/src/utils/config-utils';
import type { StorybookConfig } from '@storybook/types'; import type { StorybookConfig } from '@storybook/types';
import { hashObject } from 'nx/src/hasher/file-hasher';
export interface StorybookPluginOptions { export interface StorybookPluginOptions {
buildStorybookTargetName?: string; buildStorybookTargetName?: string;
@ -25,83 +30,128 @@ export interface StorybookPluginOptions {
testStorybookTargetName?: string; testStorybookTargetName?: string;
} }
const cachePath = join(workspaceDataDirectory, 'storybook.hash'); function readTargetsCache(
const targetsCache = readTargetsCache(); cachePath: string
): Record<string, Record<string, TargetConfiguration>> {
function readTargetsCache(): Record<
string,
Record<string, TargetConfiguration>
> {
return existsSync(cachePath) ? readJsonFile(cachePath) : {}; return existsSync(cachePath) ? readJsonFile(cachePath) : {};
} }
function writeTargetsToCache() { function writeTargetsToCache(
const oldCache = readTargetsCache(); cachePath: string,
writeJsonFile(cachePath, { results: Record<string, Record<string, TargetConfiguration>>
...oldCache, ) {
...targetsCache, writeJsonFile(cachePath, results);
});
} }
/**
* @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'.
*/
export const createDependencies: CreateDependencies = () => { export const createDependencies: CreateDependencies = () => {
writeTargetsToCache();
return []; return [];
}; };
export const createNodes: CreateNodes<StorybookPluginOptions> = [ const storybookConfigGlob = '**/.storybook/main.{js,ts,mjs,mts,cjs,cts}';
'**/.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 === '') { export const createNodesV2: CreateNodesV2<StorybookPluginOptions> = [
projectRoot = '.'; storybookConfigGlob,
} async (configFilePaths, options, context) => {
const normalizedOptions = normalizeOptions(options);
// Do not create a project if package.json and project.json isn't there. const optionsHash = hashObject(normalizedOptions);
const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); const cachePath = join(
if ( workspaceDataDirectory,
!siblingFiles.includes('package.json') && `storybook-${optionsHash}.hash`
!siblingFiles.includes('project.json')
) {
return {};
}
options = normalizeOptions(options);
const hash = await calculateHashForCreateNodes(
projectRoot,
options,
context,
[getLockFileName(detectPackageManager(context.workspaceRoot))]
); );
const targetsCache = readTargetsCache(cachePath);
const projectName = buildProjectName(projectRoot, context.workspaceRoot); try {
return await createNodesFromFiles(
targetsCache[hash] ??= await buildStorybookTargets( (configFile, _, context) =>
configFilePath, createNodesInternal(
projectRoot, configFile,
options, normalizedOptions,
context, context,
projectName targetsCache
); ),
configFilePaths,
const result = { normalizedOptions,
projects: { context
[projectRoot]: { );
root: projectRoot, } finally {
targets: targetsCache[hash], writeTargetsToCache(cachePath, targetsCache);
}, }
},
};
return result;
}, },
]; ];
export const createNodes: CreateNodes<StorybookPluginOptions> = [
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.'
);
return createNodesInternal(
configFilePath,
normalizeOptions(options),
context,
{}
);
},
];
async function createNodesInternal(
configFilePath: string,
options: Required<StorybookPluginOptions>,
context: CreateNodesContext,
targetsCache: Record<string, Record<string, TargetConfiguration>>
) {
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;
}
async function buildStorybookTargets( async function buildStorybookTargets(
configFilePath: string, configFilePath: string,
projectRoot: string, projectRoot: string,
@ -294,13 +344,16 @@ function getOutputs(): string[] {
function normalizeOptions( function normalizeOptions(
options: StorybookPluginOptions options: StorybookPluginOptions
): StorybookPluginOptions { ): Required<StorybookPluginOptions> {
options ??= {}; return {
options.buildStorybookTargetName ??= 'build-storybook'; buildStorybookTargetName:
options.serveStorybookTargetName ??= 'storybook'; options.buildStorybookTargetName ?? 'build-storybook',
options.testStorybookTargetName ??= 'test-storybook'; serveStorybookTargetName: options.serveStorybookTargetName ?? 'storybook',
options.staticStorybookTargetName ??= 'static-storybook'; testStorybookTargetName:
return options; options.testStorybookTargetName ?? 'test-storybook',
staticStorybookTargetName:
options.staticStorybookTargetName ?? 'static-storybook',
};
} }
function buildProjectName( function buildProjectName(