diff --git a/docs/generated/manifests/menus.json b/docs/generated/manifests/menus.json index a30c68244524e6..e9618f980830ae 100644 --- a/docs/generated/manifests/menus.json +++ b/docs/generated/manifests/menus.json @@ -9018,6 +9018,14 @@ "isExternal": false, "children": [], "disableCollapsible": false + }, + { + "name": "Overview of the @nx/vite/plugin", + "path": "/nx-api/vite/documents/vite-nodes-plugin", + "id": "vite-nodes-plugin", + "isExternal": false, + "children": [], + "disableCollapsible": false } ], "isExternal": false, diff --git a/docs/generated/manifests/nx-api.json b/docs/generated/manifests/nx-api.json index 77d6ed47867758..a7ffbf0b140beb 100644 --- a/docs/generated/manifests/nx-api.json +++ b/docs/generated/manifests/nx-api.json @@ -2524,6 +2524,17 @@ "path": "/nx-api/vite/documents/overview", "tags": [], "originalFilePath": "shared/packages/vite/vite-plugin" + }, + "/nx-api/vite/documents/vite-nodes-plugin": { + "id": "vite-nodes-plugin", + "name": "Overview of the @nx/vite/plugin", + "description": "The @nx/vite/plugin adds Vite targets/projects to the Nx project graph. This allows you to run Vite for projects that have a vite.config.ts file without creating a project.json file.", + "file": "generated/packages/vite/documents/vite-nodes-plugin", + "itemList": [], + "isExternal": false, + "path": "/nx-api/vite/documents/vite-nodes-plugin", + "tags": [], + "originalFilePath": "shared/packages/vite/vite-nodes-plugin" } }, "root": "/packages/vite", diff --git a/docs/generated/packages-metadata.json b/docs/generated/packages-metadata.json index 43a57fd00c1b7d..801c5f7aeebf58 100644 --- a/docs/generated/packages-metadata.json +++ b/docs/generated/packages-metadata.json @@ -2497,6 +2497,17 @@ "path": "vite/documents/overview", "tags": [], "originalFilePath": "shared/packages/vite/vite-plugin" + }, + { + "id": "vite-nodes-plugin", + "name": "Overview of the @nx/vite/plugin", + "description": "The @nx/vite/plugin adds Vite targets/projects to the Nx project graph. This allows you to run Vite for projects that have a vite.config.ts file without creating a project.json file.", + "file": "generated/packages/vite/documents/vite-nodes-plugin", + "itemList": [], + "isExternal": false, + "path": "vite/documents/vite-nodes-plugin", + "tags": [], + "originalFilePath": "shared/packages/vite/vite-nodes-plugin" } ], "executors": [ diff --git a/docs/generated/packages/vite/documents/vite-nodes-plugin.md b/docs/generated/packages/vite/documents/vite-nodes-plugin.md new file mode 100644 index 00000000000000..3d3b5efbc7cd70 --- /dev/null +++ b/docs/generated/packages/vite/documents/vite-nodes-plugin.md @@ -0,0 +1,64 @@ +--- +title: Overview of the @nx/vite/plugin +description: The @nx/vite/plugin adds Vite targets/projects to the Nx project graph. This allows you to run Vite for projects that have a vite.config.ts file without creating a project.json file. +--- + +{% callout type="note" title="Still in beta" %} +This feature is still in beta. To use it you must have the `NX_PCV3` environment variable set to `"true"`. +{% /callout %} + +## What is the @nx/vite/plugin? + +The `@nx/vite/plugin` adds Vite targets/projects to the Nx project graph. This allows you to run Vite for projects that have a `vite.config.ts` file without creating a `project.json` file. + +This is an effort to reduce duplicate code and configurations for your projects that use Vite. + +## How it works + +The `@nx/vite:init` generator (which is called through the `@nx/vite:configuration` generator, and the `@nx/react:app|lib`, `@nx/vue:app|lib` and `@nx/web:app|lib` generators) the following entry is added in the `plugins` array of your `nx.json`: + +```json +... +plugins: [ + { + "plugin": "@nx/vite/plugin", + "options": { + "buildTargetName": "build", + "previewTargetName": "preview", + "serveTargetName": "serve", + "testTargetName": "test", + "serveStaticTargetName": "static-serve", + }, + }, + ... +] +``` + +This will create targets for `build`, `preview`, `test` and `serve` in your project graph, so that you can run `nx build my-app`, `nx test my-app` etc, without manually defining these targets in each of your projects' `project.json` files. This will work for every single project in your workspace that has a `vite.config.ts|js` file. + +## How to configure + +The plugin generates these targets with bare minimum (no extra) options configuration. Any options you need for your Vite app, you can add in your project's `vite.config.ts`. + +If you want to add some universal options for all your `build` targets, for example, you can still do so in the [`targetDefaults`](/recipes/running-tasks/reduce-repetitive-configuration#reduce-configuration-with-targetdefaults). + +You can edit the name of your targets if you want to use a different name. For example, you can have this configuration: + +```json +... +plugins: [ + { + "plugin": "@nx/vite/plugin", + "options": { + "buildTargetName": "build-core", + "previewTargetName": "preview", + "serveTargetName": "serve", + "testTargetName": "test", + "serveStaticTargetName": "static-serve", + }, + }, + ... +] +``` + +and this will create `build-core`, `preview`, `test` and `serve` targets in your project graph. You can then call `nx build-core my-app` for all your projects that have a `vite.config.ts|js` file. diff --git a/docs/map.json b/docs/map.json index 34c4b8c81f8b88..fc6ee75d191d50 100644 --- a/docs/map.json +++ b/docs/map.json @@ -2145,6 +2145,13 @@ "name": "Overview of the Nx Vite Plugin", "description": "The Nx Plugin for Vite contains executors and generators that support building applications using Vite. This page also explains how to configure Vite on your Nx workspace.", "file": "shared/packages/vite/vite-plugin" + }, + { + "id": "vite-nodes-plugin", + "path": "/nx-api/vite-nodes-plugin", + "name": "Overview of the @nx/vite/plugin", + "description": "The @nx/vite/plugin adds Vite targets/projects to the Nx project graph. This allows you to run Vite for projects that have a vite.config.ts file without creating a project.json file.", + "file": "shared/packages/vite/vite-nodes-plugin" } ] }, diff --git a/docs/shared/packages/vite/vite-nodes-plugin.md b/docs/shared/packages/vite/vite-nodes-plugin.md new file mode 100644 index 00000000000000..3d3b5efbc7cd70 --- /dev/null +++ b/docs/shared/packages/vite/vite-nodes-plugin.md @@ -0,0 +1,64 @@ +--- +title: Overview of the @nx/vite/plugin +description: The @nx/vite/plugin adds Vite targets/projects to the Nx project graph. This allows you to run Vite for projects that have a vite.config.ts file without creating a project.json file. +--- + +{% callout type="note" title="Still in beta" %} +This feature is still in beta. To use it you must have the `NX_PCV3` environment variable set to `"true"`. +{% /callout %} + +## What is the @nx/vite/plugin? + +The `@nx/vite/plugin` adds Vite targets/projects to the Nx project graph. This allows you to run Vite for projects that have a `vite.config.ts` file without creating a `project.json` file. + +This is an effort to reduce duplicate code and configurations for your projects that use Vite. + +## How it works + +The `@nx/vite:init` generator (which is called through the `@nx/vite:configuration` generator, and the `@nx/react:app|lib`, `@nx/vue:app|lib` and `@nx/web:app|lib` generators) the following entry is added in the `plugins` array of your `nx.json`: + +```json +... +plugins: [ + { + "plugin": "@nx/vite/plugin", + "options": { + "buildTargetName": "build", + "previewTargetName": "preview", + "serveTargetName": "serve", + "testTargetName": "test", + "serveStaticTargetName": "static-serve", + }, + }, + ... +] +``` + +This will create targets for `build`, `preview`, `test` and `serve` in your project graph, so that you can run `nx build my-app`, `nx test my-app` etc, without manually defining these targets in each of your projects' `project.json` files. This will work for every single project in your workspace that has a `vite.config.ts|js` file. + +## How to configure + +The plugin generates these targets with bare minimum (no extra) options configuration. Any options you need for your Vite app, you can add in your project's `vite.config.ts`. + +If you want to add some universal options for all your `build` targets, for example, you can still do so in the [`targetDefaults`](/recipes/running-tasks/reduce-repetitive-configuration#reduce-configuration-with-targetdefaults). + +You can edit the name of your targets if you want to use a different name. For example, you can have this configuration: + +```json +... +plugins: [ + { + "plugin": "@nx/vite/plugin", + "options": { + "buildTargetName": "build-core", + "previewTargetName": "preview", + "serveTargetName": "serve", + "testTargetName": "test", + "serveStaticTargetName": "static-serve", + }, + }, + ... +] +``` + +and this will create `build-core`, `preview`, `test` and `serve` targets in your project graph. You can then call `nx build-core my-app` for all your projects that have a `vite.config.ts|js` file. diff --git a/docs/shared/reference/sitemap.md b/docs/shared/reference/sitemap.md index 858435559c6b8a..9f2756ea53e776 100644 --- a/docs/shared/reference/sitemap.md +++ b/docs/shared/reference/sitemap.md @@ -625,6 +625,7 @@ - [vite](/nx-api/vite) - [documents](/nx-api/vite/documents) - [Overview of the Nx Vite Plugin](/nx-api/vite/documents/overview) + - [Overview of the @nx/vite/plugin](/nx-api/vite/documents/vite-nodes-plugin) - [executors](/nx-api/vite/executors) - [dev-server](/nx-api/vite/executors/dev-server) - [build](/nx-api/vite/executors/build) diff --git a/e2e/vite/src/vite-pcv3.test.ts b/e2e/vite/src/vite-pcv3.test.ts new file mode 100644 index 00000000000000..885acd247adc9e --- /dev/null +++ b/e2e/vite/src/vite-pcv3.test.ts @@ -0,0 +1,59 @@ +import { cleanupProject, newProject, runCLI, uniq } from '@nx/e2e/utils'; + +const myApp = uniq('my-app'); + +describe('@nx/vite/plugin', () => { + let proj: string; + let originalEnv: string; + + beforeAll(() => { + originalEnv = process.env.NX_PCV3; + process.env.NX_PCV3 = 'true'; + }); + + afterAll(() => { + process.env.NODE_ENV = originalEnv; + }); + + describe('build and test React Vite app', () => { + beforeAll(() => { + proj = newProject(); + runCLI( + `generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest` + ); + }); + + afterAll(() => cleanupProject()); + + it('should build application', () => { + const result = runCLI(`build ${myApp}`); + expect(result).toContain('Successfully ran target build'); + }, 200_000); + + it('should test application', () => { + const result = runCLI(`test ${myApp}`); + expect(result).toContain('Successfully ran target test'); + }, 200_000); + }); + + describe('build and test Vue app', () => { + beforeAll(() => { + proj = newProject(); + runCLI(`generate @nx/vue:app ${myApp} --unitTestRunner=vitest`); + }); + + afterAll(() => { + cleanupProject(); + }); + + it('should build application', () => { + const result = runCLI(`build ${myApp}`); + expect(result).toContain('Successfully ran target build'); + }, 200_000); + + it('should test application', () => { + const result = runCLI(`test ${myApp}`); + expect(result).toContain('Successfully ran target test'); + }, 200_000); + }); +}); diff --git a/packages/react/src/generators/application/application.pcv3.spec.ts b/packages/react/src/generators/application/application.pcv3.spec.ts index 38e1c44e3d8e6c..91717eb9761549 100644 --- a/packages/react/src/generators/application/application.pcv3.spec.ts +++ b/packages/react/src/generators/application/application.pcv3.spec.ts @@ -1,60 +1,44 @@ -import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; -import { - readNxJson, - readProjectConfiguration, - Tree, - updateNxJson, -} from '@nx/devkit'; +import { getProjects, Tree } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { Linter } from '@nx/eslint'; import { applicationGenerator } from './application'; import { Schema } from './schema'; -// need to mock cypress otherwise it'll use the nx installed version from package.json -// which is v9 while we are testing for the new v10 version -jest.mock('@nx/cypress/src/utils/cypress-version'); -describe('react app generator (PCv3)', () => { + +describe('app generated with Project Configuration v3', () => { + let originalValue; let appTree: Tree; - let schema: Schema = { - compiler: 'babel', - e2eTestRunner: 'cypress', + const schema: Schema = { + e2eTestRunner: 'none', skipFormat: false, name: 'my-app', linter: Linter.EsLint, style: 'css', strict: true, + bundler: 'vite', + unitTestRunner: 'vitest', projectNameAndRootFormat: 'as-provided', }; - let mockedInstalledCypressVersion: jest.Mock< - ReturnType - > = installedCypressVersion as never; + beforeEach(() => { - mockedInstalledCypressVersion.mockReturnValue(10); appTree = createTreeWithEmptyWorkspace(); - const nxJson = readNxJson(appTree); - nxJson.plugins ??= []; - nxJson.plugins.push('@nx/webpack/plugin'); - updateNxJson(appTree, nxJson); + originalValue = process.env['NX_PCV3']; + process.env['NX_PCV3'] = 'true'; }); - it('should setup webpack config that is compatible without project targets', async () => { - await applicationGenerator(appTree, { - ...schema, - name: 'my-app', - bundler: 'webpack', - }); - - const targets = readProjectConfiguration(appTree, 'my-app').targets; - expect(targets.build).toBeUndefined(); - expect(targets.serve).toBeUndefined(); + afterEach(() => { + if (originalValue) { + process.env['NX_PCV3'] = originalValue; + } else { + delete process.env['NX_PCV3']; + } + }); - const webpackConfig = appTree.read('my-app/webpack.config.js', 'utf-8'); - expect(webpackConfig).toContain(`new NxWebpackPlugin`); - expect(webpackConfig).toContain(`'../dist/my-app'`); - expect(webpackConfig).toContain(`main: './src/main.tsx'`); - expect(webpackConfig).toContain(`tsConfig: './tsconfig.app.json'`); - expect(webpackConfig).toContain(`styles: ['./src/styles.css']`); - expect(webpackConfig).toContain( - `assets: ['./src/favicon.ico', './src/assets']` - ); + it('should not add targets', async () => { + await applicationGenerator(appTree, schema); + const projects = getProjects(appTree); + expect(projects.get('my-app').targets.build).toBeUndefined(); + expect(projects.get('my-app').targets.serve).toBeUndefined(); + expect(projects.get('my-app').targets.preview).toBeUndefined(); + expect(projects.get('my-app').targets.test).toBeUndefined(); }); }); diff --git a/packages/react/src/generators/application/lib/add-e2e.ts b/packages/react/src/generators/application/lib/add-e2e.ts index 6b25bb945df76b..b699f60384164e 100644 --- a/packages/react/src/generators/application/lib/add-e2e.ts +++ b/packages/react/src/generators/application/lib/add-e2e.ts @@ -4,6 +4,7 @@ import { ensurePackage, getPackageManagerCommand, joinPathFragments, + readNxJson, } from '@nx/devkit'; import { webStaticServeGenerator } from '@nx/web'; diff --git a/packages/react/src/generators/library/__snapshots__/library.spec.ts.snap b/packages/react/src/generators/library/__snapshots__/library.spec.ts.snap index 4d4970fca14d7d..b6537f9624b706 100644 --- a/packages/react/src/generators/library/__snapshots__/library.spec.ts.snap +++ b/packages/react/src/generators/library/__snapshots__/library.spec.ts.snap @@ -17,6 +17,7 @@ export default defineConfig({ // }, test: { + reportsDirectory: '../coverage/my-lib', globals: true, cache: { dir: '../node_modules/.vitest' }, environment: 'jsdom', @@ -71,6 +72,7 @@ export default defineConfig({ // External packages that should not be bundled into your library. external: ['react', 'react-dom', 'react/jsx-runtime'], }, + outDir: 'dist/my-lib', }, test: { diff --git a/packages/vite/migrations.json b/packages/vite/migrations.json index bbca74289a12ee..2c4928c3d57e40 100644 --- a/packages/vite/migrations.json +++ b/packages/vite/migrations.json @@ -45,6 +45,11 @@ "version": "17.2.0-beta.10", "description": "Update vite config.", "implementation": "./src/migrations/update-17-2-0/update-vite-config" + }, + "add-vite-plugin": { + "version": "17.3.0-beta.0", + "description": "Add @nx/vite/plugin", + "implementation": "./src/migrations/update-17-2-0/add-vite-plugin" } }, "packageJsonUpdates": { diff --git a/packages/vite/migrations.spec.ts b/packages/vite/migrations.spec.ts index 655cf2ebadbf6b..c92ed556d1f699 100644 --- a/packages/vite/migrations.spec.ts +++ b/packages/vite/migrations.spec.ts @@ -3,6 +3,16 @@ import json = require('./migrations.json'); import { assertValidMigrationPaths } from '@nx/devkit/internal-testing-utils'; import { MigrationsJson } from '@nx/devkit'; +jest.mock('vite', () => ({ + loadConfigFromFile: jest.fn().mockImplementation(() => { + return Promise.resolve({ + path: 'vite.config.ts', + config: {}, + dependencies: [], + }); + }), +})); + describe('vite migrations', () => { assertValidMigrationPaths(json as MigrationsJson, __dirname); }); diff --git a/packages/vite/package.json b/packages/vite/package.json index a86c4f6e6ea6bf..8393287987add2 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -29,10 +29,10 @@ "migrations": "./migrations.json" }, "dependencies": { + "@nx/devkit": "file:../devkit", "@phenomnomnominal/tsquery": "~5.0.1", "@swc/helpers": "~0.5.0", "enquirer": "~2.3.6", - "@nx/devkit": "file:../devkit", "@nx/js": "file:../js", "tsconfig-paths": "^4.1.2" }, @@ -45,6 +45,7 @@ }, "exports": { ".": "./index.js", + "./plugin": "./plugin.js", "./package.json": "./package.json", "./migrations.json": "./migrations.json", "./generators.json": "./generators.json", diff --git a/packages/vite/plugin.ts b/packages/vite/plugin.ts new file mode 100644 index 00000000000000..ae222554bb649a --- /dev/null +++ b/packages/vite/plugin.ts @@ -0,0 +1,5 @@ +export { + createNodes, + VitePluginOptions, + createDependencies, +} from './src/plugins/plugin'; diff --git a/packages/vite/src/generators/configuration/__snapshots__/configuration.spec.ts.snap b/packages/vite/src/generators/configuration/__snapshots__/configuration.spec.ts.snap index b5ffd6de1c1b45..14596cdabcf733 100644 --- a/packages/vite/src/generators/configuration/__snapshots__/configuration.spec.ts.snap +++ b/packages/vite/src/generators/configuration/__snapshots__/configuration.spec.ts.snap @@ -44,6 +44,7 @@ export default defineConfig({ // External packages that should not be bundled into your library. external: ['react', 'react-dom', 'react/jsx-runtime'], }, + outDir: 'dist/react-lib-nonb-jest', }, }); " @@ -93,6 +94,7 @@ export default defineConfig({ // External packages that should not be bundled into your library. external: ['react', 'react-dom', 'react/jsx-runtime'], }, + outDir: 'dist/react-lib-nonb-jest', }, test: { @@ -168,6 +170,7 @@ export default defineConfig({ // External packages that should not be bundled into your library. external: ['react', 'react-dom', 'react/jsx-runtime'], }, + outDir: 'dist/react-lib-nonb-vitest', }, plugins: [ nxViteTsPaths(), diff --git a/packages/vite/src/generators/configuration/configuration.ts b/packages/vite/src/generators/configuration/configuration.ts index 454101261a5e21..8e99b8498ecd4f 100644 --- a/packages/vite/src/generators/configuration/configuration.ts +++ b/packages/vite/src/generators/configuration/configuration.ts @@ -2,6 +2,7 @@ import { formatFiles, GeneratorCallback, joinPathFragments, + readNxJson, readProjectConfiguration, runTasksInSerial, Tree, @@ -164,19 +165,27 @@ export async function viteConfigurationGenerator( }); tasks.push(initTask); - if (!projectAlreadyHasViteTargets.build) { - addOrChangeBuildTarget(tree, schema, buildTargetName); - } + const nxJson = readNxJson(tree); + const hasPlugin = nxJson.plugins?.some((p) => + typeof p === 'string' + ? p === '@nx/vite/plugin' + : p.plugin === '@nx/vite/plugin' + ); - if (!schema.includeLib) { - if (!projectAlreadyHasViteTargets.serve) { - addOrChangeServeTarget(tree, schema, serveTargetName); + if (!hasPlugin) { + if (!projectAlreadyHasViteTargets.build) { + addOrChangeBuildTarget(tree, schema, buildTargetName); } - if (!projectAlreadyHasViteTargets.preview) { - addPreviewTarget(tree, schema, serveTargetName); + + if (!schema.includeLib) { + if (!projectAlreadyHasViteTargets.serve) { + addOrChangeServeTarget(tree, schema, serveTargetName); + } + if (!projectAlreadyHasViteTargets.preview) { + addPreviewTarget(tree, schema, serveTargetName); + } } } - if (projectType === 'library') { // update tsconfig.lib.json to include vite/client updateJson( @@ -225,7 +234,8 @@ export async function viteConfigurationGenerator( ], plugins: ['react()'], }, - false + false, + undefined ); } else { createOrEditViteConfig(tree, schema, false, projectAlreadyHasViteTargets); diff --git a/packages/vite/src/generators/init/init.ts b/packages/vite/src/generators/init/init.ts index 5bd71876211b5a..b8d57a97444081 100644 --- a/packages/vite/src/generators/init/init.ts +++ b/packages/vite/src/generators/init/init.ts @@ -1,82 +1,13 @@ -import { - addDependenciesToPackageJson, - logger, - readJson, - readNxJson, - runTasksInSerial, - Tree, - updateJson, - updateNxJson, -} from '@nx/devkit'; +import { readNxJson, runTasksInSerial, Tree, updateNxJson } from '@nx/devkit'; import { initGenerator as jsInitGenerator } from '@nx/js'; -import { - edgeRuntimeVmVersion, - happyDomVersion, - jsdomVersion, - nxVersion, - vitePluginDtsVersion, - vitePluginReactSwcVersion, - vitePluginReactVersion, - vitestVersion, - viteVersion, -} from '../../utils/versions'; import { InitGeneratorSchema } from './schema'; - -function checkDependenciesInstalled(host: Tree, schema: InitGeneratorSchema) { - const packageJson = readJson(host, 'package.json'); - const devDependencies = {}; - const dependencies = {}; - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; - - // base deps - devDependencies['@nx/vite'] = nxVersion; - devDependencies['vite'] = viteVersion; - devDependencies['vitest'] = vitestVersion; - devDependencies['@vitest/ui'] = vitestVersion; - - if (schema.testEnvironment === 'jsdom') { - devDependencies['jsdom'] = jsdomVersion; - } else if (schema.testEnvironment === 'happy-dom') { - devDependencies['happy-dom'] = happyDomVersion; - } else if (schema.testEnvironment === 'edge-runtime') { - devDependencies['@edge-runtime/vm'] = edgeRuntimeVmVersion; - } else if (schema.testEnvironment !== 'node' && schema.testEnvironment) { - logger.info( - `A custom environment was provided: ${schema.testEnvironment}. You need to install it manually.` - ); - } - - if (schema.uiFramework === 'react') { - if (schema.compiler === 'swc') { - devDependencies['@vitejs/plugin-react-swc'] = vitePluginReactSwcVersion; - } else { - devDependencies['@vitejs/plugin-react'] = vitePluginReactVersion; - } - } - - if (schema.includeLib) { - devDependencies['vite-plugin-dts'] = vitePluginDtsVersion; - } - - return addDependenciesToPackageJson(host, dependencies, devDependencies); -} - -function moveToDevDependencies(tree: Tree) { - updateJson(tree, 'package.json', (packageJson) => { - packageJson.dependencies = packageJson.dependencies || {}; - packageJson.devDependencies = packageJson.devDependencies || {}; - - if (packageJson.dependencies['@nx/vite']) { - packageJson.devDependencies['@nx/vite'] = - packageJson.dependencies['@nx/vite']; - delete packageJson.dependencies['@nx/vite']; - } - return packageJson; - }); -} +import { + addPlugin, + checkDependenciesInstalled, + moveToDevDependencies, +} from './lib/utils'; export function updateNxJsonSettings(tree: Tree) { const nxJson = readNxJson(tree); @@ -127,7 +58,10 @@ export async function initGenerator(tree: Tree, schema: InitGeneratorSchema) { tsConfigName: schema.rootProject ? 'tsconfig.json' : 'tsconfig.base.json', }) ); - + const addPlugins = process.env.NX_PCV3 === 'true'; + if (addPlugins) { + addPlugin(tree); + } tasks.push(checkDependenciesInstalled(tree, schema)); return runTasksInSerial(...tasks); } diff --git a/packages/vite/src/generators/init/lib/utils.ts b/packages/vite/src/generators/init/lib/utils.ts new file mode 100644 index 00000000000000..3450adf9f197e4 --- /dev/null +++ b/packages/vite/src/generators/init/lib/utils.ts @@ -0,0 +1,133 @@ +import { + addDependenciesToPackageJson, + logger, + readJson, + readNxJson, + Tree, + updateJson, + updateNxJson, +} from '@nx/devkit'; + +import { + edgeRuntimeVmVersion, + happyDomVersion, + jsdomVersion, + nxVersion, + vitePluginDtsVersion, + vitePluginReactSwcVersion, + vitePluginReactVersion, + vitestVersion, + viteVersion, +} from '../../../utils/versions'; +import { InitGeneratorSchema } from '../schema'; + +export function checkDependenciesInstalled( + host: Tree, + schema: InitGeneratorSchema +) { + const packageJson = readJson(host, 'package.json'); + const devDependencies = {}; + const dependencies = {}; + packageJson.dependencies = packageJson.dependencies || {}; + packageJson.devDependencies = packageJson.devDependencies || {}; + + // base deps + devDependencies['@nx/vite'] = nxVersion; + devDependencies['vite'] = viteVersion; + devDependencies['vitest'] = vitestVersion; + devDependencies['@vitest/ui'] = vitestVersion; + + if (schema.testEnvironment === 'jsdom') { + devDependencies['jsdom'] = jsdomVersion; + } else if (schema.testEnvironment === 'happy-dom') { + devDependencies['happy-dom'] = happyDomVersion; + } else if (schema.testEnvironment === 'edge-runtime') { + devDependencies['@edge-runtime/vm'] = edgeRuntimeVmVersion; + } else if (schema.testEnvironment !== 'node' && schema.testEnvironment) { + logger.info( + `A custom environment was provided: ${schema.testEnvironment}. You need to install it manually.` + ); + } + + if (schema.uiFramework === 'react') { + if (schema.compiler === 'swc') { + devDependencies['@vitejs/plugin-react-swc'] = vitePluginReactSwcVersion; + } else { + devDependencies['@vitejs/plugin-react'] = vitePluginReactVersion; + } + } + + if (schema.includeLib) { + devDependencies['vite-plugin-dts'] = vitePluginDtsVersion; + } + + return addDependenciesToPackageJson(host, dependencies, devDependencies); +} + +export function moveToDevDependencies(tree: Tree) { + updateJson(tree, 'package.json', (packageJson) => { + packageJson.dependencies = packageJson.dependencies || {}; + packageJson.devDependencies = packageJson.devDependencies || {}; + + if (packageJson.dependencies['@nx/vite']) { + packageJson.devDependencies['@nx/vite'] = + packageJson.dependencies['@nx/vite']; + delete packageJson.dependencies['@nx/vite']; + } + return packageJson; + }); +} + +export function createVitestConfig(tree: Tree) { + const nxJson = readNxJson(tree); + + const productionFileSet = nxJson.namedInputs?.production; + if (productionFileSet) { + productionFileSet.push( + '!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)', + '!{projectRoot}/tsconfig.spec.json' + ); + + nxJson.namedInputs.production = Array.from(new Set(productionFileSet)); + } + + nxJson.targetDefaults ??= {}; + nxJson.targetDefaults['@nx/vite:test'] ??= {}; + nxJson.targetDefaults['@nx/vite:test'].cache ??= true; + nxJson.targetDefaults['@nx/vite:test'].inputs ??= [ + 'default', + productionFileSet ? '^production' : '^default', + ]; + nxJson.targetDefaults['@nx/vite:test'].options ??= { + passWithNoTests: true, + }; + + updateNxJson(tree, nxJson); +} + +export function addPlugin(tree: Tree) { + const nxJson = readNxJson(tree); + nxJson.plugins ??= []; + + for (const plugin of nxJson.plugins) { + if ( + typeof plugin === 'string' + ? plugin === '@nx/vite/plugin' + : plugin.plugin === '@nx/vite/plugin' + ) { + return; + } + } + + nxJson.plugins.push({ + plugin: '@nx/vite/plugin', + options: { + buildTargetName: 'build', + previewTargetName: 'preview', + testTargetName: 'test', + serveTargetName: 'serve', + serveStaticTargetName: 'serve-static', + }, + }); + updateNxJson(tree, nxJson); +} diff --git a/packages/vite/src/generators/vitest/vitest-generator.ts b/packages/vite/src/generators/vitest/vitest-generator.ts index f20fbf6e6d30a0..c8bc605e2ac9d8 100644 --- a/packages/vite/src/generators/vitest/vitest-generator.ts +++ b/packages/vite/src/generators/vitest/vitest-generator.ts @@ -5,6 +5,7 @@ import { GeneratorCallback, joinPathFragments, offsetFromRoot, + readNxJson, readProjectConfiguration, runTasksInSerial, Tree, @@ -36,13 +37,20 @@ export async function vitestGenerator( tree, schema.project ); - let testTarget = - schema.testTarget ?? - findExistingTargetsInProject(targets).validFoundTargetName.test ?? - 'test'; - - addOrChangeTestTarget(tree, schema, testTarget); + const nxJson = readNxJson(tree); + const hasPlugin = nxJson.plugins?.some((p) => + typeof p === 'string' + ? p === '@nx/vite/plugin' + : p.plugin === '@nx/vite/plugin' + ); + if (!hasPlugin) { + const testTarget = + schema.testTarget ?? + findExistingTargetsInProject(targets).validFoundTargetName.test ?? + 'test'; + addOrChangeTestTarget(tree, schema, testTarget); + } const initTask = await initGenerator(tree, { uiFramework: schema.uiFramework, testEnvironment: schema.testEnvironment, diff --git a/packages/vite/src/generators/vitest/vitest.spec.ts b/packages/vite/src/generators/vitest/vitest.spec.ts index 80336aa433bf28..36b9eb9a42b985 100644 --- a/packages/vite/src/generators/vitest/vitest.spec.ts +++ b/packages/vite/src/generators/vitest/vitest.spec.ts @@ -54,9 +54,6 @@ describe('vitest generator', () => { expect(config.targets['test']).toMatchInlineSnapshot(` { "executor": "@nx/vite:test", - "options": { - "reportsDirectory": "../../coverage/apps/my-test-react-app", - }, "outputs": [ "{options.reportsDirectory}", ], diff --git a/packages/vite/src/migrations/update-15-3-1/update-vite-tsconfig-paths.ts b/packages/vite/src/migrations/update-15-3-1/update-vite-tsconfig-paths.ts index d2bdfc2c38f00e..196b4ca531f997 100644 --- a/packages/vite/src/migrations/update-15-3-1/update-vite-tsconfig-paths.ts +++ b/packages/vite/src/migrations/update-15-3-1/update-vite-tsconfig-paths.ts @@ -36,24 +36,26 @@ function findAllProjectsWithViteConfig(tree: Tree): void { ); let startOfProjects, endOfProjects; - defineConfig?.[0]?.getChildren().forEach((defineConfigContentNode) => { - // Make sure it's the one we are looking for - // We cannot assume that it's called tsConfigPaths - // So make sure it includes `projects` and `root` - if ( - defineConfigContentNode.getText().includes('projects') && - defineConfigContentNode.getText().includes('root') - ) { - findNodes(defineConfigContentNode, [ - ts.SyntaxKind.PropertyAssignment, - ]).forEach((nodePA) => { - if (nodePA.getText().startsWith('projects')) { - startOfProjects = nodePA.getStart(); - endOfProjects = nodePA.getEnd(); - } - }); - } - }); + defineConfig?.[0] + ?.getChildren() + .forEach((defineConfigContentNode: any) => { + // Make sure it's the one we are looking for + // We cannot assume that it's called tsConfigPaths + // So make sure it includes `projects` and `root` + if ( + defineConfigContentNode.getText().includes('projects') && + defineConfigContentNode.getText().includes('root') + ) { + findNodes(defineConfigContentNode, [ + ts.SyntaxKind.PropertyAssignment, + ]).forEach((nodePA) => { + if (nodePA.getText().startsWith('projects')) { + startOfProjects = nodePA.getStart(); + endOfProjects = nodePA.getEnd(); + } + }); + } + }); if (startOfProjects && endOfProjects) { newContents = applyChangesToString(newContents, [ diff --git a/packages/vite/src/migrations/update-16-6-0-change-ts-paths-plugin/change-ts-paths-plugin.ts b/packages/vite/src/migrations/update-16-6-0-change-ts-paths-plugin/change-ts-paths-plugin.ts index ffadb39c139a23..4f6c5ebd7f265d 100644 --- a/packages/vite/src/migrations/update-16-6-0-change-ts-paths-plugin/change-ts-paths-plugin.ts +++ b/packages/vite/src/migrations/update-16-6-0-change-ts-paths-plugin/change-ts-paths-plugin.ts @@ -2,7 +2,6 @@ import { Tree, getProjects, joinPathFragments } from '@nx/devkit'; import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils'; import { ViteBuildExecutorOptions } from '../../executors/build/schema'; import { tsquery } from '@phenomnomnominal/tsquery'; -import { ImportDeclaration } from 'typescript'; export default function update(tree: Tree) { const projects = getProjects(tree); @@ -20,7 +19,7 @@ export default function update(tree: Tree) { const configContents = tree.read(config, 'utf-8'); const oldTsConfigPathPlugin = - tsquery.query( + tsquery.query( configContents, 'ImportDeclaration:has(StringLiteral[value="vite-tsconfig-paths"])' ) ?? []; @@ -30,7 +29,7 @@ export default function update(tree: Tree) { } const importName = - oldTsConfigPathPlugin[0]?.importClause?.name?.text ?? + oldTsConfigPathPlugin[0]?.['importClause']?.name?.text ?? 'viteTsConfigPaths'; const updatedContent = tsquery.replace( configContents, diff --git a/packages/vite/src/migrations/update-17-2-0/add-vite-plugin.spec.ts b/packages/vite/src/migrations/update-17-2-0/add-vite-plugin.spec.ts new file mode 100644 index 00000000000000..594340403d9f48 --- /dev/null +++ b/packages/vite/src/migrations/update-17-2-0/add-vite-plugin.spec.ts @@ -0,0 +1,223 @@ +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { + readProjectConfiguration, + Tree, + updateProjectConfiguration, +} from '@nx/devkit'; +import update from './add-vite-plugin'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; + +jest.mock('vite', () => ({ + loadConfigFromFile: jest.fn().mockImplementation(() => { + return Promise.resolve({ + path: 'vite.config.ts', + config: {}, + dependencies: [], + }); + }), +})); +describe('add-vite-plugin migration', () => { + let tree: Tree; + let tempFs: TempFs; + + describe('not root project', () => { + beforeEach(async () => { + tempFs = new TempFs('test'); + tree = createTreeWithEmptyWorkspace(); + tree.root = tempFs.tempDir; + await tempFs.createFiles({ + 'apps/my-app/vite.config.ts': '', + 'apps/my-app/project.json': '{ "name": "my-app" }', + }); + tree.write('apps/my-app/vite.config.ts', `console.log('hi');`); + }); + + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + }); + + it('should remove the serve target', async () => { + updateProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + targets: { + serve: { + executor: '@nx/vite:dev-server', + defaultConfiguration: 'development', + options: { + buildTarget: 'build', + }, + configurations: { + development: { + buildTarget: 'build:development', + hmr: true, + }, + production: { + buildTarget: 'build:production', + hmr: false, + }, + }, + }, + }, + }); + + await update(tree); + + expect( + readProjectConfiguration(tree, 'my-app').targets.serve + ).toBeUndefined(); + }); + + it('should remove the serve, serve-static and build target, but keep other targets', async () => { + updateProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + targets: { + build: { + executor: '@nx/vite:build', + outputs: ['{options.outputPath}'], + defaultConfiguration: 'production', + options: { + outputPath: 'dist/apps/my-app', + }, + configurations: { + development: { + mode: 'development', + }, + production: { + mode: 'production', + }, + }, + }, + serve: { + executor: '@nx/vite:dev-server', + defaultConfiguration: 'development', + options: { + buildTarget: 'build', + }, + configurations: { + development: { + buildTarget: 'build:development', + hmr: true, + }, + production: { + buildTarget: 'build:production', + hmr: false, + }, + }, + }, + 'preview-custom': { + executor: '@nx/vite:preview-server', + defaultConfiguration: 'development', + options: { + buildTarget: 'build', + }, + configurations: { + development: { + buildTarget: 'build:development', + }, + production: { + buildTarget: 'build:production', + }, + }, + }, + 'test-custom': { + executor: '@nx/vite:test', + outputs: ['{options.reportsDirectory}'], + options: { + passWithNoTests: true, + reportsDirectory: '../../coverage/apps/my-app', + }, + }, + lint: { + executor: '@nx/eslint:lint', + outputs: ['{options.outputFile}'], + options: { + lintFilePatterns: ['apps/my-app/**/*.{ts,tsx,js,jsx}'], + }, + }, + 'serve-static': { + executor: '@nx/web:file-server', + options: { + buildTarget: 'my-app:build', + }, + }, + }, + }); + + await update(tree); + + expect( + readProjectConfiguration(tree, 'my-app').targets.serve + ).toBeUndefined(); + + expect( + readProjectConfiguration(tree, 'my-app').targets.build + ).toBeUndefined(); + + expect( + readProjectConfiguration(tree, 'my-app').targets['preview-custom'] + ).toBeDefined(); + + expect( + readProjectConfiguration(tree, 'my-app').targets['lint'] + ).toBeDefined(); + + expect( + readProjectConfiguration(tree, 'my-app').targets['test-custom'] + ).toBeDefined(); + + expect( + readProjectConfiguration(tree, 'my-app').targets['serve-static'] + ).toBeUndefined(); + }); + }); + + describe('root project', () => { + beforeEach(async () => { + tempFs = new TempFs('test'); + tree = createTreeWithEmptyWorkspace(); + tree.root = tempFs.tempDir; + await tempFs.createFiles({ + 'vite.config.ts': '', + 'project.json': '{ "name": "my-app" }', + }); + tree.write('vite.config.ts', `console.log('hi');`); + }); + + afterEach(() => { + jest.resetModules(); + tempFs.cleanup(); + }); + + it('should remove the serve target', async () => { + updateProjectConfiguration(tree, 'my-app', { + root: '.', + targets: { + serve: { + executor: '@nx/vite:dev-server', + defaultConfiguration: 'development', + options: { + buildTarget: 'build', + }, + configurations: { + development: { + buildTarget: 'build:development', + hmr: true, + }, + production: { + buildTarget: 'build:production', + hmr: false, + }, + }, + }, + }, + }); + + await update(tree); + + expect( + readProjectConfiguration(tree, 'my-app').targets.serve + ).toBeUndefined(); + }); + }); +}); diff --git a/packages/vite/src/migrations/update-17-2-0/add-vite-plugin.ts b/packages/vite/src/migrations/update-17-2-0/add-vite-plugin.ts new file mode 100644 index 00000000000000..3bb9b1916cba7f --- /dev/null +++ b/packages/vite/src/migrations/update-17-2-0/add-vite-plugin.ts @@ -0,0 +1,31 @@ +import { formatFiles, getProjects, Tree } from '@nx/devkit'; +import { createNodes } from '../../plugins/plugin'; + +import { createProjectRootMappingsFromProjectConfigurations } from 'nx/src/project-graph/utils/find-project-for-path'; +import { replaceProjectConfigurationsWithPlugin } from '@nx/devkit/src/utils/replace-project-configuration-with-plugin'; + +export default async function update(tree: Tree) { + const proj = Object.fromEntries(getProjects(tree).entries()); + + const rootMappings = createProjectRootMappingsFromProjectConfigurations(proj); + + // add function here to remove + // passWithNoTests: true, + // reportsDirectory: '../../coverage/apps/my-app', + + await replaceProjectConfigurationsWithPlugin( + tree, + rootMappings, + '@nx/vite/plugin', + createNodes, + { + buildTargetName: 'build', + serveTargetName: 'serve', + previewTargetName: 'preview', + testTargetName: 'test', + serveStaticTargetName: 'serve-static', + } + ); + + await formatFiles(tree); +} diff --git a/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap new file mode 100644 index 00000000000000..da0769cf83861b --- /dev/null +++ b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -0,0 +1,117 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`@nx/vite/plugin not root project should create nodes 1`] = ` +{ + "projects": { + "my-app": { + "root": "my-app", + "targets": { + "build-something": { + "cache": true, + "command": "pnpm exec vite build", + "inputs": [ + "production", + "^production", + ], + "options": { + "cwd": "my-app", + }, + "outputs": [ + "{options.outputPath}", + ], + }, + "my-serve": { + "command": "pnpm exec vite serve", + "options": { + "cwd": "my-app", + }, + }, + "preview-site": { + "command": "pnpm exec vite preview", + "options": { + "cwd": "my-app", + }, + }, + "serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build-something", + }, + }, + "vitest": { + "cache": true, + "command": "pnpm exec vitest run", + "inputs": [ + "default", + "^production", + ], + "options": { + "cwd": "my-app", + }, + "outputs": [ + "{options.reportsDirectory}", + ], + }, + }, + }, + }, +} +`; + +exports[`@nx/vite/plugin root project should create nodes 1`] = ` +{ + "projects": { + ".": { + "root": ".", + "targets": { + "build": { + "cache": true, + "command": "pnpm exec vite build", + "inputs": [ + "production", + "^production", + ], + "options": { + "cwd": ".", + }, + "outputs": [ + "{options.outputPath}", + ], + }, + "preview": { + "command": "pnpm exec vite preview", + "options": { + "cwd": ".", + }, + }, + "serve": { + "command": "pnpm exec vite serve", + "options": { + "cwd": ".", + }, + }, + "serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build", + }, + }, + "test": { + "cache": true, + "command": "pnpm exec vitest run", + "inputs": [ + "default", + "^production", + ], + "options": { + "cwd": ".", + }, + "outputs": [ + "{options.reportsDirectory}", + ], + }, + }, + }, + }, +} +`; diff --git a/packages/vite/src/plugins/plugin.spec.ts b/packages/vite/src/plugins/plugin.spec.ts new file mode 100644 index 00000000000000..67c6763014786f --- /dev/null +++ b/packages/vite/src/plugins/plugin.spec.ts @@ -0,0 +1,93 @@ +import { CreateNodesContext } from '@nx/devkit'; +import { createNodes } from './plugin'; +import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; + +jest.mock('vite', () => ({ + loadConfigFromFile: jest.fn().mockImplementation(() => { + return Promise.resolve({ + path: 'vite.config.ts', + config: {}, + dependencies: [], + }); + }), +})); + +describe('@nx/vite/plugin', () => { + let createNodesFunction = createNodes[1]; + let context: CreateNodesContext; + describe('root project', () => { + beforeEach(async () => { + context = { + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: '', + }; + }); + + afterEach(() => { + jest.resetModules(); + }); + + it('should create nodes', async () => { + const nodes = await createNodesFunction( + 'vite.config.ts', + { + buildTargetName: 'build', + serveTargetName: 'serve', + previewTargetName: 'preview', + testTargetName: 'test', + serveStaticTargetName: 'serve-static', + }, + context + ); + + expect(nodes).toMatchSnapshot(); + }); + }); + + // some issue wiht the tempfs + describe('not root project', () => { + const tempFs = new TempFs('test'); + beforeEach(() => { + context = { + nxJsonConfiguration: { + namedInputs: { + default: ['{projectRoot}/**/*'], + production: ['!{projectRoot}/**/*.spec.ts'], + }, + }, + workspaceRoot: tempFs.tempDir, + }; + + tempFs.createFileSync( + 'my-app/project.json', + JSON.stringify({ name: 'my-app' }) + ); + tempFs.createFileSync('my-app/vite.config.ts', ''); + }); + + afterEach(() => { + jest.resetModules(); + }); + + it('should create nodes', async () => { + const nodes = await createNodesFunction( + 'my-app/vite.config.ts', + { + buildTargetName: 'build-something', + serveTargetName: 'my-serve', + previewTargetName: 'preview-site', + testTargetName: 'vitest', + serveStaticTargetName: 'serve-static', + }, + context + ); + + expect(nodes).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/vite/src/plugins/plugin.ts b/packages/vite/src/plugins/plugin.ts new file mode 100644 index 00000000000000..b6589421867e6f --- /dev/null +++ b/packages/vite/src/plugins/plugin.ts @@ -0,0 +1,293 @@ +import { + CreateDependencies, + CreateNodes, + CreateNodesContext, + TargetConfiguration, + detectPackageManager, + getPackageManagerCommand, + joinPathFragments, + readJsonFile, + writeJsonFile, +} from '@nx/devkit'; +import { dirname, join } from 'path'; + +import { readTargetDefaultsForTarget } from 'nx/src/project-graph/utils/project-configuration-utils'; +import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; +import { UserConfig, loadConfigFromFile } from 'vite'; +import { existsSync, readdirSync } from 'fs'; +import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; +import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; +export interface VitePluginOptions { + buildTargetName?: string; + testTargetName?: string; + serveTargetName?: string; + previewTargetName?: string; + serveStaticTargetName?: string; +} + +const cachePath = join(projectGraphCacheDirectory, 'vite.hash'); +const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; + +const calculatedTargets: Record< + string, + Record +> = {}; + +function readTargetsCache(): Record< + string, + Record +> { + return readJsonFile(cachePath); +} + +function writeTargetsToCache( + targets: Record> +) { + writeJsonFile(cachePath, targets); +} + +export const createDependencies: CreateDependencies = () => { + writeTargetsToCache(calculatedTargets); + return []; +}; + +export const createNodes: CreateNodes = [ + '**/vite.config.{js,ts}', + async (configFilePath, options, context) => { + const projectRoot = dirname(configFilePath); + // Do not create a project if package.json and project.json isn't there. + const siblingFiles = readdirSync(join(context.workspaceRoot, projectRoot)); + if ( + !siblingFiles.includes('package.json') && + !siblingFiles.includes('project.json') + ) { + return {}; + } + + options = normalizeOptions(options); + + const hash = calculateHashForCreateNodes(projectRoot, options, context); + const targets = targetsCache[hash] + ? targetsCache[hash] + : await buildViteTargets(configFilePath, projectRoot, options, context); + + calculatedTargets[hash] = targets; + + return { + projects: { + [projectRoot]: { + root: projectRoot, + targets, + }, + }, + }; + }, +]; + +async function buildViteTargets( + configFilePath: string, + projectRoot: string, + options: VitePluginOptions, + context: CreateNodesContext +) { + const viteConfig = await loadConfigFromFile( + { + command: 'build', + mode: 'production', + }, + configFilePath + ); + + const { buildOutputs, testOutputs } = getOutputs( + projectRoot, + viteConfig?.config + ); + + const namedInputs = getNamedInputs(projectRoot, context); + + const targets: Record = {}; + + targets[options.buildTargetName] = await buildTarget( + context, + namedInputs, + buildOutputs, + options, + projectRoot + ); + + targets[options.serveTargetName] = serveTarget(projectRoot); + + targets[options.previewTargetName] = previewTarget(projectRoot); + + targets[options.testTargetName] = await testTarget( + context, + namedInputs, + testOutputs, + options, + projectRoot + ); + + targets[options.serveStaticTargetName] = serveStaticTarget(options) as {}; + + return targets; +} + +async function buildTarget( + context: CreateNodesContext, + namedInputs: { + [inputName: string]: any[]; + }, + outputs: string[], + options: VitePluginOptions, + projectRoot: string +) { + const targetDefaults = readTargetDefaultsForTarget( + options.buildTargetName, + context.nxJsonConfiguration.targetDefaults, + '@nx/vite:build' + ); + + const targetConfig: TargetConfiguration = { + command: `${ + getPackageManagerCommand(detectPackageManager()).exec + } vite build`, + options: { + cwd: joinPathFragments(projectRoot), + }, + }; + + if (targetDefaults?.outputs === undefined) { + targetConfig.outputs = outputs; + } + + if (targetDefaults?.cache === undefined) { + targetConfig.cache = true; + } + + if (targetDefaults?.inputs === undefined) { + targetConfig.inputs = + 'production' in namedInputs + ? ['production', '^production'] + : ['default', '^default']; + } + + return targetConfig; +} + +function serveTarget(projectRoot: string) { + const targetConfig: TargetConfiguration = { + command: `${ + getPackageManagerCommand(detectPackageManager()).exec + } vite serve`, + options: { + cwd: joinPathFragments(projectRoot), + }, + }; + + return targetConfig; +} + +function previewTarget(projectRoot: string) { + const targetConfig: TargetConfiguration = { + command: `${ + getPackageManagerCommand(detectPackageManager()).exec + } vite preview`, + options: { + cwd: joinPathFragments(projectRoot), + }, + }; + + return targetConfig; +} + +async function testTarget( + context: CreateNodesContext, + namedInputs: { + [inputName: string]: any[]; + }, + outputs: string[], + options: VitePluginOptions, + projectRoot: string +) { + const targetDefaults = readTargetDefaultsForTarget( + options.testTargetName, + context.nxJsonConfiguration.targetDefaults, + '@nx/vite:test' + ); + + const targetConfig: TargetConfiguration = { + command: `${ + getPackageManagerCommand(detectPackageManager()).exec + } vitest run`, + options: { + cwd: joinPathFragments(projectRoot), + }, + }; + + if (targetDefaults?.outputs === undefined) { + targetConfig.outputs = outputs; + } + + if (targetDefaults?.cache === undefined) { + targetConfig.cache = true; + } + + if (targetDefaults?.inputs === undefined) { + targetConfig.inputs = + 'production' in namedInputs + ? ['default', '^production'] + : ['default', '^default']; + } + return targetConfig; +} + +function serveStaticTarget(options: VitePluginOptions) { + const targetConfig: TargetConfiguration = { + executor: '@nx/web:file-server', + options: { + buildTarget: `${options.buildTargetName}`, + }, + }; + + return targetConfig; +} + +function getOutputs( + projectRoot: string, + viteConfig: UserConfig +): { + buildOutputs: string[]; + testOutputs: string[]; +} { + const { build, test } = viteConfig; + const buildOutputs = ['{options.outputPath}']; + const testOutputs = ['{options.reportsDirectory}']; + + function getOutput(path: string, projectRoot: string): string { + if (path.startsWith('..')) { + return join('{workspaceRoot}', join(projectRoot, path)); + } else { + return join('{projectRoot}', path); + } + } + + if (build?.outDir) { + buildOutputs.push(getOutput(build.outDir, projectRoot)); + } + + if (test?.coverage?.reportsDirectory) { + testOutputs.push(getOutput(test.coverage.reportsDirectory, projectRoot)); + } + + return { buildOutputs, testOutputs }; +} + +function normalizeOptions(options: VitePluginOptions): VitePluginOptions { + options ??= {}; + options.buildTargetName ??= 'build'; + options.serveTargetName ??= 'serve'; + options.previewTargetName ??= 'preview'; + options.testTargetName ??= 'test'; + options.serveStaticTargetName ??= 'serve-static'; + return options; +} diff --git a/packages/vue/src/generators/application/lib/add-e2e.ts b/packages/vue/src/generators/application/lib/add-e2e.ts index d1e265885e9e95..600dbef16ececa 100644 --- a/packages/vue/src/generators/application/lib/add-e2e.ts +++ b/packages/vue/src/generators/application/lib/add-e2e.ts @@ -4,6 +4,7 @@ import { ensurePackage, getPackageManagerCommand, joinPathFragments, + readNxJson, } from '@nx/devkit'; import { webStaticServeGenerator } from '@nx/web'; @@ -16,10 +17,18 @@ export async function addE2e( ): Promise { switch (options.e2eTestRunner) { case 'cypress': { - webStaticServeGenerator(tree, { - buildTarget: `${options.projectName}:build`, - targetName: 'serve-static', - }); + const nxJson = readNxJson(tree); + const hasPlugin = nxJson.plugins?.some((p) => + typeof p === 'string' + ? p === '@nx/vite/plugin' + : p.plugin === '@nx/vite/plugin' + ); + if (!hasPlugin) { + webStaticServeGenerator(tree, { + buildTarget: `${options.projectName}:build`, + targetName: 'serve-static', + }); + } const { configurationGenerator } = ensurePackage< typeof import('@nx/cypress') diff --git a/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap b/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap index dc9843b800d76a..b477b8007ed937 100644 --- a/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap +++ b/packages/vue/src/generators/library/__snapshots__/library.spec.ts.snap @@ -71,6 +71,7 @@ export default defineConfig({ // External packages that should not be bundled into your library. external: [], }, + outDir: 'dist/my-lib', }, test: { diff --git a/packages/webpack/src/utils/module-federation/share.ts b/packages/webpack/src/utils/module-federation/share.ts index b0188c909564d7..5ac0802a59bc2b 100644 --- a/packages/webpack/src/utils/module-federation/share.ts +++ b/packages/webpack/src/utils/module-federation/share.ts @@ -16,7 +16,6 @@ import { workspaceRoot, logger, readJsonFile, - ProjectGraphProjectNode, joinPathFragments, } from '@nx/devkit'; import { existsSync } from 'fs';