feat(rspack): move logic for withWeb to applyWebConfig and bring in line with webpack (#28803)

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

<!-- If this is a particularly complex change or feature addition, you
can request a dedicated Nx release for this pull request branch. Mention
someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they
will confirm if the PR warrants its own release for testing purposes,
and generate it for you if appropriate. -->

## Current Behavior
<!-- This is the behavior we have today -->
`withWeb` from `@nx/rspack` is not reflective of what `@nx/webpack`
does.


## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Bring `withWeb` in line with `@nx/webpack`

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #
This commit is contained in:
Colum Ferry 2024-11-07 02:13:56 +00:00 committed by GitHub
parent 2c9fc572f0
commit fd2e8d0f55
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1502 additions and 162 deletions

View File

@ -26,15 +26,13 @@
"type": "string",
"description": "The tsconfig file to build the project."
},
"typeCheck": {
"skipTypeChecking": {
"alias": "typeCheck",
"type": "boolean",
"description": "Skip the type checking."
},
"indexHtml": {
"type": "string",
"description": "The path to the index.html file."
"description": "Skip the type checking. Default is `false`."
},
"index": {
"alias": "indexHtml",
"type": "string",
"description": "HTML File which will be contain the application.",
"x-completion-type": "file",

View File

@ -65,7 +65,11 @@ describe('rspack e2e', () => {
});
expect(result).toContain('Successfully ran target build');
// Make sure expected files are present.
expect(listFiles(`dist/${project}`)).toHaveLength(5);
/**
* The files that are generated are:
* ["3rdpartylicenses.txt", "assets", "favicon.ico", "index.html", "main.bf7851e6.js", "runtime.e4294127.js"]
*/
expect(listFiles(`dist/${project}`)).toHaveLength(6);
result = runCLI(`test ${project}`);
expect(result).toContain('Successfully ran target test');
@ -83,7 +87,7 @@ describe('rspack e2e', () => {
env: { NODE_ENV: 'production' },
});
expect(result).toContain('Successfully ran target build');
expect(listFiles(`dist/${project}`)).toHaveLength(5); // same length as before
expect(listFiles(`dist/${project}`)).toHaveLength(6); // same length as before
// Generate a new app and check that the files are correct
const app2 = uniq('app2');
@ -116,7 +120,7 @@ describe('rspack e2e', () => {
});
expect(result).toContain('Successfully ran target build');
// Make sure expected files are present.
expect(listFiles(`dist/${app2}`)).toHaveLength(5);
expect(listFiles(`dist/${app2}`)).toHaveLength(6);
result = runCLI(`test ${app2}`);
expect(result).toContain('Successfully ran target test');

View File

@ -39,7 +39,8 @@
"@nx/workspace",
// Imported types only
"@module-federation/sdk",
"@module-federation/enhanced"
"@module-federation/enhanced",
"css-loader"
]
}
]

View File

