From 83db767b27cdf08f6b99788e461261aafba5a7c1 Mon Sep 17 00:00:00 2001 From: Katerina Skroumpelou Date: Thu, 30 Nov 2023 22:56:16 +0200 Subject: [PATCH] feat(vite): nodes for build, serve, test, preview targets (#20086) --- e2e/vite/src/vite-pcv3.test.ts | 59 ++++ .../application/application.pcv3.spec.ts | 15 + .../src/generators/application/lib/add-e2e.ts | 6 +- packages/react/src/utils/has-vite-plugin.ts | 10 + packages/vite/migrations.spec.ts | 10 + packages/vite/package.json | 3 +- packages/vite/plugin.ts | 5 + .../generators/configuration/configuration.ts | 30 +- packages/vite/src/generators/init/init.ts | 86 +---- .../vite/src/generators/init/lib/utils.ts | 122 ++++++++ .../src/generators/vitest/vitest-generator.ts | 15 +- .../update-vite-tsconfig-paths.ts | 38 +-- .../change-ts-paths-plugin.ts | 5 +- .../plugins/__snapshots__/plugin.spec.ts.snap | 137 ++++++++ packages/vite/src/plugins/plugin.spec.ts | 93 ++++++ packages/vite/src/plugins/plugin.ts | 296 ++++++++++++++++++ .../src/generators/application/lib/add-e2e.ts | 17 +- .../application/application.pcv3.spec.ts | 14 + .../src/utils/module-federation/share.ts | 1 - 19 files changed, 846 insertions(+), 116 deletions(-) create mode 100644 e2e/vite/src/vite-pcv3.test.ts create mode 100644 packages/react/src/utils/has-vite-plugin.ts create mode 100644 packages/vite/plugin.ts create mode 100644 packages/vite/src/generators/init/lib/utils.ts create mode 100644 packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap create mode 100644 packages/vite/src/plugins/plugin.spec.ts create mode 100644 packages/vite/src/plugins/plugin.ts diff --git a/e2e/vite/src/vite-pcv3.test.ts b/e2e/vite/src/vite-pcv3.test.ts new file mode 100644 index 0000000000..885acd247a --- /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 38e1c44e3d..2d8bd59291 100644 --- a/packages/react/src/generators/application/application.pcv3.spec.ts +++ b/packages/react/src/generators/application/application.pcv3.spec.ts @@ -1,5 +1,6 @@ import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; import { + getProjects, readNxJson, readProjectConfiguration, Tree, @@ -33,6 +34,7 @@ describe('react app generator (PCv3)', () => { const nxJson = readNxJson(appTree); nxJson.plugins ??= []; nxJson.plugins.push('@nx/webpack/plugin'); + nxJson.plugins.push('@nx/vite/plugin'); updateNxJson(appTree, nxJson); }); @@ -57,4 +59,17 @@ describe('react app generator (PCv3)', () => { `assets: ['./src/favicon.ico', './src/assets']` ); }); + + it('should not add targets for vite', async () => { + await applicationGenerator(appTree, { + ...schema, + name: 'my-vite-app', + bundler: 'vite', + }); + const projects = getProjects(appTree); + expect(projects.get('my-vite-app').targets.build).toBeUndefined(); + expect(projects.get('my-vite-app').targets.serve).toBeUndefined(); + expect(projects.get('my-vite-app').targets.preview).toBeUndefined(); + expect(projects.get('my-vite-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 6b25bb945d..2b58d96633 100644 --- a/packages/react/src/generators/application/lib/add-e2e.ts +++ b/packages/react/src/generators/application/lib/add-e2e.ts @@ -9,6 +9,7 @@ import { webStaticServeGenerator } from '@nx/web'; import { nxVersion } from '../../../utils/versions'; import { hasWebpackPlugin } from '../../../utils/has-webpack-plugin'; +import { hasVitePlugin } from '../../../utils/has-vite-plugin'; import { NormalizedSchema } from '../schema'; export async function addE2e( @@ -17,7 +18,10 @@ export async function addE2e( ): Promise { switch (options.e2eTestRunner) { case 'cypress': { - if (!hasWebpackPlugin(tree)) { + if ( + (options.bundler === 'webpack' && !hasWebpackPlugin(tree)) || + (options.bundler === 'vite' && !hasVitePlugin(tree)) + ) { webStaticServeGenerator(tree, { buildTarget: `${options.projectName}:build`, targetName: 'serve-static', diff --git a/packages/react/src/utils/has-vite-plugin.ts b/packages/react/src/utils/has-vite-plugin.ts new file mode 100644 index 0000000000..9430f5030e --- /dev/null +++ b/packages/react/src/utils/has-vite-plugin.ts @@ -0,0 +1,10 @@ +import { readNxJson, Tree } from '@nx/devkit'; + +export function hasVitePlugin(tree: Tree) { + const nxJson = readNxJson(tree); + return !!nxJson.plugins?.some((p) => + typeof p === 'string' + ? p === '@nx/vite/plugin' + : p.plugin === '@nx/vite/plugin' + ); +} diff --git a/packages/vite/migrations.spec.ts b/packages/vite/migrations.spec.ts index 655cf2ebad..c92ed556d1 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 a86c4f6e6e..8393287987 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 0000000000..ae222554bb --- /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/configuration.ts b/packages/vite/src/generators/configuration/configuration.ts index 454101261a..8e99b8498e 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 5bd7187621..b8d57a9744 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 0000000000..428693b440 --- /dev/null +++ b/packages/vite/src/generators/init/lib/utils.ts @@ -0,0 +1,122 @@ +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)); + } + + 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 f077e01c7e..828efc9f88 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, @@ -42,10 +43,20 @@ export async function vitestGenerator( findExistingTargetsInProject(targets).validFoundTargetName.test ?? 'test'; - if (!hasPlugin) { + const nxJson = readNxJson(tree); + const hasPluginCheck = nxJson.plugins?.some( + (p) => + (typeof p === 'string' + ? p === '@nx/vite/plugin' + : p.plugin === '@nx/vite/plugin') || hasPlugin + ); + if (!hasPluginCheck) { + 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/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 d2bdfc2c38..196b4ca531 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 ffadb39c13..4f6c5ebd7f 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/plugins/__snapshots__/plugin.spec.ts.snap b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap new file mode 100644 index 0000000000..d35229bffc --- /dev/null +++ b/packages/vite/src/plugins/__snapshots__/plugin.spec.ts.snap @@ -0,0 +1,137 @@ +// 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": "vite build", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "vite", + ], + }, + ], + "options": { + "cwd": "my-app", + }, + "outputs": [ + "{options.outputPath}", + ], + }, + "my-serve": { + "command": "vite serve", + "options": { + "cwd": "my-app", + }, + }, + "preview-site": { + "command": "vite preview", + "options": { + "cwd": "my-app", + }, + }, + "serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build-something", + }, + }, + "vitest": { + "cache": true, + "command": "vitest run", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "vitest", + ], + }, + ], + "options": { + "cwd": "my-app", + }, + "outputs": [ + "{options.reportsDirectory}", + ], + }, + }, + }, + }, +} +`; + +exports[`@nx/vite/plugin root project should create nodes 1`] = ` +{ + "projects": { + ".": { + "root": ".", + "targets": { + "build": { + "cache": true, + "command": "vite build", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "vite", + ], + }, + ], + "options": { + "cwd": ".", + }, + "outputs": [ + "{options.outputPath}", + ], + }, + "preview": { + "command": "vite preview", + "options": { + "cwd": ".", + }, + }, + "serve": { + "command": "vite serve", + "options": { + "cwd": ".", + }, + }, + "serve-static": { + "executor": "@nx/web:file-server", + "options": { + "buildTarget": "build", + }, + }, + "test": { + "cache": true, + "command": "vitest run", + "inputs": [ + "production", + "^production", + { + "externalDependencies": [ + "vitest", + ], + }, + ], + "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 0000000000..67c6763014 --- /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 0000000000..4d07ed9b2b --- /dev/null +++ b/packages/vite/src/plugins/plugin.ts @@ -0,0 +1,296 @@ +import { + CreateDependencies, + CreateNodes, + CreateNodesContext, + TargetConfiguration, + detectPackageManager, + joinPathFragments, + readJsonFile, + workspaceRoot, + writeJsonFile, +} from '@nx/devkit'; +import { dirname, isAbsolute, join, relative, resolve } 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'; +import { getLockFileName } from '@nx/js'; +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, [ + getLockFileName(detectPackageManager(context.workspaceRoot)), + ]); + 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 + ); + + const targetConfig: TargetConfiguration = { + command: `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']), + { + externalDependencies: ['vite'], + }, + ]; + } + + return targetConfig; +} + +function serveTarget(projectRoot: string) { + const targetConfig: TargetConfiguration = { + command: `vite serve`, + options: { + cwd: joinPathFragments(projectRoot), + }, + }; + + return targetConfig; +} + +function previewTarget(projectRoot: string) { + const targetConfig: TargetConfiguration = { + command: `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 + ); + + const targetConfig: TargetConfiguration = { + command: `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 + ? ['production', '^production'] + : ['default', '^default']), + { + externalDependencies: ['vitest'], + }, + ]; + } + 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 if (isAbsolute(resolve(path))) { + return `{workspaceRoot}/${relative(workspaceRoot, 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 d1e265885e..600dbef16e 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/web/src/generators/application/application.pcv3.spec.ts b/packages/web/src/generators/application/application.pcv3.spec.ts index d3cb40cb17..f420f56f2e 100644 --- a/packages/web/src/generators/application/application.pcv3.spec.ts +++ b/packages/web/src/generators/application/application.pcv3.spec.ts @@ -1,5 +1,6 @@ import { installedCypressVersion } from '@nx/cypress/src/utils/cypress-version'; import { + getProjects, readNxJson, readProjectConfiguration, Tree, @@ -28,6 +29,7 @@ describe('web app generator (PCv3)', () => { const nxJson = readNxJson(tree); nxJson.plugins ??= []; nxJson.plugins.push('@nx/webpack/plugin'); + nxJson.plugins.push('@nx/vite/plugin'); updateNxJson(tree, nxJson); }); @@ -50,4 +52,16 @@ describe('web app generator (PCv3)', () => { `assets: ['./src/favicon.ico', './src/assets']` ); }); + + it('should not add targets for vite', async () => { + await applicationGenerator(tree, { + name: 'my-vite-app', + bundler: 'vite', + }); + const projects = getProjects(tree); + expect(projects.get('my-vite-app').targets.build).toBeUndefined(); + expect(projects.get('my-vite-app').targets.serve).toBeUndefined(); + expect(projects.get('my-vite-app').targets.preview).toBeUndefined(); + expect(projects.get('my-vite-app').targets.test).toBeUndefined(); + }); }); diff --git a/packages/webpack/src/utils/module-federation/share.ts b/packages/webpack/src/utils/module-federation/share.ts index b0188c9095..5ac0802a59 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';