-
Notifications
You must be signed in to change notification settings - Fork 2.7k
feat(nx-plugin): add vitest support for e2e tests #34041
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 23 commits
5da3e11
6bd21d6
8a9fa56
144234d
ac78f43
d8f573b
9015c90
4c9e6a5
7092c91
c246768
8737919
511f523
a4cd531
7fe23b8
805ad3e
785bcf0
062cf10
68f0e3f
32ed73e
92f6dab
14d9ad7
b797bda
6a6b249
4c65167
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<PackageJson>( | ||
| 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, { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @copilot This call should use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated to use |
||
| project: options.projectName, | ||
| testTarget: 'e2e', | ||
| skipFormat: true, | ||
| addPlugin: options.addPlugin, | ||
| testEnvironment: 'node', | ||
| coverageProvider: 'none', | ||
| } satisfies Partial<VitestGeneratorSchema>); | ||
|
|
||
| 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') { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets add an e2e test to e2e/plugin that tests that these work for vitest backed plugins, I'm a bit hesitant without it. @copilot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added e2e test in
e2e/plugin/src/nx-plugin.test.tsthat verifies plugins can be generated with vitest as the e2e test runner and that the tests execute successfully. Commit 6add8ac.