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.",
"default": false
},
"base": {
"type": "string",
"description": "Base public path when served in development or production.",
"alias": "baseHref"
},
"configFile": {
"type": "string",
"description": "The name of the Vite.js configuration file.",
@ -59,49 +54,6 @@
},
"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": {
"description": "Enable re-building when files change.",
"oneOf": [{ "type": "boolean" }, { "type": "object" }],

View File

@ -27,45 +27,6 @@
"type": "string",
"description": "Path to the proxy configuration 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": {},

View File

@ -22,29 +22,6 @@
"description": "Path to the proxy configuration 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": {
"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",

View File

@ -9,50 +9,12 @@
"description": "Test using Vitest.",
"type": "object",
"properties": {
"config": {
"configFile": {
"type": "string",
"description": "The path to the local vitest config",
"x-completion-type": "file",
"x-completion-glob": "@(vitest|vite).config@(.js|.ts)"
},
"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"
"x-completion-glob": "@(vitest|vite).config@(.js|.ts)",
"aliases": ["config"]
},
"reportsDirectory": {
"type": "string",
@ -62,6 +24,10 @@
"aliases": ["testFile"],
"type": "array",
"items": { "type": "string" }
},
"watch": {
"description": "Watch files for changes and rerun tests related to changed files.",
"type": "boolean"
}
},
"required": [],

View File

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

View File

@ -6,10 +6,8 @@ import {
exists,
fileExists,
getPackageManagerCommand,
killPorts,
listFiles,
newProject,
promisifiedTreeKill,
readFile,
readJson,
removeFile,
@ -17,7 +15,6 @@ import {
runCLI,
runCommand,
runCLIAsync,
runCommandUntil,
tmpProjPath,
uniq,
updateFile,
@ -32,87 +29,11 @@ describe('Vite Plugin', () => {
let proj: string;
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', () => {
beforeEach(async () => {
proj = newProject();
runCLI(`generate @nx/react:app ${myApp} --bundler=vite`);
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());
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}/hello.md`)).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();
}, 200_000);
});
@ -202,48 +115,7 @@ describe('Vite Plugin', () => {
}, 200_000);
});
describe('convert @nx/web webpack app to vite using the vite:configuration generator', () => {
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;
100_000;
});
describe('incremental building', () => {
@ -363,6 +235,8 @@ export default App;
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/${lib}',
server: {
port: 4200,
host: 'localhost',
@ -371,14 +245,18 @@ export default App;
react(),
nxViteTsPaths()
],
build: {
outDir: '../../dist/libs/${lib}',
},
test: {
globals: true,
cache: {
dir: './node_modules/.vitest',
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
coverage: {
reportsDirectory: '../../coverage/libs/${lib}',
provider: "v8",
enabled: true,
lines: 100,
@ -531,7 +409,7 @@ export default defineConfig({
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
cacheDir: './node_modules/.vite/root-app',
cacheDir: '../../node_modules/.vite/root-app',
server: {
port: 4200,
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/_should_not_remove.txt`
);
runCLI(`build ${appName}`);
runCLI(`build ${libName}`);
runCLI(`build ${appName} --emptyOutDir`);
runCLI(`build ${libName} --emptyOutDir`);
checkFilesDoNotExist(
`dist/apps/${appName}/_should_remove.txt`,
`dist/libs/${libName}/_should_remove.txt`

View File

@ -129,8 +129,12 @@ export default defineVitestConfig({
cache: {
dir: '../node_modules/.vitest',
},
include: ['my-app/src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
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({
plugins: [nxViteTsPaths()],
test: {
globals: true,
cache: {
globals: true,
cache: {
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';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-lib',
plugins: [react(), nxViteTsPaths()],
@ -20,6 +21,7 @@ export default defineConfig({
cache: { dir: '../node_modules/.vitest' },
environment: 'jsdom',
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';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-lib',
plugins: [
@ -54,6 +57,7 @@ export default defineConfig({
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: '../dist/my-lib',
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
@ -76,6 +80,11 @@ export default defineConfig({
},
environment: 'jsdom',
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",
"description": "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": {

View File

@ -53,6 +53,7 @@
"./src/executors/*/schema.json": "./src/executors/*/schema.json",
"./src/executors/*.impl": "./src/executors/*.impl.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
* @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) {
return null;
}

View File

@ -1,14 +1,15 @@
import {
detectPackageManager,
ExecutorContext,
joinPathFragments,
logger,
offsetFromRoot,
stripIndents,
writeJsonFile,
} from '@nx/devkit';
import {
getProjectTsConfigPath,
getViteBuildOptions,
getViteSharedConfig,
normalizeViteConfigFilePath,
} from '../../utils/options-utils';
import { ViteBuildExecutorOptions } from './schema';
import {
@ -18,33 +19,63 @@ import {
getLockFileName,
} from '@nx/js';
import { existsSync, writeFileSync } from 'fs';
import { resolve } from 'path';
import { relative, resolve } from 'path';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
import {
createBuildableTsConfig,
validateTypes,
} from '../../utils/executor-utils';
import { BuildOptions } from 'vite';
export async function* viteBuildExecutor(
options: ViteBuildExecutorOptions,
options: Record<string, any> & ViteBuildExecutorOptions,
context: ExecutorContext
) {
// 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")'
)() as Promise<typeof import('vite')>);
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
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(
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 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.
if (options.generatePackageJson) {
@ -83,7 +121,10 @@ export async function* viteBuildExecutor(
builtPackageJson.type = 'module';
writeJsonFile(`${options.outputPath}/package.json`, builtPackageJson);
writeJsonFile(
`${outDirRelativeToWorkspaceRoot}/package.json`,
builtPackageJson
);
const packageManager = detectPackageManager(context.root);
const lockFile = createLockFile(
@ -92,7 +133,7 @@ export async function* viteBuildExecutor(
packageManager
);
writeFileSync(
`${options.outputPath}/${getLockFileName(packageManager)}`,
`${outDirRelativeToWorkspaceRoot}/${getLockFileName(packageManager)}`,
lockFile,
{
encoding: 'utf-8',
@ -107,7 +148,7 @@ export async function* viteBuildExecutor(
) {
await copyAssets(
{
outputPath: normalizedOptions.outputPath,
outputPath: outDirRelativeToWorkspaceRoot,
assets: [
{
input: projectRoot,
@ -142,22 +183,66 @@ export async function* viteBuildExecutor(
} else {
const output = watcherOrOutput?.['output'] || watcherOrOutput?.[0]?.output;
const fileName = output?.[0]?.fileName || 'main.cjs';
const outfile = resolve(normalizedOptions.outputPath, fileName);
const outfile = resolve(outDirRelativeToWorkspaceRoot, fileName);
yield { success: true, outfile };
}
}
function normalizeOptions(options: ViteBuildExecutorOptions) {
const normalizedOptions = { ...options };
// coerce watch to null or {} to match with Vite's watch config
if (options.watch === false) {
normalizedOptions.watch = null;
} else if (options.watch === true) {
normalizedOptions.watch = {};
async function getExtraArgs(options: ViteBuildExecutorOptions): Promise<{
buildOptions: BuildOptions;
otherOptions: Record<string, any>;
}> {
// 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 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;

View File

@ -1,23 +1,11 @@
import type { FileReplacement } from '../../plugins/rollup-replace-files.plugin';
export interface ViteBuildExecutorOptions {
outputPath: string;
emptyOutDir?: boolean;
base?: string;
outputPath?: string;
skipTypeCheck?: boolean;
configFile?: string;
fileReplacements?: FileReplacement[];
force?: 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[];
watch?: boolean;
generatePackageJson?: boolean;
includeDevDependenciesInPackageJson?: boolean;
cssCodeSplit?: 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.",
"default": false
},
"base": {
"type": "string",
"description": "Base public path when served in development or production.",
"alias": "baseHref"
},
"configFile": {
"type": "string",
"description": "The name of the Vite.js configuration file.",
@ -61,87 +56,6 @@
},
"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": {
"description": "Enable re-building when files change.",
"oneOf": [

View File

@ -1,16 +1,20 @@
import { ExecutorContext } from '@nx/devkit';
import type { InlineConfig, ViteDevServer } from 'vite';
import { ExecutorContext, joinPathFragments } from '@nx/devkit';
import {
loadConfigFromFile,
type InlineConfig,
type ViteDevServer,
} from 'vite';
import {
getNxTargetOptions,
getViteBuildOptions,
getViteServerOptions,
getViteSharedConfig,
normalizeViteConfigFilePath,
} from '../../utils/options-utils';
import { ViteDevServerExecutorOptions } from './schema';
import { ViteBuildExecutorOptions } from '../build/schema';
import { createBuildableTsConfig } from '../../utils/executor-utils';
import { relative } from 'path';
export async function* viteDevServerExecutor(
options: ViteDevServerExecutorOptions,
@ -23,7 +27,10 @@ export async function* viteDevServerExecutor(
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const root =
projectRoot === '.'
? process.cwd()
: relative(context.cwd, joinPathFragments(context.root, projectRoot));
createBuildableTsConfig(projectRoot, options, context);
// Retrieve the option for the configured buildTarget.
@ -31,20 +38,33 @@ export async function* viteDevServerExecutor(
options.buildTarget,
context
);
// Merge the options from the build and dev-serve targets.
// The latter takes precedence.
const mergedOptions = {
...buildTargetOptions,
...options,
};
// Add the server specific configuration.
const serverConfig: InlineConfig = mergeConfig(
getViteSharedConfig(mergedOptions, options.clearScreen, context),
const viteConfigPath = normalizeViteConfigFilePath(
context.root,
projectRoot,
buildTargetOptions.configFile
);
const extraArgs = await getExtraArgs(options);
const resolved = await loadConfigFromFile(
{
build: getViteBuildOptions(mergedOptions, context),
server: await getViteServerOptions(mergedOptions, context),
mode: extraArgs?.mode ?? 'production',
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;
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;
buildLibsFromSource?: boolean;
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",
"description": "Path to the proxy configuration 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": {},

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 {
getNxTargetOptions,
getViteBuildOptions,
getVitePreviewOptions,
getViteSharedConfig,
normalizeViteConfigFilePath,
} from '../../utils/options-utils';
import { ViteBuildExecutorOptions } from '../build/schema';
import { VitePreviewServerExecutorOptions } from './schema';
interface CustomBuildTargetOptions {
outputPath: string;
}
import { relative } from 'path';
export async function* vitePreviewServerExecutor(
options: VitePreviewServerExecutorOptions,
context: ExecutorContext
) {
// 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")'
)() as Promise<typeof import('vite')>);
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const target = parseTargetString(options.buildTarget, context);
const targetConfiguration =
context.projectsConfigurations.projects[target.project]?.targets[
@ -36,35 +39,63 @@ export async function* vitePreviewServerExecutor(
targetConfiguration.executor !== '@nrwl/vite:build';
// Retrieve the option for the configured buildTarget.
const buildTargetOptions:
| ViteBuildExecutorOptions
| CustomBuildTargetOptions = getNxTargetOptions(
const buildTargetOptions: ViteBuildExecutorOptions = getNxTargetOptions(
options.buildTarget,
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(
`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.
// The latter takes precedence.
const mergedOptions = {
...{ watch: {} },
build: {
outDir,
},
...(isCustomBuildTarget ? {} : buildTargetOptions),
...options,
outputPath,
...extraArgs,
};
// Retrieve the server configuration.
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),
}
);
@ -146,3 +177,18 @@ function closeServer(server?: PreviewServer): Promise<void> {
}
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 {
buildTarget: 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;
}

View File

@ -25,56 +25,6 @@
"description": "Path to the proxy configuration 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": {
"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",

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 {
config?: string;
passWithNoTests?: boolean;
testNamePattern?: string;
mode?: 'test' | 'benchmark' | 'typecheck';
reporters?: string[];
watch?: boolean;
update?: boolean;
configFile?: string;
reportsDirectory?: string;
coverage?: boolean;
testFiles?: string[];
watch?: boolean;
}

View File

@ -6,52 +6,12 @@
"description": "Test using Vitest.",
"type": "object",
"properties": {
"config": {
"configFile": {
"type": "string",
"description": "The path to the local vitest config",
"x-completion-type": "file",
"x-completion-glob": "@(vitest|vite).config@(.js|.ts)"
},
"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"
"x-completion-glob": "@(vitest|vite).config@(.js|.ts)",
"aliases": ["config"]
},
"reportsDirectory": {
"type": "string",
@ -61,6 +21,10 @@
"aliases": ["testFile"],
"type": "array",
"items": { "type": "string" }
},
"watch": {
"description": "Watch files for changes and rerun tests related to changed files.",
"type": "boolean"
}
},
"required": [],

View File

@ -1,51 +1,9 @@
import {
ExecutorContext,
joinPathFragments,
logger,
readJsonFile,
stripIndents,
workspaceRoot,
} from '@nx/devkit';
import type { CoverageOptions, File, Reporter } from 'vitest';
import { ExecutorContext, workspaceRoot } from '@nx/devkit';
import { VitestExecutorOptions } from './schema';
import { join, relative, resolve } from 'path';
import { existsSync } from 'fs';
import { resolve } from 'path';
import { registerTsConfigPaths } from '@nx/js/src/internal';
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);
}
}
import { NxReporter } from './lib/nx-reporter';
import { getExtraArgs, getOptions } from './lib/utils';
export async function* vitestExecutor(
options: VitestExecutorOptions,
@ -53,18 +11,32 @@ export async function* vitestExecutor(
) {
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
registerTsConfigPaths(resolve(workspaceRoot, projectRoot, 'tsconfig.json'));
const { startVitest } = await (Function(
'return import("vitest/node")'
)() as Promise<typeof import('vitest/node')>);
const nxReporter = new NxReporter(options.watch);
const settings = await getSettings(options, context, projectRoot);
settings.reporters.push(nxReporter);
const extraArgs = await getExtraArgs(options);
const resolvedOptions =
(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 ctx = await startVitest(options.mode, cliFilters, settings);
const ctx = await startVitest(
resolvedOptions['mode'] ?? 'test',
cliFilters,
resolvedOptions
);
let hasErrors = false;
@ -77,7 +49,7 @@ export async function* vitestExecutor(
}
};
if (options.watch) {
if (resolvedOptions['watch'] === true) {
process.on('SIGINT', processExit);
process.on('SIGTERM', 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;

View File

@ -9,7 +9,8 @@ import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
cacheDir: '../../node_modules/.vite/react-lib-nonb-jest',
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/react-lib-nonb-jest',
plugins: [
react(),
@ -29,6 +30,7 @@ export default defineConfig({
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: '../../dist/libs/react-lib-nonb-jest',
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
@ -56,7 +58,8 @@ import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
cacheDir: '../../node_modules/.vite/react-lib-nonb-jest',
root: __dirname,
cacheDir: '../../node_modules/.vite/libs/react-lib-nonb-jest',
plugins: [
react(),
@ -76,6 +79,7 @@ export default defineConfig({
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: '../../dist/libs/react-lib-nonb-jest',
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
@ -98,6 +102,11 @@ export default defineConfig({
},
environment: 'jsdom',
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({
// Configuration for building your library.
// 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: {
outDir: '../../dist/libs/react-lib-nonb-vitest',
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
@ -174,6 +184,10 @@ export default defineConfig({
cache: { dir: '../../node_modules/.vitest' },
environment: 'jsdom',
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';
export default defineConfig({
cacheDir: '../../node_modules/.vite/my-test-react-app',
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-test-react-app',
server: {
port: 4200,
@ -289,6 +304,10 @@ export default defineConfig({
// worker: {
// 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';
export default defineConfig({
cacheDir: '../../node_modules/.vite/my-test-web-app',
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-test-web-app',
server: {
port: 4200,
@ -427,6 +447,10 @@ export default defineConfig({
// worker: {
// 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';
export default defineConfig({
cacheDir: '../../node_modules/.vite/my-test-react-app',
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/my-test-react-app',
server: {
port: 4200,
@ -553,6 +578,10 @@ export default defineConfig({
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/my-test-react-app',
},
test: {
globals: true,
cache: {
@ -560,6 +589,11 @@ export default defineConfig({
},
environment: 'jsdom',
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 { targets, projectType, root } = readProjectConfiguration(
tree,
schema.project
);
const {
targets,
projectType,
root: projectRoot,
} = readProjectConfiguration(tree, schema.project);
let buildTargetName = 'build';
let serveTargetName = 'serve';
let testTargetName = 'test';
@ -147,7 +148,7 @@ export async function viteConfigurationGenerator(
deleteWebpackConfig(
tree,
root,
projectRoot,
targets?.[buildTargetName]?.options?.webpackConfig
);
@ -159,7 +160,7 @@ export async function viteConfigurationGenerator(
includeLib: schema.includeLib,
compiler: schema.compiler,
testEnvironment: schema.testEnvironment,
rootProject: root === '.',
rootProject: projectRoot === '.',
});
tasks.push(initTask);
@ -178,24 +179,28 @@ export async function viteConfigurationGenerator(
if (projectType === 'library') {
// update tsconfig.lib.json to include vite/client
updateJson(tree, joinPathFragments(root, 'tsconfig.lib.json'), (json) => {
if (!json.compilerOptions) {
json.compilerOptions = {};
updateJson(
tree,
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) {

View File

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

View File

@ -34,23 +34,8 @@ function checkDependenciesInstalled(host: Tree, schema: InitGeneratorSchema) {
// base deps
devDependencies['@nx/vite'] = nxVersion;
devDependencies['vite'] = viteVersion;
// Do not install latest version if vitest already exists
// 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;
}
devDependencies['vitest'] = vitestVersion;
devDependencies['@vitest/ui'] = vitestVersion;
if (schema.testEnvironment === 'jsdom') {
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 productionFileSet = nxJson.namedInputs?.production;
@ -113,13 +98,26 @@ export function createVitestConfig(tree: Tree) {
'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);
}
export async function initGenerator(tree: Tree, schema: InitGeneratorSchema) {
moveToDevDependencies(tree);
createVitestConfig(tree);
updateNxJsonSettings(tree);
const tasks = [];
tasks.push(

View File

@ -7,7 +7,8 @@ import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
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()],
@ -27,6 +28,10 @@ export default defineConfig({
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{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';
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()],
@ -55,6 +61,11 @@ export default defineConfig({
},
environment: 'jsdom',
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';
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()],
@ -83,6 +95,11 @@ export default defineConfig({
},
environment: 'jsdom',
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": [
"vite.config.ts",
"vitest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",

View File

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

View File

@ -55,7 +55,6 @@ describe('vitest generator', () => {
{
"executor": "@nx/vite:test",
"options": {
"passWithNoTests": true,
"reportsDirectory": "../../coverage/apps/my-test-react-app",
},
"outputs": [
@ -102,6 +101,7 @@ describe('vitest generator', () => {
"extends": "./tsconfig.json",
"include": [
"vite.config.ts",
"vitest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"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
) {
const tsConfig = resolve(projectRoot, 'tsconfig.json');
options.buildLibsFromSource ??= true;
options['buildLibsFromSource'] ??= true;
if (!options.buildLibsFromSource) {
if (!options['buildLibsFromSource']) {
const { dependencies } = calculateProjectBuildableDependencies(
context.taskGraph,
context.projectGraph,

View File

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

View File

@ -6,23 +6,14 @@ import {
readTargetOptions,
} from '@nx/devkit';
import { existsSync } from 'fs';
import { relative } from 'path';
import {
BuildOptions,
InlineConfig,
PluginOption,
PreviewOptions,
ServerOptions,
} from 'vite';
import { PreviewOptions, ServerOptions } from 'vite';
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.
*/
export function normalizeViteConfigFilePath(
contextRoot: string,
projectRoot: string,
configFile?: string
): string | undefined {
@ -35,11 +26,28 @@ export function normalizeViteConfigFilePath(
}
return normalized;
}
return existsSync(joinPathFragments(projectRoot, 'vite.config.ts'))
? joinPathFragments(projectRoot, 'vite.config.ts')
: existsSync(joinPathFragments(projectRoot, 'vite.config.js'))
? joinPathFragments(projectRoot, 'vite.config.js')
: undefined;
const allowsExt = ['js', 'mjs', 'ts', 'cjs', 'mts', 'cts'];
for (const ext of allowsExt) {
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(
@ -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.
*/
@ -119,12 +97,6 @@ export async function getViteServerOptions(
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const serverOptions: ServerOptions = {
host: options.host,
port: options.port,
https: options.https,
hmr: options.hmr,
open: options.open,
cors: options.cors,
fs: {
allow: [
searchForWorkspaceRoot(joinPathFragments(projectRoot)),
@ -145,53 +117,14 @@ export async function getViteServerOptions(
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.
*/
export function getVitePreviewOptions(
options: VitePreviewServerExecutorOptions,
options: Record<string, any>,
context: ExecutorContext
): PreviewOptions {
const serverOptions: ServerOptions = {
host: options.host,
port: options.port,
https: options.https,
open: options.open,
};
const serverOptions: ServerOptions = {};
const proxyConfigPath = getViteServerProxyConfigPath(
options.proxyConfig,
context

View File

@ -85,7 +85,12 @@ function handleBuildOrTestNode(
let updatedPropsString = '';
for (const prop of existingProperties) {
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`;
}
}

View File

@ -46,6 +46,7 @@ import vue from '@vitejs/plugin-vue';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/test',
server: {
@ -65,6 +66,10 @@ export default defineConfig({
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../dist/test',
},
test: {
globals: true,
cache: {
@ -72,6 +77,11 @@ export default defineConfig({
},
environment: 'jsdom',
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",
"outputs": ["{options.reportsDirectory}"],
"options": {
"passWithNoTests": true,
"reportsDirectory": "../coverage/test"
}
},

View File

@ -36,6 +36,7 @@ import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-lib',
plugins: [
@ -56,6 +57,7 @@ export default defineConfig({
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: '../dist/my-lib',
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
@ -78,6 +80,11 @@ export default defineConfig({
},
environment: 'jsdom',
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 () => {
await applicationGenerator(tree, {
name: 'my-app',
bundler: 'vite',
projectNameAndRootFormat: 'as-provided',
});

View File

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