fix(vite): adjust config generation (#20367)

This commit is contained in:
Katerina Skroumpelou 2023-11-29 15:58:49 +02:00 committed by GitHub
parent b9b75a79fc
commit 2c88282e8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 1467 additions and 982 deletions

View File

@ -26,11 +26,6 @@
"description": "Skip type-checking via TypeScript. Skipping type-checking speeds up the build but type errors are not caught.", "description": "Skip type-checking via TypeScript. Skipping type-checking speeds up the build but type errors are not caught.",
"default": false "default": false
}, },
"base": {
"type": "string",
"description": "Base public path when served in development or production.",
"alias": "baseHref"
},
"configFile": { "configFile": {
"type": "string", "type": "string",
"description": "The name of the Vite.js configuration file.", "description": "The name of the Vite.js configuration file.",
@ -59,49 +54,6 @@
}, },
"default": [] "default": []
}, },
"emptyOutDir": {
"description": "When set to false, outputPath will not be emptied during the build process.",
"type": "boolean",
"default": true
},
"sourcemap": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"target": {
"description": "Browser compatibility target for the final bundle. For more info: https://vitejs.dev/config/build-options.html#build-target",
"type": "string"
},
"minify": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"manifest": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"ssrManifest": {
"description": "When set to true, the build will also generate an SSR manifest for determining style links and asset preload directives in production. When the value is a string, it will be used as the manifest file name.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"ssr": {
"description": "Produce SSR-oriented build. The value can be a string to directly specify the SSR entry, or true, which requires specifying the SSR entry via rollupOptions.input.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"logLevel": {
"type": "string",
"description": "Adjust console output verbosity.",
"enum": ["info", "warn", "error", "silent"]
},
"mode": { "type": "string", "description": "Mode to run the build in." },
"force": {
"description": "Force the optimizer to ignore the cache and re-bundle",
"type": "boolean"
},
"cssCodeSplit": {
"description": "Enable/disable CSS code splitting. When enabled, CSS imported in async chunks will be inlined into the async chunk itself and inserted when the chunk is loaded.",
"type": "boolean"
},
"watch": { "watch": {
"description": "Enable re-building when files change.", "description": "Enable re-building when files change.",
"oneOf": [{ "type": "boolean" }, { "type": "object" }], "oneOf": [{ "type": "boolean" }, { "type": "object" }],

View File

@ -27,45 +27,6 @@
"type": "string", "type": "string",
"description": "Path to the proxy configuration file.", "description": "Path to the proxy configuration file.",
"x-completion-type": "file" "x-completion-type": "file"
},
"port": {
"type": "number",
"description": "Port to listen on.",
"x-priority": "important"
},
"host": {
"description": "Specify which IP addresses the server should listen on.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"https": {
"oneOf": [{ "type": "boolean" }, { "type": "object" }],
"description": "Serve using HTTPS. https://vitejs.dev/config/server-options.html#server-https"
},
"hmr": {
"description": "Enable hot module replacement. For more options, use the 'hmr' option in the Vite configuration file.",
"type": "boolean"
},
"open": {
"description": "Automatically open the app in the browser on server start. When the value is a string, it will be used as the URL's pathname.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"cors": {
"description": "Configure CORS for the dev server.",
"type": "boolean"
},
"logLevel": {
"type": "string",
"description": "Adjust console output verbosity.",
"enum": ["info", "warn", "error", "silent"]
},
"mode": { "type": "string", "description": "Mode to run the server in." },
"clearScreen": {
"description": "Set to false to prevent Vite from clearing the terminal screen when logging certain messages.",
"type": "boolean"
},
"force": {
"description": "Force the optimizer to ignore the cache and re-bundle",
"type": "boolean"
} }
}, },
"definitions": {}, "definitions": {},

View File

