fix(vite): environments api support in executor (#30183)

## Current Behavior
`@nx/vite:build` executor does not support Vite 6 Environments API

## Expected Behavior
`@nx/vite:build` executor builds all environments when Vite 6 is
detected
This commit is contained in:
Colum Ferry 2025-02-27 17:02:19 +00:00 committed by GitHub
parent 320709f66f
commit 6fcb310e54
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 234 additions and 95 deletions

View File

@ -58,6 +58,12 @@
"skipPackageManager": {
"type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option."
},
"useEnvironmentsApi": {
"alias": "app",
"type": "boolean",
"description": "Use the new Environments API for building multiple environments at once. Only works with Vite 6.0.0 or higher.",
"default": false
}
},
"definitions": {},

View File

@ -89,6 +89,103 @@ describe('Vite Plugin', () => {
}, 200_000);
});
});
describe('set up new React app with --bundler=vite option and use environments api', () => {
let myApp;
beforeAll(() => {
myApp = uniq('my-app');
runCLI(
`generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest`
);
updateJson(`${myApp}/project.json`, (json) => {
json.targets.build.options.useEnvironmentsApi = true;
return json;
});
updateFile(
`${myApp}/vite.config.ts`,
`/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
export default defineConfig({
root: __dirname,
cacheDir: './node_modules/.vite/${myApp}',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
builder: {},
environments: {
ssr: {
build: {
rollupOptions: {
input: '${myApp}/src/main.server.tsx'
}
}
}
},
build: {
outDir: './dist/${myApp}',
emptyOutDir: false,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './coverage/${myApp}',
provider: 'v8',
},
},
});
`
);
updateFile(
`${myApp}/src/main.server.tsx`,
`import React from 'react'
import ReactDOMServer from 'react-dom/server'
import App from './app/app';
export default async function render(_url: string, document: string) {
const html = ReactDOMServer.renderToString(
<React.StrictMode>
<App />
</React.StrictMode>
)
return document.replace('<!--app-html-->', html);
}`
);
});
afterEach(() => {
rmDist();
});
it('should build application', async () => {
runCLI(`build ${myApp}`);
expect(readFile(`dist/${myApp}/favicon.ico`)).toBeDefined();
expect(readFile(`dist/${myApp}/index.html`)).toBeDefined();
expect(readFile(`dist/${myApp}/main.server.mjs`)).toBeDefined();
}, 200_000);
});
});
describe('Vite on Web apps', () => {

View File

@ -20,7 +20,10 @@ import {
} from '@nx/js';
import { existsSync, writeFileSync } from 'fs';
import { relative, resolve } from 'path';
import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
import {
combineAsyncIterables,
createAsyncIterable,
} from '@nx/devkit/src/utils/async-iterable';
import {
createBuildableTsConfig,
loadViteDynamicImport,
@ -35,7 +38,8 @@ export async function* viteBuildExecutor(
) {
process.env.VITE_CJS_IGNORE_WARNING = 'true';
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { mergeConfig, build, resolveConfig } = await loadViteDynamicImport();
const { mergeConfig, build, resolveConfig, createBuilder } =
await loadViteDynamicImport();
const projectRoot =
context.projectsConfigurations.projects[context.projectName].root;
const tsConfigForBuild = createBuildableTsConfig(
@ -50,7 +54,7 @@ export async function* viteBuildExecutor(
options.configFile
);
const root =
projectRoot === '.'
projectRoot === '.' || projectRoot === ''
? process.cwd()
: relative(context.cwd, joinPathFragments(context.root, projectRoot));
@ -100,108 +104,133 @@ export async function* viteBuildExecutor(
});
}
const watcherOrOutput = await build(buildConfig);
const builder =
createBuilder !== undefined && options.useEnvironmentsApi
? await createBuilder(buildConfig)
: // This is needed to ensure support for Vite 5
{
build: (inlineConfig) => build(inlineConfig),
environments: { build: buildConfig },
};
const libraryPackageJson = resolve(projectRoot, 'package.json');
const rootPackageJson = resolve(context.root, 'package.json');
let iterables: AsyncIterable<{ success: boolean; outfile?: string }>[] = [];
for (const env of Object.values(builder.environments)) {
// This is needed to overwrite the resolve build config with executor options in Vite 6
if (env.config?.build) {
env.config.build = {
...env.config.build,
...buildConfig.build,
};
}
const watcherOrOutput = await builder.build(env as any);
// 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'
);
const libraryPackageJson = resolve(projectRoot, 'package.json');
const rootPackageJson = resolve(context.root, 'package.json');
// Generate a package.json if option has been set.
if (options.generatePackageJson) {
if (context.projectGraph.nodes[context.projectName].type !== 'app') {
logger.warn(
stripIndents`The project ${context.projectName} is using the 'generatePackageJson' option which is deprecated for library projects. It should only be used for applications.
// 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) {
if (context.projectGraph.nodes[context.projectName].type !== 'app') {
logger.warn(
stripIndents`The project ${context.projectName} is using the 'generatePackageJson' option which is deprecated for library projects. It should only be used for applications.
For libraries, configure the project to use the '@nx/dependency-checks' ESLint rule instead (https://nx.dev/nx-api/eslint-plugin/documents/dependency-checks).`
);
}
const builtPackageJson = createPackageJson(
context.projectName,
context.projectGraph,
{
target: context.targetName,
root: context.root,
isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build.
skipOverrides: options.skipOverrides,
skipPackageManager: options.skipPackageManager,
}
);
builtPackageJson.type ??= 'module';
writeJsonFile(
`${outDirRelativeToWorkspaceRoot}/package.json`,
builtPackageJson
);
const packageManager = detectPackageManager(context.root);
const lockFile = createLockFile(
builtPackageJson,
context.projectGraph,
packageManager
);
writeFileSync(
`${outDirRelativeToWorkspaceRoot}/${getLockFileName(packageManager)}`,
lockFile,
{
encoding: 'utf-8',
}
);
}
// For buildable libs, copy package.json if it exists.
else if (
options.generatePackageJson !== false &&
!existsSync(distPackageJson) &&
existsSync(libraryPackageJson) &&
rootPackageJson !== libraryPackageJson
) {
await copyAssets(
{
outputPath: outDirRelativeToWorkspaceRoot,
assets: [
{
input: projectRoot,
output: '.',
glob: 'package.json',
},
],
},
context
);
}
const builtPackageJson = createPackageJson(
context.projectName,
context.projectGraph,
{
target: context.targetName,
root: context.root,
isProduction: !options.includeDevDependenciesInPackageJson, // By default we remove devDependencies since this is a production build.
skipOverrides: options.skipOverrides,
skipPackageManager: options.skipPackageManager,
const iterable = createAsyncIterable<{
success: boolean;
outfile?: string;
}>(({ next, done }) => {
if ('on' in watcherOrOutput) {
let success = true;
watcherOrOutput.on('event', (event) => {
if (event.code === 'START') {
success = true;
} else if (event.code === 'ERROR') {
success = false;
} else if (event.code === 'END') {
next({ success });
}
// result must be closed when present.
// see https://rollupjs.org/guide/en/#rollupwatch
if ('result' in event && event.result) {
event.result.close();
}
});
} else {
const output =
watcherOrOutput?.['output'] || watcherOrOutput?.[0]?.output;
const fileName = output?.[0]?.fileName || 'main.cjs';
const outfile = resolve(outDirRelativeToWorkspaceRoot, fileName);
next({ success: true, outfile });
done();
}
);
builtPackageJson.type ??= 'module';
writeJsonFile(
`${outDirRelativeToWorkspaceRoot}/package.json`,
builtPackageJson
);
const packageManager = detectPackageManager(context.root);
const lockFile = createLockFile(
builtPackageJson,
context.projectGraph,
packageManager
);
writeFileSync(
`${outDirRelativeToWorkspaceRoot}/${getLockFileName(packageManager)}`,
lockFile,
{
encoding: 'utf-8',
}
);
}
// For buildable libs, copy package.json if it exists.
else if (
options.generatePackageJson !== false &&
!existsSync(distPackageJson) &&
existsSync(libraryPackageJson) &&
rootPackageJson !== libraryPackageJson
) {
await copyAssets(
{
outputPath: outDirRelativeToWorkspaceRoot,
assets: [
{
input: projectRoot,
output: '.',
glob: 'package.json',
},
],
},
context
);
}
if ('on' in watcherOrOutput) {
const iterable = createAsyncIterable<{ success: boolean }>(({ next }) => {
let success = true;
watcherOrOutput.on('event', (event) => {
if (event.code === 'START') {
success = true;
} else if (event.code === 'ERROR') {
success = false;
} else if (event.code === 'END') {
next({ success });
}
// result must be closed when present.
// see https://rollupjs.org/guide/en/#rollupwatch
if ('result' in event && event.result) {
event.result.close();
}
});
});
yield* iterable;
} else {
const output = watcherOrOutput?.['output'] || watcherOrOutput?.[0]?.output;
const fileName = output?.[0]?.fileName || 'main.cjs';
const outfile = resolve(outDirRelativeToWorkspaceRoot, fileName);
yield { success: true, outfile };
iterables.push(iterable);
}
return yield* combineAsyncIterables(iterables.shift(), ...(iterables ?? []));
}
export async function getBuildExtraArgs(

View File

@ -9,4 +9,5 @@ export interface ViteBuildExecutorOptions {
skipTypeCheck?: boolean;
tsConfig?: string;
watch?: boolean;
useEnvironmentsApi?: boolean;
}

View File

@ -67,6 +67,12 @@
"skipPackageManager": {
"type": "boolean",
"description": "Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option."
},
"useEnvironmentsApi": {
"alias": "app",
"type": "boolean",
"description": "Use the new Environments API for building multiple environments at once. Only works with Vite 6.0.0 or higher.",
"default": false
}
},
"definitions": {},