@ -28,20 +28,27 @@
"@nx/devkit": "file:../devkit",
"@nx/web": "file:../web",
"@phenomnomnominal/tsquery": "~5.0.1",
"@rspack/core": "^1.0.4",
"@rspack/dev-server": "^1.0.4",
"@rspack/plugin-react-refresh": "^1.0.0",
"autoprefixer": "^10.4.9",
"chalk": "~4.1.0",
"css-loader": "^6.4.0",
"enquirer": "~2.3.6",
"express": "^4.19.2",
"http-proxy-middleware": "^3.0.3",
"less-loader": "11.1.0",
"license-webpack-plugin": "^4.0.2",
"loader-utils": "^2.0.3",
"sass": "^1.42.1",
"sass-loader": "^12.2.0",
"stylus-loader": "^7.1.0",
"style-loader": "^3.3.0",
"postcss-import": "~14.1.0",
"postcss-loader": "^8.1.1",
"@rspack/core": "^1.0.4",
"@rspack/dev-server": "^1.0.4",
"@rspack/plugin-react-refresh": "^1.0.0",
"chalk": "~4.1.0",
"postcss": "^8.4.38",
"tsconfig-paths": "^4.1.2",
"tslib": "^2.3.0"
"tslib": "^2.3.0",
"webpack-subresource-integrity": "^5.1.0"
},
"peerDependencies": {
"@module-federation/enhanced": "~0.6.0",

View File

@ -6,8 +6,10 @@ export interface RspackExecutorSchema {
index?: string;
tsConfig?: string;
typeCheck?: boolean;
skipTypeChecking?: boolean;
outputPath?: string;
outputFileName?: string;
index?: string;
indexHtml?: string;
mode?: Mode;
watch?: boolean;
@ -23,6 +25,13 @@ export interface RspackExecutorSchema {
generatePackageJson?: boolean;
}
export interface AssetGlobPattern {
glob: string;
input: string;
output: string;
ignore?: string[];
}
export interface FileReplacement {
replace: string;
with: string;
@ -32,3 +41,11 @@ export interface OptimizationOptions {
scripts: boolean;
styles: boolean;
}
export interface NormalizedRspackExecutorSchema extends RspackExecutorSchema {
outputFileName: string;
assets: AssetGlobPattern[];
root: string;
projectRoot: string;
sourceRoot: string;
}

View File

@ -26,15 +26,13 @@
"type": "string",
"description": "The tsconfig file to build the project."
},
"typeCheck": {
"skipTypeChecking": {
"alias": "typeCheck",
"type": "boolean",
"description": "Skip the type checking."
},
"indexHtml": {
"type": "string",
"description": "The path to the index.html file."
"description": "Skip the type checking. Default is `false`."
},
"index": {
"alias": "indexHtml",
"type": "string",
"description": "HTML File which will be contain the application.",
"x-completion-type": "file",

View File

@ -0,0 +1,459 @@
import {
type RspackPluginInstance,
type Configuration,
type RuleSetRule,
LightningCssMinimizerRspackPlugin,
DefinePlugin,
HtmlRspackPlugin,
CssExtractRspackPlugin,
EnvironmentPlugin,
} from '@rspack/core';
import { instantiateScriptPlugins } from './instantiate-script-plugins';
import { join, resolve } from 'path';
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
import { getOutputHashFormat } from './hash-format';
import { normalizeExtraEntryPoints } from './normalize-entry';
import {
getCommonLoadersForCssModules,
getCommonLoadersForGlobalCss,
getCommonLoadersForGlobalStyle,
} from './loaders/stylesheet-loaders';
import { NormalizedNxAppRspackPluginOptions } from './models';
export function applyWebConfig(
options: NormalizedNxAppRspackPluginOptions,
config: Configuration = {},
{
useNormalizedEntry,
}: {
// rspack.Configuration allows arrays to be set on a single entry
// rspack then normalizes them to { import: "..." } objects
// This option allows use to preserve existing composePlugins behavior where entry.main is an array.
useNormalizedEntry?: boolean;
} = {}
): void {
if (global.NX_GRAPH_CREATION) return;
// Defaults that was applied from executor schema previously.
options.runtimeChunk ??= true; // need this for HMR and other things to work
options.extractCss ??= true;
options.generateIndexHtml ??= true;
options.index = options.index
? join(options.root, options.index)
: join(
options.root,
options.projectGraph.nodes[options.projectName].data.sourceRoot,
'index.html'
);
options.styles ??= [];
options.scripts ??= [];
const isProd =
process.env.NODE_ENV === 'production' || options.mode === 'production';
const plugins: RspackPluginInstance[] = [
new EnvironmentPlugin({
NODE_ENV: isProd ? 'production' : 'development',
}),
];
const stylesOptimization =
typeof options.optimization === 'object'
? options.optimization.styles
: options.optimization;
if (Array.isArray(options.scripts)) {
plugins.push(...instantiateScriptPlugins(options));
}
if (options.index && options.generateIndexHtml) {
plugins.push(
new HtmlRspackPlugin({
template: options.index,
sri: options.subresourceIntegrity ? 'sha256' : undefined,
...(options.baseHref ? { base: { href: options.baseHref } } : {}),
})
);
}
if (options.subresourceIntegrity) {
plugins.push(new SubresourceIntegrityPlugin() as any);
}
const minimizer: RspackPluginInstance[] = [];
if (stylesOptimization) {
minimizer.push(
new LightningCssMinimizerRspackPlugin({
test: /\.(?:css|scss|sass|less|styl)$/,
})
);
}
if (!options.ssr) {
plugins.push(
new DefinePlugin(getClientEnvironment(process.env.NODE_ENV).stringified)
);
}
const entries: { [key: string]: { import: string[] } } = {};
const globalStylePaths: string[] = [];
// Determine hashing format.
const hashFormat = getOutputHashFormat(options.outputHashing as string);
const includePaths: string[] = [];
if (options?.stylePreprocessorOptions?.includePaths?.length > 0) {
options.stylePreprocessorOptions.includePaths.forEach(
(includePath: string) =>
includePaths.push(resolve(options.root, includePath))
);
}
let lessPathOptions: { paths?: string[] } = {};
if (includePaths.length > 0) {
lessPathOptions = {
paths: includePaths,
};
}
// Process global styles.
if (options.styles.length > 0) {
normalizeExtraEntryPoints(options.styles, 'styles').forEach((style) => {
const resolvedPath = style.input.startsWith('.')
? style.input
: resolve(options.root, style.input);
// Add style entry points.
if (entries[style.bundleName]) {
entries[style.bundleName].import.push(resolvedPath);
} else {
entries[style.bundleName] = { import: [resolvedPath] };
}
// Add global css paths.
globalStylePaths.push(resolvedPath);
});
}
const cssModuleRules: RuleSetRule[] = [
{
test: /\.module\.css$/,
exclude: globalStylePaths,
use: getCommonLoadersForCssModules(options, includePaths),
},
{
test: /\.module\.(scss|sass)$/,
exclude: globalStylePaths,
use: [
...getCommonLoadersForCssModules(options, includePaths),
{
loader: require.resolve('sass-loader'),
options: {
implementation: require('sass'),
sassOptions: {
fiber: false,
precision: 8,
includePaths,
},
},
},
],
},
{
test: /\.module\.less$/,
exclude: globalStylePaths,
use: [
...getCommonLoadersForCssModules(options, includePaths),
{
loader: require.resolve('less-loader'),
options: {
lessOptions: {
paths: includePaths,
},
},
},
],
},
{
test: /\.module\.styl$/,
exclude: globalStylePaths,
use: [
...getCommonLoadersForCssModules(options, includePaths),
{
loader: join(
__dirname,
'../../../utils/webpack/deprecated-stylus-loader.js'
),
options: {
stylusOptions: {
include: includePaths,
},
},
},
],
},
];
const globalCssRules: RuleSetRule[] = [
{
test: /\.css$/,
exclude: globalStylePaths,
use: getCommonLoadersForGlobalCss(options, includePaths),
},
{
test: /\.scss$|\.sass$/,
exclude: globalStylePaths,
use: [
...getCommonLoadersForGlobalCss(options, includePaths),
{
loader: require.resolve('sass-loader'),
options: {
implementation: require('sass'),
sourceMap: !!options.sourceMap,
sassOptions: {
fiber: false,
// bootstrap-sass requires a minimum precision of 8
precision: 8,
includePaths,
},
},
},
],
},
{
test: /\.less$/,
exclude: globalStylePaths,
use: [
...getCommonLoadersForGlobalCss(options, includePaths),
{
loader: require.resolve('less-loader'),
options: {
sourceMap: !!options.sourceMap,
lessOptions: {
javascriptEnabled: true,
...lessPathOptions,
},
},
},
],
},
{
test: /\.styl$/,
exclude: globalStylePaths,
use: [
...getCommonLoadersForGlobalCss(options, includePaths),
{
loader: join(
__dirname,
'../../../utils/webpack/deprecated-stylus-loader.js'
),
options: {
sourceMap: !!options.sourceMap,
stylusOptions: {
include: includePaths,
},
},
},
],
},
];
const globalStyleRules: RuleSetRule[] = [
{
test: /\.css$/,
include: globalStylePaths,
use: getCommonLoadersForGlobalStyle(options, includePaths),
},
{
test: /\.scss$|\.sass$/,
include: globalStylePaths,
use: [
...getCommonLoadersForGlobalStyle(options, includePaths),
{
loader: require.resolve('sass-loader'),
options: {
implementation: require('sass'),
sourceMap: !!options.sourceMap,
sassOptions: {
fiber: false,
// bootstrap-sass requires a minimum precision of 8
precision: 8,
includePaths,
},
},
},
],
},
{
test: /\.less$/,
include: globalStylePaths,
use: [
...getCommonLoadersForGlobalStyle(options, includePaths),
{
loader: require.resolve('less-loader'),
options: {
sourceMap: !!options.sourceMap,
lessOptions: {
javascriptEnabled: true,
...lessPathOptions,
},
},
},
],
},
{
test: /\.styl$/,
include: globalStylePaths,
use: [
...getCommonLoadersForGlobalStyle(options, includePaths),
{
loader: join(
__dirname,
'../../../utils/webpack/deprecated-stylus-loader.js'
),
options: {
sourceMap: !!options.sourceMap,
stylusOptions: {
include: includePaths,
},
},
},
],
},
];
const rules: RuleSetRule[] = [
{
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
oneOf: [...cssModuleRules, ...globalCssRules, ...globalStyleRules],
},
];
if (options.extractCss) {
plugins.push(
// extract global css from js files into own css file
new CssExtractRspackPlugin({
filename: `[name]${hashFormat.extract}.css`,
})
);
}
config.output = {
...(config.output ?? {}),
assetModuleFilename: '[name].[contenthash:20][ext]',
crossOriginLoading: options.subresourceIntegrity
? ('anonymous' as const)
: (false as const),
};
// In case users customize their webpack config with unsupported entry.
if (typeof config.entry === 'function')
throw new Error('Entry function is not supported. Use an object.');
if (typeof config.entry === 'string')
throw new Error('Entry string is not supported. Use an object.');
if (Array.isArray(config.entry))
throw new Error('Entry array is not supported. Use an object.');
Object.entries(entries).forEach(([entryName, entryData]) => {
if (useNormalizedEntry) {
config.entry[entryName] = { import: entryData.import };
} else {
config.entry[entryName] = entryData.import;
}
});
config.optimization = {
...(config.optimization ?? {}),
minimizer: [...(config.optimization?.minimizer ?? []), ...minimizer],
emitOnErrors: false,
moduleIds: 'deterministic' as const,
runtimeChunk: options.runtimeChunk ? { name: 'runtime' } : false,
splitChunks: {
defaultSizeTypes:
config.optimization?.splitChunks !== false
? config.optimization?.splitChunks?.defaultSizeTypes
: ['...'],
maxAsyncRequests: Infinity,
cacheGroups: {
default: !!options.commonChunk && {
chunks: 'async' as const,
minChunks: 2,
priority: 10,
},
common: !!options.commonChunk && {
name: 'common',
chunks: 'async' as const,
minChunks: 2,
enforce: true,
priority: 5,
},
vendors: false as const,
vendor: !!options.vendorChunk && {
name: 'vendor',
chunks: (chunk) => chunk.name === 'main',
enforce: true,
test: /[\\/]node_modules[\\/]/,
},
},
},
};
config.resolve.mainFields = ['browser', 'module', 'main'];
config.module = {
...(config.module ?? {}),
rules: [
...(config.module.rules ?? []),
// Images: Inline small images, and emit a separate file otherwise.
{
test: /\.(avif|bmp|gif|ico|jpe?g|png|webp)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10_000, // 10 kB
},
},
},
// SVG: same as image but we need to separate it so it can be swapped for SVGR in the React plugin.
{
test: /\.svg$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10_000, // 10 kB
},
},
},
// Fonts: Emit separate file and export the URL.
{
test: /\.(eot|otf|ttf|woff|woff2)$/,
type: 'asset/resource',
},
...rules,
],
};
config.plugins ??= [];
config.plugins.push(...plugins);
}
function getClientEnvironment(mode?: string) {
// Grab NODE_ENV and NX_PUBLIC_* environment variables and prepare them to be
// injected into the application via DefinePlugin in webpack configuration.
const nxPublicKeyRegex = /^NX_PUBLIC_/i;
const raw = Object.keys(process.env)
.filter((key) => nxPublicKeyRegex.test(key))
.reduce((env, key) => {
env[key] = process.env[key];
return env;
}, {});
// Stringify all values so we can feed into webpack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);
return env;
}, {}),
};
return { stringified };
}

