feat(testing): add vitest generators (#13301)

This commit is contained in:
Jonathan Cammisuli 2022-11-23 10:00:29 -05:00 committed by GitHub
parent 17514d2366
commit 02e22de7ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 678 additions and 38 deletions

View File

@ -162,10 +162,15 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "none"], "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests.",
"default": "jest" "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": { "e2eTestRunner": {
"type": "string", "type": "string",
"enum": ["cypress", "none"], "enum": ["cypress", "none"],
@ -332,10 +337,15 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "none"], "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests.",
"default": "jest" "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": { "tags": {
"type": "string", "type": "string",
"description": "Add tags to the library (used for linting).", "description": "Add tags to the library (used for linting).",

View File

@ -81,6 +81,48 @@
"hidden": false, "hidden": false,
"implementation": "/packages/vite/src/generators/configuration/configuration.ts", "implementation": "/packages/vite/src/generators/configuration/configuration.ts",
"path": "/packages/vite/src/generators/configuration/schema.json" "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": [ "executors": [

View File

@ -136,10 +136,15 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "none"], "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests", "description": "Test runner to use for unit tests",
"default": "jest" "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": { "e2eTestRunner": {
"type": "string", "type": "string",
"enum": ["cypress", "none"], "enum": ["cypress", "none"],

View File

@ -372,7 +372,7 @@
"path": "generated/packages/vite.json", "path": "generated/packages/vite.json",
"schemas": { "schemas": {
"executors": ["dev-server", "build", "test"], "executors": ["dev-server", "build", "test"],
"generators": ["init", "configuration"] "generators": ["init", "configuration", "vitest"]
} }
}, },
{ {

View File

@ -1,6 +1,7 @@
import { import {
cleanupProject, cleanupProject,
createFile, createFile,
exists,
killPorts, killPorts,
listFiles, listFiles,
newProject, newProject,
@ -10,6 +11,7 @@ import {
runCLI, runCLI,
runCLIAsync, runCLIAsync,
runCommandUntil, runCommandUntil,
tmpProjPath,
uniq, uniq,
updateFile, updateFile,
updateProjectConfig, 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`);
});
});
}); });

View File

@ -29,9 +29,7 @@ describe('Web Components Applications with bundler set as vite', () => {
const testResults = await runCLIAsync(`test ${appName}`); const testResults = await runCLIAsync(`test ${appName}`);
expect(testResults.combinedOutput).toContain( expect(testResults.combinedOutput).toContain('Tests 2 passed (2)');
'Test Suites: 1 passed, 1 total'
);
const lintE2eResults = runCLI(`lint ${appName}-e2e`); const lintE2eResults = runCLI(`lint ${appName}-e2e`);

View File

@ -4,7 +4,7 @@ import {
} from '../../utils/lint'; } from '../../utils/lint';
import { NormalizedSchema, Schema } from './schema'; import { NormalizedSchema, Schema } from './schema';
import { createApplicationFiles } from './lib/create-application-files'; 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 { normalizeOptions } from './lib/normalize-options';
import { addProject } from './lib/add-project'; import { addProject } from './lib/add-project';
import { addCypress } from './lib/add-cypress'; import { addCypress } from './lib/add-cypress';
@ -26,7 +26,7 @@ import reactInitGenerator from '../init/init';
import { Linter, lintProjectGenerator } from '@nrwl/linter'; import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { swcCoreVersion } from '@nrwl/js/src/utils/versions'; import { swcCoreVersion } from '@nrwl/js/src/utils/versions';
import { swcLoaderVersion } from '@nrwl/webpack/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) { async function addLinting(host: Tree, options: NormalizedSchema) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
@ -89,10 +89,20 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
uiFramework: 'react', uiFramework: 'react',
project: options.projectName, project: options.projectName,
newProject: true, newProject: true,
includeVitest: true,
}); });
tasks.push(viteTask); 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); const lintTask = await addLinting(host, options);
tasks.push(lintTask); tasks.push(lintTask);
@ -100,7 +110,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
tasks.push(cypressTask); tasks.push(cypressTask);
const jestTask = await addJest(host, options); const jestTask = await addJest(host, options);
tasks.push(jestTask); tasks.push(jestTask);
updateJestConfig(host, options); updateSpecConfig(host, options);
const styledTask = addStyledModuleDependencies(host, options.styledModule); const styledTask = addStyledModuleDependencies(host, options.styledModule);
tasks.push(styledTask); tasks.push(styledTask);
const routingTask = addRouting(host, options); const routingTask = addRouting(host, options);

View File

@ -68,7 +68,10 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
templateVariables templateVariables
); );
if (options.unitTestRunner === 'none') { if (
options.unitTestRunner === 'none' ||
(options.unitTestRunner === 'vitest' && options.inSourceTests == true)
) {
host.delete( host.delete(
`${options.appProjectRoot}/src/app/${options.fileName}.spec.tsx` `${options.appProjectRoot}/src/app/${options.fileName}.spec.tsx`
); );
@ -80,6 +83,18 @@ export function createApplicationFiles(host: Tree, options: NormalizedSchema) {
templateVariables 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) { if (options.js) {
toJS(host); toJS(host);
} }

View File

@ -40,6 +40,10 @@ export function normalizeOptions(
assertValidStyle(options.style); assertValidStyle(options.style);
if (options.bundler === 'vite') {
options.unitTestRunner = 'vitest';
}
options.routing = options.routing ?? false; options.routing = options.routing ?? false;
options.strict = options.strict ?? true; options.strict = options.strict ?? true;
options.classComponent = options.classComponent ?? false; options.classComponent = options.classComponent ?? false;

View File

@ -2,8 +2,8 @@ import { updateJestConfigContent } from '../../../utils/jest-utils';
import { NormalizedSchema } from '../schema'; import { NormalizedSchema } from '../schema';
import { offsetFromRoot, Tree, updateJson } from '@nrwl/devkit'; import { offsetFromRoot, Tree, updateJson } from '@nrwl/devkit';
export function updateJestConfig(host: Tree, options: NormalizedSchema) { export function updateSpecConfig(host: Tree, options: NormalizedSchema) {
if (options.unitTestRunner !== 'jest') { if (options.unitTestRunner === 'none') {
return; return;
} }
@ -21,6 +21,10 @@ export function updateJestConfig(host: Tree, options: NormalizedSchema) {
return json; return json;
}); });
if (options.unitTestRunner !== 'jest') {
return;
}
const configPath = `${options.appProjectRoot}/jest.config.${ const configPath = `${options.appProjectRoot}/jest.config.${
options.js ? 'js' : 'ts' options.js ? 'js' : 'ts'
}`; }`;

View File

@ -7,7 +7,8 @@ export interface Schema {
skipFormat: boolean; skipFormat: boolean;
directory?: string; directory?: string;
tags?: string; tags?: string;
unitTestRunner: 'jest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
inSourceTests?: boolean;
/** /**
* @deprecated * @deprecated
*/ */

View File

@ -103,10 +103,15 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "none"], "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests.",
"default": "jest" "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": { "e2eTestRunner": {
"type": "string", "type": "string",
"enum": ["cypress", "none"], "enum": ["cypress", "none"],

View File

@ -7,7 +7,7 @@ export interface Schema {
skipFormat: boolean; skipFormat: boolean;
directory?: string; directory?: string;
tags?: string; tags?: string;
unitTestRunner: 'jest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
e2eTestRunner: 'cypress' | 'none'; e2eTestRunner: 'cypress' | 'none';
linter: Linter; linter: Linter;
pascalCaseFiles?: boolean; pascalCaseFiles?: boolean;

View File

@ -1,5 +1,5 @@
export interface InitSchema { export interface InitSchema {
unitTestRunner?: 'jest' | 'none'; unitTestRunner?: 'jest' | 'vitest' | 'none';
e2eTestRunner?: 'cypress' | 'none'; e2eTestRunner?: 'cypress' | 'none';
skipFormat?: boolean; skipFormat?: boolean;
skipPackageJson?: boolean; skipPackageJson?: boolean;

View File

@ -47,6 +47,7 @@ import componentGenerator from '../component/component';
import init from '../init/init'; import init from '../init/init';
import { Schema } from './schema'; import { Schema } from './schema';
import { updateJestConfigContent } from '../../utils/jest-utils'; import { updateJestConfigContent } from '../../utils/jest-utils';
import { vitestGenerator } from '@nrwl/vite';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
name: string; name: string;
fileName: string; fileName: string;
@ -109,6 +110,13 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
); );
host.write(jestConfigPath, updatedContent); 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) { if (options.component) {
@ -117,7 +125,9 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
project: options.name, project: options.name,
flat: true, flat: true,
style: options.style, style: options.style,
skipTests: options.unitTestRunner === 'none', skipTests:
options.unitTestRunner === 'none' ||
(options.unitTestRunner === 'vitest' && options.inSourceTests == true),
export: true, export: true,
routing: options.routing, routing: options.routing,
js: options.js, js: options.js,

View File

@ -11,7 +11,8 @@ export interface Schema {
pascalCaseFiles?: boolean; pascalCaseFiles?: boolean;
routing?: boolean; routing?: boolean;
appProject?: string; appProject?: string;
unitTestRunner: 'jest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
inSourceTests?: boolean;
linter: Linter; linter: Linter;
component?: boolean; component?: boolean;
publishable?: boolean; publishable?: boolean;

View File

@ -80,10 +80,15 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "none"], "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests.",
"default": "jest" "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": { "tags": {
"type": "string", "type": "string",
"description": "Add tags to the library (used for linting).", "description": "Add tags to the library (used for linting).",

View File

@ -8,7 +8,7 @@ export interface Schema {
skipFormat: boolean; skipFormat: boolean;
directory?: string; directory?: string;
tags?: string; tags?: string;
unitTestRunner: 'jest' | 'none'; unitTestRunner: 'jest' | 'vitest' | 'none';
e2eTestRunner: 'cypress' | 'none'; e2eTestRunner: 'cypress' | 'none';
linter: Linter; linter: Linter;
pascalCaseFiles?: boolean; pascalCaseFiles?: boolean;

View File

@ -15,6 +15,11 @@
"description": "Add Vite configuration to an application.", "description": "Add Vite configuration to an application.",
"aliases": ["ng-add"], "aliases": ["ng-add"],
"hidden": false "hidden": false
},
"vitest": {
"factory": "./src/generators/vitest/vitest-generator#vitestSchematic",
"schema": "./src/generators/vitest/schema.json",
"description": "Generate a vitest configuration"
} }
}, },
"generators": { "generators": {
@ -31,6 +36,11 @@
"description": "Add Vite configuration to an application.", "description": "Add Vite configuration to an application.",
"aliases": ["ng-add"], "aliases": ["ng-add"],
"hidden": false "hidden": false
},
"vitest": {
"factory": "./src/generators/vitest/vitest-generator",
"schema": "./src/generators/vitest/schema.json",
"description": "Generate a vitest configuration"
} }
} }
} }

View File

@ -1,2 +1,3 @@
export * from './src/utils/versions'; export * from './src/utils/versions';
export { viteConfigurationGenerator } from './src/generators/configuration/configuration'; export { viteConfigurationGenerator } from './src/generators/configuration/configuration';
export { vitestGenerator } from './src/generators/vitest/vitest-generator';

View File

@ -1,9 +1,9 @@
export interface VitestExecutorSchema { export interface VitestExecutorOptions {
config: string; config?: string;
passWithNoTests: boolean; passWithNoTests?: boolean;
testNamePattern?: string; testNamePattern?: string;
mode: 'test' | 'benchmark' | 'typecheck'; mode?: 'test' | 'benchmark' | 'typecheck';
reporters?: string[]; reporters?: string[];
watch: boolean; watch?: boolean;
update: boolean; update?: boolean;
} }

View File

@ -1,6 +1,6 @@
import { ExecutorContext } from '@nrwl/devkit'; import { ExecutorContext } from '@nrwl/devkit';
import { File, Reporter } from 'vitest'; import { File, Reporter } from 'vitest';
import { VitestExecutorSchema } from './schema'; import { VitestExecutorOptions } from './schema';
class NxReporter implements Reporter { class NxReporter implements Reporter {
deferred: { deferred: {
@ -38,7 +38,7 @@ class NxReporter implements Reporter {
} }
export default async function* runExecutor( export default async function* runExecutor(
options: VitestExecutorSchema, options: VitestExecutorOptions,
context: ExecutorContext context: ExecutorContext
) { ) {
const { startVitest } = await (Function( const { startVitest } = await (Function(

View File

@ -91,4 +91,29 @@ describe('@nrwl/vite:configuration', () => {
expect(tree.exists('apps/my-test-web-app/vite.config.ts')).toBe(true); 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');
});
});
}); });

View File

@ -7,7 +7,7 @@ import {
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { import {
findServeAndBuildTargets, findExistingTargets,
addOrChangeBuildTarget, addOrChangeBuildTarget,
addOrChangeServeTarget, addOrChangeServeTarget,
editTsConfig, editTsConfig,
@ -16,6 +16,7 @@ import {
} from '../../utils/generator-utils'; } from '../../utils/generator-utils';
import initGenerator from '../init/init'; import initGenerator from '../init/init';
import vitestGenerator from '../vitest/vitest-generator';
import { Schema } from './schema'; import { Schema } from './schema';
export async function viteConfigurationGenerator(tree: Tree, schema: Schema) { export async function viteConfigurationGenerator(tree: Tree, schema: Schema) {
@ -26,8 +27,8 @@ export async function viteConfigurationGenerator(tree: Tree, schema: Schema) {
let serveTarget = 'serve'; let serveTarget = 'serve';
if (!schema.newProject) { if (!schema.newProject) {
buildTarget = findServeAndBuildTargets(targets).buildTarget; buildTarget = findExistingTargets(targets).buildTarget;
serveTarget = findServeAndBuildTargets(targets).serveTarget; serveTarget = findExistingTargets(targets).serveTarget;
moveAndEditIndexHtml(tree, schema, buildTarget); moveAndEditIndexHtml(tree, schema, buildTarget);
editTsConfig(tree, schema); editTsConfig(tree, schema);
} }
@ -39,8 +40,19 @@ export async function viteConfigurationGenerator(tree: Tree, schema: Schema) {
addOrChangeBuildTarget(tree, schema, buildTarget); addOrChangeBuildTarget(tree, schema, buildTarget);
addOrChangeServeTarget(tree, schema, serveTarget); addOrChangeServeTarget(tree, schema, serveTarget);
writeViteConfig(tree, schema); 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); await formatFiles(tree);
return runTasksInSerial(...tasks); return runTasksInSerial(...tasks);

View File

@ -2,4 +2,6 @@ export interface Schema {
uiFramework: 'react' | 'none'; uiFramework: 'react' | 'none';
project: string; project: string;
newProject?: boolean; newProject?: boolean;
includeVitest?: boolean;
inSourceTests?: boolean;
} }

View File

@ -10,6 +10,7 @@ Object {
"@vitejs/plugin-react": "^2.2.0", "@vitejs/plugin-react": "^2.2.0",
"@vitest/ui": "^0.9.3", "@vitest/ui": "^0.9.3",
"existing": "1.0.0", "existing": "1.0.0",
"jsdom": "~20.0.3",
"vite": "^3.0.5", "vite": "^3.0.5",
"vite-plugin-eslint": "^1.6.0", "vite-plugin-eslint": "^1.6.0",
"vite-tsconfig-paths": "^3.5.2", "vite-tsconfig-paths": "^3.5.2",

View File

@ -15,6 +15,7 @@ import {
vitestUiVersion, vitestUiVersion,
vitestVersion, vitestVersion,
viteTsConfigPathsVersion, viteTsConfigPathsVersion,
jsdomVersion,
} from '../../utils/versions'; } from '../../utils/versions';
import { Schema } from './schema'; import { Schema } from './schema';
@ -23,7 +24,7 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) {
const devDependencies = {}; const devDependencies = {};
const dependencies = {}; const dependencies = {};
packageJson.dependencies = packageJson.dependencies || {}; packageJson.dependencies = packageJson.dependencies || {};
packageJson.devDependencices = packageJson.devDependencices || {}; packageJson.devDependencies = packageJson.devDependencies || {};
// base deps // base deps
devDependencies['@nrwl/vite'] = nxVersion; devDependencies['@nrwl/vite'] = nxVersion;
@ -32,6 +33,7 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) {
devDependencies['vite-tsconfig-paths'] = viteTsConfigPathsVersion; devDependencies['vite-tsconfig-paths'] = viteTsConfigPathsVersion;
devDependencies['vitest'] = vitestVersion; devDependencies['vitest'] = vitestVersion;
devDependencies['@vitest/ui'] = vitestUiVersion; devDependencies['@vitest/ui'] = vitestUiVersion;
devDependencies['jsdom'] = jsdomVersion;
if (schema.uiFramework === 'react') { if (schema.uiFramework === 'react') {
devDependencies['@vitejs/plugin-react'] = vitePluginReactVersion; devDependencies['@vitejs/plugin-react'] = vitePluginReactVersion;

View File

@ -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"
]
}

View File

@ -0,0 +1,6 @@
export interface VitestGeneratorSchema {
project: string;
uiFramework: 'react' | 'none';
inSourceTests?: boolean;
skipViteConfig?: boolean;
}

View File

@ -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"]
}

View File

@ -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);

View File

@ -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(`
"
/// <reference types=\\"vitest\\" />
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(`
"
/// <reference types=\\"vitest\\" />
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}']
},
});"
`);
});
});
});

View File

@ -11,6 +11,7 @@ import {
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { ViteBuildExecutorOptions } from '../executors/build/schema'; import { ViteBuildExecutorOptions } from '../executors/build/schema';
import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema'; import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema';
import { VitestExecutorOptions } from '../executors/test/schema';
import { Schema } from '../generators/configuration/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 * they are using, and infer from the executor that the target
* is a build target. * is a build target.
*/ */
export function findServeAndBuildTargets(targets: { export function findExistingTargets(targets: {
[targetName: string]: TargetConfiguration; [targetName: string]: TargetConfiguration;
}): { }): {
buildTarget: string; buildTarget: string;
serveTarget: string; serveTarget: string;
testTarget: string;
} { } {
const returnObject: { const returnObject: {
buildTarget: string; buildTarget: string;
serveTarget: string; serveTarget: string;
testTarget: string;
} = { } = {
buildTarget: 'build', buildTarget: 'build',
serveTarget: 'serve', serveTarget: 'serve',
testTarget: 'test',
}; };
Object.entries(targets).forEach(([target, targetConfig]) => { Object.entries(targets).forEach(([target, targetConfig]) => {
@ -68,9 +72,13 @@ export function findServeAndBuildTargets(targets: {
case '@nxext/vite:build': case '@nxext/vite:build':
returnObject.buildTarget = target; returnObject.buildTarget = target;
break; break;
case '@nrwl/jest:jest':
case 'nxext/vitest:vitest':
returnObject.testTarget = target;
default: default:
returnObject.buildTarget = 'build'; returnObject.buildTarget = 'build';
returnObject.serveTarget = 'serve'; returnObject.serveTarget = 'serve';
returnObject.testTarget = 'test';
break; break;
} }
}); });
@ -78,6 +86,39 @@ export function findServeAndBuildTargets(targets: {
return returnObject; 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( export function addOrChangeBuildTarget(
tree: Tree, tree: Tree,
options: Schema, options: Schema,
@ -315,9 +356,22 @@ export function writeViteConfig(tree: Tree, options: Schema) {
let viteConfigContent = ''; 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) { switch (options.uiFramework) {
case 'react': case 'react':
viteConfigContent = ` viteConfigContent = `
${options.includeVitest ? '/// <reference types="vitest" />' : ''}
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths'; import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
@ -330,10 +384,13 @@ export function writeViteConfig(tree: Tree, options: Schema) {
projects: ['tsconfig.base.json'], projects: ['tsconfig.base.json'],
}), }),
], ],
${options.inSourceTests ? defineOption : ''}
${options.includeVitest ? testOption : ''}
});`; });`;
break; break;
case 'none': case 'none':
viteConfigContent = ` viteConfigContent = `
${options.includeVitest ? '/// <reference types="vitest" />' : ''}
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths'; import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
@ -344,6 +401,8 @@ export function writeViteConfig(tree: Tree, options: Schema) {
projects: ['tsconfig.base.json'], projects: ['tsconfig.base.json'],
}), }),
], ],
${options.inSourceTests ? defineOption : ''}
${options.includeVitest ? testOption : ''}
});`; });`;
break; break;
default: default:

