From 4a5eb23302cc68ecedf2f4733f1e9aacfb14584e Mon Sep 17 00:00:00 2001 From: Jack Hsu Date: Tue, 11 Jun 2024 16:55:58 -0400 Subject: [PATCH] fix(bundling): vite init generator supports updating vite projects to use workspace libraries (#26503) This PR adds a `@nx/vite:setup-paths-plugin` generator to add `nxViteTsPaths` plugin to all vite config files in the workspace. This can also be used with init/add as follows: ```shell nx add @nx/vite --setupPathsPlugin nx g @nx/vite:init --setupPathsPlugin ``` Which takes a config such as: ```ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], }) ``` And updates it to: ```ts import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; export default defineConfig({ plugins: [react(), nxViteTsPaths()], }) ``` Taking into account ESM (default) and CJS (deprecated). ## Current Behavior ## Expected Behavior ## Related Issue(s) Fixes # --- docs/generated/manifests/menus.json | 8 + docs/generated/manifests/nx-api.json | 11 +- docs/generated/packages-metadata.json | 11 +- .../packages/vite/documents/overview.md | 6 + .../packages/vite/generators/init.json | 5 + .../vite/generators/setup-paths-plugin.json | 25 +++ .../packages/vite/generators/vitest.json | 2 +- docs/shared/packages/vite/vite-plugin.md | 6 + docs/shared/reference/sitemap.md | 1 + packages/vite/generators.json | 7 +- .../vite/src/generators/init/init.spec.ts | 31 +++- packages/vite/src/generators/init/init.ts | 5 + packages/vite/src/generators/init/schema.d.ts | 1 + packages/vite/src/generators/init/schema.json | 5 + .../setup-paths-plugin/lib/utils.ts | 0 .../generators/setup-paths-plugin/schema.d.ts | 9 + .../generators/setup-paths-plugin/schema.json | 14 ++ .../setup-paths-plugin.spec.ts | 79 +++++++++ .../setup-paths-plugin/setup-paths-plugin.ts | 156 ++++++++++++++++++ 19 files changed, 376 insertions(+), 6 deletions(-) create mode 100644 docs/generated/packages/vite/generators/setup-paths-plugin.json create mode 100644 packages/vite/src/generators/setup-paths-plugin/lib/utils.ts create mode 100644 packages/vite/src/generators/setup-paths-plugin/schema.d.ts create mode 100644 packages/vite/src/generators/setup-paths-plugin/schema.json create mode 100644 packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.spec.ts create mode 100644 packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.ts diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index 2d04847817..a73a8cf405 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -9577,6 +9577,14 @@ "isExternal": false, "disableCollapsible": false }, + { + "id": "setup-paths-plugin", + "path": "/nx-api/vite/generators/setup-paths-plugin", + "name": "setup-paths-plugin", + "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 5acb369ee3..9d2444a3df 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2950,8 +2950,17 @@ "path": "/nx-api/vite/generators/configuration", "type": "generator" }, + "/nx-api/vite/generators/setup-paths-plugin": { + "description": "Sets up the nxViteTsPaths plugin to enable support for workspace libraries.", + "file": "generated/packages/vite/generators/setup-paths-plugin.json", + "hidden": false, + "name": "setup-paths-plugin", + "originalFilePath": "/packages/vite/src/generators/setup-paths-plugin/schema.json", + "path": "/nx-api/vite/generators/setup-paths-plugin", + "type": "generator" + }, "/nx-api/vite/generators/vitest": { - "description": "Generate a vitest configuration", + "description": "Generate a vitest configuration.", "file": "generated/packages/vite/generators/vitest.json", "hidden": false, "name": "vitest", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 20da3c9e98..1b84b631aa 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2919,7 +2919,16 @@ "type": "generator" }, { - "description": "Generate a vitest configuration", + "description": "Sets up the nxViteTsPaths plugin to enable support for workspace libraries.", + "file": "generated/packages/vite/generators/setup-paths-plugin.json", + "hidden": false, + "name": "setup-paths-plugin", + "originalFilePath": "/packages/vite/src/generators/setup-paths-plugin/schema.json", + "path": "vite/generators/setup-paths-plugin", + "type": "generator" + }, + { + "description": "Generate a vitest configuration.", "file": "generated/packages/vite/generators/vitest.json", "hidden": false, "name": "vitest", diff --git a/docs/generated/packages/vite/documents/overview.md b/docs/generated/packages/vite/documents/overview.md index 213af28133..e6319ee1b9 100644 --- a/docs/generated/packages/vite/documents/overview.md +++ b/docs/generated/packages/vite/documents/overview.md @@ -39,6 +39,12 @@ In any Nx workspace, you can install `@nx/vite` by running the following command nx add @nx/vite ``` +You can also pass the `--setupPathsPlugin` flag to add [`nxViteTsPaths` plugin](/recipes/vite/configure-vite#typescript-paths), so your projects can use workspace libraries. + +```shell {% skipRescope=true %} +nx add @nx/vite --setupPathsPlugin +``` + This will install the correct version of `@nx/vite`. ### How @nx/vite Infers Tasks diff --git a/docs/generated/packages/vite/generators/init.json b/docs/generated/packages/vite/generators/init.json index b1948fddc6..be270157d4 100644 --- a/docs/generated/packages/vite/generators/init.json +++ b/docs/generated/packages/vite/generators/init.json @@ -18,6 +18,11 @@ "type": "boolean", "default": false }, + "setupPathsPlugin": { + "type": "boolean", + "description": "Updates vite config files to enable support for workspace libraries via the nxViteTsPaths plugin.", + "default": false + }, "keepExistingVersions": { "type": "boolean", "x-priority": "internal", diff --git a/docs/generated/packages/vite/generators/setup-paths-plugin.json b/docs/generated/packages/vite/generators/setup-paths-plugin.json new file mode 100644 index 0000000000..42de897f18 --- /dev/null +++ b/docs/generated/packages/vite/generators/setup-paths-plugin.json @@ -0,0 +1,25 @@ +{ + "name": "setup-paths-plugin", + "factory": "./src/generators/setup-paths-plugin/setup-paths-plugin", + "schema": { + "cli": "nx", + "title": "Sets up the nxViteTsPaths plugin.", + "description": "Updates vite config files to enable support for workspace libraries via the nxViteTsPaths plugin.", + "$id": "setup-paths-plugin-vite-plugin", + "type": "object", + "properties": { + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false + } + }, + "presets": [] + }, + "description": "Sets up the nxViteTsPaths plugin to enable support for workspace libraries.", + "implementation": "/packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.ts", + "aliases": [], + "hidden": false, + "path": "/packages/vite/src/generators/setup-paths-plugin/schema.json", + "type": "generator" +} diff --git a/docs/generated/packages/vite/generators/vitest.json b/docs/generated/packages/vite/generators/vitest.json index 1ce2e8f96f..2e4395df93 100644 --- a/docs/generated/packages/vite/generators/vitest.json +++ b/docs/generated/packages/vite/generators/vitest.json @@ -56,7 +56,7 @@ "required": ["project"], "presets": [] }, - "description": "Generate a vitest configuration", + "description": "Generate a vitest configuration.", "implementation": "/packages/vite/src/generators/vitest/vitest-generator#vitestGeneratorInternal.ts", "aliases": [], "hidden": false, diff --git a/docs/shared/packages/vite/vite-plugin.md b/docs/shared/packages/vite/vite-plugin.md index 213af28133..e6319ee1b9 100644 --- a/docs/shared/packages/vite/vite-plugin.md +++ b/docs/shared/packages/vite/vite-plugin.md @@ -39,6 +39,12 @@ In any Nx workspace, you can install `@nx/vite` by running the following command nx add @nx/vite ``` +You can also pass the `--setupPathsPlugin` flag to add [`nxViteTsPaths` plugin](/recipes/vite/configure-vite#typescript-paths), so your projects can use workspace libraries. + +```shell {% skipRescope=true %} +nx add @nx/vite --setupPathsPlugin +``` + This will install the correct version of `@nx/vite`. ### How @nx/vite Infers Tasks diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index a2943de2cd..a7f8cc26e8 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -674,6 +674,7 @@ - [generators](/nx-api/vite/generators) - [init](/nx-api/vite/generators/init) - [configuration](/nx-api/vite/generators/configuration) + - [setup-paths-plugin](/nx-api/vite/generators/setup-paths-plugin) - [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 9301ca06f6..8bc9cdef31 100644 --- a/packages/vite/generators.json +++ b/packages/vite/generators.json @@ -16,10 +16,15 @@ "aliases": ["config"], "hidden": false }, + "setup-paths-plugin": { + "factory": "./src/generators/setup-paths-plugin/setup-paths-plugin", + "schema": "./src/generators/setup-paths-plugin/schema.json", + "description": "Sets up the nxViteTsPaths plugin to enable support for workspace libraries." + }, "vitest": { "factory": "./src/generators/vitest/vitest-generator#vitestGeneratorInternal", "schema": "./src/generators/vitest/schema.json", - "description": "Generate a vitest configuration" + "description": "Generate a vitest configuration." } } } diff --git a/packages/vite/src/generators/init/init.spec.ts b/packages/vite/src/generators/init/init.spec.ts index 66e70f9d6d..002049cd98 100644 --- a/packages/vite/src/generators/init/init.spec.ts +++ b/packages/vite/src/generators/init/init.spec.ts @@ -4,11 +4,13 @@ import { ProjectGraph, readJson, readNxJson, + stripIndents, Tree, updateJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { nxVersion } from '../../utils/versions'; +import { initGenerator } from './init'; let projectGraph: ProjectGraph; jest.mock('@nx/devkit', () => ({ @@ -18,8 +20,6 @@ jest.mock('@nx/devkit', () => ({ }), })); -import { initGenerator } from './init'; - describe('@nx/vite:init', () => { let tree: Tree; @@ -99,4 +99,31 @@ describe('@nx/vite:init', () => { `); }); }); + + it('should add nxViteTsPaths plugin to vite config files when setupPathsPlugin is set to true', async () => { + tree.write( + 'proj/vite.config.ts', + stripIndents` + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + export default defineConfig({ + plugins: [react()], + })` + ); + + await initGenerator(tree, { + addPlugin: true, + setupPathsPlugin: true, + }); + + expect(tree.read('proj/vite.config.ts').toString()).toMatchInlineSnapshot(` + "import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + export default defineConfig({ + plugins: [react(), nxViteTsPaths()], + }); + " + `); + }); }); diff --git a/packages/vite/src/generators/init/init.ts b/packages/vite/src/generators/init/init.ts index c6210ac1ff..ee3f2ee709 100644 --- a/packages/vite/src/generators/init/init.ts +++ b/packages/vite/src/generators/init/init.ts @@ -9,6 +9,7 @@ import { } from '@nx/devkit'; import { addPluginV1 } from '@nx/devkit/src/utils/add-plugin'; +import { setupPathsPlugin } from '../setup-paths-plugin/setup-paths-plugin'; import { createNodes } from '../../plugins/plugin'; import { InitGeneratorSchema } from './schema'; import { checkDependenciesInstalled, moveToDevDependencies } from './lib/utils'; @@ -82,6 +83,10 @@ export async function initGeneratorInternal( updateNxJsonSettings(tree); + if (schema.setupPathsPlugin) { + await setupPathsPlugin(tree, { skipFormat: true }); + } + const tasks: GeneratorCallback[] = []; if (!schema.skipPackageJson) { tasks.push(moveToDevDependencies(tree)); diff --git a/packages/vite/src/generators/init/schema.d.ts b/packages/vite/src/generators/init/schema.d.ts index 970386bd65..d68405d006 100644 --- a/packages/vite/src/generators/init/schema.d.ts +++ b/packages/vite/src/generators/init/schema.d.ts @@ -1,5 +1,6 @@ export interface InitGeneratorSchema { skipFormat?: boolean; + setupPathsPlugin?: boolean; skipPackageJson?: boolean; keepExistingVersions?: boolean; updatePackageScripts?: boolean; diff --git a/packages/vite/src/generators/init/schema.json b/packages/vite/src/generators/init/schema.json index 028676411f..353c9e261d 100644 --- a/packages/vite/src/generators/init/schema.json +++ b/packages/vite/src/generators/init/schema.json @@ -15,6 +15,11 @@ "type": "boolean", "default": false }, + "setupPathsPlugin": { + "type": "boolean", + "description": "Updates vite config files to enable support for workspace libraries via the nxViteTsPaths plugin.", + "default": false + }, "keepExistingVersions": { "type": "boolean", "x-priority": "internal", diff --git a/packages/vite/src/generators/setup-paths-plugin/lib/utils.ts b/packages/vite/src/generators/setup-paths-plugin/lib/utils.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/vite/src/generators/setup-paths-plugin/schema.d.ts b/packages/vite/src/generators/setup-paths-plugin/schema.d.ts new file mode 100644 index 0000000000..8a7087d01a --- /dev/null +++ b/packages/vite/src/generators/setup-paths-plugin/schema.d.ts @@ -0,0 +1,9 @@ +export interface InitGeneratorSchema { + skipFormat?: boolean; + addTsPathsPlugin?: boolean; + skipPackageJson?: boolean; + keepExistingVersions?: boolean; + updatePackageScripts?: boolean; + addPlugin?: boolean; + vitestOnly?: boolean; +} diff --git a/packages/vite/src/generators/setup-paths-plugin/schema.json b/packages/vite/src/generators/setup-paths-plugin/schema.json new file mode 100644 index 0000000000..b6032b61ad --- /dev/null +++ b/packages/vite/src/generators/setup-paths-plugin/schema.json @@ -0,0 +1,14 @@ +{ + "cli": "nx", + "title": "Sets up the nxViteTsPaths plugin.", + "description": "Updates vite config files to enable support for workspace libraries via the nxViteTsPaths plugin.", + "$id": "setup-paths-plugin-vite-plugin", + "type": "object", + "properties": { + "skipFormat": { + "description": "Skip formatting files.", + "type": "boolean", + "default": false + } + } +} diff --git a/packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.spec.ts b/packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.spec.ts new file mode 100644 index 0000000000..eba20a27b5 --- /dev/null +++ b/packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.spec.ts @@ -0,0 +1,79 @@ +import { ProjectGraph, stripIndents, Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { setupPathsPlugin } from './setup-paths-plugin'; + +let projectGraph: ProjectGraph; +jest.mock('@nx/devkit', () => ({ + ...jest.requireActual('@nx/devkit'), + createProjectGraphAsync: jest.fn().mockImplementation(async () => { + return projectGraph; + }), +})); + +describe('@nx/vite:init', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + projectGraph = { + nodes: {}, + dependencies: {}, + }; + }); + + it('should add nxViteTsPaths plugin to vite config files', async () => { + tree.write( + 'proj1/vite.config.ts', + stripIndents` + import { defineConfig } from 'vite'; + export default defineConfig({});` + ); + tree.write( + 'proj2/vite.config.ts', + stripIndents` + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + export default defineConfig({ + plugins: [react()], + })` + ); + tree.write( + 'proj3/vite.config.cts', + stripIndents` + const { defineConfig } = require('vite'); + const react = require('@vitejs/plugin-react'); + module.exports = defineConfig({ + plugins: [react()], + }); + ` + ); + + await setupPathsPlugin(tree, {}); + + expect(tree.read('proj1/vite.config.ts').toString()).toMatchInlineSnapshot(` + "import { defineConfig } from 'vite'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + export default defineConfig({ plugins: [nxViteTsPaths()] }); + " + `); + expect(tree.read('proj2/vite.config.ts').toString()).toMatchInlineSnapshot(` + "import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + export default defineConfig({ + plugins: [react(), nxViteTsPaths()], + }); + " + `); + expect(tree.read('proj3/vite.config.cts').toString()) + .toMatchInlineSnapshot(` + "const { nxViteTsPaths } = require('@nx/vite/plugins/nx-tsconfig-paths.plugin'); + const { defineConfig } = require('vite'); + const react = require('@vitejs/plugin-react'); + module.exports = defineConfig({ + plugins: [react(), nxViteTsPaths()], + }); + " + `); + }); +}); diff --git a/packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.ts b/packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.ts new file mode 100644 index 0000000000..71a3f0fef7 --- /dev/null +++ b/packages/vite/src/generators/setup-paths-plugin/setup-paths-plugin.ts @@ -0,0 +1,156 @@ +import { + applyChangesToString, + ChangeType, + formatFiles, + globAsync, + Tree, +} from '@nx/devkit'; +import type { ArrayLiteralExpression, Node } from 'typescript'; + +export async function setupPathsPlugin( + tree: Tree, + schema: { skipFormat?: boolean } +) { + const files = await globAsync(tree, [ + '**/vite.config.{js,ts,mjs,mts,cjs,cts}', + ]); + + for (const file of files) { + ensureImportExists(tree, file); + ensurePluginAdded(tree, file); + } + + if (!schema.skipFormat) { + await formatFiles(tree); + } +} + +function ensureImportExists(tree, file) { + const { tsquery } = require('@phenomnomnominal/tsquery'); + let content = tree.read(file, 'utf-8'); + const ast = tsquery.ast(content); + const allImports = tsquery.query(ast, 'ImportDeclaration'); + if (allImports.length) { + const lastImport = allImports[allImports.length - 1]; + tree.write( + file, + applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: lastImport.end + 1, + text: `import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';\n`, + }, + ]) + ); + } else { + if (file.endsWith('.cts') || file.endsWith('.cjs')) { + tree.write( + file, + applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: 0, + text: `const { nxViteTsPaths } = require('@nx/vite/plugins/nx-tsconfig-paths.plugin');\n`, + }, + ]) + ); + } else { + tree.write( + file, + applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: 0, + text: `import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';\n`, + }, + ]) + ); + } + } +} + +function ensurePluginAdded(tree, file) { + const { tsquery } = require('@phenomnomnominal/tsquery'); + const content = tree.read(file, 'utf-8'); + const ast = tsquery.ast(content); + const foundDefineConfig = tsquery.query( + ast, + 'CallExpression:has(Identifier[name="defineConfig"])' + ); + if (!foundDefineConfig.length) return content; + + // Do not update defineConfig if it has an arrow function since it can be tricky and error-prone. + const defineUsingArrowFunction = tsquery.query( + foundDefineConfig[0], + 'ArrowFunction' + ); + if (defineUsingArrowFunction.length) return content; + + const propertyAssignments = tsquery.query( + foundDefineConfig[0], + 'PropertyAssignment' + ); + + if (propertyAssignments.length) { + const pluginsNode = tsquery.query( + foundDefineConfig[0], + 'PropertyAssignment:has(Identifier[name="plugins"])' + ); + + if (pluginsNode.length) { + const updated = tsquery.replace( + content, + 'PropertyAssignment:has(Identifier[name="plugins"])', + (node: Node) => { + const found = tsquery.query( + node, + 'ArrayLiteralExpression' + ) as ArrayLiteralExpression[]; + let updatedPluginsString = ''; + + const existingPluginNodes = found?.[0].elements ?? []; + + for (const plugin of existingPluginNodes) { + updatedPluginsString += `${plugin.getText()},`; + } + + if ( + !existingPluginNodes?.some((node: Node) => + node.getText().includes('nxViteTsPaths') + ) + ) { + updatedPluginsString += ` nxViteTsPaths(),`; + } + + return `plugins: [${updatedPluginsString}]`; + } + ); + tree.write(file, updated); + } else { + tree.write( + file, + applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: propertyAssignments[0].getStart(), + text: `plugins: [nxViteTsPaths()], + `, + }, + ]) + ); + } + } else { + tree.write( + file, + applyChangesToString(content, [ + { + type: ChangeType.Insert, + index: foundDefineConfig[0].getStart() + 14, // length of "defineConfig(" + 1 + text: `plugins: [nxViteTsPaths()],`, + }, + ]) + ); + } +} + +export default setupPathsPlugin;