From 02e22de7ed1da351b4a04ade323c31f7b4603b2e Mon Sep 17 00:00:00 2001 From: Jonathan Cammisuli Date: Wed, 23 Nov 2022 10:00:29 -0500 Subject: [PATCH] feat(testing): add vitest generators (#13301) --- docs/generated/packages/react.json | 14 +- docs/generated/packages/vite.json | 42 +++++ docs/generated/packages/web.json | 7 +- docs/packages.json | 2 +- e2e/vite/src/vite.test.ts | 43 +++++ e2e/web/src/web-vite.test.ts | 4 +- .../src/generators/application/application.ts | 16 +- .../lib/create-application-files.ts | 17 +- .../application/lib/normalize-options.ts | 4 + .../application/lib/update-jest-config.ts | 8 +- .../src/generators/application/schema.d.ts | 3 +- .../src/generators/application/schema.json | 7 +- .../react/src/generators/host/schema.d.ts | 2 +- .../react/src/generators/init/schema.d.ts | 2 +- .../react/src/generators/library/library.ts | 12 +- .../react/src/generators/library/schema.d.ts | 3 +- .../react/src/generators/library/schema.json | 7 +- .../react/src/generators/remote/schema.d.ts | 2 +- packages/vite/generators.json | 10 ++ packages/vite/index.ts | 1 + packages/vite/src/executors/test/schema.d.ts | 12 +- .../vite/src/executors/test/vitest.impl.ts | 4 +- .../configuration/configuration.spec.ts | 25 +++ .../generators/configuration/configuration.ts | 18 +- .../src/generators/configuration/schema.d.ts | 2 + .../init/__snapshots__/init.spec.ts.snap | 1 + packages/vite/src/generators/init/init.ts | 4 +- .../vitest/files/tsconfig.spec.json__tmpl__ | 19 ++ .../vite/src/generators/vitest/schema.d.ts | 6 + .../vite/src/generators/vitest/schema.json | 32 ++++ .../src/generators/vitest/vitest-generator.ts | 109 ++++++++++++ .../vite/src/generators/vitest/vitest.spec.ts | 162 ++++++++++++++++++ packages/vite/src/utils/generator-utils.ts | 61 ++++++- packages/vite/src/utils/test-utils.ts | 26 +++ packages/vite/src/utils/versions.ts | 1 + .../src/generators/application/application.ts | 16 +- .../src/generators/application/schema.d.ts | 3 +- .../src/generators/application/schema.json | 7 +- packages/web/src/generators/init/schema.d.ts | 2 +- 39 files changed, 678 insertions(+), 38 deletions(-) create mode 100644 packages/vite/src/generators/vitest/files/tsconfig.spec.json__tmpl__ create mode 100644 packages/vite/src/generators/vitest/schema.d.ts create mode 100644 packages/vite/src/generators/vitest/schema.json create mode 100644 packages/vite/src/generators/vitest/vitest-generator.ts create mode 100644 packages/vite/src/generators/vitest/vitest.spec.ts diff --git a/docs/generated/packages/react.json b/docs/generated/packages/react.json index 54c2406221d10..66edd8e954cdd 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 566e0f84871a6..19c9c784fe205 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 1f9baa4a5c37b..aba85caa4b0e6 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 0d607e0a520b8..e0f42db814105 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 956d40181dcf7..dc5b3ffe7eed8 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 3bd8a3bc45d65..2c5bcfa423c69 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 fad877b60a629..cf5ffe7c68e55 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 b52c34c5ccf2e..c8312b5c59e2a 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 d066fb3c34898..27e3a60ddd3cb 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 5e4c5b4c109dc..0e4260e597860 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 7ac3ae2a83f66..7d5f4e49c6b07 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 8b421f149ff5e..040af31521bd6 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 47efc8a024dd6..6a49187dd294e 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 eb6b71b9d9870..7fccc4697c21c 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 9e24c73af255c..25d4c356b7fcc 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 dc02528e61d12..9adde64ac382d 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 c8c722bc3e1ac..ff4ade4465550 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 e4f88c7e10541..144c6326affe0 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 7b0e688f4fda0..1e259477bb34c 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 5744aa1700cd6..2c2e3587f17f1 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 95dbaf8fe7871..72821ad3c28ab 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 722a76629daf8..d9665b25c6723 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 426d6e2045cd3..c95a8b30d4920 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 63e15cbbaeb1c..97d0fe7354aed 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 78bb79b4fdd18..40b05c200d841 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 5f2aefbf5646d..95efd18dc9929 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 e698eeab90f01..3221ca0879f1f 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 0000000000000..c0cbb6b7b46b9 --- /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 0000000000000..ee105d43264f0 --- /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 0000000000000..26c26083ce329 --- /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 0000000000000..f3995c423b7c4 --- /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 0000000000000..9d2eb82821ae7 --- /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 a5af43c617ba1..46623ffc3a38f 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 82bf7287e2d57..a2b0a88923865 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 f3acad51216dd..f16ddd298e685 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 8a0379cbec014..3e658531b5cde 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 d5b95c7ae632d..8c66c906e0a48 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 0b16a1bd02e3f..69c1c5c7116e3 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 700a18ba13b45..00c2cc6a74767 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;