View File

@ -0,0 +1,31 @@
import { posix } from 'path';
import { getHashDigest, interpolateName } from 'loader-utils';
export function getCSSModuleLocalIdent(
ctx,
localIdentName,
localName,
options
) {
// Use the filename or folder name, based on some uses the index.js / index.module.(css|scss|sass) project style
const fileNameOrFolder = ctx.resourcePath.match(
/index\.module\.(css|scss|sass)$/
)
? '[folder]'
: '[name]';
// Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique.
const hash = getHashDigest(
posix.relative(ctx.rootContext, ctx.resourcePath) + localName,
'md5',
'base64',
5
);
// Use loaderUtils to find the file or folder name
const className = interpolateName(
ctx,
`${fileNameOrFolder}_${localName}__${hash}`,
options
);
// Remove the .module that appears in every classname when based on the file and replace all "." with "_".
return className.replace('.module_', '_').replace(/\./g, '_');
}

View File

@ -0,0 +1,26 @@
export interface HashFormat {
chunk: string;
extract: string;
file: string;
script: string;
}
export function getOutputHashFormat(option: string, length = 20): HashFormat {
const hashFormats: { [option: string]: HashFormat } = {
none: { chunk: '', extract: '', file: '', script: '' },
media: { chunk: '', extract: '', file: `.[hash:${length}]`, script: '' },
bundles: {
chunk: `.[chunkhash:${length}]`,
extract: `.[contenthash:${length}]`,
file: '',
script: `.[contenthash:${length}]`,
},
all: {
chunk: `.[chunkhash:${length}]`,
extract: `.[contenthash:${length}]`,
file: `.[contenthash:${length}]`,
script: `.[contenthash:${length}]`,
},
};
return hashFormats[option] || hashFormats['none'];
}

View File

