diff --git a/docs/generated/packages/react.json b/docs/generated/packages/react.json
index 54c2406221..66edd8e954 100644
--- a/docs/generated/packages/react.json
+++ b/docs/generated/packages/react.json
@@ -162,10 +162,15 @@
},
"unitTestRunner": {
"type": "string",
- "enum": ["jest", "none"],
+ "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
},
+ "inSourceTests": {
+ "type": "boolean",
+ "default": false,
+ "description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files. Read more on the Vitest docs site: https://vitest.dev/guide/in-source.html"
+ },
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "none"],
@@ -332,10 +337,15 @@
},
"unitTestRunner": {
"type": "string",
- "enum": ["jest", "none"],
+ "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
},
+ "inSourceTests": {
+ "type": "boolean",
+ "default": false,
+ "description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files."
+ },
"tags": {
"type": "string",
"description": "Add tags to the library (used for linting).",
diff --git a/docs/generated/packages/vite.json b/docs/generated/packages/vite.json
index 566e0f8487..19c9c784fe 100644
--- a/docs/generated/packages/vite.json
+++ b/docs/generated/packages/vite.json
@@ -81,6 +81,48 @@
"hidden": false,
"implementation": "/packages/vite/src/generators/configuration/configuration.ts",
"path": "/packages/vite/src/generators/configuration/schema.json"
+ },
+ {
+ "name": "vitest",
+ "factory": "./src/generators/vitest/vitest-generator",
+ "schema": {
+ "$schema": "http://json-schema.org/schema",
+ "cli": "nx",
+ "$id": "Vitest",
+ "title": "",
+ "type": "object",
+ "description": "Generate a vitest setup for a project.",
+ "properties": {
+ "project": {
+ "type": "string",
+ "description": "The name of the project to test.",
+ "$default": { "$source": "projectName" }
+ },
+ "uiFramework": {
+ "type": "string",
+ "enum": ["react", "none"],
+ "default": "none",
+ "description": "UI framework to use with vitest"
+ },
+ "inSourceTests": {
+ "type": "boolean",
+ "default": false,
+ "description": "Do not generate separate spec files and set up in-source testing"
+ },
+ "skipViteConfig": {
+ "type": "boolean",
+ "default": false,
+ "description": "Skip generating a vite config file"
+ }
+ },
+ "required": ["project"],
+ "presets": []
+ },
+ "description": "Generate a vitest configuration",
+ "implementation": "/packages/vite/src/generators/vitest/vitest-generator.ts",
+ "aliases": [],
+ "hidden": false,
+ "path": "/packages/vite/src/generators/vitest/schema.json"
}
],
"executors": [
diff --git a/docs/generated/packages/web.json b/docs/generated/packages/web.json
index 1f9baa4a5c..aba85caa4b 100644
--- a/docs/generated/packages/web.json
+++ b/docs/generated/packages/web.json
@@ -136,10 +136,15 @@
},
"unitTestRunner": {
"type": "string",
- "enum": ["jest", "none"],
+ "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests",
"default": "jest"
},
+ "inSourceTests": {
+ "type": "boolean",
+ "default": false,
+ "description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files."
+ },
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "none"],
diff --git a/docs/packages.json b/docs/packages.json
index 0d607e0a52..e0f42db814 100644
--- a/docs/packages.json
+++ b/docs/packages.json
@@ -372,7 +372,7 @@
"path": "generated/packages/vite.json",
"schemas": {
"executors": ["dev-server", "build", "test"],
- "generators": ["init", "configuration"]
+ "generators": ["init", "configuration", "vitest"]
}
},
{
diff --git a/e2e/vite/src/vite.test.ts b/e2e/vite/src/vite.test.ts
index 956d40181d..dc5b3ffe7e 100644
--- a/e2e/vite/src/vite.test.ts
+++ b/e2e/vite/src/vite.test.ts
@@ -1,6 +1,7 @@
import {
cleanupProject,
createFile,
+ exists,
killPorts,
listFiles,
newProject,
@@ -10,6 +11,7 @@ import {
runCLI,
runCLIAsync,
runCommandUntil,
+ tmpProjPath,
uniq,
updateFile,
updateProjectConfig,
@@ -391,4 +393,45 @@ describe('Vite Plugin', () => {
});
});
});
+
+ describe('should be able to create libs that use vitest', () => {
+ const lib = uniq('my-lib');
+ beforeEach(() => {
+ proj = newProject();
+ });
+
+ it('should be able to run tests', async () => {
+ runCLI(`generate @nrwl/react:lib ${lib} --unitTestRunner=vitest`);
+ expect(exists(tmpProjPath(`libs/${lib}/vite.config.ts`))).toBeTruthy();
+
+ const result = await runCLIAsync(`test ${lib}`);
+ expect(result.combinedOutput).toContain(
+ `Successfully ran target test for project ${lib}`
+ );
+ });
+
+ it('should be able to run tests with inSourceTests set to true', async () => {
+ runCLI(
+ `generate @nrwl/react:lib ${lib} --unitTestRunner=vitest --inSourceTests`
+ );
+ expect(
+ exists(tmpProjPath(`libs/${lib}/src/lib/${lib}.spec.tsx`))
+ ).toBeFalsy();
+
+ updateFile(`libs/${lib}/src/lib/${lib}.tsx`, (content) => {
+ content += `
+ if (import.meta.vitest) {
+ const { expect, it } = import.meta.vitest;
+ it('should be successful', () => {
+ expect(1 + 1).toBe(2);
+ });
+ }
+ `;
+ return content;
+ });
+
+ const result = await runCLIAsync(`test ${lib}`);
+ expect(result.combinedOutput).toContain(`1 passed`);
+ });
+ });
});
diff --git a/e2e/web/src/web-vite.test.ts b/e2e/web/src/web-vite.test.ts
index 3bd8a3bc45..2c5bcfa423 100644
--- a/e2e/web/src/web-vite.test.ts
+++ b/e2e/web/src/web-vite.test.ts
@@ -29,9 +29,7 @@ describe('Web Components Applications with bundler set as vite', () => {
const testResults = await runCLIAsync(`test ${appName}`);
- expect(testResults.combinedOutput).toContain(
- 'Test Suites: 1 passed, 1 total'
- );
+ expect(testResults.combinedOutput).toContain('Tests 2 passed (2)');
const lintE2eResults = runCLI(`lint ${appName}-e2e`);
diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts
index fad877b60a..cf5ffe7c68 100644
--- a/packages/react/src/generators/application/application.ts
+++ b/packages/react/src/generators/application/application.ts
@@ -4,7 +4,7 @@ import {
} from '../../utils/lint';
import { NormalizedSchema, Schema } from './schema';
import { createApplicationFiles } from './lib/create-application-files';
-import { updateJestConfig } from './lib/update-jest-config';
+import { updateSpecConfig } from './lib/update-jest-config';
import { normalizeOptions } from './lib/normalize-options';
import { addProject } from './lib/add-project';
import { addCypress } from './lib/add-cypress';
@@ -26,7 +26,7 @@ import reactInitGenerator from '../init/init';
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { swcCoreVersion } from '@nrwl/js/src/utils/versions';
import { swcLoaderVersion } from '@nrwl/webpack/src/utils/versions';
-import { viteConfigurationGenerator } from '@nrwl/vite';
+import { viteConfigurationGenerator, vitestGenerator } from '@nrwl/vite';
async function addLinting(host: Tree, options: NormalizedSchema) {
const tasks: GeneratorCallback[] = [];
@@ -89,10 +89,20 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
uiFramework: 'react',
project: options.projectName,
newProject: true,
+ includeVitest: true,
});
tasks.push(viteTask);
}
+ if (options.bundler !== 'vite' && options.unitTestRunner === 'vitest') {
+ const vitestTask = await vitestGenerator(host, {
+ uiFramework: 'react',
+ project: options.projectName,
+ inSourceTests: options.inSourceTests,
+ });
+ tasks.push(vitestTask);
+ }
+
const lintTask = await addLinting(host, options);
tasks.push(lintTask);
@@ -100,7 +110,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
tasks.push(cypressTask);
const jestTask = await addJest(host, options);
tasks.push(jestTask);
- updateJestConfig(host, options);
+ updateSpecConfig(host, options);
const styledTask = addStyledModuleDependencies(host, options.styledModule);
tasks.push(styledTask);
const routingTask = addRouting(host, options);
diff --git a/packages/react/src/generators/application/lib/create-application-files.ts b/packages/react/src/generators/application/lib/create-application-files.ts
index b52c34c5cc..c8312b5c59 100644
--- a/packages/react/src/generators/application/lib/create-application-files.ts
+++ b/packages/react/src/generators/application/lib/create-application-files.ts
@@ -68,7 +68,10 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
templateVariables
);
- if (options.unitTestRunner === 'none') {
+ if (
+ options.unitTestRunner === 'none' ||
+ (options.unitTestRunner === 'vitest' && options.inSourceTests == true)
+ ) {
host.delete(
`${options.appProjectRoot}/src/app/${options.fileName}.spec.tsx`
);
@@ -80,6 +83,18 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
templateVariables
);
+ if (options.unitTestRunner === 'vitest' && options.inSourceTests == true) {
+ let originalAppContents = host
+ .read(`${options.appProjectRoot}/src/app/${options.fileName}.tsx`)
+ .toString();
+ originalAppContents += `
+ if (import.meta.vitest) {
+ // add tests related to your file here
+ // For more information please visit the Vitest docs site here: https://vitest.dev/guide/in-source.html
+ }
+ `;
+ }
+
if (options.js) {
toJS(host);
}
diff --git a/packages/react/src/generators/application/lib/normalize-options.ts b/packages/react/src/generators/application/lib/normalize-options.ts
index d066fb3c34..27e3a60ddd 100644
--- a/packages/react/src/generators/application/lib/normalize-options.ts
+++ b/packages/react/src/generators/application/lib/normalize-options.ts
@@ -40,6 +40,10 @@ export function normalizeOptions(
assertValidStyle(options.style);
+ if (options.bundler === 'vite') {
+ options.unitTestRunner = 'vitest';
+ }
+
options.routing = options.routing ?? false;
options.strict = options.strict ?? true;
options.classComponent = options.classComponent ?? false;
diff --git a/packages/react/src/generators/application/lib/update-jest-config.ts b/packages/react/src/generators/application/lib/update-jest-config.ts
index 5e4c5b4c10..0e4260e597 100644
--- a/packages/react/src/generators/application/lib/update-jest-config.ts
+++ b/packages/react/src/generators/application/lib/update-jest-config.ts
@@ -2,8 +2,8 @@ import { updateJestConfigContent } from '../../../utils/jest-utils';
import { NormalizedSchema } from '../schema';
import { offsetFromRoot, Tree, updateJson } from '@nrwl/devkit';
-export function updateJestConfig(host: Tree, options: NormalizedSchema) {
- if (options.unitTestRunner !== 'jest') {
+export function updateSpecConfig(host: Tree, options: NormalizedSchema) {
+ if (options.unitTestRunner === 'none') {
return;
}
@@ -21,6 +21,10 @@ export function updateJestConfig(host: Tree, options: NormalizedSchema) {
return json;
});
+ if (options.unitTestRunner !== 'jest') {
+ return;
+ }
+
const configPath = `${options.appProjectRoot}/jest.config.${
options.js ? 'js' : 'ts'
}`;
diff --git a/packages/react/src/generators/application/schema.d.ts b/packages/react/src/generators/application/schema.d.ts
index 7ac3ae2a83..7d5f4e49c6 100644
--- a/packages/react/src/generators/application/schema.d.ts
+++ b/packages/react/src/generators/application/schema.d.ts
@@ -7,7 +7,8 @@ export interface Schema {
skipFormat: boolean;
directory?: string;
tags?: string;
- unitTestRunner: 'jest' | 'none';
+ unitTestRunner: 'jest' | 'vitest' | 'none';
+ inSourceTests?: boolean;
/**
* @deprecated
*/
diff --git a/packages/react/src/generators/application/schema.json b/packages/react/src/generators/application/schema.json
index 8b421f149f..040af31521 100644
--- a/packages/react/src/generators/application/schema.json
+++ b/packages/react/src/generators/application/schema.json
@@ -103,10 +103,15 @@
},
"unitTestRunner": {
"type": "string",
- "enum": ["jest", "none"],
+ "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
},
+ "inSourceTests": {
+ "type": "boolean",
+ "default": false,
+ "description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files. Read more on the Vitest docs site: https://vitest.dev/guide/in-source.html"
+ },
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "none"],
diff --git a/packages/react/src/generators/host/schema.d.ts b/packages/react/src/generators/host/schema.d.ts
index 47efc8a024..6a49187dd2 100644
--- a/packages/react/src/generators/host/schema.d.ts
+++ b/packages/react/src/generators/host/schema.d.ts
@@ -7,7 +7,7 @@ export interface Schema {
skipFormat: boolean;
directory?: string;
tags?: string;
- unitTestRunner: 'jest' | 'none';
+ unitTestRunner: 'jest' | 'vitest' | 'none';
e2eTestRunner: 'cypress' | 'none';
linter: Linter;
pascalCaseFiles?: boolean;
diff --git a/packages/react/src/generators/init/schema.d.ts b/packages/react/src/generators/init/schema.d.ts
index eb6b71b9d9..7fccc4697c 100644
--- a/packages/react/src/generators/init/schema.d.ts
+++ b/packages/react/src/generators/init/schema.d.ts
@@ -1,5 +1,5 @@
export interface InitSchema {
- unitTestRunner?: 'jest' | 'none';
+ unitTestRunner?: 'jest' | 'vitest' | 'none';
e2eTestRunner?: 'cypress' | 'none';
skipFormat?: boolean;
skipPackageJson?: boolean;
diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts
index 9e24c73af2..25d4c356b7 100644
--- a/packages/react/src/generators/library/library.ts
+++ b/packages/react/src/generators/library/library.ts
@@ -47,6 +47,7 @@ import componentGenerator from '../component/component';
import init from '../init/init';
import { Schema } from './schema';
import { updateJestConfigContent } from '../../utils/jest-utils';
+import { vitestGenerator } from '@nrwl/vite';
export interface NormalizedSchema extends Schema {
name: string;
fileName: string;
@@ -109,6 +110,13 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
);
host.write(jestConfigPath, updatedContent);
}
+ } else if (options.unitTestRunner === 'vitest') {
+ const vitestTask = await vitestGenerator(host, {
+ uiFramework: 'react',
+ project: options.name,
+ inSourceTests: options.inSourceTests,
+ });
+ tasks.push(vitestTask);
}
if (options.component) {
@@ -117,7 +125,9 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
project: options.name,
flat: true,
style: options.style,
- skipTests: options.unitTestRunner === 'none',
+ skipTests:
+ options.unitTestRunner === 'none' ||
+ (options.unitTestRunner === 'vitest' && options.inSourceTests == true),
export: true,
routing: options.routing,
js: options.js,
diff --git a/packages/react/src/generators/library/schema.d.ts b/packages/react/src/generators/library/schema.d.ts
index dc02528e61..9adde64ac3 100644
--- a/packages/react/src/generators/library/schema.d.ts
+++ b/packages/react/src/generators/library/schema.d.ts
@@ -11,7 +11,8 @@ export interface Schema {
pascalCaseFiles?: boolean;
routing?: boolean;
appProject?: string;
- unitTestRunner: 'jest' | 'none';
+ unitTestRunner: 'jest' | 'vitest' | 'none';
+ inSourceTests?: boolean;
linter: Linter;
component?: boolean;
publishable?: boolean;
diff --git a/packages/react/src/generators/library/schema.json b/packages/react/src/generators/library/schema.json
index c8c722bc3e..ff4ade4465 100644
--- a/packages/react/src/generators/library/schema.json
+++ b/packages/react/src/generators/library/schema.json
@@ -80,10 +80,15 @@
},
"unitTestRunner": {
"type": "string",
- "enum": ["jest", "none"],
+ "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
},
+ "inSourceTests": {
+ "type": "boolean",
+ "default": false,
+ "description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files."
+ },
"tags": {
"type": "string",
"description": "Add tags to the library (used for linting).",
diff --git a/packages/react/src/generators/remote/schema.d.ts b/packages/react/src/generators/remote/schema.d.ts
index e4f88c7e10..144c6326af 100644
--- a/packages/react/src/generators/remote/schema.d.ts
+++ b/packages/react/src/generators/remote/schema.d.ts
@@ -8,7 +8,7 @@ export interface Schema {
skipFormat: boolean;
directory?: string;
tags?: string;
- unitTestRunner: 'jest' | 'none';
+ unitTestRunner: 'jest' | 'vitest' | 'none';
e2eTestRunner: 'cypress' | 'none';
linter: Linter;
pascalCaseFiles?: boolean;
diff --git a/packages/vite/generators.json b/packages/vite/generators.json
index 7b0e688f4f..1e259477bb 100644
--- a/packages/vite/generators.json
+++ b/packages/vite/generators.json
@@ -15,6 +15,11 @@
"description": "Add Vite configuration to an application.",
"aliases": ["ng-add"],
"hidden": false
+ },
+ "vitest": {
+ "factory": "./src/generators/vitest/vitest-generator#vitestSchematic",
+ "schema": "./src/generators/vitest/schema.json",
+ "description": "Generate a vitest configuration"
}
},
"generators": {
@@ -31,6 +36,11 @@
"description": "Add Vite configuration to an application.",
"aliases": ["ng-add"],
"hidden": false
+ },
+ "vitest": {
+ "factory": "./src/generators/vitest/vitest-generator",
+ "schema": "./src/generators/vitest/schema.json",
+ "description": "Generate a vitest configuration"
}
}
}
diff --git a/packages/vite/index.ts b/packages/vite/index.ts
index 5744aa1700..2c2e3587f1 100644
--- a/packages/vite/index.ts
+++ b/packages/vite/index.ts
@@ -1,2 +1,3 @@
export * from './src/utils/versions';
export { viteConfigurationGenerator } from './src/generators/configuration/configuration';
+export { vitestGenerator } from './src/generators/vitest/vitest-generator';
diff --git a/packages/vite/src/executors/test/schema.d.ts b/packages/vite/src/executors/test/schema.d.ts
index 95dbaf8fe7..72821ad3c2 100644
--- a/packages/vite/src/executors/test/schema.d.ts
+++ b/packages/vite/src/executors/test/schema.d.ts
@@ -1,9 +1,9 @@
-export interface VitestExecutorSchema {
- config: string;
- passWithNoTests: boolean;
+export interface VitestExecutorOptions {
+ config?: string;
+ passWithNoTests?: boolean;
testNamePattern?: string;
- mode: 'test' | 'benchmark' | 'typecheck';
+ mode?: 'test' | 'benchmark' | 'typecheck';
reporters?: string[];
- watch: boolean;
- update: boolean;
+ watch?: boolean;
+ update?: boolean;
}
diff --git a/packages/vite/src/executors/test/vitest.impl.ts b/packages/vite/src/executors/test/vitest.impl.ts
index 722a76629d..d9665b25c6 100644
--- a/packages/vite/src/executors/test/vitest.impl.ts
+++ b/packages/vite/src/executors/test/vitest.impl.ts
@@ -1,6 +1,6 @@
import { ExecutorContext } from '@nrwl/devkit';
import { File, Reporter } from 'vitest';
-import { VitestExecutorSchema } from './schema';
+import { VitestExecutorOptions } from './schema';
class NxReporter implements Reporter {
deferred: {
@@ -38,7 +38,7 @@ class NxReporter implements Reporter {
}
export default async function* runExecutor(
- options: VitestExecutorSchema,
+ options: VitestExecutorOptions,
context: ExecutorContext
) {
const { startVitest } = await (Function(
diff --git a/packages/vite/src/generators/configuration/configuration.spec.ts b/packages/vite/src/generators/configuration/configuration.spec.ts
index 426d6e2045..c95a8b30d4 100644
--- a/packages/vite/src/generators/configuration/configuration.spec.ts
+++ b/packages/vite/src/generators/configuration/configuration.spec.ts
@@ -91,4 +91,29 @@ describe('@nrwl/vite:configuration', () => {
expect(tree.exists('apps/my-test-web-app/vite.config.ts')).toBe(true);
});
});
+
+ describe('vitest', () => {
+ beforeAll(async () => {
+ tree = createTreeWithEmptyV1Workspace();
+ await mockReactAppGenerator(tree);
+ const existing = 'existing';
+ const existingVersion = '1.0.0';
+ addDependenciesToPackageJson(
+ tree,
+ { '@nrwl/vite': nxVersion, [existing]: existingVersion },
+ { [existing]: existingVersion }
+ );
+ await viteConfigurationGenerator(tree, {
+ uiFramework: 'react',
+ project: 'my-test-react-app',
+ includeVitest: true,
+ });
+ });
+ it('should create a vitest configuration if "includeVitest" is true', () => {
+ const viteConfig = tree
+ .read('apps/my-test-react-app/vite.config.ts')
+ .toString();
+ expect(viteConfig).toContain('test');
+ });
+ });
});
diff --git a/packages/vite/src/generators/configuration/configuration.ts b/packages/vite/src/generators/configuration/configuration.ts
index 63e15cbbae..97d0fe7354 100644
--- a/packages/vite/src/generators/configuration/configuration.ts
+++ b/packages/vite/src/generators/configuration/configuration.ts
@@ -7,7 +7,7 @@ import {
} from '@nrwl/devkit';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import {
- findServeAndBuildTargets,
+ findExistingTargets,
addOrChangeBuildTarget,
addOrChangeServeTarget,
editTsConfig,
@@ -16,6 +16,7 @@ import {
} from '../../utils/generator-utils';
import initGenerator from '../init/init';
+import vitestGenerator from '../vitest/vitest-generator';
import { Schema } from './schema';
export async function viteConfigurationGenerator(tree: Tree, schema: Schema) {
@@ -26,8 +27,8 @@ export async function viteConfigurationGenerator(tree: Tree, schema: Schema) {
let serveTarget = 'serve';
if (!schema.newProject) {
- buildTarget = findServeAndBuildTargets(targets).buildTarget;
- serveTarget = findServeAndBuildTargets(targets).serveTarget;
+ buildTarget = findExistingTargets(targets).buildTarget;
+ serveTarget = findExistingTargets(targets).serveTarget;
moveAndEditIndexHtml(tree, schema, buildTarget);
editTsConfig(tree, schema);
}
@@ -39,8 +40,19 @@ export async function viteConfigurationGenerator(tree: Tree, schema: Schema) {
addOrChangeBuildTarget(tree, schema, buildTarget);
addOrChangeServeTarget(tree, schema, serveTarget);
+
writeViteConfig(tree, schema);
+ if (schema.includeVitest) {
+ const vitestTask = await vitestGenerator(tree, {
+ project: schema.project,
+ uiFramework: schema.uiFramework,
+ inSourceTests: schema.inSourceTests,
+ skipViteConfig: true,
+ });
+ tasks.push(vitestTask);
+ }
+
await formatFiles(tree);
return runTasksInSerial(...tasks);
diff --git a/packages/vite/src/generators/configuration/schema.d.ts b/packages/vite/src/generators/configuration/schema.d.ts
index 78bb79b4fd..40b05c200d 100644
--- a/packages/vite/src/generators/configuration/schema.d.ts
+++ b/packages/vite/src/generators/configuration/schema.d.ts
@@ -2,4 +2,6 @@ export interface Schema {
uiFramework: 'react' | 'none';
project: string;
newProject?: boolean;
+ includeVitest?: boolean;
+ inSourceTests?: boolean;
}
diff --git a/packages/vite/src/generators/init/__snapshots__/init.spec.ts.snap b/packages/vite/src/generators/init/__snapshots__/init.spec.ts.snap
index 5f2aefbf56..95efd18dc9 100644
--- a/packages/vite/src/generators/init/__snapshots__/init.spec.ts.snap
+++ b/packages/vite/src/generators/init/__snapshots__/init.spec.ts.snap
@@ -10,6 +10,7 @@ Object {
"@vitejs/plugin-react": "^2.2.0",
"@vitest/ui": "^0.9.3",
"existing": "1.0.0",
+ "jsdom": "~20.0.3",
"vite": "^3.0.5",
"vite-plugin-eslint": "^1.6.0",
"vite-tsconfig-paths": "^3.5.2",
diff --git a/packages/vite/src/generators/init/init.ts b/packages/vite/src/generators/init/init.ts
index e698eeab90..3221ca0879 100644
--- a/packages/vite/src/generators/init/init.ts
+++ b/packages/vite/src/generators/init/init.ts
@@ -15,6 +15,7 @@ import {
vitestUiVersion,
vitestVersion,
viteTsConfigPathsVersion,
+ jsdomVersion,
} from '../../utils/versions';
import { Schema } from './schema';
@@ -23,7 +24,7 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) {
const devDependencies = {};
const dependencies = {};
packageJson.dependencies = packageJson.dependencies || {};
- packageJson.devDependencices = packageJson.devDependencices || {};
+ packageJson.devDependencies = packageJson.devDependencies || {};
// base deps
devDependencies['@nrwl/vite'] = nxVersion;
@@ -32,6 +33,7 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) {
devDependencies['vite-tsconfig-paths'] = viteTsConfigPathsVersion;
devDependencies['vitest'] = vitestVersion;
devDependencies['@vitest/ui'] = vitestUiVersion;
+ devDependencies['jsdom'] = jsdomVersion;
if (schema.uiFramework === 'react') {
devDependencies['@vitejs/plugin-react'] = vitePluginReactVersion;
diff --git a/packages/vite/src/generators/vitest/files/tsconfig.spec.json__tmpl__ b/packages/vite/src/generators/vitest/files/tsconfig.spec.json__tmpl__
new file mode 100644
index 0000000000..c0cbb6b7b4
--- /dev/null
+++ b/packages/vite/src/generators/vitest/files/tsconfig.spec.json__tmpl__
@@ -0,0 +1,19 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc",
+ "types": ["vitest/globals", "node"]
+ },
+ "include": [
+ "vite.config.ts",
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "**/*.test.tsx",
+ "**/*.spec.tsx",
+ "**/*.test.js",
+ "**/*.spec.js",
+ "**/*.test.jsx",
+ "**/*.spec.jsx",
+ "**/*.d.ts"
+ ]
+}
diff --git a/packages/vite/src/generators/vitest/schema.d.ts b/packages/vite/src/generators/vitest/schema.d.ts
new file mode 100644
index 0000000000..ee105d4326
--- /dev/null
+++ b/packages/vite/src/generators/vitest/schema.d.ts
@@ -0,0 +1,6 @@
+export interface VitestGeneratorSchema {
+ project: string;
+ uiFramework: 'react' | 'none';
+ inSourceTests?: boolean;
+ skipViteConfig?: boolean;
+}
diff --git a/packages/vite/src/generators/vitest/schema.json b/packages/vite/src/generators/vitest/schema.json
new file mode 100644
index 0000000000..26c26083ce
--- /dev/null
+++ b/packages/vite/src/generators/vitest/schema.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/schema",
+ "cli": "nx",
+ "$id": "Vitest",
+ "title": "",
+ "type": "object",
+ "description": "Generate a vitest setup for a project.",
+ "properties": {
+ "project": {
+ "type": "string",
+ "description": "The name of the project to test.",
+ "$default": { "$source": "projectName" }
+ },
+ "uiFramework": {
+ "type": "string",
+ "enum": ["react", "none"],
+ "default": "none",
+ "description": "UI framework to use with vitest"
+ },
+ "inSourceTests": {
+ "type": "boolean",
+ "default": false,
+ "description": "Do not generate separate spec files and set up in-source testing"
+ },
+ "skipViteConfig": {
+ "type": "boolean",
+ "default": false,
+ "description": "Skip generating a vite config file"
+ }
+ },
+ "required": ["project"]
+}
diff --git a/packages/vite/src/generators/vitest/vitest-generator.ts b/packages/vite/src/generators/vitest/vitest-generator.ts
new file mode 100644
index 0000000000..f3995c423b
--- /dev/null
+++ b/packages/vite/src/generators/vitest/vitest-generator.ts
@@ -0,0 +1,109 @@
+import {
+ convertNxGenerator,
+ formatFiles,
+ generateFiles,
+ GeneratorCallback,
+ joinPathFragments,
+ offsetFromRoot,
+ readProjectConfiguration,
+ Tree,
+ updateJson,
+} from '@nrwl/devkit';
+import {
+ addOrChangeTestTarget,
+ findExistingTargets,
+ writeViteConfig,
+} from '../../utils/generator-utils';
+import { VitestGeneratorSchema } from './schema';
+
+import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
+import initGenerator from '../init/init';
+
+export async function vitestGenerator(
+ tree: Tree,
+ schema: VitestGeneratorSchema
+) {
+ const tasks: GeneratorCallback[] = [];
+
+ const { targets, root } = readProjectConfiguration(tree, schema.project);
+ let testTarget = findExistingTargets(targets).testTarget;
+
+ addOrChangeTestTarget(tree, schema, testTarget);
+
+ const initTask = await initGenerator(tree, {
+ uiFramework: schema.uiFramework,
+ });
+ tasks.push(initTask);
+
+ if (!schema.skipViteConfig) {
+ writeViteConfig(tree, {
+ ...schema,
+ includeVitest: true,
+ });
+ }
+
+ createFiles(tree, schema, root);
+ updateTsConfig(tree, schema, root);
+
+ await formatFiles(tree);
+
+ return runTasksInSerial(...tasks);
+}
+
+function updateTsConfig(
+ tree: Tree,
+ options: VitestGeneratorSchema,
+ projectRoot: string
+) {
+ updateJson(tree, joinPathFragments(projectRoot, 'tsconfig.json'), (json) => {
+ if (
+ json.references &&
+ !json.references.some((r) => r.path === './tsconfig.spec.json')
+ ) {
+ json.references.push({
+ path: './tsconfig.spec.json',
+ });
+ }
+ return json;
+ });
+
+ if (options.inSourceTests) {
+ const tsconfigLibPath = joinPathFragments(projectRoot, 'tsconfig.lib.json');
+ const tsconfigAppPath = joinPathFragments(projectRoot, 'tsconfig.app.json');
+ if (tree.exists(tsconfigLibPath)) {
+ updateJson(
+ tree,
+ joinPathFragments(projectRoot, 'tsconfig.lib.json'),
+ (json) => {
+ (json.compilerOptions.types ??= []).push('vitest/importMeta');
+ return json;
+ }
+ );
+ } else if (tree.exists(tsconfigAppPath)) {
+ updateJson(
+ tree,
+ joinPathFragments(projectRoot, 'tsconfig.app.json'),
+ (json) => {
+ (json.compilerOptions.types ??= []).push('vitest/importMeta');
+ return json;
+ }
+ );
+ }
+ }
+}
+
+function createFiles(
+ tree: Tree,
+ options: VitestGeneratorSchema,
+ projectRoot: string
+) {
+ generateFiles(tree, joinPathFragments(__dirname, 'files'), projectRoot, {
+ tmpl: '',
+ ...options,
+ projectRoot,
+ offsetFromRoot: offsetFromRoot(projectRoot),
+ });
+}
+
+export default vitestGenerator;
+export const vitestSchematic = convertNxGenerator(vitestGenerator);
diff --git a/packages/vite/src/generators/vitest/vitest.spec.ts b/packages/vite/src/generators/vitest/vitest.spec.ts
new file mode 100644
index 0000000000..9d2eb82821
--- /dev/null
+++ b/packages/vite/src/generators/vitest/vitest.spec.ts
@@ -0,0 +1,162 @@
+import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
+import { Tree, readProjectConfiguration } from '@nrwl/devkit';
+
+import generator from './vitest-generator';
+import { VitestGeneratorSchema } from './schema';
+import { mockReactAppGenerator } from '../../utils/test-utils';
+
+describe('vitest generator', () => {
+ let appTree: Tree;
+ const options: VitestGeneratorSchema = {
+ project: 'my-test-react-app',
+ uiFramework: 'react',
+ };
+
+ beforeEach(async () => {
+ appTree = createTreeWithEmptyWorkspace();
+ await mockReactAppGenerator(appTree);
+ });
+
+ it('Should add the test target', async () => {
+ await generator(appTree, options);
+ const config = readProjectConfiguration(appTree, 'my-test-react-app');
+ expect(config.targets['test']).toMatchInlineSnapshot(`
+ Object {
+ "executor": "@nrwl/vite:test",
+ "options": Object {
+ "passWithNoTests": true,
+ },
+ "outputs": Array [
+ "{workspaceRoot}/coverage/{projectRoot}",
+ ],
+ }
+ `);
+ });
+
+ describe('tsconfig', () => {
+ it('should add a tsconfig.spec.json file', async () => {
+ await generator(appTree, options);
+ const tsconfig = JSON.parse(
+ appTree.read('apps/my-test-react-app/tsconfig.json')?.toString() ?? '{}'
+ );
+ expect(tsconfig.references).toMatchInlineSnapshot(`
+ Array [
+ Object {
+ "path": "./tsconfig.app.json",
+ },
+ Object {
+ "path": "./tsconfig.spec.json",
+ },
+ ]
+ `);
+
+ const tsconfigSpec = JSON.parse(
+ appTree.read('apps/my-test-react-app/tsconfig.spec.json')?.toString() ??
+ '{}'
+ );
+ expect(tsconfigSpec).toMatchInlineSnapshot(`
+ Object {
+ "compilerOptions": Object {
+ "outDir": "../../dist/out-tsc",
+ "types": Array [
+ "vitest/globals",
+ "node",
+ ],
+ },
+ "extends": "./tsconfig.json",
+ "include": Array [
+ "vite.config.ts",
+ "**/*.test.ts",
+ "**/*.spec.ts",
+ "**/*.test.tsx",
+ "**/*.spec.tsx",
+ "**/*.test.js",
+ "**/*.spec.js",
+ "**/*.test.jsx",
+ "**/*.spec.jsx",
+ "**/*.d.ts",
+ ],
+ }
+ `);
+ });
+
+ it('should add vitest/importMeta when inSourceTests is true', async () => {
+ await generator(appTree, { ...options, inSourceTests: true });
+ const tsconfig = JSON.parse(
+ appTree.read('apps/my-test-react-app/tsconfig.app.json')?.toString() ??
+ '{}'
+ );
+ expect(tsconfig.compilerOptions.types).toMatchInlineSnapshot(`
+ Array [
+ "vitest/importMeta",
+ ]
+ `);
+ });
+ });
+
+ describe('vite.config', () => {
+ it('should modify the vite.config.js file to include the test options', async () => {
+ await generator(appTree, options);
+ const viteConfig = appTree
+ .read('apps/my-test-react-app/vite.config.ts')
+ .toString();
+ expect(viteConfig).toMatchInlineSnapshot(`
+ "
+ ///
+ import { defineConfig } from 'vite';
+ import react from '@vitejs/plugin-react';
+ import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
+
+ export default defineConfig({
+ plugins: [
+ react(),
+ ViteTsConfigPathsPlugin({
+ root: '../../',
+ projects: ['tsconfig.base.json'],
+ }),
+ ],
+
+ test: {
+ globals: true,
+ environment: 'jsdom',
+
+ },
+ });"
+ `);
+ });
+ });
+
+ describe('insourceTests', () => {
+ it('should add the insourceSource option in the vite config', async () => {
+ await generator(appTree, { ...options, inSourceTests: true });
+ const viteConfig = appTree
+ .read('apps/my-test-react-app/vite.config.ts')
+ .toString();
+ expect(viteConfig).toMatchInlineSnapshot(`
+ "
+ ///
+ import { defineConfig } from 'vite';
+ import react from '@vitejs/plugin-react';
+ import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
+
+ export default defineConfig({
+ plugins: [
+ react(),
+ ViteTsConfigPathsPlugin({
+ root: '../../',
+ projects: ['tsconfig.base.json'],
+ }),
+ ],
+ define: {
+ 'import.meta.vitest': undefined
+ },
+ test: {
+ globals: true,
+ environment: 'jsdom',
+ includeSource: ['src/**/*.{js,ts,jsx,tsx}']
+ },
+ });"
+ `);
+ });
+ });
+});
diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts
index a5af43c617..46623ffc3a 100644
--- a/packages/vite/src/utils/generator-utils.ts
+++ b/packages/vite/src/utils/generator-utils.ts
@@ -11,6 +11,7 @@ import {
} from '@nrwl/devkit';
import { ViteBuildExecutorOptions } from '../executors/build/schema';
import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema';
+import { VitestExecutorOptions } from '../executors/test/schema';
import { Schema } from '../generators/configuration/schema';
/**
@@ -27,18 +28,21 @@ import { Schema } from '../generators/configuration/schema';
* they are using, and infer from the executor that the target
* is a build target.
*/
-export function findServeAndBuildTargets(targets: {
+export function findExistingTargets(targets: {
[targetName: string]: TargetConfiguration;
}): {
buildTarget: string;
serveTarget: string;
+ testTarget: string;
} {
const returnObject: {
buildTarget: string;
serveTarget: string;
+ testTarget: string;
} = {
buildTarget: 'build',
serveTarget: 'serve',
+ testTarget: 'test',
};
Object.entries(targets).forEach(([target, targetConfig]) => {
@@ -68,9 +72,13 @@ export function findServeAndBuildTargets(targets: {
case '@nxext/vite:build':
returnObject.buildTarget = target;
break;
+ case '@nrwl/jest:jest':
+ case 'nxext/vitest:vitest':
+ returnObject.testTarget = target;
default:
returnObject.buildTarget = 'build';
returnObject.serveTarget = 'serve';
+ returnObject.testTarget = 'test';
break;
}
});
@@ -78,6 +86,39 @@ export function findServeAndBuildTargets(targets: {
return returnObject;
}
+export function addOrChangeTestTarget(
+ tree: Tree,
+ options: Schema,
+ target: string
+) {
+ const project = readProjectConfiguration(tree, options.project);
+ const targets = {
+ ...project.targets,
+ };
+
+ const testOptions: VitestExecutorOptions = {
+ passWithNoTests: true,
+ };
+
+ if (targets[target]) {
+ targets[target].executor = '@nrwl/vite:test';
+ delete targets[target].options.jestConfig;
+ } else {
+ targets[target] = {
+ executor: '@nrwl/vite:test',
+ outputs: ['{projectRoot}/coverage'],
+ options: testOptions,
+ };
+ }
+
+ updateProjectConfiguration(tree, options.project, {
+ ...project,
+ targets: {
+ ...targets,
+ },
+ });
+}
+
export function addOrChangeBuildTarget(
tree: Tree,
options: Schema,
@@ -315,9 +356,22 @@ export function writeViteConfig(tree: Tree, options: Schema) {
let viteConfigContent = '';
+ const testOption = `test: {
+ globals: true,
+ environment: 'jsdom',
+ ${
+ options.inSourceTests ? `includeSource: ['src/**/*.{js,ts,jsx,tsx}']` : ''
+ }
+ },`;
+
+ const defineOption = `define: {
+ 'import.meta.vitest': undefined
+ },`;
+
switch (options.uiFramework) {
case 'react':
viteConfigContent = `
+${options.includeVitest ? '/// ' : ''}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
@@ -330,10 +384,13 @@ export function writeViteConfig(tree: Tree, options: Schema) {
projects: ['tsconfig.base.json'],
}),
],
+ ${options.inSourceTests ? defineOption : ''}
+ ${options.includeVitest ? testOption : ''}
});`;
break;
case 'none':
viteConfigContent = `
+ ${options.includeVitest ? '/// ' : ''}
import { defineConfig } from 'vite';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
@@ -344,6 +401,8 @@ export function writeViteConfig(tree: Tree, options: Schema) {
projects: ['tsconfig.base.json'],
}),
],
+ ${options.inSourceTests ? defineOption : ''}
+ ${options.includeVitest ? testOption : ''}
});`;
break;
default:
diff --git a/packages/vite/src/utils/test-utils.ts b/packages/vite/src/utils/test-utils.ts
index 82bf7287e2..a2b0a88923 100644
--- a/packages/vite/src/utils/test-utils.ts
+++ b/packages/vite/src/utils/test-utils.ts
@@ -39,6 +39,32 @@ export function mockReactAppGenerator(tree: Tree): Tree {
}
`
);
+ tree.write(
+ `apps/${appName}/tsconfig.app.json`,
+ `{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "outDir": "../../dist/out-tsc"
+ },
+ "files": [
+ "../../node_modules/@nrwl/react/typings/cssmodule.d.ts",
+ "../../node_modules/@nrwl/react/typings/image.d.ts"
+ ],
+ "exclude": [
+ "jest.config.ts",
+ "**/*.spec.ts",
+ "**/*.test.ts",
+ "**/*.spec.tsx",
+ "**/*.test.tsx",
+ "**/*.spec.js",
+ "**/*.test.js",
+ "**/*.spec.jsx",
+ "**/*.test.jsx"
+ ],
+ "include": ["**/*.js", "**/*.jsx", "**/*.ts", "**/*.tsx"]
+ }
+ `
+ );
tree.write(
`apps/${appName}/src/index.html`,
diff --git a/packages/vite/src/utils/versions.ts b/packages/vite/src/utils/versions.ts
index f3acad5121..f16ddd298e 100644
--- a/packages/vite/src/utils/versions.ts
+++ b/packages/vite/src/utils/versions.ts
@@ -7,3 +7,4 @@ export const vitePluginReactVersion = '^2.2.0';
export const vitePluginVueVersion = '^3.2.0';
export const vitePluginVueJsxVersion = '^2.1.1';
export const viteTsConfigPathsVersion = '^3.5.2';
+export const jsdomVersion = '~20.0.3';
diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts
index 8a0379cbec..3e658531b5 100644
--- a/packages/web/src/generators/application/application.ts
+++ b/packages/web/src/generators/application/application.ts
@@ -24,7 +24,7 @@ import { swcCoreVersion } from '@nrwl/js/src/utils/versions';
import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript';
-import { viteConfigurationGenerator } from '@nrwl/vite';
+import { viteConfigurationGenerator, vitestGenerator } from '@nrwl/vite';
import { swcLoaderVersion } from '../../utils/versions';
import { webInitGenerator } from '../init/init';
@@ -203,10 +203,20 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
uiFramework: 'react',
project: options.projectName,
newProject: true,
+ includeVitest: true,
});
tasks.push(viteTask);
}
+ if (options.bundler !== 'vite' && options.unitTestRunner === 'vitest') {
+ const vitestTask = await vitestGenerator(host, {
+ uiFramework: 'none',
+ project: options.projectName,
+ inSourceTests: options.inSourceTests,
+ });
+ tasks.push(vitestTask);
+ }
+
const lintTask = await lintProjectGenerator(host, {
linter: options.linter,
project: options.projectName,
@@ -273,6 +283,10 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
? options.tags.split(',').map((s) => s.trim())
: [];
+ if (options.bundler === 'vite') {
+ options.unitTestRunner = 'vitest';
+ }
+
options.style = options.style || 'css';
options.linter = options.linter || Linter.EsLint;
options.unitTestRunner = options.unitTestRunner || 'jest';
diff --git a/packages/web/src/generators/application/schema.d.ts b/packages/web/src/generators/application/schema.d.ts
index d5b95c7ae6..8c66c906e0 100644
--- a/packages/web/src/generators/application/schema.d.ts
+++ b/packages/web/src/generators/application/schema.d.ts
@@ -9,7 +9,8 @@ export interface Schema {
skipFormat?: boolean;
directory?: string;
tags?: string;
- unitTestRunner?: 'jest' | 'none';
+ unitTestRunner?: 'jest' | 'vitest' | 'none';
+ inSourceTests?: boolean;
e2eTestRunner?: 'cypress' | 'none';
linter?: Linter;
standaloneConfig?: boolean;
diff --git a/packages/web/src/generators/application/schema.json b/packages/web/src/generators/application/schema.json
index 0b16a1bd02..69c1c5c711 100644
--- a/packages/web/src/generators/application/schema.json
+++ b/packages/web/src/generators/application/schema.json
@@ -73,10 +73,15 @@
},
"unitTestRunner": {
"type": "string",
- "enum": ["jest", "none"],
+ "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests",
"default": "jest"
},
+ "inSourceTests": {
+ "type": "boolean",
+ "default": false,
+ "description": "When using Vitest, separate spec files will not be generated and instead will be included within the source files."
+ },
"e2eTestRunner": {
"type": "string",
"enum": ["cypress", "none"],
diff --git a/packages/web/src/generators/init/schema.d.ts b/packages/web/src/generators/init/schema.d.ts
index 700a18ba13..00c2cc6a74 100644
--- a/packages/web/src/generators/init/schema.d.ts
+++ b/packages/web/src/generators/init/schema.d.ts
@@ -1,6 +1,6 @@
export interface Schema {
bundler?: 'webpack' | 'none' | 'vite';
- unitTestRunner?: 'jest' | 'none';
+ unitTestRunner?: 'jest' | 'vitest' | 'none';
e2eTestRunner?: 'cypress' | 'none';
skipFormat?: boolean;
skipPackageJson?: boolean;