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:
parent
cfb67cf124
commit
098d8a64a1
@ -1,5 +1,6 @@
|
||||
export {
|
||||
createNodes,
|
||||
createNodesV2,
|
||||
StorybookPluginOptions,
|
||||
createDependencies,
|
||||
} from './src/plugins/plugin';
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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,83 +30,128 @@ export interface StorybookPluginOptions {
|
||||
testStorybookTargetName?: string;
|
||||
}
|
||||
|
||||
const cachePath = join(workspaceDataDirectory, 'storybook.hash');
|
||||
const targetsCache = readTargetsCache();
|
||||
|
||||
function readTargetsCache(): Record<
|
||||
string,
|
||||
Record<string, TargetConfiguration>
|
||||
> {
|
||||
function readTargetsCache(
|
||||
cachePath: string
|
||||
): Record<string, Record<string, TargetConfiguration>> {
|
||||
return existsSync(cachePath) ? readJsonFile(cachePath) : {};
|
||||
}
|
||||
|
||||
function writeTargetsToCache() {
|
||||
const oldCache = readTargetsCache();
|
||||
writeJsonFile(cachePath, {
|
||||
...oldCache,
|
||||
...targetsCache,
|
||||
});
|
||||
function writeTargetsToCache(
|
||||
cachePath: string,
|
||||
results: Record<string, Record<string, TargetConfiguration>>
|
||||
) {
|
||||
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<StorybookPluginOptions> = [
|
||||
'**/.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 {};
|
||||
}
|
||||
|
||||
options = normalizeOptions(options);
|
||||
const hash = await calculateHashForCreateNodes(
|
||||
projectRoot,
|
||||
options,
|
||||
context,
|
||||
[getLockFileName(detectPackageManager(context.workspaceRoot))]
|
||||
export const createNodesV2: CreateNodesV2<StorybookPluginOptions> = [
|
||||
storybookConfigGlob,
|
||||
async (configFilePaths, options, context) => {
|
||||
const normalizedOptions = normalizeOptions(options);
|
||||
const optionsHash = hashObject(normalizedOptions);
|
||||
const cachePath = join(
|
||||
workspaceDataDirectory,
|
||||
`storybook-${optionsHash}.hash`
|
||||
);
|
||||
const targetsCache = readTargetsCache(cachePath);
|
||||
|
||||
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;
|
||||
try {
|
||||
return await createNodesFromFiles(
|
||||
(configFile, _, context) =>
|
||||
createNodesInternal(
|
||||
configFile,
|
||||
normalizedOptions,
|
||||
context,
|
||||
targetsCache
|
||||
),
|
||||
configFilePaths,
|
||||
normalizedOptions,
|
||||
context
|
||||
);
|
||||
} finally {
|
||||
writeTargetsToCache(cachePath, targetsCache);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
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(
|
||||
configFilePath: string,
|
||||
projectRoot: 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<StorybookPluginOptions> {
|
||||
return {
|
||||
buildStorybookTargetName:
|
||||
options.buildStorybookTargetName ?? 'build-storybook',
|
||||
serveStorybookTargetName: options.serveStorybookTargetName ?? 'storybook',
|
||||
testStorybookTargetName:
|
||||
options.testStorybookTargetName ?? 'test-storybook',
|
||||
staticStorybookTargetName:
|
||||
options.staticStorybookTargetName ?? 'static-storybook',
|
||||
};
|
||||
}
|
||||
|
||||
function buildProjectName(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user