feat(rspack): update executor to be in line with webpack (#28913)

This PR brings the rspack executor `@nx/rspack:rspack` inline with
webpack.

It also prepares the executor to be used with the soon to be implemented
`NxRspackAppPlugin` so that we can support executor and inferred
targets.
This commit is contained in:
Nicholas Cunningham 2024-11-14 11:31:54 -07:00 committed by GitHub
parent d8f9161f85
commit da60c38a34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 582 additions and 79 deletions

View File

@ -151,6 +151,203 @@
"generatePackageJson": {
"type": "boolean",
"description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated."
},
"additionalEntryPoints": {
"type": "array",
"items": {
"type": "object",
"properties": {
"entryName": {
"type": "string",
"description": "Name of the additional entry file."
},
"entryPath": {
"type": "string",
"description": "Path to the additional entry file.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts)"
}
}
}
},
"buildLibsFromSource": {
"type": "boolean",
"description": "Read buildable libraries from source instead of building them separately. If set to `false`, the `tsConfig` option must also be set to remap paths.",
"default": true
},
"extractCss": {
"type": "boolean",
"description": "Extract CSS into a `.css` file."
},
"externalDependencies": {
"oneOf": [
{ "type": "string", "enum": ["none", "all"] },
{ "type": "array", "items": { "type": "string" } }
],
"description": "Dependencies to keep external to the bundle. (`all` (default), `none`, or an array of module names)"
},
"generateIndexHtml": {
"type": "boolean",
"description": "Generates `index.html` file to the output path. This can be turned off if using a webpack plugin to generate HTML such as `html-webpack-plugin`."
},
"memoryLimit": {
"type": "number",
"description": "Memory limit for type checking service process in `MB`."
},
"namedChunks": {
"type": "boolean",
"description": "Names the produced bundles according to their entry file."
},
"outputHashing": {
"type": "string",
"description": "Define the output filename cache-busting hashing mode.",
"enum": ["none", "all", "media", "bundles"]
},
"poll": {
"type": "number",
"description": "Enable and define the file watching poll time period."
},
"polyfills": {
"type": "string",
"description": "Polyfills to load before application",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts|.tsx)"
},
"postcssConfig": {
"type": "string",
"description": "Set a path to PostCSS config that applies to the app and all libs. Defaults to `undefined`, which auto-detects postcss.config.js files in each `app`/`lib` directory."
},
"progress": {
"type": "boolean",
"description": "Log progress to the console while building."
},
"publicPath": {
"type": "string",
"description": "Set a public path for assets resources with absolute paths."
},
"rebaseRootRelative": {
"type": "boolean",
"description": "Whether to rebase absolute path for assets in postcss cli resources."
},
"runtimeChunk": {
"type": "boolean",
"description": "Use a separate bundle containing the runtime."
},
"scripts": {
"type": "array",
"description": "External Scripts which will be included before the main application entry.",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
},
"bundleName": {
"type": "string",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
}
},
"standardRspackConfigFunction": {
"type": "boolean",
"description": "Set to true if the rspack config exports a standard rspack function, not an Nx-specific one. See: https://rspack.dev/config/",
"default": false
},
"statsJson": {
"type": "boolean",
"description": "Generates a 'stats.json' file which can be analyzed using tools such as: 'webpack-bundle-analyzer' See: https://rspack.dev/guide/optimization/analysis"
},
"stylePreprocessorOptions": {
"description": "Options to pass to style preprocessors.",
"type": "object",
"properties": {
"includePaths": {
"description": "Paths to include. Paths will be resolved to project root.",
"type": "array",
"items": { "type": "string" }
}
},
"additionalProperties": false
},
"styles": {
"type": "array",
"description": "External Styles which will be included with the application",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
},
"bundleName": {
"type": "string",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
}
},
"transformers": {
"type": "array",
"description": "List of TypeScript Compiler Transfomers Plugins.",
"aliases": ["tsPlugins"],
"items": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"options": { "type": "object", "additionalProperties": true }
},
"additionalProperties": false,
"required": ["name"]
}
]
}
},
"vendorChunk": {
"type": "boolean",
"description": "Use a separate bundle containing only vendor libraries."
}
},
"required": ["rspackConfig"],
@ -188,6 +385,52 @@
},
{ "type": "string" }
]
},
"extraEntryPoint": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
},
"bundleName": {
"type": "string",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
},
"transformerPattern": {
"oneOf": [
{ "type": "string" },
{
"type": "object",
"properties": {
"name": { "type": "string" },
"options": { "type": "object", "additionalProperties": true }
},
"additionalProperties": false,
"required": ["name"]
}
]
}
},
"presets": []