@ -0,0 +1,55 @@
import * as path from 'path';
import type { RspackPluginInstance } from '@rspack/core';
import { getOutputHashFormat } from './hash-format';
import { ScriptsRspackPlugin } from './plugins/scripts-rspack-plugin';
import { normalizeExtraEntryPoints } from './normalize-entry';
export function instantiateScriptPlugins(options: any): RspackPluginInstance[] {
// process global scripts
const globalScriptsByBundleName = normalizeExtraEntryPoints(
options.scripts || [],
'scripts'
).reduce(
(
prev: { inject: boolean; bundleName: string; paths: string[] }[],
curr
) => {
const bundleName = curr.bundleName;
const resolvedPath = path.resolve(options.root, curr.input);
const existingEntry = prev.find((el) => el.bundleName === bundleName);
if (existingEntry) {
existingEntry.paths.push(resolvedPath);
} else {
prev.push({
inject: curr.inject,
bundleName,
paths: [resolvedPath],
});
}
return prev;
},
[]
);
const hashFormat = getOutputHashFormat(options.outputHashing as string);
const plugins = [];
// Add a new asset for each entry.
globalScriptsByBundleName.forEach((script) => {
const hash = script.inject ? hashFormat.script : '';
const bundleName = script.bundleName;
plugins.push(
new ScriptsRspackPlugin({
name: bundleName,
sourceMap: !!options.sourceMap,
filename: `${path.basename(bundleName)}${hash}.js`,
scripts: script.paths,
basePath: options.sourceRoot,
})
);
});
return plugins;
}

View File

@ -0,0 +1,145 @@
import * as path from 'path';
import autoprefixer = require('autoprefixer');
import postcssImports = require('postcss-import');
import { CssExtractRspackPlugin } from '@rspack/core';
import { getCSSModuleLocalIdent } from '../get-css-module-local-ident';
import { getOutputHashFormat } from '../hash-format';
import { PostcssCliResources } from '../plugins/postcss-cli-resources';
interface PostcssOptions {
(loader: any): any;
config?: string;
}
export function getCommonLoadersForCssModules(
options: any,
includePaths: string[]
) {
// load component css as raw strings
return [
{
loader: options.extractCss
? CssExtractRspackPlugin.loader
: require.resolve('style-loader'),
},
{
loader: require.resolve('css-loader'),
options: {
modules: {
mode: 'local',
getLocalIdent: getCSSModuleLocalIdent,
},
importLoaders: 1,
},
},
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require('postcss'),
postcssOptions: postcssOptionsCreator(options, {
includePaths,
forCssModules: true,
}),
},
},
];
}
export function getCommonLoadersForGlobalCss(
options: any,
includePaths: string[]
) {
return [
{
loader: options.extractCss
? CssExtractRspackPlugin.loader
: require.resolve('style-loader'),
},
{ loader: require.resolve('css-loader'), options: { url: false } },
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require('postcss'),
postcssOptions: postcssOptionsCreator(options, {
includePaths,
}),
},
},
];
}
export function getCommonLoadersForGlobalStyle(
options: any,
includePaths: string[]
) {
return [
{
loader: options.extractCss
? CssExtractRspackPlugin.loader
: require.resolve('style-loader'),
options: { esModule: true },
},
{ loader: require.resolve('css-loader'), options: { url: false } },
{
loader: require.resolve('postcss-loader'),
options: {
implementation: require('postcss'),
postcssOptions: postcssOptionsCreator(options, {
includePaths,
}),
},
},
];
}
function postcssOptionsCreator(
options: any,
{
includePaths,
forCssModules = false,
}: {
includePaths: string[];
forCssModules?: boolean;
}
) {
const hashFormat = getOutputHashFormat(options.outputHashing as string);
// PostCSS options depend on the webpack loader, but we need to set the `config` path as a string due to this check:
// https://github.com/webpack-contrib/postcss-loader/blob/0d342b1/src/utils.js#L36
const postcssOptions: PostcssOptions = (loader) => ({
map: options.sourceMap &&
options.sourceMap !== 'hidden' && {
inline: true,
annotation: false,
},
plugins: [
postcssImports({
addModulesDirectories: includePaths,
resolve: (url: string) => (url.startsWith('~') ? url.slice(1) : url),
}),
...(forCssModules
? []
: [
PostcssCliResources({
baseHref: options.baseHref,
deployUrl: options.deployUrl,
loader,
filename: `[name]${hashFormat.file}.[ext]`,
publicPath: options.publicPath,
rebaseRootRelative: options.rebaseRootRelative,
}),
autoprefixer(),
]),
],
});
// If a path to postcssConfig is passed in, set it for app and all libs, otherwise
// use automatic detection.
if (typeof options.postcssConfig === 'string') {
postcssOptions.config = path.join(options.root, options.postcssConfig);
}
return postcssOptions;
}

View File

