diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 35ffbb0d3e..6380a6e27e 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -9593,6 +9593,14 @@ "isExternal": false, "disableCollapsible": false }, + { + "id": "convert-to-inferred", + "path": "/nx-api/vite/generators/convert-to-inferred", + "name": "convert-to-inferred", + "children": [], + "isExternal": false, + "disableCollapsible": false + }, { "id": "vitest", "path": "/nx-api/vite/generators/vitest", diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 5a20b749a9..97326f5378 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2968,6 +2968,15 @@ "path": "/nx-api/vite/generators/setup-paths-plugin", "type": "generator" }, + "/nx-api/vite/generators/convert-to-inferred": { + "description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "file": "generated/packages/vite/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/vite/src/generators/convert-to-inferred/schema.json", + "path": "/nx-api/vite/generators/convert-to-inferred", + "type": "generator" + }, "/nx-api/vite/generators/vitest": { "description": "Generate a vitest configuration.", "file": "generated/packages/vite/generators/vitest.json", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 83b23013ff..0031c2ff4d 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2936,6 +2936,15 @@ "path": "vite/generators/setup-paths-plugin", "type": "generator" }, + { + "description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "file": "generated/packages/vite/generators/convert-to-inferred.json", + "hidden": false, + "name": "convert-to-inferred", + "originalFilePath": "/packages/vite/src/generators/convert-to-inferred/schema.json", + "path": "vite/generators/convert-to-inferred", + "type": "generator" + }, { "description": "Generate a vitest configuration.", "file": "generated/packages/vite/generators/vitest.json", diff --git a/docs/generated/packages/vite/generators/convert-to-inferred.json b/docs/generated/packages/vite/generators/convert-to-inferred.json new file mode 100644 index 0000000000..a6d2c1445d --- /dev/null +++ b/docs/generated/packages/vite/generators/convert-to-inferred.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": "NxViteConvertToInferred", + "description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "title": "Convert Vite project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/vite:*` executors to use `@nx/vite/plugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + }, + "presets": [] + }, + "description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "implementation": "/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts", + "aliases": [], + "hidden": false, + "path": "/packages/vite/src/generators/convert-to-inferred/schema.json", + "type": "generator" +} diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 60feafbc67..ca75c6e88b 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -676,6 +676,7 @@ - [init](/nx-api/vite/generators/init) - [configuration](/nx-api/vite/generators/configuration) - [setup-paths-plugin](/nx-api/vite/generators/setup-paths-plugin) + - [convert-to-inferred](/nx-api/vite/generators/convert-to-inferred) - [vitest](/nx-api/vite/generators/vitest) - [vue](/nx-api/vue) - [documents](/nx-api/vue/documents) diff --git a/packages/vite/generators.json b/packages/vite/generators.json index 8bc9cdef31..8da210d845 100644 --- a/packages/vite/generators.json +++ b/packages/vite/generators.json @@ -21,6 +21,11 @@ "schema": "./src/generators/setup-paths-plugin/schema.json", "description": "Sets up the nxViteTsPaths plugin to enable support for workspace libraries." }, + "convert-to-inferred": { + "factory": "./src/generators/convert-to-inferred/convert-to-inferred", + "schema": "./src/generators/convert-to-inferred/schema.json", + "description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target." + }, "vitest": { "factory": "./src/generators/vitest/vitest-generator#vitestGeneratorInternal", "schema": "./src/generators/vitest/schema.json", diff --git a/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.spec.ts b/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.spec.ts new file mode 100644 index 0000000000..f603d4af2b --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.spec.ts @@ -0,0 +1,634 @@ +import { + getRelativeProjectJsonSchemaPath, + updateProjectConfiguration, +} from 'nx/src/generators/utils/project-configuration'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { convertToInferred } from './convert-to-inferred'; +import { + addProjectConfiguration as _addProjectConfiguration, + type ExpandedPluginConfiguration, + joinPathFragments, + type ProjectConfiguration, + type ProjectGraph, + readNxJson, + readProjectConfiguration, + type Tree, + updateNxJson, + writeJson, +} from '@nx/devkit'; +import { TempFs } from '@nx/devkit/internal-testing-utils'; +import { join } from 'node:path'; + +let fs: TempFs; + +let projectGraph: ProjectGraph; + +let mockedConfigs = {}; +const getMockedConfig = ( + opts: { configFile: string; mode: 'development' }, + target: string +) => { + const relativeConfigFile = opts.configFile.replace(`${fs.tempDir}/`, ''); + return Promise.resolve({ + path: opts.configFile, + config: mockedConfigs[relativeConfigFile], + build: mockedConfigs[relativeConfigFile]['build'], + test: mockedConfigs[relativeConfigFile]['test'], + dependencies: [], + }); +}; + +jest.mock('vite', () => ({ + resolveConfig: jest.fn().mockImplementation(getMockedConfig), +})); + +jest.mock('../../utils/executor-utils', () => ({ + loadViteDynamicImport: jest.fn().mockImplementation(() => ({ + resolveConfig: jest.fn().mockImplementation(getMockedConfig), + })), +})); + +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return 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 addProjectConfiguration( + tree: Tree, + name: string, + project: ProjectConfiguration +) { + _addProjectConfiguration(tree, name, project); + projectGraph.nodes[name] = { + name: name, + type: project.projectType === 'application' ? 'app' : 'lib', + data: { + projectType: project.projectType, + root: project.root, + targets: project.targets, + }, + }; +} + +interface CreateViteTestProjectOptions { + appName: string; + appRoot: string; + buildTargetName: string; + serveTargetName: string; + previewTargetName: string; + testTargetName: string; + outputPath: string; +} + +const defaultCreateViteTestProjectOptions: CreateViteTestProjectOptions = { + appName: 'myapp', + appRoot: 'myapp', + buildTargetName: 'build', + serveTargetName: 'serve', + previewTargetName: 'preview', + testTargetName: 'test', + outputPath: '{workspaceRoot}/dist/myapp', +}; + +function createTestProject( + tree: Tree, + opts: Partial = defaultCreateViteTestProjectOptions +) { + let projectOpts = { ...defaultCreateViteTestProjectOptions, ...opts }; + const project: ProjectConfiguration = { + name: projectOpts.appName, + root: projectOpts.appRoot, + projectType: 'application', + targets: { + [projectOpts.buildTargetName]: { + executor: '@nx/vite:build', + outputs: [projectOpts.outputPath], + options: { + configFile: `${projectOpts.appRoot}/vite.config.ts`, + }, + }, + [projectOpts.serveTargetName]: { + executor: '@nx/vite:dev-server', + options: { + buildTarget: `${projectOpts.appName}:${projectOpts.buildTargetName}`, + }, + }, + [projectOpts.previewTargetName]: { + executor: '@nx/vite:preview-server', + options: { + buildTarget: `${projectOpts.appName}:${projectOpts.buildTargetName}`, + }, + }, + [projectOpts.testTargetName]: { + executor: '@nx/vite:test', + options: { + configFile: `${projectOpts.appRoot}/vite.config.ts`, + }, + }, + }, + }; + + const viteConfigContents = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});`; + + tree.write(`${projectOpts.appRoot}/vite.config.ts`, viteConfigContents); + fs.createFileSync( + `${projectOpts.appRoot}/vite.config.ts`, + viteConfigContents + ); + tree.write(`${projectOpts.appRoot}/index.html`, ``); + fs.createFileSync(`${projectOpts.appRoot}/index.html`, ``); + + mockedConfigs[`${projectOpts.appRoot}/vite.config.ts`] = { + root: `${projectOpts.appRoot}`, + cacheDir: `../../node_modules/.vite/${projectOpts.appName}`, + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + build: { + outDir: `../../dist/${projectOpts.appRoot}`, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: `../../coverage/${projectOpts.appRoot}`, + provider: 'v8', + }, + }, + }; + + jest.doMock( + join(fs.tempDir, `${projectOpts.appRoot}/vite.config.ts`), + () => ({ + default: mockedConfigs[`${projectOpts.appRoot}/vite.config.ts`], + }), + { + virtual: true, + } + ); + + addProjectConfiguration(tree, project.name, project); + fs.createFileSync( + `${projectOpts.appRoot}/project.json`, + JSON.stringify(project) + ); + return project; +} + +describe('Vite - Convert Executors To Plugin', () => { + let tree: Tree; + + beforeEach(() => { + fs = new TempFs('vite'); + tree = createTreeWithEmptyWorkspace(); + tree.root = fs.tempDir; + mockedConfigs = {}; + + projectGraph = { + nodes: {}, + dependencies: {}, + externalNodes: {}, + }; + }); + + afterEach(() => { + fs.reset(); + }); + + describe('--project', () => { + it('should setup a new Vite plugin and only migrate one specific project', async () => { + // ARRANGE + const existingProject = createTestProject(tree, { + appRoot: 'existing', + appName: 'existing', + buildTargetName: 'build', + }); + const project = createTestProject(tree, { + buildTargetName: 'build-base', + }); + const secondProject = createTestProject(tree, { + appRoot: 'second', + appName: 'second', + buildTargetName: 'build-base', + }); + const thirdProject = createTestProject(tree, { + appRoot: 'third', + appName: 'third', + buildTargetName: 'package', + }); + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/vite/plugin', + options: { + buildTargetName: 'build', + testTargetName: 'test', + previewTargetName: 'preview', + serveTargetName: 'serve', + }, + }); + updateNxJson(tree, nxJson); + + // ACT + await convertToInferred(tree, { project: 'myapp', skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets).toMatchInlineSnapshot(` + { + "build-base": { + "options": { + "config": "./vite.config.ts", + }, + }, + "test": { + "options": { + "config": "./vite.config.ts", + }, + }, + } + `); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const addedTestVitePlugin = nxJsonPlugins.find((plugin) => { + if ( + typeof plugin !== 'string' && + plugin.plugin === '@nx/vite/plugin' && + plugin.include?.length === 1 + ) { + return true; + } + }); + expect(addedTestVitePlugin).toBeTruthy(); + expect( + (addedTestVitePlugin as ExpandedPluginConfiguration).include + ).toEqual(['myapp/**/*']); + }); + }); + + describe('--all', () => { + it('should successfully migrate a project using Vite executors to plugin', async () => { + const project = createTestProject(tree); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + const targetKeys = Object.keys(updatedProject.targets); + expect(targetKeys).not.toContain('e2e'); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const hasVitePlugin = nxJsonPlugins.find((plugin) => + typeof plugin === 'string' + ? plugin === '@nx/vite/plugin' + : plugin.plugin === '@nx/vite/plugin' + ); + expect(hasVitePlugin).toBeTruthy(); + if (typeof hasVitePlugin !== 'string') { + [ + ['buildTargetName', 'build'], + ['serveTargetName', 'serve'], + ['previewTargetName', 'preview'], + ['testTargetName', 'test'], + ].forEach(([targetOptionName, targetName]) => { + expect(hasVitePlugin.options[targetOptionName]).toEqual(targetName); + }); + } + }); + + it('should setup Vite plugin to match projects', async () => { + // ARRANGE + const project = createTestProject(tree, { + buildTargetName: 'bundle', + }); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets).toMatchInlineSnapshot(` + { + "bundle": { + "options": { + "config": "./vite.config.ts", + }, + }, + "test": { + "options": { + "config": "./vite.config.ts", + }, + }, + } + `); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const hasVitePlugin = nxJsonPlugins.find((plugin) => + typeof plugin === 'string' + ? plugin === '@nx/vite/plugin' + : plugin.plugin === '@nx/vite/plugin' + ); + expect(hasVitePlugin).toBeTruthy(); + if (typeof hasVitePlugin !== 'string') { + [ + ['buildTargetName', 'bundle'], + ['serveTargetName', 'serve'], + ['previewTargetName', 'preview'], + ['testTargetName', 'test'], + ].forEach(([targetOptionName, targetName]) => { + expect(hasVitePlugin.options[targetOptionName]).toEqual(targetName); + }); + } + }); + + it('should setup a new Vite plugin to match only projects migrated', async () => { + // ARRANGE + const existingProject = createTestProject(tree, { + appRoot: 'existing', + appName: 'existing', + buildTargetName: 'build', + }); + const project = createTestProject(tree, { + buildTargetName: 'bundle', + }); + const secondProject = createTestProject(tree, { + appRoot: 'second', + appName: 'second', + buildTargetName: 'bundle', + }); + const thirdProject = createTestProject(tree, { + appRoot: 'third', + appName: 'third', + buildTargetName: 'build-base', + }); + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + nxJson.plugins.push({ + plugin: '@nx/vite/plugin', + options: { + buildTargetName: 'build', + serveTargetName: 'serve', + previewTargetName: 'preview', + testTargetName: 'test', + }, + }); + updateNxJson(tree, nxJson); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets).toMatchInlineSnapshot(` + { + "bundle": { + "options": { + "config": "./vite.config.ts", + }, + }, + "test": { + "options": { + "config": "./vite.config.ts", + }, + }, + } + `); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const addedTestVitePlugin = nxJsonPlugins.find((plugin) => { + if ( + typeof plugin !== 'string' && + plugin.plugin === '@nx/vite/plugin' && + plugin.include?.length === 2 + ) { + return true; + } + }); + expect(addedTestVitePlugin).toBeTruthy(); + expect( + (addedTestVitePlugin as ExpandedPluginConfiguration).include + ).toEqual(['myapp/**/*', 'second/**/*']); + + const addedIntegrationVitePlugin = nxJsonPlugins.find((plugin) => { + if ( + typeof plugin !== 'string' && + plugin.plugin === '@nx/vite/plugin' && + plugin.include?.length === 1 + ) { + return true; + } + }); + expect(addedIntegrationVitePlugin).toBeTruthy(); + expect( + (addedIntegrationVitePlugin as ExpandedPluginConfiguration).include + ).toEqual(['third/**/*']); + }); + + it('should keep Vite options in project.json', async () => { + // ARRANGE + const project = createTestProject(tree); + project.targets.build.options.mode = 'development'; + updateProjectConfiguration(tree, project.name, project); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.build).toMatchInlineSnapshot(` + { + "options": { + "config": "./vite.config.ts", + "mode": "development", + }, + } + `); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const hasVitePlugin = nxJsonPlugins.find((plugin) => + typeof plugin === 'string' + ? plugin === '@nx/vite/plugin' + : plugin.plugin === '@nx/vite/plugin' + ); + expect(hasVitePlugin).toBeTruthy(); + if (typeof hasVitePlugin !== 'string') { + [ + ['buildTargetName', 'build'], + ['serveTargetName', 'serve'], + ['previewTargetName', 'preview'], + ['testTargetName', 'test'], + ].forEach(([targetOptionName, targetName]) => { + expect(hasVitePlugin.options[targetOptionName]).toEqual(targetName); + }); + } + }); + + it('should add Vite options found in targetDefaults for the executor to the project.json', async () => { + // ARRANGE + const nxJson = readNxJson(tree); + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['@nx/vite:build'] = { + options: { + mode: 'production', + }, + }; + updateNxJson(tree, nxJson); + const project = createTestProject(tree); + + // ACT + await convertToInferred(tree, { skipFormat: true }); + + // ASSERT + // project.json modifications + const updatedProject = readProjectConfiguration(tree, project.name); + expect(updatedProject.targets.build).toMatchInlineSnapshot(` + { + "options": { + "config": "./vite.config.ts", + "mode": "production", + }, + } + `); + + // nx.json modifications + const nxJsonPlugins = readNxJson(tree).plugins; + const hasVitePlugin = nxJsonPlugins.find((plugin) => + typeof plugin === 'string' + ? plugin === '@nx/vite/plugin' + : plugin.plugin === '@nx/vite/plugin' + ); + expect(hasVitePlugin).toBeTruthy(); + if (typeof hasVitePlugin !== 'string') { + [ + ['buildTargetName', 'build'], + ['serveTargetName', 'serve'], + ['previewTargetName', 'preview'], + ['testTargetName', 'test'], + ].forEach(([targetOptionName, targetName]) => { + expect(hasVitePlugin.options[targetOptionName]).toEqual(targetName); + }); + } + }); + }); +}); diff --git a/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts b/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts new file mode 100644 index 0000000000..3c85db6412 --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts @@ -0,0 +1,105 @@ +import { createProjectGraphAsync, formatFiles, type Tree } from '@nx/devkit'; +import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator'; +import { createNodesV2, VitePluginOptions } from '../../plugins/plugin'; +import { buildPostTargetTransformer } from './lib/build-post-target-transformer'; +import { servePostTargetTransformer } from './lib/serve-post-target-transformer'; +import { previewPostTargetTransformer } from './lib/preview-post-target-transformer'; +import { testPostTargetTransformer } from './lib/test-post-target-transformer'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; + +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 migrateExecutorToPlugin( + tree, + projectGraph, + '@nx/vite:build', + '@nx/vite/plugin', + (targetName) => ({ + buildTargetName: targetName, + serveTargetName: 'serve', + previewTargetName: 'preview', + testTargetName: 'test', + serveStaticTargetName: 'serve-static', + }), + buildPostTargetTransformer, + createNodesV2, + options.project + ); + const migratedServeProjects = + await migrateExecutorToPlugin( + tree, + projectGraph, + '@nx/vite:dev-server', + '@nx/vite/plugin', + (targetName) => ({ + buildTargetName: 'build', + serveTargetName: targetName, + previewTargetName: 'preview', + testTargetName: 'test', + serveStaticTargetName: 'serve-static', + }), + servePostTargetTransformer(migrationLogs), + createNodesV2, + options.project + ); + const migratedPreviewProjects = + await migrateExecutorToPlugin( + tree, + projectGraph, + '@nx/vite:preview-server', + '@nx/vite/plugin', + (targetName) => ({ + buildTargetName: 'build', + serveTargetName: 'serve', + previewTargetName: targetName, + testTargetName: 'test', + serveStaticTargetName: 'serve-static', + }), + previewPostTargetTransformer(migrationLogs), + createNodesV2, + options.project + ); + const migratedTestProjects = await migrateExecutorToPlugin( + tree, + projectGraph, + '@nx/vite:test', + '@nx/vite/plugin', + (targetName) => ({ + buildTargetName: 'build', + serveTargetName: 'serve', + previewTargetName: 'preview', + testTargetName: targetName, + serveStaticTargetName: 'serve-static', + }), + testPostTargetTransformer, + createNodesV2, + options.project + ); + + const migratedProjects = + migratedBuildProjects.size + + migratedServeProjects.size + + migratedPreviewProjects.size + + migratedTestProjects.size; + + if (migratedProjects === 0) { + throw new Error('Could not find any targets to migrate.'); + } + + if (!options.skipFormat) { + await formatFiles(tree); + } + + return () => { + migrationLogs.flushLogs(); + }; +} + +export default convertToInferred; diff --git a/packages/vite/src/generators/convert-to-inferred/lib/__snapshots__/build-post-target-transformer.spec.ts.snap b/packages/vite/src/generators/convert-to-inferred/lib/__snapshots__/build-post-target-transformer.spec.ts.snap new file mode 100644 index 0000000000..b91d6c1abb --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/lib/__snapshots__/build-post-target-transformer.spec.ts.snap @@ -0,0 +1,271 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`buildPostTargetTransformer moveBuildLibsFromSourceToViteConfig should add buildLibsFromSource to existing nxViteTsPaths plugin with existing options 1`] = ` +"/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths({buildLibsFromSource: options.buildLibsFromSource, debug: true })], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});" +`; + +exports[`buildPostTargetTransformer moveBuildLibsFromSourceToViteConfig should add buildLibsFromSource to existing nxViteTsPaths plugin with no existing options 1`] = ` +"/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});" +`; + +exports[`buildPostTargetTransformer moveBuildLibsFromSourceToViteConfig should add buildLibsFromSource to new nxViteTsPaths plugin when the plugin is not added 1`] = ` +"import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin"; +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),react()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});" +`; + +exports[`buildPostTargetTransformer moveBuildLibsFromSourceToViteConfig should add buildLibsFromSource to new nxViteTsPaths plugin when the plugins property does not exist 1`] = ` +"import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin"; +/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({plugins: [nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),], + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});" +`; + +exports[`buildPostTargetTransformer should remove the correct options and move the AST options to the vite config file correctly and remove outputs when they match inferred 1`] = ` +"/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + + // These options were migrated by @nx/vite:convert-to-inferred from the project.json file. + const configValues = {"default":{"buildLibsFromSource":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] ?? {}) + } + + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});" +`; diff --git a/packages/vite/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts b/packages/vite/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts new file mode 100644 index 0000000000..b204f4e94d --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/lib/build-post-target-transformer.spec.ts @@ -0,0 +1,414 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { + buildPostTargetTransformer, + moveBuildLibsFromSourceToViteConfig, +} from './build-post-target-transformer'; + +describe('buildPostTargetTransformer', () => { + it('should remove the correct options and move the AST options to the vite config file correctly and remove outputs when they match inferred', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + const targetConfiguration = { + outputs: ['{options.outputPath}'], + options: { + outputPath: 'build/apps/myapp', + configFile: 'vite.config.ts', + buildLibsFromSource: true, + skipTypeCheck: false, + watch: true, + generatePackageJson: true, + includeDevDependenciesInPackageJson: false, + tsConfig: 'apps/myapp/tsconfig.json', + }, + }; + + const inferredTargetConfiguration = { + outputs: ['{projectRoot}/{options.outDir}'], + }; + + tree.write('vite.config.ts', viteConfigFileV17); + + // ACT + const target = buildPostTargetTransformer( + targetConfiguration, + tree, + { + projectName: 'myapp', + root: 'apps/myapp', + }, + inferredTargetConfiguration + ); + + // ASSERT + const configFile = tree.read('vite.config.ts', 'utf-8'); + expect(configFile).toMatchSnapshot(); + expect(target).toMatchInlineSnapshot(` + { + "options": { + "config": "../../vite.config.ts", + "outDir": "../../build/apps/myapp", + "watch": true, + }, + } + `); + }); + + it('should add inferred outputs when a custom output exists', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + + const targetConfiguration = { + outputs: ['{options.outputPath}', '{workspaceRoot}/my/custom/path'], + options: { + outputPath: 'build/apps/myapp', + configFile: 'vite.config.ts', + buildLibsFromSource: true, + skipTypeCheck: false, + watch: true, + generatePackageJson: true, + includeDevDependenciesInPackageJson: false, + tsConfig: 'apps/myapp/tsconfig.json', + }, + }; + + const inferredTargetConfiguration = { + outputs: ['{projectRoot}/{options.outDir}'], + }; + + tree.write('vite.config.ts', viteConfigFileV17); + + // ACT + const target = buildPostTargetTransformer( + targetConfiguration, + tree, + { + projectName: 'myapp', + root: 'apps/myapp', + }, + inferredTargetConfiguration + ); + + // ASSERT + expect(target).toMatchInlineSnapshot(` + { + "options": { + "config": "../../vite.config.ts", + "outDir": "../../build/apps/myapp", + "watch": true, + }, + "outputs": [ + "{projectRoot}/{options.outDir}", + "{workspaceRoot}/my/custom/path", + ], + } + `); + }); + + describe('moveBuildLibsFromSourceToViteConfig', () => { + it('should add buildLibsFromSource to existing nxViteTsPaths plugin with no existing options', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write('vite.config.ts', viteConfigFileV17); + + // ACT + moveBuildLibsFromSourceToViteConfig(tree, 'vite.config.ts'); + + // ASSERT + const newContents = tree.read('vite.config.ts', 'utf-8'); + expect(newContents).toContain( + 'nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource })' + ); + expect(newContents).toMatchSnapshot(); + }); + + it('should add buildLibsFromSource to existing nxViteTsPaths plugin with existing options', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write('vite.config.ts', viteConfigFileV17NxViteTsPathsOpts); + + // ACT + moveBuildLibsFromSourceToViteConfig(tree, 'vite.config.ts'); + + // ASSERT + const newContents = tree.read('vite.config.ts', 'utf-8'); + expect(newContents).toContain( + 'nxViteTsPaths({buildLibsFromSource: options.buildLibsFromSource, debug: true })' + ); + expect(newContents).toMatchSnapshot(); + }); + + it('should add buildLibsFromSource to new nxViteTsPaths plugin when the plugin is not added', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write('vite.config.ts', viteConfigFileV17NoNxViteTsPaths); + + // ACT + moveBuildLibsFromSourceToViteConfig(tree, 'vite.config.ts'); + + // ASSERT + const newContents = tree.read('vite.config.ts', 'utf-8'); + expect(newContents).toContain( + 'nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource })' + ); + expect(newContents).toMatchSnapshot(); + }); + + it('should add buildLibsFromSource to new nxViteTsPaths plugin when the plugins property does not exist', () => { + // ARRANGE + const tree = createTreeWithEmptyWorkspace(); + tree.write('vite.config.ts', viteConfigFileV17NoPlugins); + + // ACT + moveBuildLibsFromSourceToViteConfig(tree, 'vite.config.ts'); + + // ASSERT + const newContents = tree.read('vite.config.ts', 'utf-8'); + expect(newContents).toContain( + 'nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource })' + ); + expect(newContents).toMatchSnapshot(); + }); + }); +}); + +const viteConfigFileV17 = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});`; +const viteConfigFileV17NoOutDir = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});`; +const viteConfigFileV17NxViteTsPathsOpts = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths({ debug: true })], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});`; +const viteConfigFileV17NoNxViteTsPaths = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react()], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});`; +const viteConfigFileV17NoPlugins = `/// +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/myapp', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + build: { + outDir: '../../dist/apps/myapp', + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + + test: { + globals: true, + cache: { + dir: '../../node_modules/.vitest', + }, + environment: 'jsdom', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/myapp', + provider: 'v8', + }, + }, +});`; diff --git a/packages/vite/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts b/packages/vite/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts new file mode 100644 index 0000000000..9bf4bb8a21 --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/lib/build-post-target-transformer.ts @@ -0,0 +1,234 @@ +import { type TargetConfiguration, type Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { extname } from 'path/posix'; +import { + addConfigValuesToViteConfig, + getViteConfigPath, + toProjectRelativePath, +} from './utils'; +import { processTargetOutputs } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils'; + +export function buildPostTargetTransformer( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTargetConfiguration: TargetConfiguration +) { + let viteConfigPath = getViteConfigPath(tree, projectDetails.root); + + const configValues: Record> = { + default: {}, + }; + + if (target.options) { + if (target.options.configFile) { + viteConfigPath = target.options.configFile; + } + + removePropertiesFromTargetOptions( + tree, + target.options, + viteConfigPath, + projectDetails.root, + configValues['default'], + true + ); + } + + if (target.configurations) { + for (const configurationName in target.configurations) { + const configuration = target.configurations[configurationName]; + configValues[configuration] = {}; + removePropertiesFromTargetOptions( + tree, + configuration, + viteConfigPath, + projectDetails.root, + configValues[configuration] + ); + + if (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: 'outDir', oldName: 'outputPath' }], + inferredTargetConfiguration, + { + projectName: projectDetails.projectName, + projectRoot: projectDetails.root, + } + ); + } + + if ( + target.inputs && + target.inputs.every((i) => i === 'production' || i === '^production') + ) { + delete target.inputs; + } + + addConfigValuesToViteConfig(tree, viteConfigPath, configValues); + + return target; +} + +function removePropertiesFromTargetOptions( + tree: Tree, + targetOptions: any, + viteConfigPath: string, + projectRoot: string, + configValues: Record, + defaultOptions = false +) { + if ('configFile' in targetOptions) { + targetOptions.config = toProjectRelativePath( + targetOptions.configFile, + projectRoot + ); + delete targetOptions.configFile; + } + if (targetOptions.outputPath) { + targetOptions.outDir = toProjectRelativePath( + targetOptions.outputPath, + projectRoot + ); + + delete targetOptions.outputPath; + } + if ('buildLibsFromSource' in targetOptions) { + configValues['buildLibsFromSource'] = targetOptions.buildLibsFromSource; + + if (defaultOptions) { + moveBuildLibsFromSourceToViteConfig(tree, viteConfigPath); + } + delete targetOptions.buildLibsFromSource; + } + if ('skipTypeCheck' in targetOptions) { + delete targetOptions.skipTypeCheck; + } + if ('generatePackageJson' in targetOptions) { + delete targetOptions.generatePackageJson; + } + if ('includeDevDependenciesInPackageJson' in targetOptions) { + delete targetOptions.includeDevDependenciesInPackageJson; + } + if ('tsConfig' in targetOptions) { + delete targetOptions.tsConfig; + } +} + +export function moveBuildLibsFromSourceToViteConfig( + tree: Tree, + configPath: string +) { + const PLUGINS_PROPERTY_SELECTOR = + 'PropertyAssignment:has(Identifier[name=plugins])'; + const PLUGINS_NX_VITE_TS_PATHS_SELECTOR = + 'PropertyAssignment:has(Identifier[name=plugins]) CallExpression:has(Identifier[name=nxViteTsPaths])'; + const BUILD_LIBS_FROM_SOURCE_SELECTOR = + 'PropertyAssignment:has(Identifier[name=plugins]) CallExpression:has(Identifier[name=nxViteTsPaths]) ObjectLiteralExpression > PropertyAssignment:has(Identifier[name=buildLibsFromSource])'; + + const nxViteTsPathsImport = + extname(configPath) === 'js' + ? 'const {nxViteTsPaths} = require("@nx/vite/plugins/nx-tsconfig-paths.plugin");' + : 'import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";'; + const plugin = `nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),`; + + const viteConfigContents = tree.read(configPath, 'utf-8'); + let newViteConfigContents = viteConfigContents; + + const ast = tsquery.ast(viteConfigContents); + const buildLibsFromSourceNodes = tsquery( + ast, + BUILD_LIBS_FROM_SOURCE_SELECTOR, + { visitAllChildren: true } + ); + if (buildLibsFromSourceNodes.length > 0) { + return; + } + + const nxViteTsPathsNodes = tsquery(ast, PLUGINS_NX_VITE_TS_PATHS_SELECTOR, { + visitAllChildren: true, + }); + if (nxViteTsPathsNodes.length === 0) { + const pluginsNodes = tsquery(ast, PLUGINS_PROPERTY_SELECTOR, { + visitAllChildren: true, + }); + if (pluginsNodes.length === 0) { + // Add plugin property + const configNodes = tsquery( + ast, + 'CallExpression:has(Identifier[name=defineConfig]) > ObjectLiteralExpression', + { visitAllChildren: true } + ); + if (configNodes.length === 0) { + return; + } + + newViteConfigContents = `${nxViteTsPathsImport}\n${viteConfigContents.slice( + 0, + configNodes[0].getStart() + 1 + )}plugins: [${plugin}],${viteConfigContents.slice( + configNodes[0].getStart() + 1 + )}`; + } else { + // Add nxViteTsPaths plugin + + const pluginsArrayNodes = tsquery( + pluginsNodes[0], + 'ArrayLiteralExpression' + ); + if (pluginsArrayNodes.length === 0) { + return; + } + + newViteConfigContents = `${nxViteTsPathsImport}\n${viteConfigContents.slice( + 0, + pluginsArrayNodes[0].getStart() + 1 + )}${plugin}${viteConfigContents.slice( + pluginsArrayNodes[0].getStart() + 1 + )}`; + } + } else { + const pluginOptionsNodes = tsquery( + nxViteTsPathsNodes[0], + 'ObjectLiteralExpression' + ); + if (pluginOptionsNodes.length === 0) { + // Add the options + newViteConfigContents = `${viteConfigContents.slice( + 0, + nxViteTsPathsNodes[0].getStart() + )}${plugin}${viteConfigContents.slice(nxViteTsPathsNodes[0].getEnd())}`; + } else { + // update the object + newViteConfigContents = `${viteConfigContents.slice( + 0, + pluginOptionsNodes[0].getStart() + 1 + )}buildLibsFromSource: options.buildLibsFromSource, ${viteConfigContents.slice( + pluginOptionsNodes[0].getStart() + 1 + )}`; + } + } + + tree.write(configPath, newViteConfigContents); +} diff --git a/packages/vite/src/generators/convert-to-inferred/lib/preview-post-target-transformer.ts b/packages/vite/src/generators/convert-to-inferred/lib/preview-post-target-transformer.ts new file mode 100644 index 0000000000..c0bee4801c --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/lib/preview-post-target-transformer.ts @@ -0,0 +1,76 @@ +import { type TargetConfiguration, type Tree } from '@nx/devkit'; +import { getViteConfigPath } from './utils'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; + +export function previewPostTargetTransformer(migrationLogs: AggregatedLog) { + return ( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTargetConfiguration: TargetConfiguration + ) => { + const viteConfigPath = getViteConfigPath(tree, projectDetails.root); + + if (target.options) { + removePropertiesFromTargetOptions( + target.options, + projectDetails.projectName, + migrationLogs + ); + } + + if (target.configurations) { + for (const configurationName in target.configurations) { + const configuration = target.configurations[configurationName]; + removePropertiesFromTargetOptions( + configuration, + projectDetails.projectName, + migrationLogs + ); + + if (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; + } + } + + return target; + }; +} + +function removePropertiesFromTargetOptions( + targetOptions: any, + projectName: string, + migrationLogs: AggregatedLog +) { + if ('buildTarget' in targetOptions) { + delete targetOptions.buildTarget; + } + + if ('staticFilePath' in targetOptions) { + delete targetOptions.staticFilePath; + } + + if ('proxyConfig' in targetOptions) { + migrationLogs.addLog({ + executorName: '@nx/vite:preview-server', + project: projectName, + log: `Encountered 'proxyConfig' in project.json. You will need to copy the contents of this file to the 'server.proxy' property in your Vite config file.`, + }); + delete targetOptions.proxyConfig; + } +} diff --git a/packages/vite/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts b/packages/vite/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts new file mode 100644 index 0000000000..6bdf366499 --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/lib/serve-post-target-transformer.ts @@ -0,0 +1,96 @@ +import { type TargetConfiguration, type Tree } from '@nx/devkit'; +import { getViteConfigPath } from './utils'; +import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util'; + +export function servePostTargetTransformer(migrationLogs: AggregatedLog) { + return ( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTargetConfiguration: TargetConfiguration + ) => { + const viteConfigPath = getViteConfigPath(tree, projectDetails.root); + + if (target.options) { + removePropertiesFromTargetOptions( + tree, + target.options, + viteConfigPath, + projectDetails.root, + projectDetails.projectName, + migrationLogs, + true + ); + } + + if (target.configurations) { + for (const configurationName in target.configurations) { + const configuration = target.configurations[configurationName]; + removePropertiesFromTargetOptions( + tree, + configuration, + viteConfigPath, + projectDetails.root, + projectDetails.projectName, + migrationLogs + ); + + if (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; + } + } + + return target; + }; +} + +function removePropertiesFromTargetOptions( + tree: Tree, + targetOptions: any, + viteConfigPath: string, + projectRoot: string, + projectName: string, + migrationLogs: AggregatedLog, + defaultOptions = false +) { + if ('buildTarget' in targetOptions) { + delete targetOptions.buildTarget; + } + + if ('buildLibsFromSource' in targetOptions) { + migrationLogs.addLog({ + executorName: '@nx/vite:dev-server', + project: projectName, + log: `Encountered 'buildLibsFromSource' in project.json. This property will be added to your Vite config file via the '@nx/vite:build' executor migration.`, + }); + delete targetOptions.buildLibsFromSource; + } + + if ('hmr' in targetOptions) { + delete targetOptions.hmr; + } + + if ('proxyConfig' in targetOptions) { + migrationLogs.addLog({ + executorName: '@nx/vite:dev-server', + project: projectName, + log: `Encountered 'proxyConfig' in project.json. You will need to copy the contents of this file to the 'server.proxy' property in your Vite config file.`, + }); + delete targetOptions.proxyConfig; + } +} diff --git a/packages/vite/src/generators/convert-to-inferred/lib/test-post-target-transformer.ts b/packages/vite/src/generators/convert-to-inferred/lib/test-post-target-transformer.ts new file mode 100644 index 0000000000..e99fd0458b --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/lib/test-post-target-transformer.ts @@ -0,0 +1,94 @@ +import { type TargetConfiguration, type Tree } from '@nx/devkit'; +import { toProjectRelativePath } from './utils'; +import { processTargetOutputs } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils'; + +export function testPostTargetTransformer( + target: TargetConfiguration, + tree: Tree, + projectDetails: { projectName: string; root: string }, + inferredTargetConfiguration: TargetConfiguration +) { + if (target.options) { + removePropertiesFromTargetOptions(target.options, projectDetails.root); + } + + if (target.configurations) { + for (const configurationName in target.configurations) { + const configuration = target.configurations[configurationName]; + removePropertiesFromTargetOptions(configuration, projectDetails.root); + + if (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: 'coverage.reportsDirectory', oldName: 'reportsDirectory' }], + inferredTargetConfiguration, + { + projectName: projectDetails.projectName, + projectRoot: projectDetails.root, + } + ); + } + + if ( + target.inputs && + target.inputs.every((i) => i === 'default' || i === '^production') + ) { + delete target.inputs; + } + + return target; +} + +function removePropertiesFromTargetOptions( + targetOptions: any, + projectRoot: string +) { + if ('configFile' in targetOptions) { + targetOptions.config = toProjectRelativePath( + targetOptions.configFile, + projectRoot + ); + delete targetOptions.configFile; + } + + if ('reportsDirectory' in targetOptions) { + if (targetOptions.reportsDirectory.startsWith('../')) { + targetOptions.reportsDirectory = targetOptions.reportsDirectory.replace( + /(\.\.\/)+/, + '' + ); + } + targetOptions['coverage.reportsDirectory'] = toProjectRelativePath( + targetOptions.reportsDirectory, + projectRoot + ); + delete targetOptions.reportsDirectory; + } + + if ('testFiles' in targetOptions) { + targetOptions.testNamePattern = `"/(${targetOptions.testFiles + .map((f) => f.replace('.', '\\.')) + .join('|')})/"`; + delete targetOptions.testFiles; + } +} diff --git a/packages/vite/src/generators/convert-to-inferred/lib/utils.ts b/packages/vite/src/generators/convert-to-inferred/lib/utils.ts new file mode 100644 index 0000000000..4af1ce0fa2 --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/lib/utils.ts @@ -0,0 +1,70 @@ +import { relative, resolve } from 'path/posix'; +import { workspaceRoot, type Tree, joinPathFragments } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; + +export function toProjectRelativePath( + path: string, + projectRoot: string +): string { + if (projectRoot === '.') { + // workspace and project root are the same, we normalize it to ensure it + // works with Jest since some paths only work when they start with `./` + return path.startsWith('.') ? path : `./${path}`; + } + + const relativePath = relative( + resolve(workspaceRoot, projectRoot), + resolve(workspaceRoot, path) + ); + + return relativePath.startsWith('.') ? relativePath : `./${relativePath}`; +} + +export function getViteConfigPath(tree: Tree, root: string) { + return [ + joinPathFragments(root, `vite.config.ts`), + joinPathFragments(root, `vite.config.cts`), + joinPathFragments(root, `vite.config.mts`), + joinPathFragments(root, `vite.config.js`), + joinPathFragments(root, `vite.config.cjs`), + joinPathFragments(root, `vite.config.mjs`), + ].find((f) => tree.exists(f)); +} + +export function addConfigValuesToViteConfig( + tree: Tree, + configFile: string, + configValues: Record> +) { + const IMPORT_PROPERTY_SELECTOR = 'ImportDeclaration'; + const viteConfigContents = tree.read(configFile, 'utf-8'); + + const ast = tsquery.ast(viteConfigContents); + // AST TO GET SECTION TO APPEND TO + const importNodes = tsquery(ast, IMPORT_PROPERTY_SELECTOR, { + visitAllChildren: true, + }); + if (importNodes.length === 0) { + return; + } + const lastImportNode = importNodes[importNodes.length - 1]; + + const configValuesString = ` + // These options were migrated by @nx/vite: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, + `${viteConfigContents.slice(0, lastImportNode.getEnd())} + ${configValuesString} + ${viteConfigContents.slice(lastImportNode.getEnd())}` + ); +} diff --git a/packages/vite/src/generators/convert-to-inferred/schema.json b/packages/vite/src/generators/convert-to-inferred/schema.json new file mode 100644 index 0000000000..565697ba83 --- /dev/null +++ b/packages/vite/src/generators/convert-to-inferred/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "NxViteConvertToInferred", + "description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.", + "title": "Convert Vite project from executor to plugin", + "type": "object", + "properties": { + "project": { + "type": "string", + "description": "The project to convert from using the `@nx/vite:*` executors to use `@nx/vite/plugin`.", + "x-priority": "important" + }, + "skipFormat": { + "type": "boolean", + "description": "Whether to format files at the end of the migration.", + "default": false + } + } +} diff --git a/packages/vite/src/plugins/plugin.spec.ts b/packages/vite/src/plugins/plugin.spec.ts index 7d734536fd..af2356902d 100644 --- a/packages/vite/src/plugins/plugin.spec.ts +++ b/packages/vite/src/plugins/plugin.spec.ts @@ -14,8 +14,7 @@ describe('@nx/vite/plugin', () => { let context: CreateNodesContext; describe('root project', () => { - let tempFs; - + let tempFs: TempFs; beforeEach(async () => { tempFs = new TempFs('vite-plugin-tests'); context = { @@ -62,8 +61,7 @@ describe('@nx/vite/plugin', () => { }); describe('not root project', () => { - let tempFs; - + let tempFs: TempFs; beforeEach(() => { tempFs = new TempFs('test'); context = {