@ -22,29 +22,6 @@
"description": "Path to the proxy configuration file.", "description": "Path to the proxy configuration file.",
"x-completion-type": "file" "x-completion-type": "file"
}, },
"port": { "type": "number", "description": "Port to listen on." },
"host": {
"description": "Specify which IP addresses the server should listen on.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"https": {
"oneOf": [{ "type": "boolean" }, { "type": "object" }],
"description": "Serve using HTTPS. https://vitejs.dev/config/server-options.html#server-https"
},
"open": {
"description": "Automatically open the app in the browser on server start. When the value is a string, it will be used as the URL's pathname.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
},
"logLevel": {
"type": "string",
"description": "Adjust console output verbosity.",
"enum": ["info", "warn", "error", "silent"]
},
"mode": { "type": "string", "description": "Mode to run the server in." },
"clearScreen": {
"description": "Set to false to prevent Vite from clearing the terminal screen when logging certain messages.",
"type": "boolean"
},
"staticFilePath": { "staticFilePath": {
"type": "string", "type": "string",
"description": "Path where the build artifacts are located. If not provided then it will be infered from the buildTarget executor options as outputPath", "description": "Path where the build artifacts are located. If not provided then it will be infered from the buildTarget executor options as outputPath",

View File

@ -9,50 +9,12 @@
"description": "Test using Vitest.", "description": "Test using Vitest.",
"type": "object", "type": "object",
"properties": { "properties": {
"config": { "configFile": {
"type": "string", "type": "string",
"description": "The path to the local vitest config", "description": "The path to the local vitest config",
"x-completion-type": "file", "x-completion-type": "file",
"x-completion-glob": "@(vitest|vite).config@(.js|.ts)" "x-completion-glob": "@(vitest|vite).config@(.js|.ts)",
}, "aliases": ["config"]
"passWithNoTests": {
"type": "boolean",
"default": true,
"description": "Pass the test even if no tests are found"
},
"testNamePattern": {
"type": "string",
"description": "Run tests with full names matching the pattern"
},
"mode": {
"type": "string",
"enum": ["test", "benchmark", "typecheck"],
"default": "test",
"description": "The mode that vitest will run on",
"x-priority": "important"
},
"watch": {
"type": "boolean",
"default": false,
"description": "Enable watch mode"
},
"reporters": {
"type": "array",
"items": { "type": "string" },
"description": "An array of reporters to pass to vitest"
},
"update": {
"type": "boolean",
"default": false,
"alias": "u",
"description": "Update snapshots",
"x-priority": "important"
},
"coverage": {
"type": "boolean",
"default": false,
"description": "Enable coverage report",
"x-priority": "important"
}, },
"reportsDirectory": { "reportsDirectory": {
"type": "string", "type": "string",
@ -62,6 +24,10 @@
"aliases": ["testFile"], "aliases": ["testFile"],
"type": "array", "type": "array",
"items": { "type": "string" } "items": { "type": "string" }
},
"watch": {
"description": "Watch files for changes and rerun tests related to changed files.",
"type": "boolean"
} }
}, },
"required": [], "required": [],

View File

@ -38,7 +38,7 @@ describe('Nuxt Plugin', () => {
it('should test application', async () => { it('should test application', async () => {
const result = runCLI(`test ${app}`); const result = runCLI(`test ${app}`);
expect(result).toContain(`Successfully ran target test for project ${app}`); expect(result).toContain(`Successfully ran target test for project ${app}`);
}); }, 150_000);
it('should lint application', async () => { it('should lint application', async () => {
const result = runCLI(`lint ${app}`); const result = runCLI(`lint ${app}`);

View File

@ -6,10 +6,8 @@ import {
exists, exists,
fileExists, fileExists,
getPackageManagerCommand, getPackageManagerCommand,
killPorts,
listFiles, listFiles,
newProject, newProject,
promisifiedTreeKill,
readFile, readFile,
readJson, readJson,
removeFile, removeFile,
@ -17,7 +15,6 @@ import {
runCLI, runCLI,
runCommand, runCommand,
runCLIAsync, runCLIAsync,
runCommandUntil,
tmpProjPath, tmpProjPath,
uniq, uniq,
updateFile, updateFile,
@ -32,87 +29,11 @@ describe('Vite Plugin', () => {
let proj: string; let proj: string;
describe('Vite on React apps', () => { describe('Vite on React apps', () => {
describe('convert React webpack app to vite using the vite:configuration generator', () => {
beforeEach(() => {
proj = newProject();
runCLI(`generate @nx/react:app ${myApp} --bundler=webpack`);
runCLI(`generate @nx/vite:configuration ${myApp}`);
});
afterEach(() => cleanupProject());
it('should serve application in dev mode with custom options', async () => {
const port = 4212;
const p = await runCommandUntil(
`run ${myApp}:serve --port=${port} --https=true`,
(output) => {
return (
output.includes('Local:') &&
output.includes(`:${port}`) &&
output.includes('https')
);
}
);
try {
await promisifiedTreeKill(p.pid, 'SIGKILL');
await killPorts(port);
} catch (e) {
// ignore
}
}, 200_000);
it('should test application', async () => {
const result = await runCLIAsync(`test ${myApp}`);
expect(result.combinedOutput).toContain(
`Successfully ran target test for project ${myApp}`
);
});
});
describe('set up new React app with --bundler=vite option', () => { describe('set up new React app with --bundler=vite option', () => {
beforeEach(async () => { beforeEach(async () => {
proj = newProject(); proj = newProject();
runCLI(`generate @nx/react:app ${myApp} --bundler=vite`); runCLI(`generate @nx/react:app ${myApp} --bundler=vite`);
createFile(`apps/${myApp}/public/hello.md`, `# Hello World`); createFile(`apps/${myApp}/public/hello.md`, `# Hello World`);
updateFile(
`apps/${myApp}/src/environments/environment.prod.ts`,
`export const environment = {
production: true,
myTestVar: 'MyProductionValue',
};`
);
updateFile(
`apps/${myApp}/src/environments/environment.ts`,
`export const environment = {
production: false,
myTestVar: 'MyDevelopmentValue',
};`
);
updateFile(
`apps/${myApp}/src/app/app.tsx`,
`
import { environment } from './../environments/environment';
export function App() {
return (
<>
<h1>{environment.myTestVar}</h1>
<p>Welcome ${myApp}!</p>
</>
);
}
export default App;
`
);
updateJson(join('apps', myApp, 'project.json'), (config) => {
config.targets.build.options.fileReplacements = [
{
replace: `apps/${myApp}/src/environments/environment.ts`,
with: `apps/${myApp}/src/environments/environment.prod.ts`,
},
];
return config;
});
}); });
afterEach(() => cleanupProject()); afterEach(() => cleanupProject());
it('should build application', async () => { it('should build application', async () => {
@ -120,14 +41,6 @@ describe('Vite Plugin', () => {
expect(readFile(`dist/apps/${myApp}/favicon.ico`)).toBeDefined(); expect(readFile(`dist/apps/${myApp}/favicon.ico`)).toBeDefined();
expect(readFile(`dist/apps/${myApp}/hello.md`)).toBeDefined(); expect(readFile(`dist/apps/${myApp}/hello.md`)).toBeDefined();
expect(readFile(`dist/apps/${myApp}/index.html`)).toBeDefined(); expect(readFile(`dist/apps/${myApp}/index.html`)).toBeDefined();
const fileArray = listFiles(`dist/apps/${myApp}/assets`);
const mainBundle = fileArray.find((file) => file.endsWith('.js'));
expect(readFile(`dist/apps/${myApp}/assets/${mainBundle}`)).toContain(
'MyProductionValue'
);
expect(
readFile(`dist/apps/${myApp}/assets/${mainBundle}`)
).not.toContain('MyDevelopmentValue');
rmDist(); rmDist();
}, 200_000); }, 200_000);
}); });
@ -202,48 +115,7 @@ describe('Vite Plugin', () => {
}, 200_000); }, 200_000);
}); });
describe('convert @nx/web webpack app to vite using the vite:configuration generator', () => { 100_000;
beforeEach(() => {
proj = newProject();
runCLI(`generate @nx/web:app ${myApp} --bundler=webpack`);
runCLI(`generate @nx/vite:configuration ${myApp}`);
});
afterEach(() => cleanupProject());
it('should build application', async () => {
runCLI(`build ${myApp}`);
expect(readFile(`dist/apps/${myApp}/index.html`)).toBeDefined();
const fileArray = listFiles(`dist/apps/${myApp}/assets`);
const mainBundle = fileArray.find((file) => file.endsWith('.js'));
expect(
readFile(`dist/apps/${myApp}/assets/${mainBundle}`)
).toBeDefined();
rmDist();
}, 200_000);
it('should serve application in dev mode with custom port', async () => {
const port = 4212;
const p = await runCommandUntil(
`run ${myApp}:serve --port=${port}`,
(output) => {
return output.includes('Local:') && output.includes(`:${port}`);
}
);
try {
await promisifiedTreeKill(p.pid, 'SIGKILL');
await killPorts(port);
} catch {
// ignore
}
}, 200_000);
it('should test application', async () => {
const result = await runCLIAsync(`test ${myApp}`);
expect(result.combinedOutput).toContain(
`Successfully ran target test for project ${myApp}`
);
});
}),
100_000;
}); });
describe('incremental building', () => { describe('incremental building', () => {
@ -363,6 +235,8 @@ export default App;
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/${lib}',
server: { server: {
port: 4200, port: 4200,
host: 'localhost', host: 'localhost',
@ -371,14 +245,18 @@ export default App;
react(), react(),
nxViteTsPaths() nxViteTsPaths()
], ],
build: {
outDir: '../../dist/libs/${lib}',
},
test: { test: {
globals: true, globals: true,
cache: { cache: {
dir: './node_modules/.vitest', dir: '../../node_modules/.vitest',
}, },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: { coverage: {
reportsDirectory: '../../coverage/libs/${lib}',
provider: "v8", provider: "v8",
enabled: true, enabled: true,
lines: 100, lines: 100,
@ -531,7 +409,7 @@ export default defineConfig({
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: './node_modules/.vite/root-app', cacheDir: '../../node_modules/.vite/root-app',
server: { server: {
port: 4200, port: 4200,
host: 'localhost', host: 'localhost',

View File

@ -61,8 +61,8 @@ describe('Web Components Applications with bundler set as vite', () => {
`dist/apps/${appName}/_should_remove.txt`, `dist/apps/${appName}/_should_remove.txt`,
`dist/apps/_should_not_remove.txt` `dist/apps/_should_not_remove.txt`
); );
runCLI(`build ${appName}`); runCLI(`build ${appName} --emptyOutDir`);
runCLI(`build ${libName}`); runCLI(`build ${libName} --emptyOutDir`);
checkFilesDoNotExist( checkFilesDoNotExist(
`dist/apps/${appName}/_should_remove.txt`, `dist/apps/${appName}/_should_remove.txt`,
`dist/libs/${libName}/_should_remove.txt` `dist/libs/${libName}/_should_remove.txt`

View File

@ -129,8 +129,12 @@ export default defineVitestConfig({
cache: { cache: {
dir: '../node_modules/.vitest', dir: '../node_modules/.vitest',
}, },
include: ['my-app/src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
environment: 'nuxt', environment: 'nuxt',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../coverage/app5176218',
provider: 'v8',
},
}, },
}); });
" "

View File

@ -54,12 +54,16 @@ export function addVitest(
export default defineVitestConfig({ export default defineVitestConfig({
plugins: [nxViteTsPaths()], plugins: [nxViteTsPaths()],
test: { test: {
globals: true, globals: true,
cache: { cache: {
dir: '${projectOffsetFromRoot}node_modules/.vitest', dir: '${projectOffsetFromRoot}node_modules/.vitest',
}, },
include: ['${projectRoot}/src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], environment: 'nuxt',
environment: 'nuxt', include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '${projectOffsetFromRoot}coverage/app5176218',
provider: 'v8',
},
}, },
}); });
` `

View File

@ -6,6 +6,7 @@ import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-lib', cacheDir: '../node_modules/.vite/my-lib',
plugins: [react(), nxViteTsPaths()], plugins: [react(), nxViteTsPaths()],
@ -20,6 +21,7 @@ export default defineConfig({
cache: { dir: '../node_modules/.vitest' }, cache: { dir: '../node_modules/.vitest' },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: { reportsDirectory: '../coverage/my-lib', provider: 'v8' },
}, },
}); });
" "
@ -34,6 +36,7 @@ import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-lib', cacheDir: '../node_modules/.vite/my-lib',
plugins: [ plugins: [
@ -54,6 +57,7 @@ export default defineConfig({
// Configuration for building your library. // Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode // See: https://vitejs.dev/guide/build.html#library-mode
build: { build: {
outDir: '../dist/my-lib',
lib: { lib: {
// Could also be a dictionary or array of multiple entry points. // Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts', entry: 'src/index.ts',
@ -76,6 +80,11 @@ export default defineConfig({
}, },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../coverage/my-lib',
provider: 'v8',
},
}, },
}); });
" "

View File

@ -40,6 +40,11 @@
"version": "17.1.0-beta.2", "version": "17.1.0-beta.2",
"description": "Move target defaults", "description": "Move target defaults",
"implementation": "./src/migrations/update-17-1-0/move-target-defaults" "implementation": "./src/migrations/update-17-1-0/move-target-defaults"
},
"update-vite-config": {
"version": "17.2.0-beta.10",
"description": "Update vite config.",
"implementation": "./src/migrations/update-17-2-0/update-vite-config"
} }
}, },
"packageJsonUpdates": { "packageJsonUpdates": {

View File

@ -53,6 +53,7 @@
"./src/executors/*/schema.json": "./src/executors/*/schema.json", "./src/executors/*/schema.json": "./src/executors/*/schema.json",
"./src/executors/*.impl": "./src/executors/*.impl.js", "./src/executors/*.impl": "./src/executors/*.impl.js",
"./src/executors/*/compat": "./src/executors/*/compat.js", "./src/executors/*/compat": "./src/executors/*/compat.js",
"./plugins/nx-tsconfig-paths.plugin": "./plugins/nx-tsconfig-paths.plugin.js" "./plugins/nx-tsconfig-paths.plugin": "./plugins/nx-tsconfig-paths.plugin.js",
"./plugins/rollup-replace-files.plugin": "./plugins/rollup-replace-files.plugin.js"
} }
} }

View File

@ -3,9 +3,19 @@
/** /**
* @function replaceFiles * @function replaceFiles
* @param {FileReplacement[]} replacements * @param {FileReplacement[]} replacements
* @return {({name: "rollup-plugin-replace-files", enforce: "pre", Promise<resolveId>})} * @return {({name: "rollup-plugin-replace-files", enforce: "pre" | "post" | undefined, Promise<resolveId>})}
*/ */
export default function replaceFiles(replacements: FileReplacement[]) { export default function replaceFiles(replacements: FileReplacement[]): {
name: string;
enforce: 'pre' | 'post' | undefined;
resolveId(
source: any,
importer: any,
options: any
): Promise<{
id: string;
}>;
} {
if (!replacements?.length) { if (!replacements?.length) {
return null; return null;
} }

View File

@ -1,14 +1,15 @@
import { import {
detectPackageManager, detectPackageManager,
ExecutorContext, ExecutorContext,
joinPathFragments,
logger, logger,
offsetFromRoot,
stripIndents, stripIndents,
writeJsonFile, writeJsonFile,
} from '@nx/devkit'; } from '@nx/devkit';
import { import {
getProjectTsConfigPath, getProjectTsConfigPath,
getViteBuildOptions, normalizeViteConfigFilePath,
getViteSharedConfig,
} from '../../utils/options-utils'; } from '../../utils/options-utils';
import { ViteBuildExecutorOptions } from './schema'; import { ViteBuildExecutorOptions } from './schema';
import { import {
@ -18,33 +19,63 @@ import {
getLockFileName, getLockFileName,
} from '@nx/js'; } from '@nx/js';
import { existsSync, writeFileSync } from 'fs'; import { existsSync, writeFileSync } from 'fs';
import { resolve } from 'path'; import { relative, resolve } from 'path';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable'; import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
import { import {
createBuildableTsConfig, createBuildableTsConfig,
validateTypes, validateTypes,
} from '../../utils/executor-utils'; } from '../../utils/executor-utils';
import { BuildOptions } from 'vite';
export async function* viteBuildExecutor( export async function* viteBuildExecutor(
options: ViteBuildExecutorOptions, options: Record<string, any> & ViteBuildExecutorOptions,
context: ExecutorContext context: ExecutorContext
) { ) {
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future. // Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { mergeConfig, build } = await (Function( const { mergeConfig, build, loadConfigFromFile } = await (Function(
'return import("vite")' 'return import("vite")'
)() as Promise<typeof import('vite')>); )() as Promise<typeof import('vite')>);
const projectRoot = const projectRoot =
context.projectsConfigurations.projects[context.projectName].root; context.projectsConfigurations.projects[context.projectName].root;
createBuildableTsConfig(projectRoot, options, context); createBuildableTsConfig(projectRoot, options, context);
const normalizedOptions = normalizeOptions(options); const viteConfigPath = normalizeViteConfigFilePath(
context.root,
projectRoot,
options.configFile
);
const root =
projectRoot === '.'
? process.cwd()
: relative(context.cwd, joinPathFragments(context.root, projectRoot));
const { buildOptions, otherOptions } = await getExtraArgs(options);
const resolved = await loadConfigFromFile(
{
mode: otherOptions?.mode ?? 'production',
command: 'build',
},
viteConfigPath
);
const outDir =
joinPathFragments(offsetFromRoot(projectRoot), options.outputPath) ??
resolved?.config?.build?.outDir;
const buildConfig = mergeConfig( const buildConfig = mergeConfig(
getViteSharedConfig(normalizedOptions, false, context),
{ {
build: getViteBuildOptions(normalizedOptions, context), // This should not be needed as it's going to be set in vite.config.ts
// but leaving it here in case someone did not migrate correctly
root: resolved.config.root ?? root,
configFile: viteConfigPath,
},
{
build: {
outDir,
...buildOptions,
},
...otherOptions,
} }
); );
@ -60,7 +91,14 @@ export async function* viteBuildExecutor(
const libraryPackageJson = resolve(projectRoot, 'package.json'); const libraryPackageJson = resolve(projectRoot, 'package.json');
const rootPackageJson = resolve(context.root, 'package.json'); const rootPackageJson = resolve(context.root, 'package.json');
const distPackageJson = resolve(normalizedOptions.outputPath, 'package.json');
// Here, we want the outdir relative to the workspace root.
// So, we calculate the relative path from the workspace root to the outdir.
const outDirRelativeToWorkspaceRoot = outDir.replaceAll('../', '');
const distPackageJson = resolve(
outDirRelativeToWorkspaceRoot,
'package.json'
);
// Generate a package.json if option has been set. // Generate a package.json if option has been set.
if (options.generatePackageJson) { if (options.generatePackageJson) {
@ -83,7 +121,10 @@ export async function* viteBuildExecutor(
builtPackageJson.type = 'module'; builtPackageJson.type = 'module';
writeJsonFile(`${options.outputPath}/package.json`, builtPackageJson); writeJsonFile(
`${outDirRelativeToWorkspaceRoot}/package.json`,
builtPackageJson
);
const packageManager = detectPackageManager(context.root); const packageManager = detectPackageManager(context.root);
const lockFile = createLockFile( const lockFile = createLockFile(
@ -92,7 +133,7 @@ export async function* viteBuildExecutor(
packageManager packageManager
); );
writeFileSync( writeFileSync(
`${options.outputPath}/${getLockFileName(packageManager)}`, `${outDirRelativeToWorkspaceRoot}/${getLockFileName(packageManager)}`,
lockFile, lockFile,
{ {
encoding: 'utf-8', encoding: 'utf-8',
@ -107,7 +148,7 @@ export async function* viteBuildExecutor(
) { ) {
await copyAssets( await copyAssets(
{ {
outputPath: normalizedOptions.outputPath, outputPath: outDirRelativeToWorkspaceRoot,
assets: [ assets: [
{ {
input: projectRoot, input: projectRoot,
@ -142,22 +183,66 @@ export async function* viteBuildExecutor(
} else { } else {
const output = watcherOrOutput?.['output'] || watcherOrOutput?.[0]?.output; const output = watcherOrOutput?.['output'] || watcherOrOutput?.[0]?.output;
const fileName = output?.[0]?.fileName || 'main.cjs'; const fileName = output?.[0]?.fileName || 'main.cjs';
const outfile = resolve(normalizedOptions.outputPath, fileName); const outfile = resolve(outDirRelativeToWorkspaceRoot, fileName);
yield { success: true, outfile }; yield { success: true, outfile };
} }
} }
function normalizeOptions(options: ViteBuildExecutorOptions) { async function getExtraArgs(options: ViteBuildExecutorOptions): Promise<{
const normalizedOptions = { ...options }; buildOptions: BuildOptions;
otherOptions: Record<string, any>;
// coerce watch to null or {} to match with Vite's watch config }> {
if (options.watch === false) { // support passing extra args to vite cli
normalizedOptions.watch = null; const schema = await import('./schema.json');
} else if (options.watch === true) { const extraArgs = {};
normalizedOptions.watch = {}; for (const key of Object.keys(options)) {
if (!schema.properties[key]) {
extraArgs[key] = options[key];
}
} }
return normalizedOptions; const buildOptions = {} as BuildOptions;
const buildSchemaKeys = [
'target',
'polyfillModulePreload',
'modulePreload',
'outDir',
'assetsDir',
'assetsInlineLimit',
'cssCodeSplit',
'cssTarget',
'cssMinify',
'sourcemap',
'minify',
'terserOptions',
'rollupOptions',
'commonjsOptions',
'dynamicImportVarsOptions',
'write',
'emptyOutDir',
'copyPublicDir',
'manifest',
'lib',
'ssr',
'ssrManifest',
'ssrEmitAssets',
'reportCompressedSize',
'chunkSizeWarningLimit',
'watch',
];
const otherOptions = {};
for (const key of Object.keys(extraArgs)) {
if (buildSchemaKeys.includes(key)) {
buildOptions[key] = extraArgs[key];
} else {
otherOptions[key] = extraArgs[key];
}
}
return {
buildOptions,
otherOptions,
};
} }
export default viteBuildExecutor; export default viteBuildExecutor;

View File

@ -1,23 +1,11 @@
import type { FileReplacement } from '../../plugins/rollup-replace-files.plugin'; import type { FileReplacement } from '../../plugins/rollup-replace-files.plugin';
export interface ViteBuildExecutorOptions { export interface ViteBuildExecutorOptions {
outputPath: string; outputPath?: string;
emptyOutDir?: boolean; skipTypeCheck?: boolean;
base?: string;
configFile?: string; configFile?: string;
fileReplacements?: FileReplacement[]; fileReplacements?: FileReplacement[];
force?: boolean; watch?: boolean;
sourcemap?: boolean | 'inline' | 'hidden';
minify?: boolean | 'esbuild' | 'terser';
manifest?: boolean | string;
ssrManifest?: boolean | string;
logLevel?: 'info' | 'warn' | 'error' | 'silent';
mode?: string;
ssr?: boolean | string;
watch?: object | boolean;
target?: string | string[];
generatePackageJson?: boolean; generatePackageJson?: boolean;
includeDevDependenciesInPackageJson?: boolean; includeDevDependenciesInPackageJson?: boolean;
cssCodeSplit?: boolean;
buildLibsFromSource?: boolean; buildLibsFromSource?: boolean;
skipTypeCheck?: boolean;
} }

View File

@ -28,11 +28,6 @@
"description": "Skip type-checking via TypeScript. Skipping type-checking speeds up the build but type errors are not caught.", "description": "Skip type-checking via TypeScript. Skipping type-checking speeds up the build but type errors are not caught.",
"default": false "default": false
}, },
"base": {
"type": "string",
"description": "Base public path when served in development or production.",
"alias": "baseHref"
},
"configFile": { "configFile": {
"type": "string", "type": "string",
"description": "The name of the Vite.js configuration file.", "description": "The name of the Vite.js configuration file.",
@ -61,87 +56,6 @@
}, },
"default": [] "default": []
}, },
"emptyOutDir": {
"description": "When set to false, outputPath will not be emptied during the build process.",
"type": "boolean",
"default": true
},
"sourcemap": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"target": {
"description": "Browser compatibility target for the final bundle. For more info: https://vitejs.dev/config/build-options.html#build-target",
"type": "string"
},
"minify": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"manifest": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"ssrManifest": {
"description": "When set to true, the build will also generate an SSR manifest for determining style links and asset preload directives in production. When the value is a string, it will be used as the manifest file name.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"ssr": {
"description": "Produce SSR-oriented build. The value can be a string to directly specify the SSR entry, or true, which requires specifying the SSR entry via rollupOptions.input.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"logLevel": {
"type": "string",
"description": "Adjust console output verbosity.",
"enum": ["info", "warn", "error", "silent"]
},
"mode": {
"type": "string",
"description": "Mode to run the build in."
},
"force": {
"description": "Force the optimizer to ignore the cache and re-bundle",
"type": "boolean"
},
"cssCodeSplit": {
"description": "Enable/disable CSS code splitting. When enabled, CSS imported in async chunks will be inlined into the async chunk itself and inserted when the chunk is loaded.",
"type": "boolean"
},
"watch": { "watch": {
"description": "Enable re-building when files change.", "description": "Enable re-building when files change.",
"oneOf": [ "oneOf": [

View File

@ -1,16 +1,20 @@
import { ExecutorContext } from '@nx/devkit'; import { ExecutorContext, joinPathFragments } from '@nx/devkit';
import type { InlineConfig, ViteDevServer } from 'vite'; import {
loadConfigFromFile,
type InlineConfig,
type ViteDevServer,
} from 'vite';
import { import {
getNxTargetOptions, getNxTargetOptions,
getViteBuildOptions,
getViteServerOptions, getViteServerOptions,
getViteSharedConfig, normalizeViteConfigFilePath,
} from '../../utils/options-utils'; } from '../../utils/options-utils';
import { ViteDevServerExecutorOptions } from './schema'; import { ViteDevServerExecutorOptions } from './schema';
import { ViteBuildExecutorOptions } from '../build/schema'; import { ViteBuildExecutorOptions } from '../build/schema';
import { createBuildableTsConfig } from '../../utils/executor-utils'; import { createBuildableTsConfig } from '../../utils/executor-utils';
import { relative } from 'path';
export async function* viteDevServerExecutor( export async function* viteDevServerExecutor(
options: ViteDevServerExecutorOptions, options: ViteDevServerExecutorOptions,
@ -23,7 +27,10 @@ export async function* viteDevServerExecutor(
const projectRoot = const projectRoot =
context.projectsConfigurations.projects[context.projectName].root; context.projectsConfigurations.projects[context.projectName].root;
const root =
projectRoot === '.'
? process.cwd()
: relative(context.cwd, joinPathFragments(context.root, projectRoot));
createBuildableTsConfig(projectRoot, options, context); createBuildableTsConfig(projectRoot, options, context);
// Retrieve the option for the configured buildTarget. // Retrieve the option for the configured buildTarget.
@ -31,20 +38,33 @@ export async function* viteDevServerExecutor(
options.buildTarget, options.buildTarget,
context context
); );
const viteConfigPath = normalizeViteConfigFilePath(
// Merge the options from the build and dev-serve targets. context.root,
// The latter takes precedence. projectRoot,
const mergedOptions = { buildTargetOptions.configFile
...buildTargetOptions, );
...options, const extraArgs = await getExtraArgs(options);
}; const resolved = await loadConfigFromFile(
// Add the server specific configuration.
const serverConfig: InlineConfig = mergeConfig(
getViteSharedConfig(mergedOptions, options.clearScreen, context),
{ {
build: getViteBuildOptions(mergedOptions, context), mode: extraArgs?.mode ?? 'production',
server: await getViteServerOptions(mergedOptions, context), command: 'build',
},
viteConfigPath
);
const serverConfig: InlineConfig = mergeConfig(
{
// This should not be needed as it's going to be set in vite.config.ts
// but leaving it here in case someone did not migrate correctly
root: resolved.config.root ?? root,
configFile: viteConfigPath,
},
{
server: {
...(await getViteServerOptions(options, context)),
...extraArgs,
},
...extraArgs,
} }
); );
@ -90,3 +110,18 @@ async function runViteDevServer(server: ViteDevServer): Promise<void> {
} }
export default viteDevServerExecutor; export default viteDevServerExecutor;
async function getExtraArgs(
options: ViteDevServerExecutorOptions
): Promise<InlineConfig> {
// support passing extra args to vite cli
const schema = await import('./schema.json');
const extraArgs = {};
for (const key of Object.keys(options)) {
if (!schema.properties[key]) {
extraArgs[key] = options[key];
}
}
return extraArgs as InlineConfig;
}

View File

@ -2,14 +2,4 @@ export interface ViteDevServerExecutorOptions {
buildTarget: string; buildTarget: string;
buildLibsFromSource?: boolean; buildLibsFromSource?: boolean;
proxyConfig?: string; proxyConfig?: string;
port?: number;
host?: string | boolean;
https?: boolean | Json;
hmr?: boolean;
open?: string | boolean;
cors?: boolean;
logLevel?: 'info' | 'warn' | 'error' | 'silent';
mode?: string;
clearScreen?: boolean;
force?: boolean;
} }

View File

@ -30,69 +30,6 @@
"type": "string", "type": "string",
"description": "Path to the proxy configuration file.", "description": "Path to the proxy configuration file.",
"x-completion-type": "file" "x-completion-type": "file"
},
"port": {
"type": "number",
"description": "Port to listen on.",
"x-priority": "important"
},
"host": {
"description": "Specify which IP addresses the server should listen on.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"https": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "object"
}
],
"description": "Serve using HTTPS. https://vitejs.dev/config/server-options.html#server-https"
},
"hmr": {
"description": "Enable hot module replacement. For more options, use the 'hmr' option in the Vite configuration file.",
"type": "boolean"
},
"open": {
"description": "Automatically open the app in the browser on server start. When the value is a string, it will be used as the URL's pathname.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"cors": {
"description": "Configure CORS for the dev server.",
"type": "boolean"
},
"logLevel": {
"type": "string",
"description": "Adjust console output verbosity.",
"enum": ["info", "warn", "error", "silent"]
},
"mode": {
"type": "string",
"description": "Mode to run the server in."
},
"clearScreen": {
"description": "Set to false to prevent Vite from clearing the terminal screen when logging certain messages.",
"type": "boolean"
},
"force": {
"description": "Force the optimizer to ignore the cache and re-bundle",
"type": "boolean"
} }
}, },
"definitions": {}, "definitions": {},

View File

@ -1,27 +1,30 @@
import { ExecutorContext, parseTargetString, runExecutor } from '@nx/devkit'; import {
ExecutorContext,
joinPathFragments,
offsetFromRoot,
parseTargetString,
runExecutor,
} from '@nx/devkit';
import type { InlineConfig, PreviewServer } from 'vite'; import type { InlineConfig, PreviewServer } from 'vite';
import { import {
getNxTargetOptions, getNxTargetOptions,
getViteBuildOptions,
getVitePreviewOptions, getVitePreviewOptions,
getViteSharedConfig, normalizeViteConfigFilePath,
} from '../../utils/options-utils'; } from '../../utils/options-utils';
import { ViteBuildExecutorOptions } from '../build/schema'; import { ViteBuildExecutorOptions } from '../build/schema';
import { VitePreviewServerExecutorOptions } from './schema'; import { VitePreviewServerExecutorOptions } from './schema';
import { relative } from 'path';
interface CustomBuildTargetOptions {
outputPath: string;
}
export async function* vitePreviewServerExecutor( export async function* vitePreviewServerExecutor(
options: VitePreviewServerExecutorOptions, options: VitePreviewServerExecutorOptions,
context: ExecutorContext context: ExecutorContext
) { ) {
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future. // Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { mergeConfig, preview } = await (Function( const { mergeConfig, preview, loadConfigFromFile } = await (Function(
'return import("vite")' 'return import("vite")'
)() as Promise<typeof import('vite')>); )() as Promise<typeof import('vite')>);
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const target = parseTargetString(options.buildTarget, context); const target = parseTargetString(options.buildTarget, context);
const targetConfiguration = const targetConfiguration =
context.projectsConfigurations.projects[target.project]?.targets[ context.projectsConfigurations.projects[target.project]?.targets[
@ -36,35 +39,63 @@ export async function* vitePreviewServerExecutor(
targetConfiguration.executor !== '@nrwl/vite:build'; targetConfiguration.executor !== '@nrwl/vite:build';
// Retrieve the option for the configured buildTarget. // Retrieve the option for the configured buildTarget.
const buildTargetOptions: const buildTargetOptions: ViteBuildExecutorOptions = getNxTargetOptions(
| ViteBuildExecutorOptions
| CustomBuildTargetOptions = getNxTargetOptions(
options.buildTarget, options.buildTarget,
context context
); );
const viteConfigPath = normalizeViteConfigFilePath(
context.root,
projectRoot,
buildTargetOptions.configFile
);
const extraArgs = await getExtraArgs(options);
const resolved = await loadConfigFromFile(
{
mode: extraArgs?.mode ?? 'production',
command: 'build',
},
viteConfigPath
);
const outputPath = options.staticFilePath ?? buildTargetOptions.outputPath; const outDir =
options.staticFilePath ??
joinPathFragments(
offsetFromRoot(projectRoot),
buildTargetOptions.outputPath
) ??
resolved?.config?.build?.outDir;
if (!outputPath) { if (!outDir) {
throw new Error( throw new Error(
`Could not infer the "outputPath". It should either be a property of the "${options.buildTarget}" buildTarget or provided explicitly as a "staticFilePath" option.` `Could not infer the "outputPath" or "outDir". It should be set in your vite.config.ts, or as a property of the "${options.buildTarget}" buildTarget or provided explicitly as a "staticFilePath" option.`
); );
} }
const root =
projectRoot === '.'
? process.cwd()
: relative(context.cwd, joinPathFragments(context.root, projectRoot));
// Merge the options from the build and preview-serve targets. // Merge the options from the build and preview-serve targets.
// The latter takes precedence. // The latter takes precedence.
const mergedOptions = { const mergedOptions = {
...{ watch: {} }, ...{ watch: {} },
build: {
outDir,
},
...(isCustomBuildTarget ? {} : buildTargetOptions), ...(isCustomBuildTarget ? {} : buildTargetOptions),
...options, ...extraArgs,
outputPath,
}; };
// Retrieve the server configuration. // Retrieve the server configuration.
const serverConfig: InlineConfig = mergeConfig( const serverConfig: InlineConfig = mergeConfig(
getViteSharedConfig(mergedOptions, options.clearScreen, context),
{ {
build: getViteBuildOptions(mergedOptions, context), // This should not be needed as it's going to be set in vite.config.ts
// but leaving it here in case someone did not migrate correctly
root: resolved.config.root ?? root,
configFile: viteConfigPath,
},
{
...mergedOptions,
preview: getVitePreviewOptions(mergedOptions, context), preview: getVitePreviewOptions(mergedOptions, context),
} }
); );
@ -146,3 +177,18 @@ function closeServer(server?: PreviewServer): Promise<void> {
} }
export default vitePreviewServerExecutor; export default vitePreviewServerExecutor;
async function getExtraArgs(
options: VitePreviewServerExecutorOptions
): Promise<InlineConfig> {
// support passing extra args to vite cli
const schema = await import('./schema.json');
const extraArgs = {};
for (const key of Object.keys(options)) {
if (!schema.properties[key]) {
extraArgs[key] = options[key];
}
}
return extraArgs as InlineConfig;
}

View File

@ -1,12 +1,5 @@
export interface VitePreviewServerExecutorOptions { export interface VitePreviewServerExecutorOptions {
buildTarget: string; buildTarget: string;
proxyConfig?: string; proxyConfig?: string;
port?: number;
host?: string | boolean;
https?: boolean | Json;
open?: string | boolean;
logLevel?: 'info' | 'warn' | 'error' | 'silent';
mode?: string;
clearScreen?: boolean;
staticFilePath?: string; staticFilePath?: string;
} }

View File

@ -25,56 +25,6 @@
"description": "Path to the proxy configuration file.", "description": "Path to the proxy configuration file.",
"x-completion-type": "file" "x-completion-type": "file"
}, },
"port": {
"type": "number",
"description": "Port to listen on."
},
"host": {
"description": "Specify which IP addresses the server should listen on.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"https": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "object"
}
],
"description": "Serve using HTTPS. https://vitejs.dev/config/server-options.html#server-https"
},
"open": {
"description": "Automatically open the app in the browser on server start. When the value is a string, it will be used as the URL's pathname.",
"oneOf": [
{
"type": "boolean"
},
{
"type": "string"
}
]
},
"logLevel": {
"type": "string",
"description": "Adjust console output verbosity.",
"enum": ["info", "warn", "error", "silent"]
},
"mode": {
"type": "string",
"description": "Mode to run the server in."
},
"clearScreen": {
"description": "Set to false to prevent Vite from clearing the terminal screen when logging certain messages.",
"type": "boolean"
},
"staticFilePath": { "staticFilePath": {
"type": "string", "type": "string",
"description": "Path where the build artifacts are located. If not provided then it will be infered from the buildTarget executor options as outputPath", "description": "Path where the build artifacts are located. If not provided then it will be infered from the buildTarget executor options as outputPath",

View File

@ -0,0 +1,36 @@
import type { File, Reporter } from 'vitest';
export class NxReporter implements Reporter {
deferred: {
promise: Promise<boolean>;
resolve: (val: boolean) => void;
};
constructor(private watch: boolean) {
this.setupDeferred();
}
async *[Symbol.asyncIterator]() {
do {
const hasErrors = await this.deferred.promise;
yield { hasErrors };
this.setupDeferred();
} while (this.watch);
}
private setupDeferred() {
let resolve: (val: boolean) => void;
this.deferred = {
promise: new Promise((res) => {
resolve = res;
}),
resolve,
};
}
onFinished(files: File[], errors?: unknown[]) {
const hasErrors =
files.some((f) => f.result?.state === 'fail') || errors?.length > 0;
this.deferred.resolve(hasErrors);
}
}

View File

@ -0,0 +1,88 @@
import {
ExecutorContext,
joinPathFragments,
logger,
stripIndents,
} from '@nx/devkit';
import { VitestExecutorOptions } from '../schema';
import { normalizeViteConfigFilePath } from '../../../utils/options-utils';
import { relative } from 'path';
import { NxReporter } from './nx-reporter';
export async function getOptions(
options: VitestExecutorOptions,
context: ExecutorContext,
projectRoot: string,
extraArgs: Record<string, any>
) {
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { loadConfigFromFile, mergeConfig } = await (Function(
'return import("vite")'
)() as Promise<typeof import('vite')>);
const viteConfigPath = normalizeViteConfigFilePath(
context.root,
projectRoot,
options.configFile
);
if (!viteConfigPath) {
throw new Error(
stripIndents`
Unable to load test config from config file ${viteConfigPath}.
Please make sure that vitest is configured correctly,
or use the @nx/vite:vitest generator to configure it for you.
You can read more here: https://nx.dev/nx-api/vite/generators/vitest
`
);
}
const resolved = await loadConfigFromFile(
{
mode: extraArgs?.mode ?? 'production',
command: 'serve',
},
viteConfigPath
);
if (!viteConfigPath || !resolved?.config?.['test']) {
logger.warn(stripIndents`Unable to load test config from config file ${
resolved?.path ?? viteConfigPath
}
Some settings may not be applied as expected.
You can manually set the config in the project, ${
context.projectName
}, configuration.
`);
}
const root =
projectRoot === '.'
? process.cwd()
: relative(context.cwd, joinPathFragments(context.root, projectRoot));
const settings = {
...extraArgs,
// This should not be needed as it's going to be set in vite.config.ts
// but leaving it here in case someone did not migrate correctly
root: resolved.config.root ?? root,
configFile: viteConfigPath,
};
return mergeConfig(resolved?.config?.['test'] ?? {}, settings);
}
export async function getExtraArgs(
options: VitestExecutorOptions
): Promise<Record<string, any>> {
// support passing extra args to vite cli
const schema = await import('../schema.json');
const extraArgs: Record<string, any> = {};
for (const key of Object.keys(options)) {
if (!schema.properties[key]) {
extraArgs[key] = options[key];
}
}
return extraArgs;
}

View File

@ -1,12 +1,6 @@
export interface VitestExecutorOptions { export interface VitestExecutorOptions {
config?: string; configFile?: string;
passWithNoTests?: boolean;
testNamePattern?: string;
mode?: 'test' | 'benchmark' | 'typecheck';
reporters?: string[];
watch?: boolean;
update?: boolean;
reportsDirectory?: string; reportsDirectory?: string;
coverage?: boolean;
testFiles?: string[]; testFiles?: string[];
watch?: boolean;
} }

View File

@ -6,52 +6,12 @@
"description": "Test using Vitest.", "description": "Test using Vitest.",
"type": "object", "type": "object",
"properties": { "properties": {
"config": { "configFile": {
"type": "string", "type": "string",
"description": "The path to the local vitest config", "description": "The path to the local vitest config",
"x-completion-type": "file", "x-completion-type": "file",
"x-completion-glob": "@(vitest|vite).config@(.js|.ts)" "x-completion-glob": "@(vitest|vite).config@(.js|.ts)",
}, "aliases": ["config"]
"passWithNoTests": {
"type": "boolean",
"default": true,
"description": "Pass the test even if no tests are found"
},
"testNamePattern": {
"type": "string",
"description": "Run tests with full names matching the pattern"
},
"mode": {
"type": "string",
"enum": ["test", "benchmark", "typecheck"],
"default": "test",
"description": "The mode that vitest will run on",
"x-priority": "important"
},
"watch": {
"type": "boolean",
"default": false,
"description": "Enable watch mode"
},
"reporters": {
"type": "array",
"items": {
"type": "string"
},
"description": "An array of reporters to pass to vitest"
},
"update": {
"type": "boolean",
"default": false,
"alias": "u",
"description": "Update snapshots",
"x-priority": "important"
},
"coverage": {
"type": "boolean",
"default": false,
"description": "Enable coverage report",
"x-priority": "important"
}, },
"reportsDirectory": { "reportsDirectory": {
"type": "string", "type": "string",
@ -61,6 +21,10 @@
"aliases": ["testFile"], "aliases": ["testFile"],
"type": "array", "type": "array",
"items": { "type": "string" } "items": { "type": "string" }
},
"watch": {
"description": "Watch files for changes and rerun tests related to changed files.",
"type": "boolean"
} }
}, },
"required": [], "required": [],

View File

@ -1,51 +1,9 @@
import { import { ExecutorContext, workspaceRoot } from '@nx/devkit';
ExecutorContext,
joinPathFragments,
logger,
readJsonFile,
stripIndents,
workspaceRoot,
} from '@nx/devkit';
import type { CoverageOptions, File, Reporter } from 'vitest';
import { VitestExecutorOptions } from './schema'; import { VitestExecutorOptions } from './schema';
import { join, relative, resolve } from 'path'; import { resolve } from 'path';
import { existsSync } from 'fs';
import { registerTsConfigPaths } from '@nx/js/src/internal'; import { registerTsConfigPaths } from '@nx/js/src/internal';
import { NxReporter } from './lib/nx-reporter';
class NxReporter implements Reporter { import { getExtraArgs, getOptions } from './lib/utils';
deferred: {
promise: Promise<boolean>;
resolve: (val: boolean) => void;
};
constructor(private watch: boolean) {
this.setupDeferred();
}
async *[Symbol.asyncIterator]() {
do {
const hasErrors = await this.deferred.promise;
yield { hasErrors };
this.setupDeferred();
} while (this.watch);
}
private setupDeferred() {
let resolve: (val: boolean) => void;
this.deferred = {
promise: new Promise((res) => {
resolve = res;
}),
resolve,
};
}
onFinished(files: File[], errors?: unknown[]) {
const hasErrors =
files.some((f) => f.result?.state === 'fail') || errors?.length > 0;
this.deferred.resolve(hasErrors);
}
}
export async function* vitestExecutor( export async function* vitestExecutor(
options: VitestExecutorOptions, options: VitestExecutorOptions,
@ -53,18 +11,32 @@ export async function* vitestExecutor(
) { ) {
const projectRoot = const projectRoot =
context.projectsConfigurations.projects[context.projectName].root; context.projectsConfigurations.projects[context.projectName].root;
registerTsConfigPaths(resolve(workspaceRoot, projectRoot, 'tsconfig.json')); registerTsConfigPaths(resolve(workspaceRoot, projectRoot, 'tsconfig.json'));
const { startVitest } = await (Function( const { startVitest } = await (Function(
'return import("vitest/node")' 'return import("vitest/node")'
)() as Promise<typeof import('vitest/node')>); )() as Promise<typeof import('vitest/node')>);
const nxReporter = new NxReporter(options.watch); const extraArgs = await getExtraArgs(options);
const settings = await getSettings(options, context, projectRoot); const resolvedOptions =
settings.reporters.push(nxReporter); (await getOptions(options, context, projectRoot, extraArgs)) ?? {};
const nxReporter = new NxReporter(resolvedOptions['watch']);
if (resolvedOptions['reporters'] === undefined) {
resolvedOptions['reporters'] = [];
} else if (typeof resolvedOptions['reporters'] === 'string') {
resolvedOptions['reporters'] = [resolvedOptions['reporters']];
}
resolvedOptions['reporters'].push(nxReporter);
const cliFilters = options.testFiles ?? []; const cliFilters = options.testFiles ?? [];
const ctx = await startVitest(options.mode, cliFilters, settings); const ctx = await startVitest(
resolvedOptions['mode'] ?? 'test',
cliFilters,
resolvedOptions
);
let hasErrors = false; let hasErrors = false;
@ -77,7 +49,7 @@ export async function* vitestExecutor(
} }
}; };
if (options.watch) { if (resolvedOptions['watch'] === true) {
process.on('SIGINT', processExit); process.on('SIGINT', processExit);
process.on('SIGTERM', processExit); process.on('SIGTERM', processExit);
process.on('exit', processExit); process.on('exit', processExit);
@ -94,111 +66,4 @@ export async function* vitestExecutor(
}; };
} }
async function getSettings(
options: VitestExecutorOptions,
context: ExecutorContext,
projectRoot: string
) {
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { loadConfigFromFile } = await (Function(
'return import("vite")'
)() as Promise<typeof import('vite')>);
const packageJsonPath = join(workspaceRoot, 'package.json');
const packageJson = existsSync(packageJsonPath)
? readJsonFile(packageJsonPath)
: undefined;
let provider: 'v8' | 'istanbul' | 'custom';
if (
packageJson?.dependencies?.['@vitest/coverage-istanbul'] ||
packageJson?.devDependencies?.['@vitest/coverage-istanbul']
) {
provider = 'istanbul';
} else {
provider = 'v8';
}
const offset = relative(workspaceRoot, context.cwd);
// if reportsDirectory is not provided vitest will remove all files in the project root
// when coverage is enabled in the vite.config.ts
const coverage: CoverageOptions = options.reportsDirectory
? {
enabled: options.coverage,
reportsDirectory: options.reportsDirectory,
provider,
}
: ({} as CoverageOptions);
const viteConfigPath = options.config
? options.config // config is expected to be from the workspace root
: findViteConfig(joinPathFragments(context.root, projectRoot));
if (!viteConfigPath) {
throw new Error(
stripIndents`
Unable to load test config from config file ${viteConfigPath}.
Please make sure that vitest is configured correctly,
or use the @nx/vite:vitest generator to configure it for you.
You can read more here: https://nx.dev/nx-api/vite/generators/vitest
`
);
}
const resolvedProjectRoot = resolve(workspaceRoot, projectRoot);
const resolvedViteConfigPath = resolve(
workspaceRoot,
projectRoot,
relative(resolvedProjectRoot, viteConfigPath)
);
const resolved = await loadConfigFromFile(
{
mode: options.mode,
command: 'serve',
},
resolvedViteConfigPath,
resolvedProjectRoot
);
if (!viteConfigPath || !resolved?.config?.['test']) {
logger.warn(stripIndents`Unable to load test config from config file ${
resolved?.path ?? viteConfigPath
}
Some settings may not be applied as expected.
You can manually set the config in the project, ${
context.projectName
}, configuration.
`);
}
const settings = {
...options,
// when running nx from the project root, the root will get appended to the cwd.
// creating an invalid path and no tests will be found.
// instead if we are not at the root, let the cwd be root.
root: offset === '' ? resolvedProjectRoot : workspaceRoot,
config: resolvedViteConfigPath,
reporters: [
...(options.reporters ?? []),
...((resolved?.config?.['test']?.reporters as string[]) ?? []),
'default',
] as (string | Reporter)[],
coverage: { ...coverage, ...resolved?.config?.['test']?.coverage },
};
return settings;
}
function findViteConfig(projectRootFullPath: string): string {
const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
for (const ext of allowsExt) {
if (
existsSync(joinPathFragments(projectRootFullPath, `vite.config.${ext}`))
) {
return joinPathFragments(projectRootFullPath, `vite.config.${ext}`);
}
}
}
export default vitestExecutor; export default vitestExecutor;

View File

@ -9,7 +9,8 @@ import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: '../../node_modules/.vite/react-lib-nonb-jest', root: __dirname,
cacheDir: '../../node_modules/.vite/libs/react-lib-nonb-jest',
plugins: [ plugins: [
react(), react(),
@ -29,6 +30,7 @@ export default defineConfig({
// Configuration for building your library. // Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode // See: https://vitejs.dev/guide/build.html#library-mode
build: { build: {
outDir: '../../dist/libs/react-lib-nonb-jest',
lib: { lib: {
// Could also be a dictionary or array of multiple entry points. // Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts', entry: 'src/index.ts',
@ -56,7 +58,8 @@ import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: '../../node_modules/.vite/react-lib-nonb-jest', root: __dirname,
cacheDir: '../../node_modules/.vite/libs/react-lib-nonb-jest',
plugins: [ plugins: [
react(), react(),
@ -76,6 +79,7 @@ export default defineConfig({
// Configuration for building your library. // Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode // See: https://vitejs.dev/guide/build.html#library-mode
build: { build: {
outDir: '../../dist/libs/react-lib-nonb-jest',
lib: { lib: {
// Could also be a dictionary or array of multiple entry points. // Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts', entry: 'src/index.ts',
@ -98,6 +102,11 @@ export default defineConfig({
}, },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../../coverage/libs/react-lib-nonb-jest',
provider: 'v8',
},
}, },
}); });
" "
@ -143,8 +152,9 @@ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
// Configuration for building your library. // Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode // See: https://vitejs.dev/guide/build.html#library-mode
cacheDir: '../../node_modules/.vite/react-lib-nonb-vitest', cacheDir: '../../node_modules/.vite/libs/react-lib-nonb-vitest',
build: { build: {
outDir: '../../dist/libs/react-lib-nonb-vitest',
lib: { lib: {
// Could also be a dictionary or array of multiple entry points. // Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts', entry: 'src/index.ts',
@ -174,6 +184,10 @@ export default defineConfig({
cache: { dir: '../../node_modules/.vitest' }, cache: { dir: '../../node_modules/.vitest' },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../../coverage/libs/react-lib-nonb-vitest',
provider: 'v8',
},
}, },
}); });
" "
@ -271,7 +285,8 @@ import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: '../../node_modules/.vite/my-test-react-app', root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-test-react-app',
server: { server: {
port: 4200, port: 4200,
@ -289,6 +304,10 @@ export default defineConfig({
// worker: { // worker: {
// plugins: [ nxViteTsPaths() ], // plugins: [ nxViteTsPaths() ],
// }, // },
build: {
outDir: '../../dist/apps/my-test-react-app',
},
}); });
" "
`; `;
@ -409,7 +428,8 @@ import { defineConfig } from 'vite';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: '../../node_modules/.vite/my-test-web-app', root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-test-web-app',
server: { server: {
port: 4200, port: 4200,
@ -427,6 +447,10 @@ export default defineConfig({
// worker: { // worker: {
// plugins: [ nxViteTsPaths() ], // plugins: [ nxViteTsPaths() ],
// }, // },
build: {
outDir: '../../dist/apps/my-test-web-app',
},
}); });
" "
`; `;
@ -534,7 +558,8 @@ import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: '../../node_modules/.vite/my-test-react-app', root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-test-react-app',
server: { server: {
port: 4200, port: 4200,
@ -553,6 +578,10 @@ export default defineConfig({
// plugins: [ nxViteTsPaths() ], // plugins: [ nxViteTsPaths() ],
// }, // },
build: {
outDir: '../../dist/apps/my-test-react-app',
},
test: { test: {
globals: true, globals: true,
cache: { cache: {
@ -560,6 +589,11 @@ export default defineConfig({
}, },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../../coverage/apps/my-test-react-app',
provider: 'v8',
},
}, },
}); });
" "

View File

@ -33,10 +33,11 @@ export async function viteConfigurationGenerator(
) { ) {
const tasks: GeneratorCallback[] = []; const tasks: GeneratorCallback[] = [];
const { targets, projectType, root } = readProjectConfiguration( const {
tree, targets,
schema.project projectType,
); root: projectRoot,
} = readProjectConfiguration(tree, schema.project);
let buildTargetName = 'build'; let buildTargetName = 'build';
let serveTargetName = 'serve'; let serveTargetName = 'serve';
let testTargetName = 'test'; let testTargetName = 'test';
@ -147,7 +148,7 @@ export async function viteConfigurationGenerator(
deleteWebpackConfig( deleteWebpackConfig(
tree, tree,
root, projectRoot,
targets?.[buildTargetName]?.options?.webpackConfig targets?.[buildTargetName]?.options?.webpackConfig
); );
@ -159,7 +160,7 @@ export async function viteConfigurationGenerator(
includeLib: schema.includeLib, includeLib: schema.includeLib,
compiler: schema.compiler, compiler: schema.compiler,
testEnvironment: schema.testEnvironment, testEnvironment: schema.testEnvironment,
rootProject: root === '.', rootProject: projectRoot === '.',
}); });
tasks.push(initTask); tasks.push(initTask);
@ -178,24 +179,28 @@ export async function viteConfigurationGenerator(
if (projectType === 'library') { if (projectType === 'library') {
// update tsconfig.lib.json to include vite/client // update tsconfig.lib.json to include vite/client
updateJson(tree, joinPathFragments(root, 'tsconfig.lib.json'), (json) => { updateJson(
if (!json.compilerOptions) { tree,
json.compilerOptions = {}; joinPathFragments(projectRoot, 'tsconfig.lib.json'),
(json) => {
if (!json.compilerOptions) {
json.compilerOptions = {};
}
if (!json.compilerOptions.types) {
json.compilerOptions.types = [];
}
if (!json.compilerOptions.types.includes('vite/client')) {
return {
...json,
compilerOptions: {
...json.compilerOptions,
types: [...json.compilerOptions.types, 'vite/client'],
},
};
}
return json;
} }
if (!json.compilerOptions.types) { );
json.compilerOptions.types = [];
}
if (!json.compilerOptions.types.includes('vite/client')) {
return {
...json,
compilerOptions: {
...json.compilerOptions,
types: [...json.compilerOptions.types, 'vite/client'],
},
};
}
return json;
});
} }
if (!schema.newProject) { if (!schema.newProject) {

View File

@ -93,6 +93,10 @@ describe('@nx/vite:init', () => {
expect(vitestDefaults).toEqual({ expect(vitestDefaults).toEqual({
cache: true, cache: true,
inputs: ['default', '^production'], inputs: ['default', '^production'],
options: {
passWithNoTests: true,
reporters: ['default'],
},
}); });
}); });
}); });

View File

@ -34,23 +34,8 @@ function checkDependenciesInstalled(host: Tree, schema: InitGeneratorSchema) {
// base deps // base deps
devDependencies['@nx/vite'] = nxVersion; devDependencies['@nx/vite'] = nxVersion;
devDependencies['vite'] = viteVersion; devDependencies['vite'] = viteVersion;
devDependencies['vitest'] = vitestVersion;
// Do not install latest version if vitest already exists devDependencies['@vitest/ui'] = vitestVersion;
// because version 0.32 and newer versions break nuxt-vitest
// https://github.com/vitest-dev/vitest/issues/3540
// https://github.com/danielroe/nuxt-vitest/issues/213#issuecomment-1588728111
if (
!packageJson.dependencies['vitest'] &&
!packageJson.devDependencies['vitest']
) {
devDependencies['vitest'] = vitestVersion;
}
if (
!packageJson.dependencies['@vitest/ui'] &&
!packageJson.devDependencies['@vitest/ui']
) {
devDependencies['@vitest/ui'] = vitestVersion;
}
if (schema.testEnvironment === 'jsdom') { if (schema.testEnvironment === 'jsdom') {
devDependencies['jsdom'] = jsdomVersion; devDependencies['jsdom'] = jsdomVersion;
@ -93,7 +78,7 @@ function moveToDevDependencies(tree: Tree) {
}); });
} }
export function createVitestConfig(tree: Tree) { export function updateNxJsonSettings(tree: Tree) {
const nxJson = readNxJson(tree); const nxJson = readNxJson(tree);
const productionFileSet = nxJson.namedInputs?.production; const productionFileSet = nxJson.namedInputs?.production;
@ -113,13 +98,26 @@ export function createVitestConfig(tree: Tree) {
'default', 'default',
productionFileSet ? '^production' : '^default', productionFileSet ? '^production' : '^default',
]; ];
nxJson.targetDefaults['@nx/vite:test'].options ??= {
passWithNoTests: true,
reporters: ['default'],
};
nxJson.targetDefaults['@nx/vite:build'] ??= {};
nxJson.targetDefaults['@nx/vite:build'].options ??= {
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
};
updateNxJson(tree, nxJson); updateNxJson(tree, nxJson);
} }
export async function initGenerator(tree: Tree, schema: InitGeneratorSchema) { export async function initGenerator(tree: Tree, schema: InitGeneratorSchema) {
moveToDevDependencies(tree); moveToDevDependencies(tree);
createVitestConfig(tree); updateNxJsonSettings(tree);
const tasks = []; const tasks = [];
tasks.push( tasks.push(

View File

@ -7,7 +7,8 @@ import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: '../../node_modules/.vite/my-test-react-app', root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-test-react-app',
plugins: [react(), nxViteTsPaths()], plugins: [react(), nxViteTsPaths()],
@ -27,6 +28,10 @@ export default defineConfig({
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../../coverage/apps/my-test-react-app',
provider: 'v8',
},
}, },
}); });
" "
@ -39,7 +44,8 @@ import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: '../../node_modules/.vite/my-test-react-app', root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-test-react-app',
plugins: [react(), nxViteTsPaths()], plugins: [react(), nxViteTsPaths()],
@ -55,6 +61,11 @@ export default defineConfig({
}, },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../../coverage/apps/my-test-react-app',
provider: 'v8',
},
}, },
}); });
" "
@ -67,7 +78,8 @@ import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
cacheDir: '../../node_modules/.vite/react-lib-nonb-jest', root: __dirname,
cacheDir: '../../node_modules/.vite/libs/react-lib-nonb-jest',
plugins: [react(), nxViteTsPaths()], plugins: [react(), nxViteTsPaths()],
@ -83,6 +95,11 @@ export default defineConfig({
}, },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../../coverage/libs/react-lib-nonb-jest',
provider: 'v8',
},
}, },
}); });
" "

View File

@ -6,6 +6,7 @@
}, },
"include": [ "include": [
"vite.config.ts", "vite.config.ts",
"vitest.config.ts",
"src/**/*.test.ts", "src/**/*.test.ts",
"src/**/*.spec.ts", "src/**/*.spec.ts",
"src/**/*.test.tsx", "src/**/*.test.tsx",

View File

@ -65,6 +65,7 @@ export async function vitestGenerator(
], ],
imports: [`import react from '@vitejs/plugin-react'`], imports: [`import react from '@vitejs/plugin-react'`],
plugins: ['react()'], plugins: ['react()'],
coverageProvider: schema.coverageProvider,
}, },
true true
); );

