diff --git a/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.spec.ts b/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.spec.ts index e69de29bb2d1d..0d6ffc891bb5f 100644 --- a/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.spec.ts +++ b/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.spec.ts @@ -0,0 +1,577 @@ +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 = 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": { + "options": { + "config-dir": ".storybook", + }, + }, + } + `); + + 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": { + "options": { + "config-dir": ".storybook", + }, + }, + } + `); + + 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": { + "options": { + "config-dir": ".storybook", + }, + }, + } + `); + + 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": { + "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": { + "options": { + "config-dir": ".storybook", + }, + }, + } + `); + + 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": { + "options": { + "config-dir": ".storybook", + }, + }, + } + `); + + 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": { + "options": { + "config-dir": ".storybook", + }, + }, + } + `); + + 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(); + }); + }); +}); diff --git a/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts index 1748eeb86d892..9015fbada9894 100644 --- a/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts +++ b/packages/storybook/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -1,9 +1,16 @@ -import { createProjectGraphAsync, formatFiles, type Tree } from '@nx/devkit'; +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; @@ -55,9 +62,15 @@ export async function convertToInferred(tree: Tree, options: Schema) { await formatFiles(tree); } - return () => { + const installTask = addDependenciesToPackageJson( + tree, + {}, + { storybook: storybookVersion } + ); + + return runTasksInSerial(installTask, () => { migrationLogs.flushLogs(); - }; + }); } export default convertToInferred; diff --git a/packages/storybook/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts b/packages/storybook/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts index e41090c929f63..4bd145b82088f 100644 --- a/packages/storybook/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts +++ b/packages/storybook/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts @@ -3,189 +3,584 @@ import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggre import { buildPostTargetTransformer } from './build-post-target-transformer'; describe('buildPostTargetTransformer', () => { - 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); - - // 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: 'apps/myapp/vite.config.ts', + 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'], + }, }, }; - export default config;" - `); - expect(target).toMatchInlineSnapshot(` - { - "options": { - "config-dir": ".storybook", - "output-dir": "../../dist/storybook/myapp", - }, - } - `); + 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;" + `); + }); }); - 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'], + 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); - tree.write('apps/myapp/dev/.storybook/main.ts', storybookConfigFileV17); - - // 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: 'apps/myapp/vite.config.ts', + }; + + 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'], + }, }, }; - export default config;" - `); - expect(target).toMatchInlineSnapshot(` - { - "configurations": { - "dev": { - "config-dir": "./dev/.storybook", - "output-dir": "../../dist/storybook/myapp/dev", + 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'], }, - "options": { - "config-dir": ".storybook", - "output-dir": "../../dist/storybook/myapp", + }; + + 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'], }, - } - `); - 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', - }, + configurations: { + dev: { + outputDir: 'dist/storybook/myapp/dev', + configDir: 'apps/myapp/dev/.storybook', + docsMode: false, + staticDir: ['dev/assets'], }, }, }; - export default config;" - `); + 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 = `import type { StorybookConfig } from '@storybook/react-vite'; +const storybookConfigFileV17_ReactVite = `import type { StorybookConfig } from '@storybook/react-vite'; const config: StorybookConfig = { stories: ['../src/app/**/*.stories.@(js|jsx|ts|tsx|mdx)'], @@ -201,3 +596,37 @@ const config: StorybookConfig = { }; 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;`; diff --git a/packages/storybook/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts b/packages/storybook/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts index 3731955cc5b23..d4264a3e5f94b 100644 --- a/packages/storybook/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts +++ b/packages/storybook/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts @@ -7,9 +7,11 @@ 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 }; @@ -68,13 +70,18 @@ export function buildPostTargetTransformer(migrationLogs: AggregatedLog) { configuration.configDir !== toProjectRelativePath(defaultConfigDir, projectDetails.root) ) { - addConfigValuesToConfigFile( + const configFilePath = getConfigFilePath( tree, - getConfigFilePath( - tree, - joinPathFragments(projectDetails.root, configuration.configDir) - ), - configValues + joinPathFragments(projectDetails.root, configuration.configDir) + ); + addConfigValuesToConfigFile(tree, configFilePath, configValues); + ensureViteConfigPathIsRelative( + tree, + configFilePath, + projectDetails.projectName, + projectDetails.root, + '@nx/storybook:build', + migrationLogs ); } } @@ -111,6 +118,14 @@ export function buildPostTargetTransformer(migrationLogs: AggregatedLog) { getConfigFilePath(tree, defaultConfigDir), configValues ); + ensureViteConfigPathIsRelative( + tree, + getConfigFilePath(tree, defaultConfigDir), + projectDetails.projectName, + projectDetails.root, + '@nx/storybook:build', + migrationLogs + ); return target; }; @@ -168,14 +183,16 @@ function handlePropertiesFromTargetOptions( delete options.staticDir; } - for (const [prevKey, newKey] of Object.entries(STORYBOOK_PROP_MAPPINGS)) { + 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) { options[newKey] = options[prevKey]; delete options[prevKey]; } } - - // AST CONFIG PATH FOR VITE CONFIG FILES } function moveDocsModeToConfigFile( diff --git a/packages/storybook/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts b/packages/storybook/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts index 8df144587b24c..9fa8a7e08c3ac 100644 --- a/packages/storybook/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts +++ b/packages/storybook/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts @@ -1,7 +1,12 @@ -import { TargetConfiguration, Tree } from '@nx/devkit'; +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 { STORYBOOK_PROP_MAPPINGS } from './utils'; +import { + ensureViteConfigPathIsRelative, + getConfigFilePath, + getInstalledPackageVersionInfo, + STORYBOOK_PROP_MAPPINGS, +} from './utils'; export function servePostTargetTransformer(migrationLogs: AggregatedLog) { return ( target: TargetConfiguration, @@ -9,7 +14,16 @@ export function servePostTargetTransformer(migrationLogs: AggregatedLog) { 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, @@ -22,6 +36,20 @@ export function servePostTargetTransformer(migrationLogs: AggregatedLog) { 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, @@ -50,6 +78,15 @@ export function servePostTargetTransformer(migrationLogs: AggregatedLog) { } } + ensureViteConfigPathIsRelative( + tree, + getConfigFilePath(tree, defaultConfigDir), + projectDetails.projectName, + projectDetails.root, + '@nx/storybook:storybook', + migrationLogs + ); + return target; }; } @@ -85,7 +122,16 @@ function handlePropertiesFromTargetOptions( delete options.open; } - for (const [prevKey, newKey] of Object.entries(STORYBOOK_PROP_MAPPINGS)) { + 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) { options[newKey] = options[prevKey]; delete options[prevKey]; diff --git a/packages/storybook/src/generators/convert-to-inferred/lib/utils.ts b/packages/storybook/src/generators/convert-to-inferred/lib/utils.ts index 8534104991d0a..1475607121f41 100644 --- a/packages/storybook/src/generators/convert-to-inferred/lib/utils.ts +++ b/packages/storybook/src/generators/convert-to-inferred/lib/utils.ts @@ -1,6 +1,9 @@ -import type { Tree } from 'nx/src/generators/tree'; import { tsquery } from '@phenomnomnominal/tsquery'; -import { joinPathFragments } from 'nx/src/utils/path'; +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 [ @@ -26,10 +29,11 @@ export function addConfigValuesToConfigFile( const importNodes = tsquery(ast, IMPORT_PROPERTY_SELECTOR, { visitAllChildren: true, }); - if (importNodes.length === 0) { - return; + let startPosition = 0; + if (importNodes.length !== 0) { + const lastImportNode = importNodes[importNodes.length - 1]; + startPosition = lastImportNode.getEnd(); } - const lastImportNode = importNodes[importNodes.length - 1]; const configValuesString = ` // These options were migrated by @nx/storybook:convert-to-inferred from the project.json file. @@ -45,28 +49,152 @@ export function addConfigValuesToConfigFile( tree.write( configFile, - `${configFileContents.slice(0, lastImportNode.getEnd())} + `${configFileContents.slice(0, startPosition)} ${configValuesString} - ${configFileContents.slice(lastImportNode.getEnd())}` + ${configFileContents.slice(startPosition)}` ); } export const STORYBOOK_PROP_MAPPINGS = { - 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', + 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, + dirname(configPath) + ); + + 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; +}