View File

@ -73,6 +73,11 @@
"type": "boolean",
"description": "Use a separate bundle containing the runtime."
},
"skipTypeChecking": {
"alias": "typeCheck",
"type": "boolean",
"description": "Skip the type checking. Default is `false`."
},
"sourceMap": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"oneOf": [{ "type": "boolean" }, { "type": "string" }]
@ -81,6 +86,10 @@
"type": "boolean",
"description": "Log progress to the console while building."
},
"poll": {
"type": "number",
"description": "Enable and define the file watching poll time period."
},
"assets": {
"type": "array",
"description": "List of static application assets.",

View File

@ -0,0 +1,55 @@
import { join } from 'path';
import { ExecutorContext } from '@nx/devkit';
import { type Configuration } from '@rspack/core';
import {
composePluginsSync,
isNxRspackComposablePlugin,
} from '../../../utils/config';
import { resolveUserDefinedRspackConfig } from '../../../utils/resolve-user-defined-rspack-config';
import { withNx } from '../../../utils/with-nx';
import { withWeb } from '../../../utils/with-web';
import { type NormalizedRspackExecutorSchema } from '../schema';
export async function getRspackConfigs(
options: NormalizedRspackExecutorSchema & { devServer?: any },
context: ExecutorContext
): Promise<Configuration | Configuration[]> {
let userDefinedConfig = resolveUserDefinedRspackConfig(
options.rspackConfig,
options.tsConfig
);
if (typeof userDefinedConfig.then === 'function') {
userDefinedConfig = await userDefinedConfig;
}
const config = (
options.target === 'web'
? composePluginsSync(withNx(options), withWeb(options))
: withNx(options)
)({}, { options, context });
if (
(typeof userDefinedConfig === 'function' &&
isNxRspackComposablePlugin(userDefinedConfig)) ||
!options.standardRspackConfigFunction
) {
// Old behavior, call the Nx-specific rspack config function that user exports
return await userDefinedConfig(config, {
options,
context,
configuration: context.configurationName,
});
} else if (userDefinedConfig) {
if (typeof userDefinedConfig === 'function') {
// assume it's an async standard rspack config function which operates similar to webpack
// https://webpack.js.org/configuration/configuration-types/#exporting-a-promise
return await userDefinedConfig(process.env.NODE_ENV, {});
}
// New behavior, we want the rspack config to export object
return userDefinedConfig;
} else {
// Fallback case, if we cannot find a rspack config path
return config;
}
}

View File

@ -3,11 +3,11 @@ import { createAsyncIterable } from '@nx/devkit/src/utils/async-iterable';
import { printDiagnostics, runTypeCheck } from '@nx/js';
import { Compiler, MultiCompiler, MultiStats, Stats } from '@rspack/core';
import { rmSync } from 'fs';
import * as path from 'path';
import { createCompiler, isMultiCompiler } from '../../utils/create-compiler';
import { isMode } from '../../utils/mode-utils';
import { RspackExecutorSchema } from './schema';
import { normalizeOptions } from './lib/normalize-options';
import { join, resolve } from 'path';
export default async function* runExecutor(
options: RspackExecutorSchema,
@ -16,21 +16,9 @@ export default async function* runExecutor(
process.env.NODE_ENV ??= options.mode ?? 'production';
options.target ??= 'web';
if (isMode(process.env.NODE_ENV)) {
options.mode = process.env.NODE_ENV;
}
if (options.typeCheck) {
await executeTypeCheck(options, context);
}
// Mimic --clean from webpack.
rmSync(path.join(context.root, options.outputPath), {
force: true,
recursive: true,
});
const metadata = context.projectsConfigurations.projects[context.projectName];
const sourceRoot = metadata.sourceRoot;
const normalizedOptions = normalizeOptions(
options,
context.root,
@ -38,6 +26,20 @@ export default async function* runExecutor(
sourceRoot
);
if (isMode(process.env.NODE_ENV)) {
normalizedOptions.mode = process.env.NODE_ENV;
}
if (normalizedOptions.typeCheck) {
await executeTypeCheck(normalizedOptions, context);
}
// Mimic --clean from webpack.
rmSync(join(context.root, normalizedOptions.outputPath), {
force: true,
recursive: true,
});
const compiler = await createCompiler(normalizedOptions, context);
const iterable = createAsyncIterable<{
@ -67,7 +69,7 @@ export default async function* runExecutor(
}
next({
success: !stats.hasErrors(),
outfile: path.resolve(
outfile: resolve(
context.root,
normalizedOptions.outputPath,
'main.js'
@ -103,7 +105,7 @@ export default async function* runExecutor(
}
next({
success: !stats.hasErrors(),
outfile: path.resolve(
outfile: resolve(
context.root,
normalizedOptions.outputPath,
'main.js'
@ -139,7 +141,7 @@ async function executeTypeCheck(
const projectConfiguration =
context.projectGraph.nodes[context.projectName].data;
const result = await runTypeCheck({
workspaceRoot: path.resolve(projectConfiguration.root),
workspaceRoot: resolve(projectConfiguration.root),
tsConfigPath: options.tsConfig,
mode: 'noEmit',
});

View File

@ -1,28 +1,49 @@
import type { Mode } from '@rspack/core';
export interface RspackExecutorSchema {
target?: 'web' | 'node';
main?: string;
index?: string;
tsConfig?: string;
typeCheck?: boolean;
skipTypeChecking?: boolean;
outputPath?: string;
outputFileName?: string;
additionalEntryPoints?: AdditionalEntryPoint[];
assets?: Array<AssetGlobPattern | string>;
baseHref?: string;
buildLibsFromSource?: boolean;
deployUrl?: string;
extractCss?: boolean;
extractLicenses?: boolean;
externalDependencies?: 'all' | 'none' | string[];
fileReplacements?: FileReplacement[];
generateIndexHtml?: boolean;
generatePackageJson?: boolean;
index?: string;
indexHtml?: string;
main?: string;
memoryLimit?: number;
mode?: Mode;
watch?: boolean;
baseHref?: string;
deployUrl?: string;
rspackConfig: string;
namedChunks?: boolean;
optimization?: boolean | OptimizationOptions;
outputFileName?: string;
outputHashing?: any;
outputPath?: string;
poll?: number;
polyfills?: string;
postcssConfig?: string;
progress?: boolean;
publicPath?: string;
rebaseRootRelative?: boolean;
rspackConfig: string;
runtimeChunk?: boolean;
scripts?: Array<ExtraEntryPointClass | string>;
skipTypeChecking?: boolean;
sourceMap?: boolean | string;
assets?: any[];
extractLicenses?: boolean;
fileReplacements?: FileReplacement[];
generatePackageJson?: boolean;
standardRspackConfigFunction?: boolean;
statsJson?: boolean;
stylePreprocessorOptions?: any;
styles?: Array<ExtraEntryPointClass | string>;
target?: 'web' | 'node';
transformers?: TransformerEntry[];
tsConfig?: string;
typeCheck?: boolean;
verbose?: boolean;
vendorChunk?: boolean;
watch?: boolean;
}
export interface AssetGlobPattern {

View File

@ -129,6 +129,144 @@
"generatePackageJson": {
"type": "boolean",
"description": "Generates a `package.json` and pruned lock file with the project's `node_module` dependencies populated for installing in a container. If a `package.json` exists in the project's directory, it will be reused with dependencies populated."
},
"additionalEntryPoints": {
"type": "array",
"items": {
"type": "object",
"properties": {
"entryName": {
"type": "string",
"description": "Name of the additional entry file."
},
"entryPath": {
"type": "string",
"description": "Path to the additional entry file.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts)"
}
}
}
},
"buildLibsFromSource": {
"type": "boolean",
"description": "Read buildable libraries from source instead of building them separately. If set to `false`, the `tsConfig` option must also be set to remap paths.",
"default": true
},
"extractCss": {
"type": "boolean",
"description": "Extract CSS into a `.css` file."
},
"externalDependencies": {
"oneOf": [
{
"type": "string",
"enum": ["none", "all"]
},
{
"type": "array",
"items": {
"type": "string"
}
}
],
"description": "Dependencies to keep external to the bundle. (`all` (default), `none`, or an array of module names)"
},
"generateIndexHtml": {
"type": "boolean",
"description": "Generates `index.html` file to the output path. This can be turned off if using a webpack plugin to generate HTML such as `html-webpack-plugin`."
},
"memoryLimit": {
"type": "number",
"description": "Memory limit for type checking service process in `MB`."
},
"namedChunks": {
"type": "boolean",
"description": "Names the produced bundles according to their entry file."
},
"outputHashing": {
"type": "string",
"description": "Define the output filename cache-busting hashing mode.",
"enum": ["none", "all", "media", "bundles"]
},
"poll": {
"type": "number",
"description": "Enable and define the file watching poll time period."
},
"polyfills": {
"type": "string",
"description": "Polyfills to load before application",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.js|.ts|.tsx)"
},
"postcssConfig": {
"type": "string",
"description": "Set a path to PostCSS config that applies to the app and all libs. Defaults to `undefined`, which auto-detects postcss.config.js files in each `app`/`lib` directory."
},
"progress": {
"type": "boolean",
"description": "Log progress to the console while building."
},
"publicPath": {
"type": "string",
"description": "Set a public path for assets resources with absolute paths."
},
"rebaseRootRelative": {
"type": "boolean",
"description": "Whether to rebase absolute path for assets in postcss cli resources."
},
"runtimeChunk": {
"type": "boolean",
"description": "Use a separate bundle containing the runtime."
},
"scripts": {
"type": "array",
"description": "External Scripts which will be included before the main application entry.",
"items": {
"$ref": "#/definitions/extraEntryPoint"
}
},
"standardRspackConfigFunction": {
"type": "boolean",
"description": "Set to true if the rspack config exports a standard rspack function, not an Nx-specific one. See: https://rspack.dev/config/",
"default": false
},
"statsJson": {
"type": "boolean",
"description": "Generates a 'stats.json' file which can be analyzed using tools such as: 'webpack-bundle-analyzer' See: https://rspack.dev/guide/optimization/analysis"
},
"stylePreprocessorOptions": {
"description": "Options to pass to style preprocessors.",
"type": "object",
"properties": {
"includePaths": {
"description": "Paths to include. Paths will be resolved to project root.",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
},
"styles": {
"type": "array",
"description": "External Styles which will be included with the application",
"items": {
"$ref": "#/definitions/extraEntryPoint"
}
},
"transformers": {
"type": "array",
"description": "List of TypeScript Compiler Transfomers Plugins.",
"aliases": ["tsPlugins"],
"items": {
"$ref": "#/definitions/transformerPattern"
}
},
"vendorChunk": {
"type": "boolean",
"description": "Use a separate bundle containing only vendor libraries."
}
},
"required": ["rspackConfig"],
@ -170,6 +308,59 @@
"type": "string"
}
]
},
"extraEntryPoint": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
},
"bundleName": {
"type": "string",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include.",
"x-completion-type": "file",
"x-completion-glob": "**/*@(.css|.scss|.less|.sass|.styl|.stylus)"
}
]
},
"transformerPattern": {
"oneOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"name": {
"type": "string"
},
"options": {
"type": "object",
"additionalProperties": true
}
},
"additionalProperties": false,
"required": ["name"]
}
]
}
}
}

View File

@ -8,7 +8,6 @@ import {
SwcJsMinimizerRspackPlugin,
CopyRspackPlugin,
RspackOptionsNormalized,
ExternalItem,
} from '@rspack/core';
import { getRootTsConfigPath } from '@nx/js';

View File

@ -5,41 +5,17 @@ import {
MultiCompiler,
rspack,
} from '@rspack/core';
import * as path from 'path';
import { RspackExecutorSchema } from '../executors/rspack/schema';
import { resolveUserDefinedRspackConfig } from './resolve-user-defined-rspack-config';
import { NormalizedRspackExecutorSchema } from '../executors/rspack/schema';
import { getRspackConfigs } from '../executors/rspack/lib/config';
export async function createCompiler(
options: RspackExecutorSchema & {
options: NormalizedRspackExecutorSchema & {
devServer?: any;
},
context: ExecutorContext
): Promise<Compiler | MultiCompiler> {
const pathToConfig = options.rspackConfig;
let userDefinedConfig: any = {};
if (options.tsConfig) {
userDefinedConfig = resolveUserDefinedRspackConfig(
pathToConfig,
options.tsConfig
);
} else {
userDefinedConfig = await import(pathToConfig).then((x) => x.default || x);
}
if (typeof userDefinedConfig.then === 'function') {
userDefinedConfig = await userDefinedConfig;
}
let config: Configuration = {};
if (typeof userDefinedConfig === 'function') {
config = await userDefinedConfig(
{ devServer: options.devServer },
{ options, context }
);
} else {
config = userDefinedConfig;
config.devServer ??= options.devServer;
}
const config = await getRspackConfigs(options, context);
validateConfig(config);
@ -52,7 +28,8 @@ export function isMultiCompiler(
return 'compilers' in compiler;
}
function validateConfig(config: Configuration) {
function validateConfig(config: Configuration | Configuration[]) {
[config].flat().forEach((config) => {
if (!config.entry) {
throw new Error(
'Entry is required. Please set the `main` option in the executor or the `entry` property in the rspack config.'
@ -63,4 +40,5 @@ function validateConfig(config: Configuration) {
'Output is required. Please set the `outputPath` option in the executor or the `output` property in the rspack config.'
);
}
});
}

View File

@ -70,6 +70,11 @@
"type": "boolean",
"description": "Use a separate bundle containing the runtime."
},
"skipTypeChecking": {
"alias": "typeCheck",
"type": "boolean",
"description": "Skip the type checking. Default is `false`."
},
"sourceMap": {
"description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.",
"oneOf": [
@ -85,6 +90,10 @@
"type": "boolean",
"description": "Log progress to the console while building."
},
"poll": {
"type": "number",
"description": "Enable and define the file watching poll time period."
},
"assets": {
"type": "array",
"description": "List of static application assets.",

View File

@ -15,10 +15,6 @@ import {
tap,
} from 'rxjs/operators';
import { resolve } from 'path';
import {
calculateProjectBuildableDependencies,
createTmpTsConfig,
} from '@nx/js/src/utils/buildable-libs-utils';
import { runWebpack } from './lib/run-webpack';
import { deleteOutputDir } from '../../utils/fs';
import { resolveUserDefinedWebpackConfig } from '../../utils/webpack/resolve-user-defined-webpack-config';