diff --git a/e2e/plugin/src/nx-plugin.test.ts b/e2e/plugin/src/nx-plugin.test.ts index a06912df67a6a8..82e7d695e3da26 100644 --- a/e2e/plugin/src/nx-plugin.test.ts +++ b/e2e/plugin/src/nx-plugin.test.ts @@ -1,6 +1,7 @@ import { ProjectConfiguration } from '@nx/devkit'; import { checkFilesExist, + checkFilesMatchingPatternExist, cleanupProject, createFile, expectTestsPass, @@ -54,6 +55,31 @@ describe('Nx Plugin', () => { runCLI(`e2e ${plugin}-e2e`); }, 90000); + it('should be able to generate a Nx Plugin with vitest e2e tests', async () => { + const plugin = uniq('plugin'); + + runCLI( + `generate @nx/plugin:plugin ${plugin} --linter=eslint --e2eTestRunner=vitest --publishable` + ); + const lintResults = runCLI(`lint ${plugin}`); + expect(lintResults).toContain('All files pass linting'); + + const buildResults = runCLI(`build ${plugin}`); + expect(buildResults).toContain('Done compiling TypeScript files'); + checkFilesExist( + `dist/${plugin}/package.json`, + `dist/${plugin}/src/index.js` + ); + + // Verify vitest config was created + checkFilesMatchingPatternExist(`${plugin}-e2e/vitest.config.(ts|mts)`); + + // Run the e2e tests with vitest + expect(() => { + runCLI(`e2e ${plugin}-e2e`); + }).not.toThrow(); + }, 120000); + it('should be able to generate a migration', async () => { const plugin = uniq('plugin'); const version = '1.0.0'; diff --git a/packages/plugin/docs/generators/e2e-project-examples.md b/packages/plugin/docs/generators/e2e-project-examples.md index 41fffa20aebd0c..671a5bf134c6c4 100644 --- a/packages/plugin/docs/generators/e2e-project-examples.md +++ b/packages/plugin/docs/generators/e2e-project-examples.md @@ -1,9 +1,17 @@ ## Examples -##### E2E Project +##### E2E Project with Jest (default) -Scaffolds an E2E project for the plugin `my-plugin`. +Scaffolds an E2E project for the plugin `my-plugin` using Jest. ```bash nx g @nx/plugin:e2e-project --pluginName my-plugin --npmPackageName my-plugin --pluginOutputPath dist/my-plugin ``` + +##### E2E Project with Vitest + +Scaffolds an E2E project for the plugin `my-plugin` using Vitest. + +```bash +nx g @nx/plugin:e2e-project --pluginName my-plugin --npmPackageName my-plugin --pluginOutputPath dist/my-plugin --testRunner vitest +``` diff --git a/packages/plugin/package.json b/packages/plugin/package.json index 4cc6ab538ac258..37639e799c3077 100644 --- a/packages/plugin/package.json +++ b/packages/plugin/package.json @@ -37,6 +37,14 @@ "devDependencies": { "nx": "workspace:*" }, + "peerDependencies": { + "@nx/vitest": "workspace:*" + }, + "peerDependenciesMeta": { + "@nx/vitest": { + "optional": true + } + }, "publishConfig": { "access": "public" } diff --git a/packages/plugin/src/generators/e2e-project/e2e.spec.ts b/packages/plugin/src/generators/e2e-project/e2e.spec.ts index 8c9bd5a5e591b6..7823f9b422e7f0 100644 --- a/packages/plugin/src/generators/e2e-project/e2e.spec.ts +++ b/packages/plugin/src/generators/e2e-project/e2e.spec.ts @@ -3,11 +3,11 @@ import 'nx/src/internal-testing-utils/mock-project-graph'; import { Tree, addProjectConfiguration, - readProjectConfiguration, - readJson, getProjects, - writeJson, + readJson, + readProjectConfiguration, updateJson, + writeJson, } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; import { e2eProjectGenerator } from './e2e'; @@ -211,6 +211,44 @@ describe('NxPlugin e2e-project Generator', () => { expect(tree.exists('my-plugin-e2e/.spec.swcrc')).toBeFalsy(); }); + it('should add vitest support', async () => { + await e2eProjectGenerator(tree, { + pluginName: 'my-plugin', + pluginOutputPath: `dist/libs/my-plugin`, + npmPackageName: '@proj/my-plugin', + testRunner: 'vitest', + addPlugin: false, + }); + + const project = readProjectConfiguration(tree, 'my-plugin-e2e'); + + expect(project.targets.e2e.executor).toBe('@nx/vitest:test'); + expect(project.targets.e2e).toMatchObject({ + dependsOn: ['^build'], + options: expect.objectContaining({ + pool: 'forks', + poolOptions: { + forks: { + singleFork: true, + }, + }, + }), + }); + + expect(tree.exists('my-plugin-e2e/tsconfig.spec.json')).toBeTruthy(); + const vitestConfigExists = + tree.exists('my-plugin-e2e/vitest.config.ts') || + tree.exists('my-plugin-e2e/vitest.config.mts'); + expect(vitestConfigExists).toBeTruthy(); + + const vitestConfigPath = tree.exists('my-plugin-e2e/vitest.config.ts') + ? 'my-plugin-e2e/vitest.config.ts' + : 'my-plugin-e2e/vitest.config.mts'; + const vitestConfig = tree.read(vitestConfigPath, 'utf-8'); + expect(vitestConfig).toContain('globalSetup'); + expect(vitestConfig).toContain('globalTeardown'); + }); + it('should setup the eslint builder', async () => { await e2eProjectGenerator(tree, { pluginName: 'my-plugin', diff --git a/packages/plugin/src/generators/e2e-project/e2e.ts b/packages/plugin/src/generators/e2e-project/e2e.ts index 6b64161ada0f0c..4cfa3f3a60a2eb 100644 --- a/packages/plugin/src/generators/e2e-project/e2e.ts +++ b/packages/plugin/src/generators/e2e-project/e2e.ts @@ -1,5 +1,6 @@ import { addProjectConfiguration, + ensurePackage, formatFiles, generateFiles, getPackageManagerCommand, @@ -10,12 +11,12 @@ import { readNxJson, readProjectConfiguration, runTasksInSerial, + Tree, updateJson, updateProjectConfiguration, writeJson, type GeneratorCallback, type ProjectConfiguration, - type Tree, } from '@nx/devkit'; import { determineProjectNameAndRootOptions } from '@nx/devkit/src/generators/project-name-and-root-utils'; import { LinterType, lintProjectGenerator } from '@nx/eslint'; @@ -32,10 +33,13 @@ import { addProjectToTsSolutionWorkspace, isUsingTsSolutionSetup, } from '@nx/js/src/utils/typescript/ts-solution-setup'; +import type { VitestGeneratorSchema } from '@nx/vitest/generators'; import type { PackageJson } from 'nx/src/utils/package-json'; import { join } from 'path'; import type { Schema } from './schema'; +const nxVersion = require('../../../package.json').version; + interface NormalizedSchema extends Schema { projectRoot: string; projectName: string; @@ -197,6 +201,130 @@ async function addJest(host: Tree, options: NormalizedSchema) { return jestTask; } +async function addVitest(host: Tree, options: NormalizedSchema) { + const projectConfiguration: ProjectConfiguration = { + name: options.projectName, + root: options.projectRoot, + projectType: 'application', + sourceRoot: `${options.projectRoot}/src`, + implicitDependencies: [options.pluginName], + }; + + if (options.isTsSolutionSetup) { + writeJson( + host, + joinPathFragments(options.projectRoot, 'package.json'), + { + name: options.projectName, + version: '0.0.1', + private: true, + } + ); + updateProjectConfiguration(host, options.projectName, projectConfiguration); + } else { + projectConfiguration.targets = {}; + addProjectConfiguration(host, options.projectName, projectConfiguration); + } + + // Ensure @nx/vitest is installed before using it + ensurePackage('@nx/vitest', nxVersion); + const { configurationGenerator: vitestConfigurationGenerator } = await import( + '@nx/vitest/generators' + ); + + const vitestTask = await vitestConfigurationGenerator(host, { + project: options.projectName, + testTarget: 'e2e', + skipFormat: true, + addPlugin: options.addPlugin, + testEnvironment: 'node', + coverageProvider: 'none', + } satisfies Partial); + + const { startLocalRegistryPath, stopLocalRegistryPath } = + addLocalRegistryScripts(host); + + // Add globalSetup and globalTeardown to vitest config + // Check for both .mts and .ts extensions (mts is checked first as it's the default created by @nx/vitest) + const vitestConfigExtensions = ['mts', 'ts']; + let vitestConfigPath: string | undefined; + + for (const ext of vitestConfigExtensions) { + const configPath = joinPathFragments( + options.projectRoot, + `vitest.config.${ext}` + ); + if (host.exists(configPath)) { + vitestConfigPath = configPath; + break; + } + } + + if (vitestConfigPath) { + let vitestConfig = host.read(vitestConfigPath, 'utf-8'); + const globalSetupPath = join( + offsetFromRoot(options.projectRoot), + startLocalRegistryPath + ); + const globalTeardownPath = join( + offsetFromRoot(options.projectRoot), + stopLocalRegistryPath + ); + + // Insert globalSetup and globalTeardown in the test config + // Look for 'test: {' and insert our properties right after the opening brace + const testConfigRegex = /(test:\s*\{\s*)/; + const match = testConfigRegex.exec(vitestConfig); + + if (match) { + // Extract the indentation from the next line to maintain consistent formatting + const afterMatch = vitestConfig.slice(match.index + match[0].length); + const nextLineMatch = afterMatch.match(/\n(\s*)/); + const indent = nextLineMatch ? nextLineMatch[1] : ' '; + + vitestConfig = vitestConfig.replace( + testConfigRegex, + `$1\n${indent}globalSetup: '${globalSetupPath}',\n${indent}globalTeardown: '${globalTeardownPath}',` + ); + host.write(vitestConfigPath, vitestConfig); + } else { + // If we can't find the test config block, log a warning + throw new Error( + `Could not find test configuration block in ${vitestConfigPath}. Please manually add globalSetup and globalTeardown properties.` + ); + } + } else { + // This should not happen as the vitest configuration generator should create the config file + throw new Error( + `Could not find Vitest config for project ${options.projectName} at ${options.projectRoot}` + ); + } + + const project = readProjectConfiguration(host, options.projectName); + project.targets ??= {}; + if (project.targets.e2e) { + const e2eTarget = project.targets.e2e; + + project.targets.e2e = { + ...e2eTarget, + dependsOn: [`^build`], + options: { + ...e2eTarget.options, + pool: 'forks', + poolOptions: { + forks: { + singleFork: true, + }, + }, + }, + }; + + updateProjectConfiguration(host, options.projectName, project); + } + + return vitestTask; +} + async function addLintingToApplication( tree: Tree, options: NormalizedSchema @@ -207,7 +335,7 @@ async function addLintingToApplication( tsConfigPaths: [ joinPathFragments(options.projectRoot, 'tsconfig.app.json'), ], - unitTestRunner: 'jest', + unitTestRunner: options.testRunner ?? 'jest', skipFormat: true, setParserOptionsProject: false, addPlugin: options.addPlugin, @@ -238,13 +366,24 @@ export async function e2eProjectGeneratorInternal(host: Tree, schema: Schema) { validatePlugin(host, schema.pluginName); const options = await normalizeOptions(host, schema); + + // Default to jest if no testRunner is specified + options.testRunner = options.testRunner ?? 'jest'; + addFiles(host, options); tasks.push( await setupVerdaccio(host, { skipFormat: true, }) ); - tasks.push(await addJest(host, options)); + + // Add test runner based on the testRunner option + if (options.testRunner === 'vitest') { + tasks.push(await addVitest(host, options)); + } else { + tasks.push(await addJest(host, options)); + } + updatePluginPackageJson(host, options); if (options.linter !== 'none') { diff --git a/packages/plugin/src/generators/e2e-project/files/src/__simplePluginName__.spec.ts__tmpl__ b/packages/plugin/src/generators/e2e-project/files/src/__simplePluginName__.spec.ts__tmpl__ index 1519076b188c4e..eaa8ec99c9bd16 100644 --- a/packages/plugin/src/generators/e2e-project/files/src/__simplePluginName__.spec.ts__tmpl__ +++ b/packages/plugin/src/generators/e2e-project/files/src/__simplePluginName__.spec.ts__tmpl__ @@ -8,14 +8,14 @@ describe('<%= pluginName %>', () => { beforeAll(() => { projectDirectory = createTestProject(); - // The plugin has been built and published to a local registry in the jest globalSetup + // The plugin has been built and published to a local registry in the globalSetup // Install the plugin built with the latest source code into the test repo execSync(`<%= packageManagerCommands.addDev %> <%= pluginPackageName %>@e2e`, { cwd: projectDirectory, stdio: 'inherit', env: process.env, }); - }); + }, 30_000); afterAll(() => { if (projectDirectory) { diff --git a/packages/plugin/src/generators/e2e-project/schema.d.ts b/packages/plugin/src/generators/e2e-project/schema.d.ts index 47f9903cc6e4df..670a7f4689ee2b 100644 --- a/packages/plugin/src/generators/e2e-project/schema.d.ts +++ b/packages/plugin/src/generators/e2e-project/schema.d.ts @@ -6,6 +6,7 @@ export interface Schema { projectDirectory?: string; pluginOutputPath?: string; jestConfig?: string; + testRunner?: 'jest' | 'vitest'; linter?: Linter | LinterType; skipFormat?: boolean; rootProject?: boolean; diff --git a/packages/plugin/src/generators/e2e-project/schema.json b/packages/plugin/src/generators/e2e-project/schema.json index 77c4b5ba091d42..2e6c814d22a295 100644 --- a/packages/plugin/src/generators/e2e-project/schema.json +++ b/packages/plugin/src/generators/e2e-project/schema.json @@ -30,6 +30,12 @@ "type": "string", "description": "Jest config file." }, + "testRunner": { + "type": "string", + "enum": ["jest", "vitest"], + "description": "Test runner to use for the e2e tests.", + "default": "jest" + }, "linter": { "description": "The tool to use for running lint checks.", "type": "string", diff --git a/packages/plugin/src/generators/plugin/plugin.spec.ts b/packages/plugin/src/generators/plugin/plugin.spec.ts index 7b1bab6943788a..a581d6083e5e96 100644 --- a/packages/plugin/src/generators/plugin/plugin.spec.ts +++ b/packages/plugin/src/generators/plugin/plugin.spec.ts @@ -341,6 +341,22 @@ describe('NxPlugin Plugin Generator', () => { const projects = getProjects(tree); expect(projects.has('my-plugin-e2e')).toBe(false); }); + + it('should generate e2e project with jest', async () => { + await pluginGenerator(tree, getSchema({ e2eTestRunner: 'jest' })); + const projects = getProjects(tree); + expect(projects.has('my-plugin-e2e')).toBe(true); + const e2eProject = projects.get('my-plugin-e2e'); + expect(e2eProject.targets.e2e.executor).toBe('@nx/jest:jest'); + }); + + it('should generate e2e project with vitest', async () => { + await pluginGenerator(tree, getSchema({ e2eTestRunner: 'vitest' })); + const projects = getProjects(tree); + expect(projects.has('my-plugin-e2e')).toBe(true); + const e2eProject = projects.get('my-plugin-e2e'); + expect(e2eProject.targets.e2e.executor).toBe('@nx/vitest:test'); + }); }); describe('TS solution setup', () => { diff --git a/packages/plugin/src/generators/plugin/plugin.ts b/packages/plugin/src/generators/plugin/plugin.ts index e86ac710851b5e..b69e6413039bd0 100644 --- a/packages/plugin/src/generators/plugin/plugin.ts +++ b/packages/plugin/src/generators/plugin/plugin.ts @@ -130,6 +130,11 @@ export async function pluginGeneratorInternal(host: Tree, schema: Schema) { { [options.unitTestRunner === 'vitest' ? '@nx/vitest' : '@nx/jest']: nxVersion, + ...(options.e2eTestRunner === 'vitest' + ? { '@nx/vitest': nxVersion } + : options.e2eTestRunner === 'jest' + ? { '@nx/jest': nxVersion } + : {}), '@nx/js': nxVersion, '@nx/plugin': nxVersion, } @@ -159,6 +164,7 @@ export async function pluginGeneratorInternal(host: Tree, schema: Schema) { linter: options.linter, useProjectJson: options.useProjectJson, addPlugin: options.addPlugin, + testRunner: options.e2eTestRunner as 'jest' | 'vitest', }) ); } diff --git a/packages/plugin/src/generators/plugin/schema.d.ts b/packages/plugin/src/generators/plugin/schema.d.ts index 6287ce0c9b6d3c..d5562124214324 100644 --- a/packages/plugin/src/generators/plugin/schema.d.ts +++ b/packages/plugin/src/generators/plugin/schema.d.ts @@ -7,7 +7,7 @@ export interface Schema { skipTsConfig?: boolean; // default is false skipFormat?: boolean; // default is false skipLintChecks?: boolean; // default is false - e2eTestRunner?: 'jest' | 'none'; + e2eTestRunner?: 'jest' | 'vitest' | 'none'; e2eProjectDirectory?: string; tags?: string; unitTestRunner?: 'jest' | 'vitest' | 'none'; diff --git a/packages/plugin/src/generators/plugin/schema.json b/packages/plugin/src/generators/plugin/schema.json index 88c74e516fd606..25cbd7f78fac82 100644 --- a/packages/plugin/src/generators/plugin/schema.json +++ b/packages/plugin/src/generators/plugin/schema.json @@ -67,7 +67,7 @@ }, "e2eTestRunner": { "type": "string", - "enum": ["jest", "none"], + "enum": ["jest", "vitest", "none"], "description": "Test runner to use for end to end (E2E) tests.", "default": "none" }, diff --git a/packages/plugin/tsconfig.lib.json b/packages/plugin/tsconfig.lib.json index da4673aea87cf9..27e9626d4ed9d8 100644 --- a/packages/plugin/tsconfig.lib.json +++ b/packages/plugin/tsconfig.lib.json @@ -24,11 +24,14 @@ { "path": "../jest/tsconfig.lib.json" }, + { + "path": "../devkit/tsconfig.lib.json" + }, { "path": "../nx/tsconfig.lib.json" }, { - "path": "../devkit/tsconfig.lib.json" + "path": "../vitest/tsconfig.lib.json" } ] } diff --git a/packages/react-native/src/utils/add-jest.ts b/packages/react-native/src/utils/add-jest.ts index 8d38676aaf0949..cd256ac895bb64 100644 --- a/packages/react-native/src/utils/add-jest.ts +++ b/packages/react-native/src/utils/add-jest.ts @@ -1,4 +1,9 @@ -import { Tree, ensurePackage, offsetFromRoot } from '@nx/devkit'; +import { + Tree, + ensurePackage, + offsetFromRoot, + type GeneratorCallback, +} from '@nx/devkit'; import { nxVersion } from './versions'; export async function addJest( @@ -10,7 +15,7 @@ export async function addJest( skipPackageJson: boolean, addPlugin: boolean, runtimeTsconfigFileName: string -) { +): Promise { if (unitTestRunner !== 'jest') { return () => {}; } diff --git a/packages/vitest/src/generators/configuration/configuration.ts b/packages/vitest/src/generators/configuration/configuration.ts index 6918f88757a07a..f29a6a81e15ff3 100644 --- a/packages/vitest/src/generators/configuration/configuration.ts +++ b/packages/vitest/src/generators/configuration/configuration.ts @@ -22,19 +22,19 @@ import { } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { typesNodeVersion } from '@nx/js/src/utils/versions'; import { join } from 'path'; +import { clean, coerce, major } from 'semver'; +import { detectUiFramework } from '../../utils/detect-ui-framework'; import { ensureDependencies } from '../../utils/ensure-dependencies'; import { addOrChangeTestTarget, createOrEditViteConfig, } from '../../utils/generator-utils'; -import initGenerator from '../init/init'; -import { VitestGeneratorSchema } from './schema'; -import { detectUiFramework } from '../../utils/detect-ui-framework'; import { getInstalledViteMajorVersion, getVitestDependenciesVersionsToInstall, } from '../../utils/version-utils'; -import { clean, coerce, major } from 'semver'; +import initGenerator from '../init/init'; +import { VitestGeneratorSchema } from './schema'; /** * Determines whether to use vitest.config.mts instead of vite.config.mts. @@ -210,7 +210,10 @@ getTestBed().initTestEnvironment( : `import react from '@vitejs/plugin-react'`, ], plugins: ['react()'], - coverageProvider: schema.coverageProvider, + coverageProvider: + schema.coverageProvider === 'none' + ? undefined + : schema.coverageProvider, useEsmExtension: true, }, true, @@ -448,6 +451,8 @@ async function getCoverageProviderDependency( return { '@vitest/coverage-istanbul': vitestCoverageIstanbul, }; + case 'none': + return {}; default: return { '@vitest/coverage-v8': vitestCoverageV8, diff --git a/packages/vitest/src/generators/configuration/schema.d.ts b/packages/vitest/src/generators/configuration/schema.d.ts index a238348ac84483..97390ce3da98db 100644 --- a/packages/vitest/src/generators/configuration/schema.d.ts +++ b/packages/vitest/src/generators/configuration/schema.d.ts @@ -1,7 +1,7 @@ export interface VitestGeneratorSchema { project: string; uiFramework?: 'angular' | 'react' | 'vue' | 'none'; - coverageProvider: 'v8' | 'istanbul' | 'custom'; + coverageProvider: 'v8' | 'istanbul' | 'custom' | 'none'; inSourceTests?: boolean; skipViteConfig?: boolean; testTarget?: string; diff --git a/packages/vitest/src/generators/configuration/schema.json b/packages/vitest/src/generators/configuration/schema.json index 2b3dbadc2e1792..f285a5958b2b17 100644 --- a/packages/vitest/src/generators/configuration/schema.json +++ b/packages/vitest/src/generators/configuration/schema.json @@ -30,7 +30,7 @@ }, "coverageProvider": { "type": "string", - "enum": ["v8", "istanbul", "custom"], + "enum": ["v8", "istanbul", "custom", "none"], "default": "v8", "description": "Coverage provider to use." }, diff --git a/packages/vitest/src/utils/__snapshots__/generator-utils.spec.ts.snap b/packages/vitest/src/utils/__snapshots__/generator-utils.spec.ts.snap new file mode 100644 index 00000000000000..5c08db99bd7954 --- /dev/null +++ b/packages/vitest/src/utils/__snapshots__/generator-utils.spec.ts.snap @@ -0,0 +1,449 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`createOrEditViteConfig test environment options should default to jsdom when testEnvironment is not specified 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig test environment options should set test environment to edge-runtime 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'edge-runtime', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig test environment options should set test environment to happy-dom 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'happy-dom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig test environment options should set test environment to jsdom 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig test environment options should set test environment to node 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vite config generation (not vitest-only) should generate vite.config.ts with vitest reference when not onlyVitest 1`] = ` +"/// +import { defineConfig } from 'vite'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + server:{ + port: 4200, + host: 'localhost', + }, + preview:{ + port: 4300, + host: 'localhost', + }, + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: () => [ nxViteTsPaths() ], + // }, + build: { + outDir: '../../dist/apps/my-app', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vite config generation (not vitest-only) should include library build config when includeLib is true 1`] = ` +"/// +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; +import * as path from 'path'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/libs/my-lib', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md']), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'), pathsToAliases: false })], + // Uncomment this if you are using workers. + // worker: { + // plugins: () => [ nxViteTsPaths() ], + // }, + // Configuration for building your library. + // See: https://vite.dev/guide/build.html#library-mode + build: { + outDir: '../../dist/libs/my-lib', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'my-lib', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es' as const] + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: [] + }, + }, + test: { + name: 'my-lib', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/libs/my-lib', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should generate valid JavaScript syntax for all coverage provider options 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/test-project', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'test-project', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/test-project', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should generate valid JavaScript syntax for all coverage provider options 2`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/test-project', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'test-project', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/test-project', + provider: 'istanbul' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should generate valid JavaScript syntax for all coverage provider options 3`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/test-project', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'test-project', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/test-project', + provider: 'custom' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should generate valid JavaScript syntax for all coverage provider options 4`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/test-project', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'test-project', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'] + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should generate vitest config with istanbul coverage provider 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'istanbul' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should generate vitest config with v8 coverage provider 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should generate vitest config without coverage when coverageProvider is none 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'] + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should include includeSource when inSourceTests is true 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + define: { + 'import.meta.vitest': undefined + }, + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; + +exports[`createOrEditViteConfig vitest config generation should include setupFiles when setupFile option is provided 1`] = ` +"import { defineConfig } from 'vitest/config'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../node_modules/.vite/apps/my-app', + plugins: [nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + test: { + name: 'my-app', + watch: false, + globals: true, + environment: 'node', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + setupFiles: ['./src/test-setup.ts'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../coverage/apps/my-app', + provider: 'v8' as const, + } + }, +})); +" +`; diff --git a/packages/vitest/src/utils/generator-utils.spec.ts b/packages/vitest/src/utils/generator-utils.spec.ts new file mode 100644 index 00000000000000..b232212947ef9d --- /dev/null +++ b/packages/vitest/src/utils/generator-utils.spec.ts @@ -0,0 +1,249 @@ +import { addProjectConfiguration, Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import { createOrEditViteConfig } from './generator-utils'; + +jest.mock('@nx/js/src/utils/typescript/ts-solution-setup', () => ({ + isUsingTsSolutionSetup: jest.fn(() => false), +})); + +describe('createOrEditViteConfig', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'my-app', { + root: 'apps/my-app', + sourceRoot: 'apps/my-app/src', + projectType: 'application', + }); + }); + + describe('vitest config generation', () => { + it('should generate vitest config with v8 coverage provider', () => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'v8', + testEnvironment: 'node', + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + const config = tree.read('apps/my-app/vitest.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + }); + + it('should generate vitest config with istanbul coverage provider', () => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'istanbul', + testEnvironment: 'jsdom', + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + const config = tree.read('apps/my-app/vitest.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + }); + + it('should generate vitest config without coverage when coverageProvider is none', () => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'none', + testEnvironment: 'node', + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + const config = tree.read('apps/my-app/vitest.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + }); + + it('should generate valid JavaScript syntax for all coverage provider options', () => { + const coverageProviders = ['v8', 'istanbul', 'custom', 'none'] as const; + + for (const provider of coverageProviders) { + const testTree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(testTree, 'test-project', { + root: 'apps/test-project', + sourceRoot: 'apps/test-project/src', + projectType: 'application', + }); + + createOrEditViteConfig( + testTree, + { + project: 'test-project', + includeVitest: true, + coverageProvider: provider, + testEnvironment: 'node', + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + const config = testTree.read( + 'apps/test-project/vitest.config.ts', + 'utf-8' + ); + + expect(config).toMatchSnapshot(); + } + }); + + it('should use .mts extension when useEsmExtension is true', () => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'v8', + testEnvironment: 'node', + useEsmExtension: true, + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + expect(tree.exists('apps/my-app/vitest.config.mts')).toBe(true); + expect(tree.exists('apps/my-app/vitest.config.ts')).toBe(false); + }); + + it('should include setupFiles when setupFile option is provided', () => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'v8', + testEnvironment: 'node', + setupFile: './src/test-setup.ts', + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + const config = tree.read('apps/my-app/vitest.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + }); + + it('should include includeSource when inSourceTests is true', () => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'v8', + testEnvironment: 'node', + inSourceTests: true, + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + const config = tree.read('apps/my-app/vitest.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + }); + }); + + describe('vite config generation (not vitest-only)', () => { + it('should generate vite.config.ts with vitest reference when not onlyVitest', () => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'v8', + testEnvironment: 'node', + }, + false, + { skipPackageJson: true } + ); + + const config = tree.read('apps/my-app/vite.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + }); + + it('should include library build config when includeLib is true', () => { + addProjectConfiguration(tree, 'my-lib', { + root: 'libs/my-lib', + sourceRoot: 'libs/my-lib/src', + projectType: 'library', + }); + + createOrEditViteConfig( + tree, + { + project: 'my-lib', + includeVitest: true, + includeLib: true, + coverageProvider: 'v8', + testEnvironment: 'node', + }, + false, + { skipPackageJson: true } + ); + + const config = tree.read('libs/my-lib/vite.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + }); + }); + + describe('test environment options', () => { + it.each(['node', 'jsdom', 'happy-dom', 'edge-runtime'] as const)( + 'should set test environment to %s', + (env) => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'v8', + testEnvironment: env, + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + const config = tree.read('apps/my-app/vitest.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + } + ); + + it('should default to jsdom when testEnvironment is not specified', () => { + createOrEditViteConfig( + tree, + { + project: 'my-app', + includeVitest: true, + coverageProvider: 'v8', + }, + true, + { vitestFileName: true, skipPackageJson: true } + ); + + const config = tree.read('apps/my-app/vitest.config.ts', 'utf-8'); + + expect(config).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/vitest/src/utils/generator-utils.ts b/packages/vitest/src/utils/generator-utils.ts index 2087e6b33c7307..6394937b2451dd 100644 --- a/packages/vitest/src/utils/generator-utils.ts +++ b/packages/vitest/src/utils/generator-utils.ts @@ -3,18 +3,15 @@ import { joinPathFragments, logger, offsetFromRoot, - readJson, readNxJson, readProjectConfiguration, - TargetConfiguration, Tree, updateProjectConfiguration, - writeJson, } from '@nx/devkit'; import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup'; import { VitestExecutorOptions } from '../executors/test/schema'; -import { ensureViteConfigIsCorrect } from './vite-config-edit-utils'; import { nxVersion } from './versions'; +import { ensureViteConfigIsCorrect } from './vite-config-edit-utils'; export type Target = 'build' | 'serve' | 'test' | 'preview'; export type TargetFlags = Partial>; @@ -22,7 +19,7 @@ export type TargetFlags = Partial>; export interface VitestGeneratorSchema { project: string; uiFramework?: 'angular' | 'react' | 'vue' | 'none'; - coverageProvider: 'v8' | 'istanbul' | 'custom'; + coverageProvider: 'v8' | 'istanbul' | 'custom' | 'none'; inSourceTests?: boolean; skipViteConfig?: boolean; testTarget?: string; @@ -86,7 +83,7 @@ export interface ViteConfigFileOptions { rollupOptionsExternal?: string[]; imports?: string[]; plugins?: string[]; - coverageProvider?: 'v8' | 'istanbul' | 'custom'; + coverageProvider?: 'v8' | 'istanbul' | 'custom' | 'none'; setupFile?: string; useEsmExtension?: boolean; port?: number; @@ -200,7 +197,9 @@ ${ ? ` includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],\n` : '' }\ - reporters: ['default'], + reporters: ['default']${ + options.coverageProvider !== 'none' + ? `, coverage: { reportsDirectory: '${reportsDirectory}', provider: ${ @@ -208,6 +207,8 @@ ${ ? `'${options.coverageProvider}' as const` : `'v8' as const` }, + }` + : '' } },` : ''; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cb072c1a397889..ae280c4ec5fc82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3475,6 +3475,9 @@ importers: '@nx/js': specifier: workspace:* version: link:../js + '@nx/vitest': + specifier: workspace:* + version: link:../vitest tslib: specifier: catalog:typescript version: 2.8.1