View File

@ -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( tree.write(
`apps/${appName}/src/index.html`, `apps/${appName}/src/index.html`,

View File

@ -7,3 +7,4 @@ export const vitePluginReactVersion = '^2.2.0';
export const vitePluginVueVersion = '^3.2.0'; export const vitePluginVueVersion = '^3.2.0';
export const vitePluginVueJsxVersion = '^2.1.1'; export const vitePluginVueJsxVersion = '^2.1.1';
export const viteTsConfigPathsVersion = '^3.5.2'; export const viteTsConfigPathsVersion = '^3.5.2';
export const jsdomVersion = '~20.0.3';

View File

@ -24,7 +24,7 @@ import { swcCoreVersion } from '@nrwl/js/src/utils/versions';
import { Linter, lintProjectGenerator } from '@nrwl/linter'; import { Linter, lintProjectGenerator } from '@nrwl/linter';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial'; import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { getRelativePathToRootTsConfig } from '@nrwl/workspace/src/utilities/typescript'; 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 { swcLoaderVersion } from '../../utils/versions';
import { webInitGenerator } from '../init/init'; import { webInitGenerator } from '../init/init';
@ -203,10 +203,20 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
uiFramework: 'react', uiFramework: 'react',
project: options.projectName, project: options.projectName,
newProject: true, newProject: true,
includeVitest: true,
}); });
tasks.push(viteTask); 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, { const lintTask = await lintProjectGenerator(host, {
linter: options.linter, linter: options.linter,
project: options.projectName, project: options.projectName,
@ -273,6 +283,10 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
? options.tags.split(',').map((s) => s.trim()) ? options.tags.split(',').map((s) => s.trim())
: []; : [];
if (options.bundler === 'vite') {
options.unitTestRunner = 'vitest';
}
options.style = options.style || 'css'; options.style = options.style || 'css';
options.linter = options.linter || Linter.EsLint; options.linter = options.linter || Linter.EsLint;
options.unitTestRunner = options.unitTestRunner || 'jest'; options.unitTestRunner = options.unitTestRunner || 'jest';

View File

@ -9,7 +9,8 @@ export interface Schema {
skipFormat?: boolean; skipFormat?: boolean;
directory?: string; directory?: string;
tags?: string; tags?: string;
unitTestRunner?: 'jest' | 'none'; unitTestRunner?: 'jest' | 'vitest' | 'none';
inSourceTests?: boolean;
e2eTestRunner?: 'cypress' | 'none'; e2eTestRunner?: 'cypress' | 'none';
linter?: Linter; linter?: Linter;
standaloneConfig?: boolean; standaloneConfig?: boolean;

View File

@ -73,10 +73,15 @@
}, },
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "none"], "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests", "description": "Test runner to use for unit tests",
"default": "jest" "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": { "e2eTestRunner": {
"type": "string", "type": "string",
"enum": ["cypress", "none"], "enum": ["cypress", "none"],

View File

@ -1,6 +1,6 @@
export interface Schema { export interface Schema {
bundler?: 'webpack' | 'none' | 'vite'; bundler?: 'webpack' | 'none' | 'vite';
unitTestRunner?: 'jest' | 'none'; unitTestRunner?: 'jest' | 'vitest' | 'none';
e2eTestRunner?: 'cypress' | 'none'; e2eTestRunner?: 'cypress' | 'none';
skipFormat?: boolean; skipFormat?: boolean;
skipPackageJson?: boolean; skipPackageJson?: boolean;