View File

@ -55,7 +55,6 @@ describe('vitest generator', () => {
{ {
"executor": "@nx/vite:test", "executor": "@nx/vite:test",
"options": { "options": {
"passWithNoTests": true,
"reportsDirectory": "../../coverage/apps/my-test-react-app", "reportsDirectory": "../../coverage/apps/my-test-react-app",
}, },
"outputs": [ "outputs": [
@ -102,6 +101,7 @@ describe('vitest generator', () => {
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"include": [ "include": [
"vite.config.ts", "vite.config.ts",
"vitest.config.ts",
"src/**/*.test.ts", "src/**/*.test.ts",
"src/**/*.spec.ts", "src/**/*.spec.ts",
"src/**/*.test.tsx", "src/**/*.test.tsx",

View File

@ -0,0 +1,156 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`change-vite-ts-paths-plugin migration should add build outDir to vite.config.ts 1`] = `
"/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
root: __dirname,
build: {
outDir: '../../dist/apps/demo',
},
cacheDir: '../../node_modules/.vite/demo',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [
react(),
viteTsConfigPaths({
root: '../../',
}),
],
// Uncomment this if you are using workers.
// worker: {
// plugins: [
// viteTsConfigPaths({
// root: '../../',
// }),
// ],
// },
test: {
coverage: {
reportsDirectory: '../../coverage/apps/demo',
provider: 'v8',
},
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
"
`;
exports[`change-vite-ts-paths-plugin migration should add build outDir to vite.config.ts if build exists 1`] = `
"/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/demo2',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [
react(),
viteTsConfigPaths({
root: '../../',
}),
],
build: {
outDir: '../dist/demo2',
someProperty: 'someValue',
},
test: {
coverage: {
reportsDirectory: '../coverage/demo2',
provider: 'v8',
},
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
"
`;
exports[`change-vite-ts-paths-plugin migration should add file replacements to vite.config.ts 1`] = `
"/// <reference types="vitest" />
import replaceFiles from '@nx/vite/plugins/rollup-replace-files.plugin';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/demo3',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [
replaceFiles([
{
replace: 'demo3/src/environments/environment.ts',
with: 'demo3/src/environments/environment.prod.ts',
},
]),
react(),
viteTsConfigPaths({
root: '../../',
}),
],
build: {
outDir: '../dist/demo3',
someProperty: 'someValue',
},
test: {
coverage: {
reportsDirectory: '../coverage/demo3',
provider: 'v8',
},
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
"
`;

View File

@ -0,0 +1,74 @@
import { ChangeType, applyChangesToString } from '@nx/devkit';
import { FileReplacement } from '../../../../plugins/rollup-replace-files.plugin';
import { tsquery } from '@phenomnomnominal/tsquery';
export function addFileReplacements(
configContents: string,
fileReplacements: FileReplacement[]
): string {
const pluginsObject = tsquery.query(
configContents,
`PropertyAssignment:has(Identifier[name="plugins"])`
)?.[0];
const replaceFilesPlugin = tsquery.query(
configContents,
`PropertyAssignment:has(Identifier[name="plugins"]) CallExpression:has(Identifier[name="replaceFiles"])`
)?.[0];
const firstImportDeclaration = tsquery.query(
configContents,
'ImportDeclaration'
)?.[0];
if (pluginsObject) {
if (replaceFilesPlugin) {
return configContents;
} else {
return applyChangesToString(configContents, [
{
type: ChangeType.Insert,
index: pluginsObject.getStart() + `plugins: [`.length + 1,
text: `replaceFiles(${JSON.stringify(fileReplacements)}),`,
},
firstImportDeclaration
? {
type: ChangeType.Insert,
index: firstImportDeclaration.getStart(),
text: `import replaceFiles from '@nx/vite/plugins/rollup-replace-files.plugin';\n`,
}
: {
type: ChangeType.Insert,
index: 0,
text: `import replaceFiles from '@nx/vite/plugins/rollup-replace-files.plugin';\n`,
},
]);
}
} else {
const foundDefineConfig = tsquery.query(
configContents,
'CallExpression:has(Identifier[name="defineConfig"])'
)?.[0];
if (!foundDefineConfig) {
return;
}
return applyChangesToString(configContents, [
{
type: ChangeType.Insert,
index: foundDefineConfig.getStart() + 14,
text: `plugins: [replaceFiles(${JSON.stringify(fileReplacements)})],`,
},
firstImportDeclaration
? {
type: ChangeType.Insert,
index: firstImportDeclaration.getStart(),
text: `import replaceFiles from '@nx/vite/plugins/rollup-replace-files.plugin';`,
}
: {
type: ChangeType.Insert,
index: 0,
text: `import replaceFiles from '@nx/vite/plugins/rollup-replace-files.plugin';`,
},
]);
}
}

View File

@ -0,0 +1,149 @@
import {
ChangeType,
ProjectConfiguration,
Tree,
applyChangesToString,
joinPathFragments,
logger,
offsetFromRoot,
updateProjectConfiguration,
} from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import ts = require('typescript');
export function updateBuildOutDirAndRoot(
options: Record<string, any>,
configContents: string,
projectConfig: ProjectConfiguration,
targetName: string,
tree: Tree,
projectName: string
): string {
const foundDefineConfig = tsquery.query(
configContents,
'CallExpression:has(Identifier[name="defineConfig"])'
)?.[0];
if (!foundDefineConfig) {
logger.warn(`
Could not find defineConfig in your vite.config file.
Please add the build.outDir and root options to your vite.config file.
`);
}
configContents = fixBuild(
options,
configContents,
projectConfig,
targetName,
tree,
projectName,
foundDefineConfig
);
configContents = addRoot(configContents, foundDefineConfig);
return configContents;
}
function fixBuild(
options: Record<string, any>,
configContents: string,
projectConfig: ProjectConfiguration,
targetName: string,
tree: Tree,
projectName: string,
foundDefineConfig?: ts.Node
) {
let outputPath = '';
// In vite.config.ts, we want to keep the path relative to workspace root
if (options.outputPath) {
outputPath = joinPathFragments(
offsetFromRoot(projectConfig.root),
options.outputPath
);
} else {
outputPath = joinPathFragments(
offsetFromRoot(projectConfig.root),
'dist',
projectConfig.root
);
}
// In project.json, we want to keep the path starting from workspace root
projectConfig.targets[targetName].options.outputPath = options.outputPath
? options.outputPath
: joinPathFragments('dist', projectConfig.root);
updateProjectConfiguration(tree, projectName, projectConfig);
const buildObject = tsquery.query(
configContents,
`PropertyAssignment:has(Identifier[name="build"])`
)?.[0];
let buildOutDir: ts.Node[];
if (buildObject) {
buildOutDir = tsquery.query(
buildObject,
`PropertyAssignment:has(Identifier[name="outDir"])`
);
}
if (buildOutDir?.length > 0) {
return configContents;
} else if (buildObject) {
// has build, has no outDir
// so add outDir
return applyChangesToString(configContents, [
{
type: ChangeType.Insert,
index: buildObject.getStart() + `build: {`.length + 1,
text: `outDir: '${outputPath}',`,
},
]);
} else {
return addBuildProperty(configContents, outputPath, foundDefineConfig);
}
}
function addRoot(
configFileContents: string,
foundDefineConfig?: ts.Node
): string {
const rootOption = tsquery.query(
configFileContents,
`PropertyAssignment:has(Identifier[name="root"]) Identifier[name="__dirname"]`
)?.[0];
if (rootOption || !foundDefineConfig) {
return configFileContents;
} else {
return applyChangesToString(configFileContents, [
{
type: ChangeType.Insert,
index: foundDefineConfig.getStart() + 14,
text: `root: __dirname,`,
},
]);
}
}
function addBuildProperty(
configFileContents: string,
outputPath: string,
foundDefineConfig: ts.Node
): string {
if (foundDefineConfig) {
return applyChangesToString(configFileContents, [
{
type: ChangeType.Insert,
index: foundDefineConfig.getStart() + 14,
text: `build: {
outDir: '${outputPath}',
},`,
},
]);
} else {
return configFileContents;
}
}

View File

@ -0,0 +1,88 @@
import {
ChangeType,
ProjectConfiguration,
applyChangesToString,
joinPathFragments,
offsetFromRoot,
} from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import ts = require('typescript');
export function updateTestConfig(
configContents: string,
projectConfig: ProjectConfiguration
): string {
const testObject = tsquery.query(
configContents,
`PropertyAssignment:has(Identifier[name="test"])`
)?.[0];
let testCoverageDir: ts.Node;
let testCoverage: ts.Node;
let provider: ts.Node;
if (testObject) {
testCoverage = tsquery.query(
testObject,
`PropertyAssignment:has(Identifier[name="coverage"])`
)?.[0];
if (testCoverage) {
testCoverageDir = tsquery.query(
testCoverage,
`PropertyAssignment:has(Identifier[name="reportsDirectory"])`
)?.[0];
provider = tsquery.query(
testCoverage,
`PropertyAssignment:has(Identifier[name="provider"])`
)?.[0];
}
}
let coverageDir = '';
if (projectConfig.targets?.test?.options?.reportsDirectory) {
coverageDir = projectConfig.targets?.test?.options?.reportsDirectory;
} else {
coverageDir = joinPathFragments(
offsetFromRoot(projectConfig.root),
'coverage',
projectConfig.root
);
}
if (testCoverageDir) {
// Do nothing
} else if (testCoverage) {
// has test.coverage, has no reportsDirectory
// so add reportsDirectory
configContents = applyChangesToString(configContents, [
{
type: ChangeType.Insert,
index: testCoverage.getStart() + `coverage: {`.length + 1,
text: `reportsDirectory: '${coverageDir}',`,
},
]);
if (!provider) {
configContents = applyChangesToString(configContents, [
{
type: ChangeType.Insert,
index: testCoverage.getStart() + `coverage: {`.length + 1,
text: `provider: 'v8',`,
},
]);
}
} else if (testObject) {
configContents = applyChangesToString(configContents, [
{
type: ChangeType.Insert,
index: testObject.getStart() + `test: {`.length + 1,
text: `coverage: {
reportsDirectory: '${coverageDir}',
provider: 'v8',
},`,
},
]);
} else {
// has no test so do nothing
}
return configContents;
}

View File

@ -0,0 +1,11 @@
import { Tree, joinPathFragments } from '@nx/devkit';
export function findViteConfig(tree: Tree, searchRoot: string) {
const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
for (const ext of allowsExt) {
if (tree.exists(joinPathFragments(searchRoot, `vite.config.${ext}`))) {
return joinPathFragments(searchRoot, `vite.config.${ext}`);
}
}
}

View File

@ -0,0 +1,262 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
Tree,
addProjectConfiguration,
readProjectConfiguration,
} from '@nx/devkit';
import updateBuildDir from './update-vite-config';
describe('change-vite-ts-paths-plugin migration', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it('should add build outDir to vite.config.ts', async () => {
addProject1(tree, 'demo');
await updateBuildDir(tree);
expect(tree.read('apps/demo/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(
readProjectConfiguration(tree, 'demo').targets.build.options.outputPath
).toBe('dist/apps/demo');
});
it('should add build outDir to vite.config.ts if build exists', async () => {
addProject2(tree, 'demo2');
await updateBuildDir(tree);
expect(tree.read('demo2/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(
readProjectConfiguration(tree, 'demo2').targets.build.options.outputPath
).toBe('dist/demo2');
});
it('should add file replacements to vite.config.ts', async () => {
addProject3(tree, 'demo3');
await updateBuildDir(tree);
expect(tree.read('demo3/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(
readProjectConfiguration(tree, 'demo3').targets.build.options.outputPath
).toBe('dist/demo3');
});
});
function addProject1(tree: Tree, name: string) {
addProjectConfiguration(tree, name, {
root: `apps/${name}`,
sourceRoot: `apps/${name}/src`,
targets: {
build: {
executor: '@nx/vite:build',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
outputPath: `dist/apps/${name}`,
buildLibsFromSource: false,
},
configurations: {
development: {
mode: 'development',
},
production: {
mode: 'production',
},
},
},
},
});
tree.write(
`apps/${name}/vite.config.ts`,
`
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
cacheDir: '../../node_modules/.vite/${name}',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [
react(),
viteTsConfigPaths({
root: '../../'
})
],
// Uncomment this if you are using workers.
// worker: {
// plugins: [
// viteTsConfigPaths({
// root: '../../',
// }),
// ],
// },
test: {
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
`
);
}
function addProject2(tree: Tree, name: string) {
addProjectConfiguration(tree, name, {
root: `${name}`,
sourceRoot: `${name}/src`,
targets: {
build: {
executor: '@nx/vite:build',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
outputPath: `dist/${name}`,
},
configurations: {
development: {
mode: 'development',
},
production: {
mode: 'production',
},
},
},
},
});
tree.write(
`${name}/vite.config.ts`,
`
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
cacheDir: '../../node_modules/.vite/${name}',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [
react(),
viteTsConfigPaths({
root: '../../'
})
],
build: {
someProperty: 'someValue',
},
test: {
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
`
);
}
function addProject3(tree: Tree, name: string) {
addProjectConfiguration(tree, name, {
root: `${name}`,
sourceRoot: `${name}/src`,
targets: {
build: {
executor: '@nx/vite:build',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
outputPath: `dist/${name}`,
},
configurations: {
development: {
mode: 'development',
},
production: {
mode: 'production',
fileReplacements: [
{
replace: `${name}/src/environments/environment.ts`,
with: `${name}/src/environments/environment.prod.ts`,
},
],
},
},
},
},
});
tree.write(
`${name}/vite.config.ts`,
`
/// <reference types="vitest" />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
cacheDir: '../../node_modules/.vite/${name}',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [
react(),
viteTsConfigPaths({
root: '../../'
})
],
build: {
someProperty: 'someValue',
},
test: {
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
`
);
}

View File

@ -0,0 +1,55 @@
import { Tree, formatFiles, getProjects, joinPathFragments } from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { ViteBuildExecutorOptions } from '../../executors/build/schema';
import { updateBuildOutDirAndRoot } from './lib/edit-build-config';
import { updateTestConfig } from './lib/edit-test-config';
import { addFileReplacements } from './lib/add-file-replacements';
export default async function updateBuildDir(tree: Tree) {
const projects = getProjects(tree);
forEachExecutorOptions<ViteBuildExecutorOptions>(
tree,
'@nx/vite:build',
(options, projectName, targetName) => {
const projectConfig = projects.get(projectName);
const config =
options.configFile || findViteConfig(tree, projectConfig.root);
if (!config || !tree.exists(config)) {
return;
}
let configContents = tree.read(config, 'utf-8');
configContents = updateBuildOutDirAndRoot(
options,
configContents,
projectConfig,
targetName,
tree,
projectName
);
configContents = updateTestConfig(configContents, projectConfig);
if (options.fileReplacements?.length > 0) {
configContents = addFileReplacements(
configContents,
options.fileReplacements
);
}
tree.write(config, configContents);
}
);
await formatFiles(tree);
}
function findViteConfig(tree: Tree, searchRoot: string) {
const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
for (const ext of allowsExt) {
if (tree.exists(joinPathFragments(searchRoot, `vite.config.${ext}`))) {
return joinPathFragments(searchRoot, `vite.config.${ext}`);
}
}
}

View File

@ -32,9 +32,9 @@ export function createBuildableTsConfig(
context: ExecutorContext context: ExecutorContext
) { ) {
const tsConfig = resolve(projectRoot, 'tsconfig.json'); const tsConfig = resolve(projectRoot, 'tsconfig.json');
options.buildLibsFromSource ??= true; options['buildLibsFromSource'] ??= true;
if (!options.buildLibsFromSource) { if (!options['buildLibsFromSource']) {
const { dependencies } = calculateProjectBuildableDependencies( const { dependencies } = calculateProjectBuildableDependencies(
context.taskGraph, context.taskGraph,
context.projectGraph, context.projectGraph,

View File

@ -170,17 +170,13 @@ export function addOrChangeTestTarget(
) { ) {
const project = readProjectConfiguration(tree, options.project); const project = readProjectConfiguration(tree, options.project);
const coveragePath = joinPathFragments( const reportsDirectory = joinPathFragments(
offsetFromRoot(project.root),
'coverage', 'coverage',
project.root === '.' ? options.project : project.root project.root === '.' ? options.project : project.root
); );
const testOptions: VitestExecutorOptions = { const testOptions: VitestExecutorOptions = {
passWithNoTests: true, reportsDirectory,
// vitest runs in the project root so we have to offset to the workspaceRoot
reportsDirectory: joinPathFragments(
offsetFromRoot(project.root),
coveragePath
),
}; };
project.targets ??= {}; project.targets ??= {};
@ -205,6 +201,7 @@ export function addOrChangeBuildTarget(
target: string target: string
) { ) {
const project = readProjectConfiguration(tree, options.project); const project = readProjectConfiguration(tree, options.project);
const buildOptions: ViteBuildExecutorOptions = { const buildOptions: ViteBuildExecutorOptions = {
outputPath: joinPathFragments( outputPath: joinPathFragments(
'dist', 'dist',
@ -219,8 +216,8 @@ export function addOrChangeBuildTarget(
project.targets[target].options?.fileReplacements; project.targets[target].options?.fileReplacements;
if (project.targets[target].executor === '@nxext/vite:build') { if (project.targets[target].executor === '@nxext/vite:build') {
buildOptions.base = project.targets[target].options?.baseHref; buildOptions['base'] = project.targets[target].options?.baseHref;
buildOptions.sourcemap = project.targets[target].options?.sourcemaps; buildOptions['sourcemap'] = project.targets[target].options?.sourcemaps;
} }
project.targets[target].options = { ...buildOptions }; project.targets[target].options = { ...buildOptions };
project.targets[target].executor = '@nx/vite:build'; project.targets[target].executor = '@nx/vite:build';
@ -257,9 +254,6 @@ export function addOrChangeServeTarget(
const serveTarget = project.targets[target]; const serveTarget = project.targets[target];
const serveOptions: ViteDevServerExecutorOptions = { const serveOptions: ViteDevServerExecutorOptions = {
buildTarget: `${options.project}:build`, buildTarget: `${options.project}:build`,
https: project.targets[target].options?.https,
hmr: project.targets[target].options?.hmr,
open: project.targets[target].options?.open,
}; };
if (serveTarget.executor === '@nxext/vite:dev') { if (serveTarget.executor === '@nxext/vite:dev') {
serveOptions.proxyConfig = project.targets[target].options.proxyConfig; serveOptions.proxyConfig = project.targets[target].options.proxyConfig;
@ -316,8 +310,8 @@ export function addPreviewTarget(
if (target.executor === '@nxext/vite:dev') { if (target.executor === '@nxext/vite:dev') {
previewOptions.proxyConfig = target.options.proxyConfig; previewOptions.proxyConfig = target.options.proxyConfig;
} }
previewOptions.https = target.options?.https; previewOptions['https'] = target.options?.https;
previewOptions.open = target.options?.open; previewOptions['open'] = target.options?.open;
} }
// Adds a preview target. // Adds a preview target.
@ -486,17 +480,21 @@ export interface ViteConfigFileOptions {
rollupOptionsExternal?: string[]; rollupOptionsExternal?: string[];
imports?: string[]; imports?: string[];
plugins?: string[]; plugins?: string[];
coverageProvider?: 'v8' | 'istanbul' | 'custom';
} }
export function createOrEditViteConfig( export function createOrEditViteConfig(
tree: Tree, tree: Tree,
options: ViteConfigFileOptions, options: ViteConfigFileOptions,
onlyVitest: boolean, onlyVitest: boolean,
projectAlreadyHasViteTargets?: TargetFlags projectAlreadyHasViteTargets?: TargetFlags,
vitestFileName?: boolean
) { ) {
const projectConfig = readProjectConfiguration(tree, options.project); const { root: projectRoot } = readProjectConfiguration(tree, options.project);
const viteConfigPath = `${projectConfig.root}/vite.config.ts`; const viteConfigPath = vitestFileName
? `${projectRoot}/vitest.config.ts`
: `${projectRoot}/vite.config.ts`;
const buildOption = onlyVitest const buildOption = onlyVitest
? '' ? ''
@ -505,6 +503,7 @@ export function createOrEditViteConfig(
// Configuration for building your library. // Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode // See: https://vitejs.dev/guide/build.html#library-mode
build: { build: {
outDir: '${offsetFromRoot(projectRoot)}dist/${projectRoot}',
lib: { lib: {
// Could also be a dictionary or array of multiple entry points. // Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts', entry: 'src/index.ts',
@ -517,9 +516,13 @@ export function createOrEditViteConfig(
rollupOptions: { rollupOptions: {
// External packages that should not be bundled into your library. // External packages that should not be bundled into your library.
external: [${options.rollupOptionsExternal ?? ''}] external: [${options.rollupOptionsExternal ?? ''}]
} },
},` },`
: ``; : `
build: {
outDir: '${offsetFromRoot(projectRoot)}dist/${projectRoot}',
},
`;
const imports: string[] = options.imports ? options.imports : []; const imports: string[] = options.imports ? options.imports : [];
@ -546,15 +549,21 @@ export function createOrEditViteConfig(
? `test: { ? `test: {
globals: true, globals: true,
cache: { cache: {
dir: '${offsetFromRoot(projectConfig.root)}node_modules/.vitest' dir: '${offsetFromRoot(projectRoot)}node_modules/.vitest'
}, },
environment: '${options.testEnvironment ?? 'jsdom'}', environment: '${options.testEnvironment ?? 'jsdom'}',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
${ ${
options.inSourceTests options.inSourceTests
? `includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']` ? `includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],`
: '' : ''
} }
coverage: {
reportsDirectory: '${offsetFromRoot(projectRoot)}coverage/${projectRoot}',
provider: ${
options.coverageProvider ? `'${options.coverageProvider}'` : `'v8'`
},
}
},` },`
: ''; : '';
@ -591,8 +600,8 @@ export function createOrEditViteConfig(
// },`; // },`;
const cacheDir = `cacheDir: '${offsetFromRoot( const cacheDir = `cacheDir: '${offsetFromRoot(
projectConfig.root projectRoot
)}node_modules/.vite/${options.project}',`; )}node_modules/.vite/${projectRoot}',`;
if (tree.exists(viteConfigPath)) { if (tree.exists(viteConfigPath)) {
handleViteConfigFileExists( handleViteConfigFileExists(
@ -604,7 +613,8 @@ export function createOrEditViteConfig(
plugins, plugins,
testOption, testOption,
cacheDir, cacheDir,
offsetFromRoot(projectConfig.root), offsetFromRoot(projectRoot),
projectRoot,
projectAlreadyHasViteTargets projectAlreadyHasViteTargets
); );
return; return;
@ -617,6 +627,7 @@ export function createOrEditViteConfig(
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
root: __dirname,
${cacheDir} ${cacheDir}
${devServerOption} ${devServerOption}
${previewServerOption} ${previewServerOption}
@ -773,6 +784,7 @@ function handleViteConfigFileExists(
testOption: string, testOption: string,
cacheDir: string, cacheDir: string,
offsetFromRoot: string, offsetFromRoot: string,
projectRoot: string,
projectAlreadyHasViteTargets?: TargetFlags projectAlreadyHasViteTargets?: TargetFlags
) { ) {
if ( if (
@ -788,17 +800,22 @@ function handleViteConfigFileExists(
); );
} }
const buildOptionObject = { const buildOptionObject = options.includeLib
lib: { ? {
entry: 'src/index.ts', lib: {
name: options.project, entry: 'src/index.ts',
fileName: 'index', name: options.project,
formats: ['es', 'cjs'], fileName: 'index',
}, formats: ['es', 'cjs'],
rollupOptions: { },
external: options.rollupOptionsExternal ?? [], rollupOptions: {
}, external: options.rollupOptionsExternal ?? [],
}; },
outDir: `${offsetFromRoot}dist/${projectRoot}`,
}
: {
outDir: `${offsetFromRoot}dist/${projectRoot}`,
};
const testOptionObject = { const testOptionObject = {
globals: true, globals: true,
@ -807,6 +824,10 @@ function handleViteConfigFileExists(
}, },
environment: options.testEnvironment ?? 'jsdom', environment: options.testEnvironment ?? 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: `${offsetFromRoot}coverage/${projectRoot}`,
provider: `${options.coverageProvider ?? 'v8'}`,
},
}; };
const changed = ensureViteConfigIsCorrect( const changed = ensureViteConfigIsCorrect(

View File

@ -6,23 +6,14 @@ import {
readTargetOptions, readTargetOptions,
} from '@nx/devkit'; } from '@nx/devkit';
import { existsSync } from 'fs'; import { existsSync } from 'fs';
import { relative } from 'path'; import { PreviewOptions, ServerOptions } from 'vite';
import {
BuildOptions,
InlineConfig,
PluginOption,
PreviewOptions,
ServerOptions,
} from 'vite';
import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema'; import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema';
import { VitePreviewServerExecutorOptions } from '../executors/preview-server/schema';
import replaceFiles from '../../plugins/rollup-replace-files.plugin';
import { ViteBuildExecutorOptions } from '../executors/build/schema';
/** /**
* Returns the path to the vite config file or undefined when not found. * Returns the path to the vite config file or undefined when not found.
*/ */
export function normalizeViteConfigFilePath( export function normalizeViteConfigFilePath(
contextRoot: string,
projectRoot: string, projectRoot: string,
configFile?: string configFile?: string
): string | undefined { ): string | undefined {
@ -35,11 +26,28 @@ export function normalizeViteConfigFilePath(
} }
return normalized; return normalized;
} }
return existsSync(joinPathFragments(projectRoot, 'vite.config.ts'))
? joinPathFragments(projectRoot, 'vite.config.ts') const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
: existsSync(joinPathFragments(projectRoot, 'vite.config.js'))
? joinPathFragments(projectRoot, 'vite.config.js') for (const ext of allowsExt) {
: undefined; if (
existsSync(
joinPathFragments(contextRoot, projectRoot, `vite.config.${ext}`)
)
) {
return joinPathFragments(contextRoot, projectRoot, `vite.config.${ext}`);
} else if (
existsSync(
joinPathFragments(contextRoot, projectRoot, `vitest.config.${ext}`)
)
) {
return joinPathFragments(
contextRoot,
projectRoot,
`vitest.config.${ext}`
);
}
}
} }
export function getProjectTsConfigPath( export function getProjectTsConfigPath(
@ -75,36 +83,6 @@ export function getViteServerProxyConfigPath(
} }
} }
/**
* Builds the shared options for vite.
*
* Most shared options are derived from the build target.
*/
export function getViteSharedConfig(
options: ViteBuildExecutorOptions,
clearScreen: boolean | undefined,
context: ExecutorContext
): InlineConfig {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const root =
projectRoot === '.'
? process.cwd()
: relative(context.cwd, joinPathFragments(context.root, projectRoot));
return {
mode: options.mode,
root,
base: options.base,
configFile: normalizeViteConfigFilePath(projectRoot, options.configFile),
plugins: [replaceFiles(options.fileReplacements) as PluginOption],
optimizeDeps: { force: options.force },
clearScreen: clearScreen,
logLevel: options.logLevel,
};
}
/** /**
* Builds the options for the vite dev server. * Builds the options for the vite dev server.
*/ */
@ -119,12 +97,6 @@ export async function getViteServerOptions(
const projectRoot = const projectRoot =
context.projectsConfigurations.projects[context.projectName].root; context.projectsConfigurations.projects[context.projectName].root;
const serverOptions: ServerOptions = { const serverOptions: ServerOptions = {
host: options.host,
port: options.port,
https: options.https,
hmr: options.hmr,
open: options.open,
cors: options.cors,
fs: { fs: {
allow: [ allow: [
searchForWorkspaceRoot(joinPathFragments(projectRoot)), searchForWorkspaceRoot(joinPathFragments(projectRoot)),
@ -145,53 +117,14 @@ export async function getViteServerOptions(
return serverOptions; return serverOptions;
} }
/**
* Builds the build options for the vite.
*/
export function getViteBuildOptions(
options: ViteBuildExecutorOptions,
context: ExecutorContext
): BuildOptions {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const outputPath = joinPathFragments(
'dist',
projectRoot != '.' ? projectRoot : context.projectName
);
return {
outDir: relative(projectRoot, options.outputPath ?? outputPath),
emptyOutDir: options.emptyOutDir,
reportCompressedSize: true,
cssCodeSplit: options.cssCodeSplit,
target: options.target,
commonjsOptions: {
transformMixedEsModules: true,
},
sourcemap: options.sourcemap,
minify: options.minify,
manifest: options.manifest,
ssrManifest: options.ssrManifest,
ssr: options.ssr,
watch: options.watch as BuildOptions['watch'],
};
}
/** /**
* Builds the options for the vite preview server. * Builds the options for the vite preview server.
*/ */
export function getVitePreviewOptions( export function getVitePreviewOptions(
options: VitePreviewServerExecutorOptions, options: Record<string, any>,
context: ExecutorContext context: ExecutorContext
): PreviewOptions { ): PreviewOptions {
const serverOptions: ServerOptions = { const serverOptions: ServerOptions = {};
host: options.host,
port: options.port,
https: options.https,
open: options.open,
};
const proxyConfigPath = getViteServerProxyConfigPath( const proxyConfigPath = getViteServerProxyConfigPath(
options.proxyConfig, options.proxyConfig,
context context

View File

@ -85,7 +85,12 @@ function handleBuildOrTestNode(
let updatedPropsString = ''; let updatedPropsString = '';
for (const prop of existingProperties) { for (const prop of existingProperties) {
const propName = prop.name.getText(); const propName = prop.name.getText();
if (!configContentObject[propName] && propName !== 'dir') { if (
!configContentObject[propName] &&
propName !== 'dir' &&
propName !== 'reportsDirectory' &&
propName !== 'provider'
) {
updatedPropsString += `'${propName}': ${prop.initializer.getText()},\n`; updatedPropsString += `'${propName}': ${prop.initializer.getText()},\n`;
} }
} }

View File

@ -46,6 +46,7 @@ import vue from '@vitejs/plugin-vue';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/test', cacheDir: '../node_modules/.vite/test',
server: { server: {
@ -65,6 +66,10 @@ export default defineConfig({
// plugins: [ nxViteTsPaths() ], // plugins: [ nxViteTsPaths() ],
// }, // },
build: {
outDir: '../dist/test',
},
test: { test: {
globals: true, globals: true,
cache: { cache: {
@ -72,6 +77,11 @@ export default defineConfig({
}, },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../coverage/test',
provider: 'v8',
},
}, },
}); });
" "
@ -141,7 +151,6 @@ exports[`application generator should set up project correctly with given option
"executor": "@nx/vite:test", "executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"], "outputs": ["{options.reportsDirectory}"],
"options": { "options": {
"passWithNoTests": true,
"reportsDirectory": "../coverage/test" "reportsDirectory": "../coverage/test"
} }
}, },

View File

@ -36,6 +36,7 @@ import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({ export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-lib', cacheDir: '../node_modules/.vite/my-lib',
plugins: [ plugins: [
@ -56,6 +57,7 @@ export default defineConfig({
// Configuration for building your library. // Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode // See: https://vitejs.dev/guide/build.html#library-mode
build: { build: {
outDir: '../dist/my-lib',
lib: { lib: {
// Could also be a dictionary or array of multiple entry points. // Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts', entry: 'src/index.ts',
@ -78,6 +80,11 @@ export default defineConfig({
}, },
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../coverage/my-lib',
provider: 'v8',
},
}, },
}); });
" "

View File

@ -404,7 +404,6 @@ describe('app', () => {
it('should setup the nrwl vite:build builder if bundler is vite', async () => { it('should setup the nrwl vite:build builder if bundler is vite', async () => {
await applicationGenerator(tree, { await applicationGenerator(tree, {
name: 'my-app', name: 'my-app',
bundler: 'vite', bundler: 'vite',
projectNameAndRootFormat: 'as-provided', projectNameAndRootFormat: 'as-provided',
}); });

View File

@ -22,7 +22,9 @@ export default async function addDroppedDependencies(tree: Tree) {
projectConfiguration.targets ?? {} projectConfiguration.targets ?? {}
)) { )) {
for (const droppedDependency of droppedDependencies) { for (const droppedDependency of droppedDependencies) {
if (targetConfiguration.executor?.startsWith(droppedDependency + ':')) { if (
targetConfiguration?.['executor']?.startsWith(droppedDependency + ':')
) {
devDependencies[droppedDependency] = NX_VERSION; devDependencies[droppedDependency] = NX_VERSION;
} }
} }
@ -35,7 +37,9 @@ export default async function addDroppedDependencies(tree: Tree) {
nxJson?.targetDefaults ?? {} nxJson?.targetDefaults ?? {}
)) { )) {
for (const droppedDependency of droppedDependencies) { for (const droppedDependency of droppedDependencies) {
if (targetConfiguration.executor?.startsWith(droppedDependency + ':')) { if (
targetConfiguration?.['executor']?.startsWith(droppedDependency + ':')
) {
devDependencies[droppedDependency] = NX_VERSION; devDependencies[droppedDependency] = NX_VERSION;
} }
} }