feat(storybook): add convert-to-inferred generator (#26595)
- feat(storybook): add convert-to-inferred generator - feat(storybook): add post target transformers - feat(storybook): convert-to-inferred handles kebab case flags <!-- 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 --> ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
b1dbf47aa2
commit
18fdd9425b
@ -9485,6 +9485,14 @@
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"id": "convert-to-inferred",
|
||||
"path": "/nx-api/storybook/generators/convert-to-inferred",
|
||||
"name": "convert-to-inferred",
|
||||
"children": [],
|
||||
"isExternal": false,
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"id": "migrate-7",
|
||||
"path": "/nx-api/storybook/generators/migrate-7",
|
||||
|
||||
@ -2867,6 +2867,15 @@
|
||||
"path": "/nx-api/storybook/generators/cypress-project",
|
||||
"type": "generator"
|
||||
},
|
||||
"/nx-api/storybook/generators/convert-to-inferred": {
|
||||
"description": "Convert existing Storybook project(s) using `@nx/storybook:*` executors to use `@nx/storybook/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||
"file": "generated/packages/storybook/generators/convert-to-inferred.json",
|
||||
"hidden": false,
|
||||
"name": "convert-to-inferred",
|
||||
"originalFilePath": "/packages/storybook/src/generators/convert-to-inferred/schema.json",
|
||||
"path": "/nx-api/storybook/generators/convert-to-inferred",
|
||||
"type": "generator"
|
||||
},
|
||||
"/nx-api/storybook/generators/migrate-7": {
|
||||
"description": "Migrate to Storybook version 7.",
|
||||
"file": "generated/packages/storybook/generators/migrate-7.json",
|
||||
|
||||
@ -2837,6 +2837,15 @@
|
||||
"path": "storybook/generators/cypress-project",
|
||||
"type": "generator"
|
||||
},
|
||||
{
|
||||
"description": "Convert existing Storybook project(s) using `@nx/storybook:*` executors to use `@nx/storybook/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||
"file": "generated/packages/storybook/generators/convert-to-inferred.json",
|
||||
"hidden": false,
|
||||
"name": "convert-to-inferred",
|
||||
"originalFilePath": "/packages/storybook/src/generators/convert-to-inferred/schema.json",
|
||||
"path": "storybook/generators/convert-to-inferred",
|
||||
"type": "generator"
|
||||
},
|
||||
{
|
||||
"description": "Migrate to Storybook version 7.",
|
||||
"file": "generated/packages/storybook/generators/migrate-7.json",
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "convert-to-inferred",
|
||||
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
|
||||
"schema": {
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxStorybookConvertToInferred",
|
||||
"description": "Convert existing Storybook project(s) using `@nx/storybook:*` executors to use `@nx/storybook/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||
"title": "Convert Storybook project from executor to plugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project to convert from using the `@nx/storybook:*` executors to use `@nx/storybook/plugin`.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to format files at the end of the migration.",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"presets": []
|
||||
},
|
||||
"description": "Convert existing Storybook project(s) using `@nx/storybook:*` executors to use `@nx/storybook/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||
"implementation": "/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts",
|
||||
"aliases": [],
|
||||
"hidden": false,
|
||||
"path": "/packages/storybook/src/generators/convert-to-inferred/schema.json",
|
||||
"type": "generator"
|
||||
}
|
||||
@ -663,6 +663,7 @@
|
||||
- [init](/nx-api/storybook/generators/init)
|
||||
- [configuration](/nx-api/storybook/generators/configuration)
|
||||
- [cypress-project](/nx-api/storybook/generators/cypress-project)
|
||||
- [convert-to-inferred](/nx-api/storybook/generators/convert-to-inferred)
|
||||
- [migrate-7](/nx-api/storybook/generators/migrate-7)
|
||||
- [tao](/nx-api/tao)
|
||||
- [vite](/nx-api/vite)
|
||||
|
||||
@ -22,6 +22,11 @@
|
||||
"x-deprecated": "Deprecated: Use 'interactionTests' instead when running '@nx/storybook:configuration'. This generator will be removed in v21.",
|
||||
"hidden": false
|
||||
},
|
||||
"convert-to-inferred": {
|
||||
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
|
||||
"schema": "./src/generators/convert-to-inferred/schema.json",
|
||||
"description": "Convert existing Storybook project(s) using `@nx/storybook:*` executors to use `@nx/storybook/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target."
|
||||
},
|
||||
"migrate-7": {
|
||||
"factory": "./src/generators/migrate-7/migrate-7",
|
||||
"schema": "./src/generators/migrate-7/schema.json",
|
||||
|
||||
@ -0,0 +1,631 @@
|
||||
import {
|
||||
type ProjectGraph,
|
||||
type Tree,
|
||||
type ProjectConfiguration,
|
||||
joinPathFragments,
|
||||
writeJson,
|
||||
addProjectConfiguration,
|
||||
readProjectConfiguration,
|
||||
readNxJson,
|
||||
type ExpandedPluginConfiguration,
|
||||
updateNxJson,
|
||||
} from '@nx/devkit';
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { TempFs } from '@nx/devkit/internal-testing-utils';
|
||||
import { getRelativeProjectJsonSchemaPath } from 'nx/src/generators/utils/project-configuration';
|
||||
import { join } from 'path';
|
||||
import { convertToInferred } from './convert-to-inferred';
|
||||
|
||||
let fs: TempFs;
|
||||
let projectGraph: ProjectGraph;
|
||||
|
||||
jest.mock('@nx/devkit', () => ({
|
||||
...jest.requireActual('@nx/devkit'),
|
||||
createProjectGraphAsync: jest
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(projectGraph)),
|
||||
updateProjectConfiguration: jest
|
||||
.fn()
|
||||
.mockImplementation((tree, projectName, projectConfiguration) => {
|
||||
function handleEmptyTargets(
|
||||
projectName: string,
|
||||
projectConfiguration: ProjectConfiguration
|
||||
): void {
|
||||
if (
|
||||
projectConfiguration.targets &&
|
||||
!Object.keys(projectConfiguration.targets).length
|
||||
) {
|
||||
// Re-order `targets` to appear after the `// target` comment.
|
||||
delete projectConfiguration.targets;
|
||||
projectConfiguration[
|
||||
'// targets'
|
||||
] = `to see all targets run: nx show project ${projectName} --web`;
|
||||
projectConfiguration.targets = {};
|
||||
} else {
|
||||
delete projectConfiguration['// targets'];
|
||||
}
|
||||
}
|
||||
|
||||
const projectConfigFile = joinPathFragments(
|
||||
projectConfiguration.root,
|
||||
'project.json'
|
||||
);
|
||||
|
||||
if (!tree.exists(projectConfigFile)) {
|
||||
throw new Error(
|
||||
`Cannot update Project ${projectName} at ${projectConfiguration.root}. It either doesn't exist yet, or may not use project.json for configuration. Use \`addProjectConfiguration()\` instead if you want to create a new project.`
|
||||
);
|
||||
}
|
||||
handleEmptyTargets(projectName, projectConfiguration);
|
||||
writeJson(tree, projectConfigFile, {
|
||||
name: projectConfiguration.name ?? projectName,
|
||||
$schema: getRelativeProjectJsonSchemaPath(tree, projectConfiguration),
|
||||
...projectConfiguration,
|
||||
root: undefined,
|
||||
});
|
||||
projectGraph.nodes[projectName].data = projectConfiguration;
|
||||
}),
|
||||
}));
|
||||
|
||||
function addProject(tree: Tree, name: string, project: ProjectConfiguration) {
|
||||
addProjectConfiguration(tree, name, project);
|
||||
projectGraph.nodes[name] = {
|
||||
name,
|
||||
type: project.projectType === 'application' ? 'app' : 'lib',
|
||||
data: {
|
||||
projectType: project.projectType,
|
||||
root: project.root,
|
||||
targets: project.targets,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
interface TestProjectOptions {
|
||||
appName: string;
|
||||
appRoot: string;
|
||||
configDir: string;
|
||||
buildTargetName: string;
|
||||
serveTargetName: string;
|
||||
}
|
||||
|
||||
const defaultTestProjectOptions: TestProjectOptions = {
|
||||
appName: 'app1',
|
||||
appRoot: 'apps/app1',
|
||||
configDir: '.storybook',
|
||||
buildTargetName: 'build-storybook',
|
||||
serveTargetName: 'storybook',
|
||||
};
|
||||
|
||||
function writeStorybookConfig(
|
||||
tree: Tree,
|
||||
projectRoot: string,
|
||||
useVite: boolean = false
|
||||
) {
|
||||
const storybookConfig = {
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: {
|
||||
name: useVite ? '@storybook/react-vite' : '@storybook/react-webpack5',
|
||||
options: useVite
|
||||
? {
|
||||
builder: {
|
||||
viteConfigPath: `${projectRoot}/vite.config.ts`,
|
||||
},
|
||||
}
|
||||
: {},
|
||||
},
|
||||
};
|
||||
const storybookConfigContents = `const config = ${JSON.stringify(
|
||||
storybookConfig
|
||||
)};
|
||||
export default config;`;
|
||||
|
||||
if (useVite) {
|
||||
tree.write(`${projectRoot}/vite.config.ts`, `module.exports = {}`);
|
||||
fs.createFileSync(`${projectRoot}/vite.config.ts`, `module.exports = {}`);
|
||||
}
|
||||
|
||||
tree.write(`${projectRoot}/.storybook/main.ts`, storybookConfigContents);
|
||||
fs.createFileSync(
|
||||
`${projectRoot}/.storybook/main.ts`,
|
||||
storybookConfigContents
|
||||
);
|
||||
jest.doMock(
|
||||
join(fs.tempDir, projectRoot, '.storybook', 'main.ts'),
|
||||
() => storybookConfig,
|
||||
{ virtual: true }
|
||||
);
|
||||
}
|
||||
|
||||
function createTestProject(
|
||||
tree: Tree,
|
||||
opts: Partial<TestProjectOptions> = defaultTestProjectOptions,
|
||||
extraTargetOptions: any = {},
|
||||
extraConfigurations: any = {},
|
||||
useVite = false
|
||||
) {
|
||||
let projectOpts = { ...defaultTestProjectOptions, ...opts };
|
||||
const project: ProjectConfiguration = {
|
||||
name: projectOpts.appName,
|
||||
root: projectOpts.appRoot,
|
||||
projectType: 'application',
|
||||
targets: {
|
||||
[projectOpts.buildTargetName]: {
|
||||
executor: '@nx/storybook:build',
|
||||
outputs: ['{options.outputDir}'],
|
||||
options: {
|
||||
configDir: `${projectOpts.appRoot}/${projectOpts.configDir}`,
|
||||
outputDir: `dist/storybook/${projectOpts.appRoot}`,
|
||||
...extraTargetOptions,
|
||||
},
|
||||
configurations: {
|
||||
...extraConfigurations,
|
||||
},
|
||||
},
|
||||
[projectOpts.serveTargetName]: {
|
||||
executor: '@nx/storybook:storybook',
|
||||
options: {
|
||||
port: 4400,
|
||||
configDir: `${projectOpts.appRoot}/${projectOpts.configDir}`,
|
||||
...extraTargetOptions,
|
||||
},
|
||||
configurations: {
|
||||
ci: {
|
||||
quiet: true,
|
||||
},
|
||||
...extraConfigurations,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
writeStorybookConfig(tree, project.root, useVite);
|
||||
|
||||
addProject(tree, project.name, project);
|
||||
fs.createFileSync(
|
||||
`${projectOpts.appRoot}/project.json`,
|
||||
JSON.stringify(project)
|
||||
);
|
||||
return project;
|
||||
}
|
||||
|
||||
describe('Storybook - Convert To Inferred', () => {
|
||||
let tree: Tree;
|
||||
beforeEach(() => {
|
||||
fs = new TempFs('storybook');
|
||||
tree = createTreeWithEmptyWorkspace();
|
||||
tree.root = fs.tempDir;
|
||||
|
||||
projectGraph = {
|
||||
nodes: {},
|
||||
dependencies: {},
|
||||
externalNodes: {},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.cleanup();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
describe('--project', () => {
|
||||
it('should correctly migrate a single project', async () => {
|
||||
// ARRANGE
|
||||
const project = createTestProject(tree);
|
||||
const project2 = createTestProject(tree, {
|
||||
appRoot: 'apps/project2',
|
||||
appName: 'project2',
|
||||
});
|
||||
|
||||
const project2Targets = project2.targets;
|
||||
|
||||
// ACT
|
||||
await convertToInferred(tree, {
|
||||
project: project.name,
|
||||
skipFormat: true,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||
expect(updatedProject.targets).toMatchInlineSnapshot(`
|
||||
{
|
||||
"build-storybook": {
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/apps/app1",
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/{options.outputDir}",
|
||||
"{workspaceRoot}/{projectRoot}/storybook-static",
|
||||
"{options.output-dir}",
|
||||
"{options.outputDir}",
|
||||
"{options.o}",
|
||||
],
|
||||
},
|
||||
"storybook": {
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"args": [
|
||||
"--quiet",
|
||||
],
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"port": 4400,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const updatedProject2 = readProjectConfiguration(tree, project2.name);
|
||||
expect(updatedProject2.targets).toStrictEqual(project2Targets);
|
||||
|
||||
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||
const storybookPlugin = nxJsonPlugins.find(
|
||||
(plugin): plugin is ExpandedPluginConfiguration =>
|
||||
typeof plugin !== 'string' &&
|
||||
plugin.plugin === '@nx/storybook/plugin' &&
|
||||
plugin.include?.length === 1
|
||||
);
|
||||
expect(storybookPlugin).toBeTruthy();
|
||||
expect(storybookPlugin.include).toEqual([`${project.root}/**/*`]);
|
||||
});
|
||||
|
||||
it('should add a new plugin registration when the target name differs', async () => {
|
||||
// ARRANGE
|
||||
const project = createTestProject(tree);
|
||||
const project2 = createTestProject(tree, {
|
||||
appRoot: 'apps/project2',
|
||||
appName: 'project2',
|
||||
});
|
||||
|
||||
const project2Targets = project2.targets;
|
||||
|
||||
const nxJson = readNxJson(tree);
|
||||
nxJson.plugins ??= [];
|
||||
nxJson.plugins.push({
|
||||
plugin: '@nx/storybook/plugin',
|
||||
options: {
|
||||
buildTargetName: 'storybook-build',
|
||||
serveTargetName: defaultTestProjectOptions.serveTargetName,
|
||||
staticStorybookTargetName: 'static-storybook',
|
||||
testStorybookTargetName: 'test-storybook',
|
||||
},
|
||||
});
|
||||
updateNxJson(tree, nxJson);
|
||||
|
||||
// ACT
|
||||
await convertToInferred(tree, {
|
||||
project: project.name,
|
||||
skipFormat: true,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||
expect(updatedProject.targets).toMatchInlineSnapshot(`
|
||||
{
|
||||
"build-storybook": {
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/apps/app1",
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/{options.outputDir}",
|
||||
"{workspaceRoot}/{projectRoot}/storybook-static",
|
||||
"{options.output-dir}",
|
||||
"{options.outputDir}",
|
||||
"{options.o}",
|
||||
],
|
||||
},
|
||||
"storybook": {
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"args": [
|
||||
"--quiet",
|
||||
],
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"port": 4400,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const updatedProject2 = readProjectConfiguration(tree, project2.name);
|
||||
expect(updatedProject2.targets).toStrictEqual(project2Targets);
|
||||
|
||||
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||
const storybookPluginRegistrations = nxJsonPlugins.filter(
|
||||
(plugin): plugin is ExpandedPluginConfiguration =>
|
||||
typeof plugin !== 'string' && plugin.plugin === '@nx/storybook/plugin'
|
||||
);
|
||||
expect(storybookPluginRegistrations.length).toBe(2);
|
||||
expect(storybookPluginRegistrations[1].include).toMatchInlineSnapshot(`
|
||||
[
|
||||
"apps/app1/**/*",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should merge target defaults', async () => {
|
||||
// ARRANGE
|
||||
const project = createTestProject(tree);
|
||||
const project2 = createTestProject(tree, {
|
||||
appRoot: 'apps/project2',
|
||||
appName: 'project2',
|
||||
});
|
||||
|
||||
const project2Targets = project2.targets;
|
||||
|
||||
const nxJson = readNxJson(tree);
|
||||
nxJson.targetDefaults ??= {};
|
||||
nxJson.targetDefaults['@nx/storybook:build'] = {
|
||||
options: {
|
||||
webpackStatsJson: true,
|
||||
},
|
||||
};
|
||||
updateNxJson(tree, nxJson);
|
||||
|
||||
// ACT
|
||||
await convertToInferred(tree, {
|
||||
project: project.name,
|
||||
skipFormat: true,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||
expect(updatedProject.targets).toMatchInlineSnapshot(`
|
||||
{
|
||||
"build-storybook": {
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/apps/app1",
|
||||
"webpack-stats-json": true,
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/{options.outputDir}",
|
||||
"{workspaceRoot}/{projectRoot}/storybook-static",
|
||||
"{options.output-dir}",
|
||||
"{options.outputDir}",
|
||||
"{options.o}",
|
||||
],
|
||||
},
|
||||
"storybook": {
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"args": [
|
||||
"--quiet",
|
||||
],
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"port": 4400,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const updatedProject2 = readProjectConfiguration(tree, project2.name);
|
||||
expect(updatedProject2.targets).toStrictEqual(project2Targets);
|
||||
});
|
||||
|
||||
it('should manage configurations correctly', async () => {
|
||||
// ARRANGE
|
||||
const project = createTestProject(tree, undefined, undefined, {
|
||||
dev: {
|
||||
docsMode: true,
|
||||
},
|
||||
});
|
||||
const project2 = createTestProject(tree, {
|
||||
appRoot: 'apps/project2',
|
||||
appName: 'project2',
|
||||
});
|
||||
|
||||
const project2Targets = project2.targets;
|
||||
// ACT
|
||||
await convertToInferred(tree, {
|
||||
project: project.name,
|
||||
skipFormat: true,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||
expect(updatedProject.targets).toMatchInlineSnapshot(`
|
||||
{
|
||||
"build-storybook": {
|
||||
"configurations": {
|
||||
"dev": {},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/apps/app1",
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/{options.outputDir}",
|
||||
"{workspaceRoot}/{projectRoot}/storybook-static",
|
||||
"{options.output-dir}",
|
||||
"{options.outputDir}",
|
||||
"{options.o}",
|
||||
],
|
||||
},
|
||||
"storybook": {
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"args": [
|
||||
"--quiet",
|
||||
],
|
||||
},
|
||||
"dev": {
|
||||
"docs": true,
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"port": 4400,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const updatedProject2 = readProjectConfiguration(tree, project2.name);
|
||||
expect(updatedProject2.targets).toStrictEqual(project2Targets);
|
||||
|
||||
const storybookConfigContents = tree.read(
|
||||
`${project.root}/.storybook/main.ts`,
|
||||
'utf-8'
|
||||
);
|
||||
expect(storybookConfigContents).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = {"default":{},"dev":{"docsMode":true}};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}
|
||||
const config = {docs: { docsMode: options.docsMode },"stories":["../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)"],"addons":["@storybook/addon-essentials","@storybook/addon-interactions"],"framework":{"name":"@storybook/react-webpack5","options":{}}};
|
||||
export default config;"
|
||||
`);
|
||||
});
|
||||
|
||||
it('should update vite config file', async () => {
|
||||
// ARRANGE
|
||||
const project = createTestProject(
|
||||
tree,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
const project2 = createTestProject(tree, {
|
||||
appRoot: 'apps/project2',
|
||||
appName: 'project2',
|
||||
});
|
||||
|
||||
const project2Targets = project2.targets;
|
||||
// ACT
|
||||
await convertToInferred(tree, {
|
||||
project: project.name,
|
||||
skipFormat: true,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
const storybookConfigContents = tree.read(
|
||||
`${project.root}/.storybook/main.ts`,
|
||||
'utf-8'
|
||||
);
|
||||
expect(storybookConfigContents).toMatchInlineSnapshot(`
|
||||
"
|
||||
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = {"default":{}};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}
|
||||
const config = {"stories":["../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)"],"addons":["@storybook/addon-essentials","@storybook/addon-interactions"],"framework":{"name":"@storybook/react-vite","options":{"builder":{"viteConfigPath":"./vite.config.ts"}}}};
|
||||
export default config;"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('all projects', () => {
|
||||
it('should correctly migrate all projects', async () => {
|
||||
// ARRANGE
|
||||
const project = createTestProject(tree);
|
||||
const project2 = createTestProject(tree, {
|
||||
appRoot: 'apps/project2',
|
||||
appName: 'project2',
|
||||
});
|
||||
// ACT
|
||||
await convertToInferred(tree, {
|
||||
skipFormat: true,
|
||||
});
|
||||
|
||||
// ASSERT
|
||||
const updatedProject = readProjectConfiguration(tree, project.name);
|
||||
expect(updatedProject.targets).toMatchInlineSnapshot(`
|
||||
{
|
||||
"build-storybook": {
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/apps/app1",
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/{options.outputDir}",
|
||||
"{workspaceRoot}/{projectRoot}/storybook-static",
|
||||
"{options.output-dir}",
|
||||
"{options.outputDir}",
|
||||
"{options.o}",
|
||||
],
|
||||
},
|
||||
"storybook": {
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"args": [
|
||||
"--quiet",
|
||||
],
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"port": 4400,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const updatedProject2 = readProjectConfiguration(tree, project2.name);
|
||||
expect(updatedProject2.targets).toMatchInlineSnapshot(`
|
||||
{
|
||||
"build-storybook": {
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/apps/project2",
|
||||
},
|
||||
"outputs": [
|
||||
"{projectRoot}/{options.outputDir}",
|
||||
"{workspaceRoot}/{projectRoot}/storybook-static",
|
||||
"{options.output-dir}",
|
||||
"{options.outputDir}",
|
||||
"{options.o}",
|
||||
],
|
||||
},
|
||||
"storybook": {
|
||||
"configurations": {
|
||||
"ci": {
|
||||
"args": [
|
||||
"--quiet",
|
||||
],
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"port": 4400,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
||||
const nxJsonPlugins = readNxJson(tree).plugins;
|
||||
const storybookPlugin = nxJsonPlugins.find(
|
||||
(plugin): plugin is ExpandedPluginConfiguration =>
|
||||
typeof plugin !== 'string' && plugin.plugin === '@nx/storybook/plugin'
|
||||
);
|
||||
expect(storybookPlugin).toBeTruthy();
|
||||
expect(storybookPlugin.include).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,76 @@
|
||||
import {
|
||||
addDependenciesToPackageJson,
|
||||
createProjectGraphAsync,
|
||||
formatFiles,
|
||||
runTasksInSerial,
|
||||
type Tree,
|
||||
} from '@nx/devkit';
|
||||
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
|
||||
import { migrateExecutorToPluginV1 } 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 { storybookVersion } from '../../utils/versions';
|
||||
|
||||
interface Schema {
|
||||
project?: string;
|
||||
skipFormat?: boolean;
|
||||
}
|
||||
|
||||
export async function convertToInferred(tree: Tree, options: Schema) {
|
||||
const projectGraph = await createProjectGraphAsync();
|
||||
const migrationLogs = new AggregatedLog();
|
||||
const migratedBuildProjects = await migrateExecutorToPluginV1(
|
||||
tree,
|
||||
projectGraph,
|
||||
'@nx/storybook:build',
|
||||
'@nx/storybook/plugin',
|
||||
(targetName) => ({
|
||||
buildStorybookTargetName: targetName,
|
||||
serveStorybookTargetName: 'storybook',
|
||||
staticStorybookTargetName: 'static-storybook',
|
||||
testStorybookTargetName: 'test-storybook',
|
||||
}),
|
||||
buildPostTargetTransformer(migrationLogs),
|
||||
createNodes,
|
||||
options.project
|
||||
);
|
||||
|
||||
const migratedServeProjects = await migrateExecutorToPluginV1(
|
||||
tree,
|
||||
projectGraph,
|
||||
'@nx/storybook:storybook',
|
||||
'@nx/storybook/plugin',
|
||||
(targetName) => ({
|
||||
buildStorybookTargetName: 'build-storybook',
|
||||
serveStorybookTargetName: targetName,
|
||||
staticStorybookTargetName: 'static-storybook',
|
||||
testStorybookTargetName: 'test-storybook',
|
||||
}),
|
||||
servePostTargetTransformer(migrationLogs),
|
||||
createNodes,
|
||||
options.project
|
||||
);
|
||||
|
||||
const migratedProjects =
|
||||
migratedBuildProjects.size + migratedServeProjects.size;
|
||||
if (migratedProjects === 0) {
|
||||
throw new Error('Could not find any targets to migrate.');
|
||||
}
|
||||
|
||||
if (!options.skipFormat) {
|
||||
await formatFiles(tree);
|
||||
}
|
||||
|
||||
const installTask = addDependenciesToPackageJson(
|
||||
tree,
|
||||
{},
|
||||
{ storybook: storybookVersion }
|
||||
);
|
||||
|
||||
return runTasksInSerial(installTask, () => {
|
||||
migrationLogs.flushLogs();
|
||||
});
|
||||
}
|
||||
|
||||
export default convertToInferred;
|
||||
@ -0,0 +1,632 @@
|
||||
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
|
||||
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
|
||||
import { buildPostTargetTransformer } from './build-post-target-transformer';
|
||||
|
||||
describe('buildPostTargetTransformer', () => {
|
||||
describe('--react-vite', () => {
|
||||
it('should migrate docsMode and staticDir to storybook config correctly', () => {
|
||||
// ARRANGE
|
||||
const tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
const targetConfiguration = {
|
||||
outputs: ['{options.outputDir}'],
|
||||
options: {
|
||||
outputDir: 'dist/storybook/myapp',
|
||||
configDir: 'apps/myapp/.storybook',
|
||||
docsMode: true,
|
||||
staticDir: ['assets'],
|
||||
},
|
||||
};
|
||||
|
||||
const inferredTargetConfiguration = {
|
||||
outputs: ['{projectRoot}/{options.outputDir}'],
|
||||
};
|
||||
|
||||
const migrationLogs = new AggregatedLog();
|
||||
|
||||
tree.write(
|
||||
'apps/myapp/.storybook/main.ts',
|
||||
storybookConfigFileV17_ReactVite
|
||||
);
|
||||
|
||||
// ACT
|
||||
const target = buildPostTargetTransformer(migrationLogs)(
|
||||
targetConfiguration,
|
||||
tree,
|
||||
{ projectName: 'myapp', root: 'apps/myapp' },
|
||||
inferredTargetConfiguration
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const configFile = tree.read('apps/myapp/.storybook/main.ts', 'utf-8');
|
||||
expect(configFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = {"default":{"docsMode":true,"staticDir":["assets"]}};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}
|
||||
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: './vite.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
expect(target).toMatchInlineSnapshot(`
|
||||
{
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/myapp",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle configurations correctly and migrate docsMode and staticDir to storybook config correctly', () => {
|
||||
// ARRANGE
|
||||
const tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
const targetConfiguration = {
|
||||
outputs: ['{options.outputDir}'],
|
||||
options: {
|
||||
outputDir: 'dist/storybook/myapp',
|
||||
configDir: 'apps/myapp/.storybook',
|
||||
docsMode: true,
|
||||
staticDir: ['assets'],
|
||||
},
|
||||
configurations: {
|
||||
dev: {
|
||||
outputDir: 'dist/storybook/myapp/dev',
|
||||
configDir: 'apps/myapp/dev/.storybook',
|
||||
docsMode: false,
|
||||
staticDir: ['dev/assets'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const inferredTargetConfiguration = {
|
||||
outputs: ['{projectRoot}/{options.outputDir}'],
|
||||
};
|
||||
|
||||
const migrationLogs = new AggregatedLog();
|
||||
|
||||
tree.write(
|
||||
'apps/myapp/.storybook/main.ts',
|
||||
storybookConfigFileV17_ReactVite
|
||||
);
|
||||
tree.write(
|
||||
'apps/myapp/dev/.storybook/main.ts',
|
||||
storybookConfigFileV17_ReactVite
|
||||
);
|
||||
|
||||
// ACT
|
||||
const target = buildPostTargetTransformer(migrationLogs)(
|
||||
targetConfiguration,
|
||||
tree,
|
||||
{ projectName: 'myapp', root: 'apps/myapp' },
|
||||
inferredTargetConfiguration
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const configFile = tree.read('apps/myapp/.storybook/main.ts', 'utf-8');
|
||||
expect(configFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = {"default":{"docsMode":true,"staticDir":["assets"]},"dev":{"docsMode":false,"staticDir":["dev/assets"]}};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}
|
||||
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: './vite.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
expect(target).toMatchInlineSnapshot(`
|
||||
{
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"config-dir": "./dev/.storybook",
|
||||
"output-dir": "../../dist/storybook/myapp/dev",
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/myapp",
|
||||
},
|
||||
}
|
||||
`);
|
||||
const devConfigFile = tree.read(
|
||||
'apps/myapp/dev/.storybook/main.ts',
|
||||
'utf-8'
|
||||
);
|
||||
expect(devConfigFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: 'apps/myapp/vite.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('--vue-vite', () => {
|
||||
it('should migrate docsMode and staticDir to storybook config correctly', () => {
|
||||
// ARRANGE
|
||||
const tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
const targetConfiguration = {
|
||||
outputs: ['{options.outputDir}'],
|
||||
options: {
|
||||
outputDir: 'dist/storybook/myapp',
|
||||
configDir: 'apps/myapp/.storybook',
|
||||
docsMode: true,
|
||||
staticDir: ['assets'],
|
||||
},
|
||||
};
|
||||
|
||||
const inferredTargetConfiguration = {
|
||||
outputs: ['{projectRoot}/{options.outputDir}'],
|
||||
};
|
||||
|
||||
const migrationLogs = new AggregatedLog();
|
||||
|
||||
tree.write(
|
||||
'apps/myapp/.storybook/main.ts',
|
||||
storybookConfigFileV17_VueVite
|
||||
);
|
||||
|
||||
// ACT
|
||||
const target = buildPostTargetTransformer(migrationLogs)(
|
||||
targetConfiguration,
|
||||
tree,
|
||||
{ projectName: 'myapp', root: 'apps/myapp' },
|
||||
inferredTargetConfiguration
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const configFile = tree.read('apps/myapp/.storybook/main.ts', 'utf-8');
|
||||
expect(configFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/vue3-vite';
|
||||
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = {"default":{"docsMode":true,"staticDir":["assets"]}};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}
|
||||
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: './vite.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
expect(target).toMatchInlineSnapshot(`
|
||||
{
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/myapp",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle configurations correctly and migrate docsMode and staticDir to storybook config correctly', () => {
|
||||
// ARRANGE
|
||||
const tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
const targetConfiguration = {
|
||||
outputs: ['{options.outputDir}'],
|
||||
options: {
|
||||
outputDir: 'dist/storybook/myapp',
|
||||
configDir: 'apps/myapp/.storybook',
|
||||
docsMode: true,
|
||||
staticDir: ['assets'],
|
||||
},
|
||||
configurations: {
|
||||
dev: {
|
||||
outputDir: 'dist/storybook/myapp/dev',
|
||||
configDir: 'apps/myapp/dev/.storybook',
|
||||
docsMode: false,
|
||||
staticDir: ['dev/assets'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const inferredTargetConfiguration = {
|
||||
outputs: ['{projectRoot}/{options.outputDir}'],
|
||||
};
|
||||
|
||||
const migrationLogs = new AggregatedLog();
|
||||
|
||||
tree.write(
|
||||
'apps/myapp/.storybook/main.ts',
|
||||
storybookConfigFileV17_VueVite
|
||||
);
|
||||
tree.write(
|
||||
'apps/myapp/dev/.storybook/main.ts',
|
||||
storybookConfigFileV17_VueVite
|
||||
);
|
||||
|
||||
// ACT
|
||||
const target = buildPostTargetTransformer(migrationLogs)(
|
||||
targetConfiguration,
|
||||
tree,
|
||||
{ projectName: 'myapp', root: 'apps/myapp' },
|
||||
inferredTargetConfiguration
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const configFile = tree.read('apps/myapp/.storybook/main.ts', 'utf-8');
|
||||
expect(configFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/vue3-vite';
|
||||
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = {"default":{"docsMode":true,"staticDir":["assets"]},"dev":{"docsMode":false,"staticDir":["dev/assets"]}};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}
|
||||
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: './vite.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
expect(target).toMatchInlineSnapshot(`
|
||||
{
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"config-dir": "./dev/.storybook",
|
||||
"output-dir": "../../dist/storybook/myapp/dev",
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/myapp",
|
||||
},
|
||||
}
|
||||
`);
|
||||
const devConfigFile = tree.read(
|
||||
'apps/myapp/dev/.storybook/main.ts',
|
||||
'utf-8'
|
||||
);
|
||||
expect(devConfigFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/vue3-vite';
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: 'apps/myapp/vite.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('--react-webpack', () => {
|
||||
it('should migrate docsMode and staticDir to storybook config correctly', () => {
|
||||
// ARRANGE
|
||||
const tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
const targetConfiguration = {
|
||||
outputs: ['{options.outputDir}'],
|
||||
options: {
|
||||
outputDir: 'dist/storybook/myapp',
|
||||
configDir: 'apps/myapp/.storybook',
|
||||
docsMode: true,
|
||||
staticDir: ['assets'],
|
||||
},
|
||||
};
|
||||
|
||||
const inferredTargetConfiguration = {
|
||||
outputs: ['{projectRoot}/{options.outputDir}'],
|
||||
};
|
||||
|
||||
const migrationLogs = new AggregatedLog();
|
||||
|
||||
tree.write(
|
||||
'apps/myapp/.storybook/main.ts',
|
||||
storybookConfigFileV17_ReactWebpack
|
||||
);
|
||||
|
||||
// ACT
|
||||
const target = buildPostTargetTransformer(migrationLogs)(
|
||||
targetConfiguration,
|
||||
tree,
|
||||
{ projectName: 'myapp', root: 'apps/myapp' },
|
||||
inferredTargetConfiguration
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const configFile = tree.read('apps/myapp/.storybook/main.ts', 'utf-8');
|
||||
expect(configFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/react-webpack5';
|
||||
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = {"default":{"docsMode":true,"staticDir":["assets"]}};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}
|
||||
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@nx/react/plugins/storybook',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
expect(target).toMatchInlineSnapshot(`
|
||||
{
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/myapp",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
it('should handle configurations correctly and migrate docsMode and staticDir to storybook config correctly', () => {
|
||||
// ARRANGE
|
||||
const tree = createTreeWithEmptyWorkspace();
|
||||
|
||||
const targetConfiguration = {
|
||||
outputs: ['{options.outputDir}'],
|
||||
options: {
|
||||
outputDir: 'dist/storybook/myapp',
|
||||
configDir: 'apps/myapp/.storybook',
|
||||
docsMode: true,
|
||||
staticDir: ['assets'],
|
||||
},
|
||||
configurations: {
|
||||
dev: {
|
||||
outputDir: 'dist/storybook/myapp/dev',
|
||||
configDir: 'apps/myapp/dev/.storybook',
|
||||
docsMode: false,
|
||||
staticDir: ['dev/assets'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const inferredTargetConfiguration = {
|
||||
outputs: ['{projectRoot}/{options.outputDir}'],
|
||||
};
|
||||
|
||||
const migrationLogs = new AggregatedLog();
|
||||
|
||||
tree.write(
|
||||
'apps/myapp/.storybook/main.ts',
|
||||
storybookConfigFileV17_ReactWebpack
|
||||
);
|
||||
tree.write(
|
||||
'apps/myapp/dev/.storybook/main.ts',
|
||||
storybookConfigFileV17_ReactWebpack
|
||||
);
|
||||
|
||||
// ACT
|
||||
const target = buildPostTargetTransformer(migrationLogs)(
|
||||
targetConfiguration,
|
||||
tree,
|
||||
{ projectName: 'myapp', root: 'apps/myapp' },
|
||||
inferredTargetConfiguration
|
||||
);
|
||||
|
||||
// ASSERT
|
||||
const configFile = tree.read('apps/myapp/.storybook/main.ts', 'utf-8');
|
||||
expect(configFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/react-webpack5';
|
||||
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = {"default":{"docsMode":true,"staticDir":["assets"]},"dev":{"docsMode":false,"staticDir":["dev/assets"]}};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}
|
||||
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@nx/react/plugins/storybook',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
expect(target).toMatchInlineSnapshot(`
|
||||
{
|
||||
"configurations": {
|
||||
"dev": {
|
||||
"config-dir": "./dev/.storybook",
|
||||
"output-dir": "../../dist/storybook/myapp/dev",
|
||||
},
|
||||
},
|
||||
"options": {
|
||||
"config-dir": ".storybook",
|
||||
"output-dir": "../../dist/storybook/myapp",
|
||||
},
|
||||
}
|
||||
`);
|
||||
const devConfigFile = tree.read(
|
||||
'apps/myapp/dev/.storybook/main.ts',
|
||||
'utf-8'
|
||||
);
|
||||
expect(devConfigFile).toMatchInlineSnapshot(`
|
||||
"import type { StorybookConfig } from '@storybook/react-webpack5';
|
||||
|
||||
const config: StorybookConfig = {staticDirs: options.staticDir,docs: { docsMode: options.docsMode },
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@nx/react/plugins/storybook',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;"
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const storybookConfigFileV17_ReactVite = `import type { StorybookConfig } from '@storybook/react-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: {
|
||||
builder: {
|
||||
viteConfigPath: 'apps/myapp/vite.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;`;
|
||||
|
||||
const storybookConfigFileV17_ReactWebpack = `import type { StorybookConfig } from '@storybook/react-webpack5';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'@nx/react/plugins/storybook',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-webpack5',
|
||||
options: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;`;
|
||||
|
||||
const storybookConfigFileV17_VueVite = `import type { StorybookConfig } from '@storybook/vue3-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
|
||||
addons: ['@storybook/addon-essentials', '@storybook/addon-interactions'],
|
||||
framework: {
|
||||
name: '@storybook/vue3-vite',
|
||||
options: {
|
||||
builder: {
|
||||
viteConfigPath: 'apps/myapp/vite.config.ts',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;`;
|
||||
@ -0,0 +1,313 @@
|
||||
import { joinPathFragments, TargetConfiguration, Tree } from '@nx/devkit';
|
||||
import {
|
||||
processTargetOutputs,
|
||||
toProjectRelativePath,
|
||||
} from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils';
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
|
||||
import {
|
||||
addConfigValuesToConfigFile,
|
||||
ensureViteConfigPathIsRelative,
|
||||
getConfigFilePath,
|
||||
STORYBOOK_PROP_MAPPINGS,
|
||||
} from './utils';
|
||||
import { getInstalledPackageVersionInfo } from './utils';
|
||||
|
||||
type StorybookConfigValues = { docsMode?: boolean; staticDir?: string };
|
||||
|
||||
export function buildPostTargetTransformer(migrationLogs: AggregatedLog) {
|
||||
return (
|
||||
target: TargetConfiguration,
|
||||
tree: Tree,
|
||||
projectDetails: { projectName: string; root: string },
|
||||
inferredTargetConfiguration: TargetConfiguration
|
||||
) => {
|
||||
let defaultConfigDir = joinPathFragments(projectDetails.root, '.storybook');
|
||||
|
||||
const configValues: Record<string, StorybookConfigValues> = {
|
||||
default: {},
|
||||
};
|
||||
|
||||
if (target.options) {
|
||||
if (target.options.configDir) {
|
||||
defaultConfigDir = target.options.configDir;
|
||||
}
|
||||
|
||||
handlePropertiesFromTargetOptions(
|
||||
tree,
|
||||
target.options,
|
||||
defaultConfigDir,
|
||||
projectDetails.projectName,
|
||||
projectDetails.root,
|
||||
configValues['default'],
|
||||
migrationLogs
|
||||
);
|
||||
}
|
||||
|
||||
if (target.configurations) {
|
||||
for (const configurationName in target.configurations) {
|
||||
const configuration = target.configurations[configurationName];
|
||||
configValues[configurationName] = {};
|
||||
handlePropertiesFromTargetOptions(
|
||||
tree,
|
||||
configuration,
|
||||
defaultConfigDir,
|
||||
projectDetails.projectName,
|
||||
projectDetails.root,
|
||||
configValues[configurationName],
|
||||
migrationLogs
|
||||
);
|
||||
}
|
||||
|
||||
for (const configurationName in target.configurations) {
|
||||
const configuration = target.configurations[configurationName];
|
||||
if (
|
||||
configuration.configDir &&
|
||||
configuration.configDir !==
|
||||
toProjectRelativePath(defaultConfigDir, projectDetails.root)
|
||||
) {
|
||||
const configFilePath = getConfigFilePath(
|
||||
tree,
|
||||
joinPathFragments(projectDetails.root, configuration.configDir)
|
||||
);
|
||||
addConfigValuesToConfigFile(tree, configFilePath, configValues);
|
||||
ensureViteConfigPathIsRelative(
|
||||
tree,
|
||||
configFilePath,
|
||||
projectDetails.projectName,
|
||||
projectDetails.root,
|
||||
'@nx/storybook:build',
|
||||
migrationLogs
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
configurationName === 'ci' &&
|
||||
Object.keys(configuration).length === 0
|
||||
) {
|
||||
delete target.configurations[configurationName];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(target.configurations).length === 0) {
|
||||
if ('defaultConfiguration' in target) {
|
||||
delete target.defaultConfiguration;
|
||||
}
|
||||
delete target.configurations;
|
||||
}
|
||||
|
||||
if (
|
||||
'defaultConfiguration' in target &&
|
||||
!target.configurations[target.defaultConfiguration]
|
||||
) {
|
||||
delete target.defaultConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
if (target.outputs) {
|
||||
processTargetOutputs(
|
||||
target,
|
||||
[{ newName: 'outputDir', oldName: 'outputDir' }],
|
||||
inferredTargetConfiguration,
|
||||
{
|
||||
projectName: projectDetails.projectName,
|
||||
projectRoot: projectDetails.root,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
addConfigValuesToConfigFile(
|
||||
tree,
|
||||
getConfigFilePath(tree, defaultConfigDir),
|
||||
configValues
|
||||
);
|
||||
ensureViteConfigPathIsRelative(
|
||||
tree,
|
||||
getConfigFilePath(tree, defaultConfigDir),
|
||||
projectDetails.projectName,
|
||||
projectDetails.root,
|
||||
'@nx/storybook:build',
|
||||
migrationLogs
|
||||
);
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
function handlePropertiesFromTargetOptions(
|
||||
tree: Tree,
|
||||
options: any,
|
||||
defaultConfigDir: string,
|
||||
projectName: string,
|
||||
projectRoot: string,
|
||||
configValues: StorybookConfigValues,
|
||||
migrationLogs: AggregatedLog
|
||||
) {
|
||||
let configDir = defaultConfigDir;
|
||||
if ('configDir' in options) {
|
||||
if (options.configDir !== defaultConfigDir) {
|
||||
configDir = options.configDir as string;
|
||||
}
|
||||
options.configDir = toProjectRelativePath(options.configDir, projectRoot);
|
||||
}
|
||||
|
||||
if (options.outputDir) {
|
||||
options.outputDir = toProjectRelativePath(options.outputDir, projectRoot);
|
||||
}
|
||||
|
||||
if ('styles' in options) {
|
||||
delete options.styles;
|
||||
}
|
||||
if ('stylePreprocessorOptions' in options) {
|
||||
delete options.stylePreprocessorOptions;
|
||||
}
|
||||
|
||||
if ('docsMode' in options) {
|
||||
configValues.docsMode = options.docsMode;
|
||||
moveDocsModeToConfigFile(
|
||||
tree,
|
||||
configDir,
|
||||
projectName,
|
||||
migrationLogs,
|
||||
configDir === defaultConfigDir
|
||||
);
|
||||
delete options.docsMode;
|
||||
}
|
||||
|
||||
if ('staticDir' in options) {
|
||||
configValues.staticDir = options.staticDir;
|
||||
moveStaticDirToConfigFile(
|
||||
tree,
|
||||
configDir,
|
||||
projectName,
|
||||
migrationLogs,
|
||||
configDir === defaultConfigDir
|
||||
);
|
||||
delete options.staticDir;
|
||||
}
|
||||
|
||||
const storybookPropMappings =
|
||||
getInstalledPackageVersionInfo(tree, 'storybook')?.major === 8
|
||||
? STORYBOOK_PROP_MAPPINGS.v8
|
||||
: STORYBOOK_PROP_MAPPINGS.v7;
|
||||
for (const [prevKey, newKey] of Object.entries(storybookPropMappings)) {
|
||||
if (prevKey in options) {
|
||||
let prevValue = options[prevKey];
|
||||
delete options[prevKey];
|
||||
options[newKey] = prevValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function moveDocsModeToConfigFile(
|
||||
tree: Tree,
|
||||
configDir: string,
|
||||
projectName: string,
|
||||
migrationLogs: AggregatedLog,
|
||||
useConfigValues = true
|
||||
) {
|
||||
const configFilePath = getConfigFilePath(tree, configDir);
|
||||
const configFileContents = tree.read(configFilePath, 'utf-8');
|
||||
|
||||
const ast = tsquery.ast(configFileContents);
|
||||
const CONFIG_OBJECT_SELECTOR =
|
||||
'VariableDeclaration:has(Identifier[name=config]) ObjectLiteralExpression';
|
||||
const DOCS_MODE_SELECTOR =
|
||||
'PropertyAssignment:has(Identifier[name=docs]) PropertyAssignment:has(Identifier[name=docsMode])';
|
||||
const DOCS_SELECTOR = 'PropertyAssignment:has(Identifier[name=docs])';
|
||||
|
||||
const configNodes = tsquery(ast, CONFIG_OBJECT_SELECTOR, {
|
||||
visitAllChildren: true,
|
||||
});
|
||||
|
||||
if (configNodes.length === 0) {
|
||||
// Invalid config file
|
||||
migrationLogs.addLog({
|
||||
project: projectName,
|
||||
executorName: '@nx/storybook:build',
|
||||
log: 'Could not find a valid Storybook Config to migrate `docsMode`. Update your `main.ts` file to add `docsMode`.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const configNode = configNodes[0];
|
||||
const hasDocsMode =
|
||||
tsquery(configNode, DOCS_MODE_SELECTOR, { visitAllChildren: true }).length >
|
||||
0;
|
||||
if (hasDocsMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
let startPosition = configNode.getStart() + 1;
|
||||
let needsDocObject = true;
|
||||
|
||||
const docsNodes = tsquery(configNode, DOCS_SELECTOR, {
|
||||
visitAllChildren: true,
|
||||
});
|
||||
if (docsNodes.length > 0) {
|
||||
needsDocObject = false;
|
||||
startPosition = docsNodes[0].getStart() + 1;
|
||||
}
|
||||
|
||||
const docsModeInsert = `options.docsMode`;
|
||||
const nodeToInsert = needsDocObject
|
||||
? `docs: { docsMode: ${docsModeInsert} },`
|
||||
: `docsMode: ${docsModeInsert},`;
|
||||
tree.write(
|
||||
configFilePath,
|
||||
`${configFileContents.slice(
|
||||
0,
|
||||
startPosition
|
||||
)}${nodeToInsert}${configFileContents.slice(startPosition)}`
|
||||
);
|
||||
}
|
||||
|
||||
function moveStaticDirToConfigFile(
|
||||
tree: Tree,
|
||||
configDir: string,
|
||||
projectName: string,
|
||||
migrationLogs: AggregatedLog,
|
||||
useConfigValues = true
|
||||
) {
|
||||
const configFilePath = getConfigFilePath(tree, configDir);
|
||||
const configFileContents = tree.read(configFilePath, 'utf-8');
|
||||
|
||||
const ast = tsquery.ast(configFileContents);
|
||||
const CONFIG_OBJECT_SELECTOR =
|
||||
'VariableDeclaration:has(Identifier[name=config]) ObjectLiteralExpression';
|
||||
const STATIC_DIRS_SELECTOR =
|
||||
'PropertyAssignment:has(Identifier[name=staticDirs])';
|
||||
|
||||
const configNodes = tsquery(ast, CONFIG_OBJECT_SELECTOR, {
|
||||
visitAllChildren: true,
|
||||
});
|
||||
|
||||
if (configNodes.length === 0) {
|
||||
// Invalid config file
|
||||
migrationLogs.addLog({
|
||||
project: projectName,
|
||||
executorName: '@nx/storybook:build',
|
||||
log: 'Could not find a valid Storybook Config to migrate `staticDir`. Update your `main.ts` file to add `staticDirs`.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const configNode = configNodes[0];
|
||||
const hasStaticDir =
|
||||
tsquery(configNode, STATIC_DIRS_SELECTOR, { visitAllChildren: true })
|
||||
.length > 0;
|
||||
if (hasStaticDir) {
|
||||
return;
|
||||
}
|
||||
|
||||
let startPosition = configNode.getStart() + 1;
|
||||
|
||||
const nodeToInsert = `staticDirs: options.staticDir,`;
|
||||
tree.write(
|
||||
configFilePath,
|
||||
`${configFileContents.slice(
|
||||
0,
|
||||
startPosition
|
||||
)}${nodeToInsert}${configFileContents.slice(startPosition)}`
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
import { joinPathFragments, TargetConfiguration, Tree } from '@nx/devkit';
|
||||
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
|
||||
import { toProjectRelativePath } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils';
|
||||
import {
|
||||
ensureViteConfigPathIsRelative,
|
||||
getConfigFilePath,
|
||||
getInstalledPackageVersionInfo,
|
||||
STORYBOOK_PROP_MAPPINGS,
|
||||
} from './utils';
|
||||
export function servePostTargetTransformer(migrationLogs: AggregatedLog) {
|
||||
return (
|
||||
target: TargetConfiguration,
|
||||
tree: Tree,
|
||||
projectDetails: { projectName: string; root: string },
|
||||
inferredTargetConfiguration: TargetConfiguration
|
||||
) => {
|
||||
let defaultConfigDir = getConfigFilePath(
|
||||
tree,
|
||||
joinPathFragments(projectDetails.root, '.storybook')
|
||||
);
|
||||
|
||||
if (target.options) {
|
||||
if (target.options.configDir) {
|
||||
defaultConfigDir = target.options.configDir;
|
||||
}
|
||||
|
||||
handlePropertiesFromTargetOptions(
|
||||
tree,
|
||||
target.options,
|
||||
projectDetails.projectName,
|
||||
projectDetails.root,
|
||||
migrationLogs
|
||||
);
|
||||
}
|
||||
|
||||
if (target.configurations) {
|
||||
for (const configurationName in target.configurations) {
|
||||
const configuration = target.configurations[configurationName];
|
||||
if (
|
||||
configuration.configDir &&
|
||||
configuration.configDir !== defaultConfigDir
|
||||
) {
|
||||
ensureViteConfigPathIsRelative(
|
||||
tree,
|
||||
getConfigFilePath(tree, configuration.configDir),
|
||||
projectDetails.projectName,
|
||||
projectDetails.root,
|
||||
'@nx/storybook:storybook',
|
||||
migrationLogs
|
||||
);
|
||||
}
|
||||
|
||||
handlePropertiesFromTargetOptions(
|
||||
tree,
|
||||
configuration,
|
||||
projectDetails.projectName,
|
||||
projectDetails.root,
|
||||
migrationLogs
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(target.configurations).length === 0) {
|
||||
if ('defaultConfiguration' in target) {
|
||||
delete target.defaultConfiguration;
|
||||
}
|
||||
delete target.configurations;
|
||||
}
|
||||
|
||||
if (
|
||||
'defaultConfiguration' in target &&
|
||||
!target.configurations[target.defaultConfiguration]
|
||||
) {
|
||||
delete target.defaultConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
ensureViteConfigPathIsRelative(
|
||||
tree,
|
||||
getConfigFilePath(tree, defaultConfigDir),
|
||||
projectDetails.projectName,
|
||||
projectDetails.root,
|
||||
'@nx/storybook:storybook',
|
||||
migrationLogs
|
||||
);
|
||||
|
||||
return target;
|
||||
};
|
||||
}
|
||||
|
||||
function handlePropertiesFromTargetOptions(
|
||||
tree: Tree,
|
||||
options: any,
|
||||
projectName: string,
|
||||
projectRoot: string,
|
||||
migrationLogs: AggregatedLog
|
||||
) {
|
||||
if ('configDir' in options) {
|
||||
options.configDir = toProjectRelativePath(options.configDir, projectRoot);
|
||||
}
|
||||
|
||||
if (options.outputDir) {
|
||||
options.outputDir = toProjectRelativePath(options.outputDir, projectRoot);
|
||||
}
|
||||
|
||||
if ('uiFramework' in options) {
|
||||
delete options.uiFramework;
|
||||
}
|
||||
if ('staticDir' in options) {
|
||||
migrationLogs.addLog({
|
||||
project: projectName,
|
||||
executorName: '@nx/storybook:storybook',
|
||||
log: 'Could not migrate `staticDir`. Update your `main.ts` file to add `staticDirs`.',
|
||||
});
|
||||
delete options.staticDir;
|
||||
}
|
||||
if ('open' in options) {
|
||||
if (!options.open) {
|
||||
options['args'] ??= [];
|
||||
options['args'].push('--no-open');
|
||||
}
|
||||
delete options.open;
|
||||
}
|
||||
|
||||
if ('no-open' in options) {
|
||||
if (options['no-open']) {
|
||||
options['args'] ??= [];
|
||||
options['args'].push('--no-open');
|
||||
}
|
||||
delete options['no-open'];
|
||||
}
|
||||
|
||||
if ('quiet' in options) {
|
||||
if (options['quiet']) {
|
||||
options['args'] ??= [];
|
||||
options['args'].push('--quiet');
|
||||
}
|
||||
delete options.quiet;
|
||||
}
|
||||
|
||||
if ('docsMode' in options) {
|
||||
options.docs = options.docsMode;
|
||||
delete options.docsMode;
|
||||
}
|
||||
|
||||
const storybookPropMappings =
|
||||
getInstalledPackageVersionInfo(tree, 'storybook')?.major === 8
|
||||
? STORYBOOK_PROP_MAPPINGS.v8
|
||||
: STORYBOOK_PROP_MAPPINGS.v7;
|
||||
for (const [prevKey, newKey] of Object.entries(storybookPropMappings)) {
|
||||
if (prevKey in options) {
|
||||
let prevValue = options[prevKey];
|
||||
delete options[prevKey];
|
||||
options[newKey] = prevValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,200 @@
|
||||
import { tsquery } from '@phenomnomnominal/tsquery';
|
||||
import { readJson, joinPathFragments, type Tree } from '@nx/devkit';
|
||||
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
|
||||
import { toProjectRelativePath } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils';
|
||||
import { dirname } from 'path/posix';
|
||||
import { coerce, major } from 'semver';
|
||||
|
||||
export function getConfigFilePath(tree: Tree, configDir: string) {
|
||||
return [
|
||||
joinPathFragments(configDir, `main.ts`),
|
||||
joinPathFragments(configDir, `main.cts`),
|
||||
joinPathFragments(configDir, `main.mts`),
|
||||
joinPathFragments(configDir, `main.js`),
|
||||
joinPathFragments(configDir, `main.cjs`),
|
||||
joinPathFragments(configDir, `main.mjs`),
|
||||
].find((f) => tree.exists(f));
|
||||
}
|
||||
|
||||
export function addConfigValuesToConfigFile(
|
||||
tree: Tree,
|
||||
configFile: string,
|
||||
configValues: Record<string, Record<string, unknown>>
|
||||
) {
|
||||
const IMPORT_PROPERTY_SELECTOR = 'ImportDeclaration';
|
||||
const configFileContents = tree.read(configFile, 'utf-8');
|
||||
|
||||
const ast = tsquery.ast(configFileContents);
|
||||
// AST TO GET SECTION TO APPEND TO
|
||||
const importNodes = tsquery(ast, IMPORT_PROPERTY_SELECTOR, {
|
||||
visitAllChildren: true,
|
||||
});
|
||||
let startPosition = 0;
|
||||
if (importNodes.length !== 0) {
|
||||
const lastImportNode = importNodes[importNodes.length - 1];
|
||||
startPosition = lastImportNode.getEnd();
|
||||
}
|
||||
|
||||
const configValuesString = `
|
||||
// These options were migrated by @nx/storybook:convert-to-inferred from the project.json file.
|
||||
const configValues = ${JSON.stringify(configValues)};
|
||||
|
||||
// Determine the correct configValue to use based on the configuration
|
||||
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
|
||||
|
||||
const options = {
|
||||
...configValues.default,
|
||||
...(configValues[nxConfiguration] ?? {})
|
||||
}`;
|
||||
|
||||
tree.write(
|
||||
configFile,
|
||||
`${configFileContents.slice(0, startPosition)}
|
||||
${configValuesString}
|
||||
${configFileContents.slice(startPosition)}`
|
||||
);
|
||||
}
|
||||
|
||||
export const STORYBOOK_PROP_MAPPINGS = {
|
||||
v7: {
|
||||
port: 'port',
|
||||
previewUrl: 'preview-url',
|
||||
host: 'host',
|
||||
docs: 'docs',
|
||||
configDir: 'config-dir',
|
||||
logLevel: 'loglevel',
|
||||
quiet: 'quiet',
|
||||
webpackStatsJson: 'webpack-stats-json',
|
||||
debugWebpack: 'debug-webpack',
|
||||
disableTelemetry: 'disable-telemetry',
|
||||
https: 'https',
|
||||
sslCa: 'ssl-ca',
|
||||
sslCert: 'ssl-cert',
|
||||
sslKey: 'ssl-key',
|
||||
smokeTest: 'smoke-test',
|
||||
noOpen: 'no-open',
|
||||
outputDir: 'output-dir',
|
||||
},
|
||||
v8: {
|
||||
port: 'port',
|
||||
previewUrl: 'preview-url',
|
||||
host: 'host',
|
||||
docs: 'docs',
|
||||
configDir: 'config-dir',
|
||||
logLevel: 'loglevel',
|
||||
quiet: 'quiet',
|
||||
webpackStatsJson: 'stats-json',
|
||||
debugWebpack: 'debug-webpack',
|
||||
disableTelemetry: 'disable-telemetry',
|
||||
https: 'https',
|
||||
sslCa: 'ssl-ca',
|
||||
sslCert: 'ssl-cert',
|
||||
sslKey: 'ssl-key',
|
||||
smokeTest: 'smoke-test',
|
||||
noOpen: 'no-open',
|
||||
outputDir: 'output-dir',
|
||||
},
|
||||
};
|
||||
|
||||
export function ensureViteConfigPathIsRelative(
|
||||
tree: Tree,
|
||||
configPath: string,
|
||||
projectName: string,
|
||||
projectRoot: string,
|
||||
executorName: string,
|
||||
migrationLogs: AggregatedLog
|
||||
) {
|
||||
const configFileContents = tree.read(configPath, 'utf-8');
|
||||
|
||||
if (configFileContents.includes('viteFinal:')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ast = tsquery.ast(configFileContents);
|
||||
const REACT_FRAMEWORK_SELECTOR_IDENTIFIERS =
|
||||
'PropertyAssignment:has(Identifier[name=framework]) PropertyAssignment:has(Identifier[name=name]) StringLiteral[value=@storybook/react-vite]';
|
||||
const REACT_FRAMEWORK_SELECTOR_STRING_LITERALS =
|
||||
'PropertyAssignment:has(StringLiteral[value=framework]) PropertyAssignment:has(StringLiteral[value=name]) StringLiteral[value=@storybook/react-vite]';
|
||||
|
||||
const VUE_FRAMEWORK_SELECTOR_IDENTIFIERS =
|
||||
'PropertyAssignment:has(Identifier[name=framework]) PropertyAssignment:has(Identifier[name=name]) StringLiteral[value=@storybook/vue3-vite]';
|
||||
const VUE_FRAMEWORK_SELECTOR_STRING_LITERALS =
|
||||
'PropertyAssignment:has(StringLiteral[value=framework]) PropertyAssignment:has(StringLiteral[value=name]) StringLiteral[value=@storybook/vue3-vite]';
|
||||
const isUsingVite =
|
||||
tsquery(ast, REACT_FRAMEWORK_SELECTOR_IDENTIFIERS, {
|
||||
visitAllChildren: true,
|
||||
}).length > 0 ||
|
||||
tsquery(ast, REACT_FRAMEWORK_SELECTOR_STRING_LITERALS, {
|
||||
visitAllChildren: true,
|
||||
}).length > 0 ||
|
||||
tsquery(ast, VUE_FRAMEWORK_SELECTOR_STRING_LITERALS, {
|
||||
visitAllChildren: true,
|
||||
}).length > 0 ||
|
||||
tsquery(ast, VUE_FRAMEWORK_SELECTOR_IDENTIFIERS, { visitAllChildren: true })
|
||||
.length > 0;
|
||||
if (!isUsingVite) {
|
||||
return;
|
||||
}
|
||||
|
||||
const VITE_CONFIG_PATH_SELECTOR =
|
||||
'PropertyAssignment:has(Identifier[name=framework]) PropertyAssignment PropertyAssignment PropertyAssignment:has(Identifier[name=viteConfigPath]) > StringLiteral';
|
||||
let viteConfigPathNodes = tsquery(ast, VITE_CONFIG_PATH_SELECTOR, {
|
||||
visitAllChildren: true,
|
||||
});
|
||||
if (viteConfigPathNodes.length === 0) {
|
||||
const VITE_CONFIG_PATH_SELECTOR_STRING_LITERALS =
|
||||
'PropertyAssignment:has(StringLiteral[value=framework]) PropertyAssignment PropertyAssignment PropertyAssignment:has(StringLiteral[value=viteConfigPath]) > StringLiteral:not(StringLiteral[value=viteConfigPath])';
|
||||
viteConfigPathNodes = tsquery(
|
||||
ast,
|
||||
VITE_CONFIG_PATH_SELECTOR_STRING_LITERALS,
|
||||
{
|
||||
visitAllChildren: true,
|
||||
}
|
||||
);
|
||||
|
||||
if (viteConfigPathNodes.length === 0) {
|
||||
migrationLogs.addLog({
|
||||
project: projectName,
|
||||
executorName,
|
||||
log: 'Unable to find `viteConfigPath` in Storybook Config. Please ensure the `viteConfigPath` is relative to the project root.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const viteConfigPathNode = viteConfigPathNodes[0];
|
||||
const pathToViteConfig = viteConfigPathNode.getText().replace(/('|")/g, '');
|
||||
if (pathToViteConfig.match(/^(\.\.\/|\.\/)/)) {
|
||||
return;
|
||||
}
|
||||
const relativePathToViteConfig = toProjectRelativePath(
|
||||
pathToViteConfig,
|
||||
projectRoot
|
||||
);
|
||||
|
||||
tree.write(
|
||||
configPath,
|
||||
`${configFileContents.slice(
|
||||
0,
|
||||
viteConfigPathNode.getStart() + 1
|
||||
)}${relativePathToViteConfig}${configFileContents.slice(
|
||||
viteConfigPathNode.getEnd() - 1
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getInstalledPackageVersion(
|
||||
tree: Tree,
|
||||
pkgName: string
|
||||
): string | null {
|
||||
const { dependencies, devDependencies } = readJson(tree, 'package.json');
|
||||
const version = dependencies?.[pkgName] ?? devDependencies?.[pkgName];
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
export function getInstalledPackageVersionInfo(tree: Tree, pkgName: string) {
|
||||
const version = getInstalledPackageVersion(tree, pkgName);
|
||||
|
||||
return version ? { major: major(coerce(version)), version } : null;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/schema",
|
||||
"$id": "NxStorybookConvertToInferred",
|
||||
"description": "Convert existing Storybook project(s) using `@nx/storybook:*` executors to use `@nx/storybook/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
|
||||
"title": "Convert Storybook project from executor to plugin",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "The project to convert from using the `@nx/storybook:*` executors to use `@nx/storybook/plugin`.",
|
||||
"x-priority": "important"
|
||||
},
|
||||
"skipFormat": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to format files at the end of the migration.",
|
||||
"default": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -38,13 +38,13 @@ export function buildPostTargetTransformer(
|
||||
if (target.configurations) {
|
||||
for (const configurationName in target.configurations) {
|
||||
const configuration = target.configurations[configurationName];
|
||||
configValues[configuration] = {};
|
||||
configValues[configurationName] = {};
|
||||
removePropertiesFromTargetOptions(
|
||||
tree,
|
||||
configuration,
|
||||
viteConfigPath,
|
||||
projectDetails.root,
|
||||
configValues[configuration]
|
||||
configValues[configurationName]
|
||||
);
|
||||
|
||||
if (Object.keys(configuration).length === 0) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user