@ -0,0 +1,241 @@
import type { Mode } from '@rspack/core';
import type { ProjectGraph } from '@nx/devkit';
import type { AssetGlob } from '@nx/js/src/utils/assets/assets';
export interface AssetGlobPattern {
glob: string;
input: string;
output: string;
ignore?: string[];
}
export interface ExtraEntryPointClass {
bundleName?: string;
inject?: boolean;
input: string;
lazy?: boolean;
}
export interface FileReplacement {
replace: string;
with: string;
}
export interface AdditionalEntryPoint {
entryName: string;
entryPath: string;
}
export interface TransformerPlugin {
name: string;
options: Record<string, unknown>;
}
export type TransformerEntry = string | TransformerPlugin;
export interface OptimizationOptions {
scripts: boolean;
styles: boolean;
}
export interface NxAppRspackPluginOptions {
/**
* The tsconfig file for the project. e.g. `tsconfig.json`
*/
tsConfig?: string;
/**
* The entry point for the bundle. e.g. `src/main.ts`
*/
main?: string;
/**
* Secondary entry points for the bundle.
*/
additionalEntryPoints?: AdditionalEntryPoint[];
/**
* Assets to be copied over to the output path.
*/
assets?: Array<AssetGlob | string>;
/**
* Set <base href> for the resulting index.html.
*/
baseHref?: string;
/**
* Build the libraries from source. Default is `true`.
*/
buildLibsFromSource?: boolean;
commonChunk?: boolean;
/**
* Delete the output path before building.
*/
deleteOutputPath?: boolean;
/**
* The deploy path for the application. e.g. `/my-app/`
*/
deployUrl?: string;
/**
* Define external packages that will not be bundled.
* Use `all` to exclude all 3rd party packages, and `none` to bundle all packages.
* Use an array to exclude specific packages from the bundle.
* Default is `none`.
*/
externalDependencies?: 'all' | 'none' | string[];
/**
* Extract CSS as an external file. Default is `true`.
*/
extractCss?: boolean;
/**
* Extract licenses from 3rd party modules and add them to the output.
*/
extractLicenses?: boolean;
/**
* Replace files at build time. e.g. `[{ "replace": "src/a.dev.ts", "with": "src/a.prod.ts" }]`
*/
fileReplacements?: FileReplacement[];
/**
* Generate an `index.html` file if `index.html` is passed. Default is `true`
*/
generateIndexHtml?: boolean;
/**
* Generate a `package.json` file for the bundle. Useful for Node applications.
*/
generatePackageJson?: boolean;
/**
* Path to the `index.html`.
*/
index?: string;
/**
* Mode to run the build in.
*/
mode?: Mode;
/**
* Set the memory limit for the type-checking process. Default is `2048`.
*/
memoryLimit?: number;
/**
* Use the source file name in output chunks. Useful for development or for Node.
*/
namedChunks?: boolean;
/**
* Optimize the bundle using Terser.
*/
optimization?: boolean | OptimizationOptions;
/**
* Specify the output filename for the bundle. Useful for Node applications that use `@nx/js:node` to serve.
*/
outputFileName?: string;
/**
* Use file hashes in the output filenames. Recommended for production web applications.
*/
outputHashing?: any;
/**
* Override `output.path` in webpack configuration. This setting is not recommended and exists for backwards compatibility.
*/
outputPath?: string;
/**
* Override `watchOptions.poll` in webpack configuration. This setting is not recommended and exists for backwards compatibility.
*/
poll?: number;
/**
* The polyfill file to use. Useful for supporting legacy browsers. e.g. `src/polyfills.ts`
*/
polyfills?: string;
/**
* Manually set the PostCSS configuration file. By default, PostCSS will look for `postcss.config.js` in the directory.
*/
postcssConfig?: string;
/**
* Display build progress in the terminal.
*/
progress?: boolean;
/**
* Add an additional chunk for the Webpack runtime. Defaults to `true` when `target === 'web'`.
*/
runtimeChunk?: boolean;
/**
* External scripts that will be included before the main application entry.
*/
scripts?: Array<ExtraEntryPointClass | string>;
/**
* Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option.
*/
skipOverrides?: boolean;
/**
* Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option.
*/
skipPackageManager?: boolean;
/**
* Skip type checking. Default is `false`.
*/
skipTypeChecking?: boolean;
/**
* Skip type checking. Default is `false`.
*/
typeCheck?: boolean;
/**
* Generate source maps.
*/
sourceMap?: boolean | string;
/**
* When `true`, `process.env.NODE_ENV` will be excluded from the bundle. Useful for building a web application to run in a Node environment.
*/
ssr?: boolean;
/**
* Generate a `stats.json` file which can be analyzed using tools such as `webpack-bundle-analyzer`.
*/
statsJson?: boolean;
/**
* Options for the style preprocessor. e.g. `{ "includePaths": [] }` for SASS.
*/
stylePreprocessorOptions?: any;
/**
* External stylesheets that will be included with the application.
*/
styles?: Array<ExtraEntryPointClass | string>;
/**
* Enables the use of subresource integrity validation.
*/
subresourceIntegrity?: boolean;
/**
* Override the `target` option in webpack configuration. This setting is not recommended and exists for backwards compatibility.
*/
target?: string | string[];
/**
* List of TypeScript Compiler Transformers Plugins.
*/
transformers?: TransformerEntry[];
/**
* Generate a separate vendor chunk for 3rd party packages.
*/
vendorChunk?: boolean;
/**
* Log additional information for debugging purposes.
*/
verbose?: boolean;
/**
* Watch for file changes.
*/
watch?: boolean;
/**
* Set a public path for assets resources with absolute paths.
*/
publicPath?: string;
/**
* Whether to rebase absolute path for assets in postcss cli resources.
*/
rebaseRootRelative?: boolean;
}
export interface NormalizedNxAppRspackPluginOptions
extends NxAppRspackPluginOptions {
projectName: string;
root: string;
projectRoot: string;
sourceRoot: string;
configurationName: string;
targetName: string;
projectGraph: ProjectGraph;
outputFileName: string;
assets: AssetGlobPattern[];
}

View File

@ -0,0 +1,30 @@
import { ExtraEntryPoint, NormalizedEntryPoint } from '../../utils/model';
export function normalizeExtraEntryPoints(
extraEntryPoints: ExtraEntryPoint[],
defaultBundleName: string
): NormalizedEntryPoint[] {
return extraEntryPoints.map((entry) => {
let normalizedEntry;
if (typeof entry === 'string') {
normalizedEntry = {
input: entry,
inject: true,
bundleName: defaultBundleName,
};
} else {
const { inject = true, ...newEntry } = entry;
let bundleName;
if (entry.bundleName) {
bundleName = entry.bundleName;
} else {
bundleName = defaultBundleName;
}
normalizedEntry = { ...newEntry, bundleName };
}
return normalizedEntry;
});
}

View File

@ -0,0 +1,198 @@
import { interpolateName } from 'loader-utils';
import * as path from 'path';
import type { Declaration } from 'postcss';
import * as url from 'node:url';
import type { LoaderContext } from '@rspack/core';
function wrapUrl(url: string): string {
let wrappedUrl;
const hasSingleQuotes = url.indexOf("'") >= 0;
if (hasSingleQuotes) {
wrappedUrl = `"${url}"`;
} else {
wrappedUrl = `'${url}'`;
}
return `url(${wrappedUrl})`;
}
export interface PostcssCliResourcesOptions {
baseHref?: string;
deployUrl?: string;
resourcesOutputPath?: string;
rebaseRootRelative?: boolean;
filename: string;
loader: LoaderContext<unknown>;
publicPath: string;
}
async function resolve(
file: string,
base: string,
resolver: (file: string, base: string) => Promise<boolean | string>
): Promise<boolean | string> {
try {
return await resolver(`./${file}`, base);
} catch {
return resolver(file, base);
}
}
module.exports.postcss = true;
export function PostcssCliResources(options: PostcssCliResourcesOptions) {
const {
deployUrl = '',
baseHref = '',
resourcesOutputPath = '',
rebaseRootRelative = false,
filename,
loader,
publicPath = '',
} = options;
const dedupeSlashes = (url: string) => url.replace(/\/\/+/g, '/');
const process = async (
inputUrl: string,
context: string,
resourceCache: Map<string, string>
) => {
// If root-relative, absolute or protocol relative url, leave as is
if (/^((?:\w+:)?\/\/|data:|chrome:|#)/.test(inputUrl)) {
return inputUrl;
}
if (!rebaseRootRelative && /^\//.test(inputUrl)) {
return inputUrl;
}
// If starts with a caret, remove and return remainder
// this supports bypassing asset processing
if (inputUrl.startsWith('^')) {
return inputUrl.slice(1);
}
const cacheKey = path.resolve(context, inputUrl);
const cachedUrl = resourceCache.get(cacheKey);
if (cachedUrl) {
return cachedUrl;
}
if (inputUrl.startsWith('~')) {
inputUrl = inputUrl.slice(1);
}
if (inputUrl.startsWith('/')) {
let outputUrl = '';
if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) {
// If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is.
outputUrl = `${deployUrl.replace(/\/$/, '')}${inputUrl}`;
} else if (baseHref.match(/:\/\//)) {
// If baseHref contains a scheme, include it as is.
outputUrl =
baseHref.replace(/\/$/, '') +
dedupeSlashes(`/${deployUrl}/${inputUrl}`);
} else {
// Join together base-href, deploy-url and the original URL.
outputUrl = dedupeSlashes(
`/${baseHref}/${deployUrl}/${publicPath}/${inputUrl}`
);
}
resourceCache.set(cacheKey, outputUrl);
return outputUrl;
}
const { pathname, hash, search } = url.parse(inputUrl.replace(/\\/g, '/'));
const resolver = (file: string, base: string) =>
new Promise<boolean | string>((resolve, reject) => {
loader.resolve(base, decodeURI(file), (err, result) => {
if (err) {
reject(err);
return;
}
resolve(result);
});
});
const result = await resolve(pathname as string, context, resolver);
return new Promise<boolean | string>((resolve, reject) => {
loader.fs.readFile(result as string, (err: Error, content: Buffer) => {
if (err) {
reject(err);
return;
}
let outputPath = interpolateName(
{ resourcePath: result } as LoaderContext<unknown>,
filename,
{ content }
);
if (resourcesOutputPath) {
outputPath = path.posix.join(resourcesOutputPath, outputPath);
}
loader.addDependency(result as string);
loader.emitFile(outputPath, content, undefined);
let outputUrl = outputPath.replace(/\\/g, '/');
if (hash || search) {
outputUrl = url.format({ pathname: outputUrl, hash, search });
}
const loaderOptions: any = loader.loaders[loader.loaderIndex].options;
if (deployUrl && loaderOptions.ident !== 'extracted') {
outputUrl = url.resolve(deployUrl, outputUrl);
}
resourceCache.set(cacheKey, outputUrl);
resolve(outputUrl);
});
});
};
return {
postcssPlugin: 'postcss-cli-resources',
Once(root) {
const urlDeclarations: Array<Declaration> = [];
/**
* TODO: Explore if this can be rewritten using the new `Declaration()`
* listener added in postcss v8
*/
root.walkDecls((decl) => {
if (decl.value && decl.value.includes('url')) {
urlDeclarations.push(decl);
}
});
if (urlDeclarations.length === 0) {
return;
}
const resourceCache = new Map<string, string>();
return Promise.all(
urlDeclarations.map(async (decl) => {
const value = decl.value;
const urlRegex = /url(?:\(\s*(['"]?))(.*?)(?:\1\s*\))/g;
const segments: string[] = [];
let match;
let lastIndex = 0;
let modified = false;
// We want to load it relative to the file that imports
const inputFile = decl.source && decl.source.input.file;
const context =
(inputFile && path.dirname(inputFile)) || loader.context;
while ((match = urlRegex.exec(value))) {
const originalUrl = match[2];
let processedUrl;
try {
processedUrl = await process(originalUrl, context, resourceCache);
} catch (err) {
loader.emitError(decl.error(err.message, { word: originalUrl }));
continue;
}
if (lastIndex < match.index) {
segments.push(value.slice(lastIndex, match.index));
}
if (!processedUrl || originalUrl === processedUrl) {
segments.push(match[0]);
} else {
segments.push(wrapUrl(processedUrl));
modified = true;
}
lastIndex = match.index + match[0].length;
}
if (lastIndex < value.length) {
segments.push(value.slice(lastIndex));
}
if (modified) {
decl.value = segments.join('');
}
})
);
},
};
}

View File

@ -0,0 +1,127 @@
import { interpolateName } from 'loader-utils';
import * as path from 'path';
import {
sources,
EntryPlugin,
type Compiler,
type Compilation,
} from '@rspack/core';
export interface ScriptsRspackPluginOptions {
name: string;
sourceMap: boolean;
scripts: string[];
filename: string;
basePath: string;
}
interface ScriptOutput {
filename: string;
source: sources.Source;
}
function addDependencies(compilation: any, scripts: string[]): void {
for (const script of scripts) {
compilation.fileDependencies.add(script);
}
}
function hook(
compiler: any,
action: (compilation: any, callback: (err?: Error) => void) => void
) {
compiler.hooks.thisCompilation.tap(
'scripts-rspack-plugin',
(compilation: any) => {
compilation.hooks.additionalAssets.tapAsync(
'scripts-rspack-plugin',
(callback: (err?: Error) => void) => action(compilation, callback)
);
}
);
}
export class ScriptsRspackPlugin {
private _lastBuildTime?: number;
private _cachedOutput?: ScriptOutput;
constructor(private options: Partial<ScriptsRspackPluginOptions> = {}) {}
private _insertOutput(
compiler: Compiler,
compilation: Compilation,
{ filename, source }: ScriptOutput,
cached = false
) {
new EntryPlugin(compiler.context, this.options.name).apply(compiler);
compilation.assets[filename] = source;
}
apply(compiler: Compiler): void {
if (!this.options.scripts || this.options.scripts.length === 0) {
return;
}
const scripts = this.options.scripts
.filter((script) => !!script)
.map((script) => path.resolve(this.options.basePath || '', script));
hook(compiler, (compilation: Compilation, callback) => {
const sourceGetters = scripts.map((fullPath) => {
return new Promise<sources.Source>((resolve, reject) => {
compilation.inputFileSystem.readFile(
fullPath,
(err: Error, data: Buffer) => {
if (err) {
reject(err);
return;
}
const content = data.toString();
let source;
if (this.options.sourceMap) {
// TODO: Look for source map file (for '.min' scripts, etc.)
let adjustedPath = fullPath;
if (this.options.basePath) {
adjustedPath = path.relative(this.options.basePath, fullPath);
}
source = new sources.OriginalSource(content, adjustedPath);
} else {
source = new sources.RawSource(content);
}
resolve(source);
}
);
});
});
Promise.all(sourceGetters)
.then((_sources) => {
const concatSource = new sources.ConcatSource();
_sources.forEach((source) => {
concatSource.add(source);
concatSource.add('\n;');
});
const combinedSource = new sources.CachedSource(concatSource);
const filename = interpolateName(
{ resourcePath: 'scripts.js' },
this.options.filename as string,
{ content: combinedSource.source() }
);
const output = { filename, source: combinedSource };
this._insertOutput(compiler, compilation, output);
this._cachedOutput = output;
addDependencies(compilation, scripts);
callback();
})
.catch((err: Error) => callback(err));
});
}
}

View File

@ -1,7 +1,13 @@
import type { ExecutorContext } from '@nx/devkit';
import {
ExecutorContext,
readCachedProjectGraph,
readProjectsConfigurationFromProjectGraph,
workspaceRoot,
} from '@nx/devkit';
import type { Configuration } from '@rspack/core';
import { readNxJson } from 'nx/src/config/configuration';
import { SharedConfigContext } from './model';
import { NormalizedRspackExecutorSchema } from '../executors/rspack/schema';
export const nxRspackComposablePlugin = 'nxRspackComposablePlugin';
@ -12,7 +18,7 @@ export function isNxRspackComposablePlugin(
}
export interface NxRspackExecutionContext {
options: unknown;
options: NormalizedRspackExecutorSchema;
context: ExecutorContext;
configuration?: string;
}
@ -27,23 +33,82 @@ export interface AsyncNxComposableRspackPlugin {
| Promise<Configuration>;
}
export function composePlugins(...plugins: any[]) {
return Object.defineProperty(
export function composePlugins(
...plugins: (
| NxComposableRspackPlugin
| AsyncNxComposableRspackPlugin
| Promise<NxComposableRspackPlugin | AsyncNxComposableRspackPlugin>
)[]
) {
return Object.assign(
async function combined(
config: Configuration,
ctx: SharedConfigContext
ctx: NxRspackExecutionContext
): Promise<Configuration> {
// Rspack may be calling us as a standard config function.
// Build up Nx context from environment variables.
// This is to enable `@nx/webpack/plugin` to work with existing projects.
if (ctx['env']) {
ensureNxRspackExecutionContext(ctx);
// Build this from scratch since what webpack passes us is the env, not config,
// and `withNX()` creates a new config object anyway.
config = {};
}
for (const plugin of plugins) {
const fn = await plugin;
config = await fn(config, ctx);
}
return config;
},
nxRspackComposablePlugin,
{
value: true,
enumerable: false,
writable: false,
[nxRspackComposablePlugin]: true,
}
);
}
export function composePluginsSync(...plugins: NxComposableRspackPlugin[]) {
return Object.assign(
function combined(
config: Configuration,
ctx: NxRspackExecutionContext
): Configuration {
for (const plugin of plugins) {
config = plugin(config, ctx);
}
return config;
},
{
[nxRspackComposablePlugin]: true,
}
);
}
function ensureNxRspackExecutionContext(ctx: NxRspackExecutionContext): void {
const projectName = process.env.NX_TASK_TARGET_PROJECT;
const targetName = process.env.NX_TASK_TARGET_TARGET;
const configurationName = process.env.NX_TASK_TARGET_CONFIGURATION;
const projectGraph = readCachedProjectGraph();
const projectNode = projectGraph.nodes[projectName];
ctx.options ??= {
root: workspaceRoot,
projectRoot: projectNode.data.root,
sourceRoot: projectNode.data.sourceRoot ?? projectNode.data.root,
// These aren't actually needed since NxRspackPlugin and withNx both support them being undefined.
assets: undefined,
outputFileName: undefined,
rspackConfig: undefined,
};
ctx.context ??= {
projectName,
targetName,
configurationName,
projectsConfigurations:
readProjectsConfigurationFromProjectGraph(projectGraph),
nxJsonConfiguration: readNxJson(workspaceRoot),
cwd: process.cwd(),
root: workspaceRoot,
isVerbose: process.env['NX_VERBOSE_LOGGING'] === 'true',
projectGraph,
};
}

View File

@ -172,6 +172,7 @@ export function addOrChangeBuildTarget(
// If standalone project then use the project's name in dist.
project.root === '.' ? project.name : project.root
),
index: joinPathFragments(project.root, 'src/index.html'),
main: determineMain(tree, options),
tsConfig: determineTsConfig(tree, options),
rspackConfig: joinPathFragments(project.root, 'rspack.config.js'),

View File

@ -1,7 +1,19 @@
import { ExecutorContext } from '@nx/devkit';
import { RspackExecutorSchema } from '../executors/rspack/schema';
export interface SharedConfigContext {
options: RspackExecutorSchema;
context: ExecutorContext;
export interface ExtraEntryPointClass {
bundleName?: string;
inject?: boolean;
input: string;
lazy?: boolean;
}
export type ExtraEntryPoint = ExtraEntryPointClass | string;
export type NormalizedEntryPoint = Required<ExtraEntryPointClass>;
export interface EmittedFile {
id?: string;
name?: string;
file: string;
extension: string;
initial: boolean;
asset?: boolean;
}

View File

@ -1,10 +1,10 @@
import { DefinePlugin } from '@rspack/core';
import { SharedConfigContext } from '../../model';
import {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
} from '../models';
import { getModuleFederationConfig } from './utils';
import { NxRspackExecutionContext } from '../../config';
export async function withModuleFederationForSSR(
options: ModuleFederationConfig,
@ -19,7 +19,7 @@ export async function withModuleFederationForSSR(
isServer: true,
});
return (config, { context }: SharedConfigContext) => {
return (config, { context }: NxRspackExecutionContext) => {
config.target = 'async-node';
config.output.uniqueName = options.name;
config.optimization = {

View File

@ -1,12 +1,12 @@
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
import type { Configuration } from '@rspack/core';
import { DefinePlugin } from '@rspack/core';
import { SharedConfigContext } from '../../model';
import {
ModuleFederationConfig,
NxModuleFederationConfigOverride,
} from '../models';
import { getModuleFederationConfig } from './utils';
import { NxRspackExecutionContext } from '../../config';
const isVarOrWindow = (libType?: string) =>
libType === 'var' || libType === 'window';
@ -31,7 +31,7 @@ export async function withModuleFederation(
return function makeConfig(
config: Configuration,
{ context }: SharedConfigContext
{ context }: NxRspackExecutionContext
): Configuration {
config.output.uniqueName = options.name;
config.output.publicPath = 'auto';

View File

@ -25,6 +25,12 @@ export async function readRspackOptions(
root: workspaceRoot,
projectRoot: '',
sourceRoot: '',
outputFileName: '',
assets: [],
main: '',
tsConfig: '',
outputPath: '',
rspackConfig: '',
},
context: {
root: workspaceRoot,

View File

@ -11,13 +11,13 @@ import * as path from 'path';
import { join } from 'path';
import { GeneratePackageJsonPlugin } from '../plugins/generate-package-json-plugin';
import { getCopyPatterns } from './get-copy-patterns';
import { SharedConfigContext } from './model';
import { normalizeAssets } from './normalize-assets';
import { NxRspackExecutionContext } from './config';
export function withNx(_opts = {}) {
return function makeConfig(
config: Configuration,
{ options, context }: SharedConfigContext
{ options, context }: NxRspackExecutionContext
): Configuration {
const isProd =
process.env.NODE_ENV === 'production' || options.mode === 'production';

View File

@ -1,11 +1,11 @@
import { Configuration } from '@rspack/core';
import { SharedConfigContext } from './model';
import { withWeb } from './with-web';
import { NxRspackExecutionContext } from './config';
export function withReact(opts = {}) {
return function makeConfig(
config: Configuration,
{ options, context }: SharedConfigContext
{ options, context }: NxRspackExecutionContext
): Configuration {
const isDev =
process.env.NODE_ENV === 'development' || options.mode === 'development';

View File

@ -1,132 +1,51 @@
import { Configuration, RuleSetRule, rspack } from '@rspack/core';
import * as path from 'path';
import { SharedConfigContext } from './model';
import { Configuration } from '@rspack/core';
import { ExtraEntryPointClass } from './model';
import { applyWebConfig } from '../plugins/utils/apply-web-config';
import { NxRspackExecutionContext } from './config';
export interface WithWebOptions {
baseHref?: string;
deployUrl?: string;
extractCss?: boolean;
generateIndexHtml?: boolean;
index?: string;
postcssConfig?: string;
scripts?: Array<ExtraEntryPointClass | string>;
styles?: Array<ExtraEntryPointClass | string>;
subresourceIntegrity?: boolean;
stylePreprocessorOptions?: {
includePaths?: string[];
};
cssModules?: boolean;
ssr?: boolean;
}
export function withWeb(opts: WithWebOptions = {}) {
const processed = new Set();
export function withWeb(pluginOptions: WithWebOptions = {}) {
return function makeConfig(
config: Configuration,
{ options, context }: SharedConfigContext
{ options, context }: NxRspackExecutionContext
): Configuration {
const isProd =
process.env.NODE_ENV === 'production' || options.mode === 'production';
if (processed.has(config)) {
return config;
}
const projectRoot = path.join(
context.root,
context.projectGraph.nodes[context.projectName].data.root
applyWebConfig(
{
...options,
...pluginOptions,
root: context.root,
projectName: context.projectName,
targetName: context.targetName,
configurationName: context.configurationName,
projectGraph: context.projectGraph,
},
config
);
const includePaths: string[] = [];
if (opts?.stylePreprocessorOptions?.includePaths?.length > 0) {
opts.stylePreprocessorOptions.includePaths.forEach(
(includePath: string) =>
includePaths.push(path.resolve(context.root, includePath))
);
}
let lessPathOptions: { paths?: string[] } = {};
if (includePaths.length > 0) {
lessPathOptions = {
paths: includePaths,
};
}
return {
...config,
target: config.target ?? 'web',
experiments: {
css: true,
},
module: {
...config.module,
rules: [
...(config.module.rules || []),
{
test: /\.css$/,
type: opts?.cssModules ? 'css/module' : undefined,
},
{
test: /\.css$/,
type: 'css',
use: [
{
loader: require.resolve('postcss-loader'),
},
],
},
{
test: /\.scss$|\.sass$/,
type: opts?.cssModules ? 'css/module' : undefined,
use: [
{
loader: require.resolve('sass-loader'),
options: {
sourceMap: !!options.sourceMap,
sassOptions: {
fiber: false,
// bootstrap-sass requires a minimum precision of 8
precision: 8,
includePaths,
},
},
},
],
},
{
test: /.less$/,
type: opts?.cssModules ? 'css/module' : undefined,
use: [
{
loader: require.resolve('less-loader'),
options: {
sourceMap: !!options.sourceMap,
lessOptions: {
javascriptEnabled: true,
...lessPathOptions,
},
},
},
],
},
{
test: /\.styl$/,
use: [
{
loader: require.resolve('stylus-loader'),
options: {
sourceMap: !!options.sourceMap,
stylusOptions: {
include: includePaths,
},
},
},
],
},
].filter((a): a is RuleSetRule => !!a),
},
plugins: [
...config.plugins,
new rspack.HtmlRspackPlugin({
template: options.indexHtml
? path.join(context.root, options.indexHtml)
: path.join(projectRoot, 'src/index.html'),
...(options.baseHref ? { base: { href: options.baseHref } } : {}),
}),
new rspack.EnvironmentPlugin({
NODE_ENV: isProd ? 'production' : 'development',
}),
new rspack.DefinePlugin(
getClientEnvironment(isProd ? 'production' : undefined).stringified
),
],
};
processed.add(config);
return config;
};
}

View File

@ -35,7 +35,7 @@ export function applyWebConfig(
useNormalizedEntry?: boolean;
} = {}
): void {
if (!process.env['NX_TASK_TARGET_PROJECT']) return;
if (global.NX_GRAPH_CREATION) return;
// Defaults that was applied from executor schema previously.
options.runtimeChunk ??= true; // need this for HMR and other things to work