feat(rspack): move logic for withNx to applyBaseConfig and bring in line with webpack (#28825)

<!-- 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 -->
`withNx` 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 `withNx` in line with `@nx/webpack`

## Notes
This commit is contained in:
Colum Ferry 2024-11-12 16:44:29 +00:00 committed by GitHub
parent cd121bd5ee
commit 048f7c61af
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1225 additions and 305 deletions

View File

@ -86,13 +86,15 @@ describe('React Applications', () => {
`
);
runCLI(`build ${appName}`);
runCLI(`build ${appName}`, { verbose: true });
checkFilesExist(`dist/${appName}/index.html`);
if (runE2ETests()) {
// TODO(Colum): investigate why webkit is failing
const e2eResults = runCLI(`e2e ${appName}-e2e -- --project=chromium`);
const e2eResults = runCLI(`e2e ${appName}-e2e -- --project=chromium`, {
verbose: true,
});
expect(e2eResults).toContain('Successfully ran target e2e for project');
expect(await killPorts()).toBeTruthy();
}

View File

@ -139,11 +139,11 @@ describe('rspack e2e', () => {
result = runCLI(`build ${app3}`);
expect(result).toContain('Successfully ran target build');
// Make sure expected files are present.
expect(listFiles(`dist/${app3}`)).toHaveLength(2);
expect(listFiles(`dist/${app3}`)).toHaveLength(3);
result = runCLI(`build ${app3} --generatePackageJson=true`);
expect(result).toContain('Successfully ran target build');
// Make sure expected files are present.
expect(listFiles(`dist/${app3}`)).toHaveLength(4);
expect(listFiles(`dist/${app3}`)).toHaveLength(5);
}, 200_000);
});

View File

@ -32,23 +32,25 @@
"@rspack/dev-server": "^1.0.4",
"@rspack/plugin-react-refresh": "^1.0.0",
"autoprefixer": "^10.4.9",
"browserslist": "^4.21.4",
"chalk": "~4.1.0",
"css-loader": "^6.4.0",
"enquirer": "~2.3.6",
"express": "^4.19.2",
"fork-ts-checker-webpack-plugin": "7.2.13",
"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",
"source-map-loader": "^5.0.0",
"style-loader": "^3.3.0",
"postcss-import": "~14.1.0",
"postcss-loader": "^8.1.1",
"postcss": "^8.4.38",
"tsconfig-paths": "^4.1.2",
"tslib": "^2.3.0",
"webpack-subresource-integrity": "^5.1.0"
"webpack-node-externals": "^3.0.0"
},
"peerDependencies": {
"@module-federation/enhanced": "~0.6.0",

View File

@ -11,6 +11,7 @@ import { createCompiler, isMultiCompiler } from '../../utils/create-compiler';
import { isMode } from '../../utils/mode-utils';
import { getDevServerOptions } from './lib/get-dev-server-config';
import { DevServerExecutorSchema } from './schema';
import { normalizeOptions } from '../rspack/lib/normalize-options';
type DevServer = Configuration['devServer'];
export default async function* runExecutor(
@ -30,14 +31,28 @@ export default async function* runExecutor(
const buildOptions = readTargetOptions(buildTarget, context);
process.env.NX_BUILD_LIBS_FROM_SOURCE = `${buildOptions.buildLibsFromSource}`;
process.env.NX_BUILD_TARGET = options.buildTarget;
const metadata = context.projectsConfigurations.projects[context.projectName];
const sourceRoot = metadata.sourceRoot;
const normalizedBuildOptions = normalizeOptions(
buildOptions,
context.root,
metadata.root,
sourceRoot
);
let devServerConfig: DevServer = getDevServerOptions(
context.root,
options,
buildOptions
normalizedBuildOptions
);
const compiler = await createCompiler(
{ ...buildOptions, devServer: devServerConfig, mode: options.mode },
{
...normalizedBuildOptions,
devServer: devServerConfig,
mode: options.mode,
},
context
);

View File

@ -26,7 +26,7 @@ export function getDevServerOptions(
const config: RspackDevServerConfiguration = {
host: serveOptions.host,
port: serveOptions.port,
port: serveOptions.port ?? 4200,
headers: { 'Access-Control-Allow-Origin': '*' },
historyApiFallback: {
index:

View File

@ -0,0 +1,56 @@
import { resolve } from 'path';
import {
normalizeAssets,
normalizeFileReplacements,
} from '../../../plugins/utils/plugins/normalize-options';
import type {
RspackExecutorSchema,
NormalizedRspackExecutorSchema,
} from '../schema';
export function normalizeOptions(
options: RspackExecutorSchema,
root: string,
projectRoot: string,
sourceRoot: string
): NormalizedRspackExecutorSchema {
const normalizedOptions = {
...options,
root,
projectRoot,
sourceRoot,
target: options.target ?? 'web',
outputFileName: options.outputFileName ?? 'main.js',
rspackConfig: normalizePluginPath(options.rspackConfig, root),
fileReplacements: normalizeFileReplacements(root, options.fileReplacements),
optimization:
typeof options.optimization !== 'object'
? {
scripts: options.optimization,
styles: options.optimization,
}
: options.optimization,
};
if (options.assets) {
normalizedOptions.assets = normalizeAssets(
options.assets,
root,
sourceRoot,
projectRoot,
false // executor assets are relative to workspace root for consistency
);
}
return normalizedOptions as NormalizedRspackExecutorSchema;
}
export function normalizePluginPath(pluginPath: void | string, root: string) {
if (!pluginPath) {
return '';
}
try {
return require.resolve(pluginPath);
} catch {
return resolve(root, pluginPath);
}
}

View File

@ -7,6 +7,7 @@ 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';
export default async function* runExecutor(
options: RspackExecutorSchema,
@ -28,8 +29,16 @@ export default async function* runExecutor(
force: true,
recursive: true,
});
const metadata = context.projectsConfigurations.projects[context.projectName];
const sourceRoot = metadata.sourceRoot;
const normalizedOptions = normalizeOptions(
options,
context.root,
metadata.root,
sourceRoot
);
const compiler = await createCompiler(options, context);
const compiler = await createCompiler(normalizedOptions, context);
const iterable = createAsyncIterable<{
success: boolean;
@ -58,7 +67,11 @@ export default async function* runExecutor(
}
next({
success: !stats.hasErrors(),
outfile: path.resolve(context.root, options.outputPath, 'main.js'),
outfile: path.resolve(
context.root,
normalizedOptions.outputPath,
'main.js'
),
});
}
);
@ -90,7 +103,11 @@ export default async function* runExecutor(
}
next({
success: !stats.hasErrors(),
outfile: path.resolve(context.root, options.outputPath, 'main.js'),
outfile: path.resolve(
context.root,
normalizedOptions.outputPath,
'main.js'
),
});
done();
});

View File

@ -0,0 +1,423 @@
import * as path from 'path';
import { type ExecutorContext } from '@nx/devkit';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import {
Configuration,
ProgressPlugin,
RspackPluginInstance,
SwcJsMinimizerRspackPlugin,
CopyRspackPlugin,
RspackOptionsNormalized,
ExternalItem,
} from '@rspack/core';
import { getRootTsConfigPath } from '@nx/js';
import { StatsJsonPlugin } from './plugins/stats-json-plugin';
import { GeneratePackageJsonPlugin } from './plugins/generate-package-json-plugin';
import { getOutputHashFormat } from './hash-format';
import { NxTsconfigPathsRspackPlugin } from './plugins/nx-tsconfig-paths-rspack-plugin';
import { getTerserEcmaVersion } from './get-terser-ecma-version';
import nodeExternals = require('webpack-node-externals');
import { NormalizedNxAppRspackPluginOptions } from './models';
const IGNORED_RSPACK_WARNINGS = [
/The comment file/i,
/could not find any license/i,
];
const extensions = ['...', '.ts', '.tsx', '.mjs', '.js', '.jsx'];
const mainFields = ['module', 'main'];
export function applyBaseConfig(
options: NormalizedNxAppRspackPluginOptions,
config: Partial<RspackOptionsNormalized | 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 {
// Defaults that was applied from executor schema previously.
options.deleteOutputPath ??= true;
options.externalDependencies ??= 'all';
options.fileReplacements ??= [];
options.memoryLimit ??= 2048;
options.transformers ??= [];
options.progress ??= true;
applyNxIndependentConfig(options, config);
// Some of the options only work during actual tasks, not when reading the rspack config during CreateNodes.
if (global.NX_GRAPH_CREATION) return;
applyNxDependentConfig(options, config, { useNormalizedEntry });
}
function applyNxIndependentConfig(
options: NormalizedNxAppRspackPluginOptions,
config: Partial<RspackOptionsNormalized | Configuration>
): void {
const isProd =
process.env.NODE_ENV === 'production' || options.mode === 'production';
const hashFormat = getOutputHashFormat(options.outputHashing as string);
config.context = path.join(options.root, options.projectRoot);
config.target ??= options.target as 'node' | 'web';
config.node = false;
config.mode =
// When the target is Node avoid any optimizations, such as replacing `process.env.NODE_ENV` with build time value.
config.target === 'node'
? 'none'
: // Otherwise, make sure it matches `process.env.NODE_ENV`.
// When mode is development or production, rspack will automatically
// configure DefinePlugin to replace `process.env.NODE_ENV` with the
// build-time value. Thus, we need to make sure it's the same value to
// avoid conflicts.
//
// When the NODE_ENV is something else (e.g. test), then set it to none
// to prevent extra behavior from rspack.
options.mode ??
(process.env.NODE_ENV === 'development' ||
process.env.NODE_ENV === 'production'
? (process.env.NODE_ENV as 'development' | 'production')
: 'none');
// When target is Node, the Webpack mode will be set to 'none' which disables in memory caching and causes a full rebuild on every change.
// So to mitigate this we enable in memory caching when target is Node and in watch mode.
config.cache = options.target === 'node' && options.watch ? true : undefined;
config.devtool =
options.sourceMap === 'hidden'
? 'hidden-source-map'
: options.sourceMap
? 'source-map'
: false;
config.output = {
...(config.output ?? {}),
libraryTarget:
(config as Configuration).output?.libraryTarget ??
(options.target === 'node' ? 'commonjs' : undefined),
path:
config.output?.path ??
(options.outputPath
? // If path is relative, it is relative from project root (aka cwd).
// Otherwise, it is relative to workspace root (legacy behavior).
options.outputPath.startsWith('.')
? path.join(options.root, options.projectRoot, options.outputPath)
: path.join(options.root, options.outputPath)
: undefined),
filename:
config.output?.filename ??
(options.outputHashing ? `[name]${hashFormat.script}.js` : '[name].js'),
chunkFilename:
config.output?.chunkFilename ??
(options.outputHashing ? `[name]${hashFormat.chunk}.js` : '[name].js'),
hashFunction: config.output?.hashFunction ?? 'xxhash64',
// Disabled for performance
pathinfo: config.output?.pathinfo ?? false,
};
config.watch = options.watch;
config.watchOptions = {
poll: options.poll,
};
config.profile = options.statsJson;
config.performance = {
...config.performance,
hints: false,
};
config.ignoreWarnings = [
(x) =>
IGNORED_RSPACK_WARNINGS.some((r) =>
typeof x === 'string' ? r.test(x) : r.test(x.message)
),
];
config.optimization = !isProd
? undefined
: {
...(config.optimization ?? {}),
sideEffects: true,
minimize:
typeof options.optimization === 'object'
? !!options.optimization.scripts
: !!options.optimization,
minimizer: [
new SwcJsMinimizerRspackPlugin({
extractComments: false,
minimizerOptions: {
// this needs to be false to allow toplevel variables to be used in the global scope
// important especially for module-federation which operates as such
module: false,
mangle: {
keep_classnames: true,
},
format: {
ecma: getTerserEcmaVersion(
path.join(options.root, options.projectRoot)
),
ascii_only: true,
comments: false,
webkit: true,
safari10: true,
},
},
}),
],
runtimeChunk: false,
concatenateModules: true,
};
config.stats = {
hash: true,
timings: false,
cached: false,
cachedAssets: false,
modules: false,
warnings: true,
errors: true,
colors: !options.verbose && !options.statsJson,
chunks: !options.verbose,
assets: !!options.verbose,
chunkOrigins: !!options.verbose,
chunkModules: !!options.verbose,
children: !!options.verbose,
reasons: !!options.verbose,
version: !!options.verbose,
errorDetails: !!options.verbose,
moduleTrace: !!options.verbose,
usedExports: !!options.verbose,
};
/**
* Initialize properties that get set when rspack is used during task execution.
* These properties may be used by consumers who expect them to not be undefined.
*
* When @nx/rspack/plugin resolves the config, it is not during a task, and therefore
* these values are not set, which can lead to errors being thrown when reading
* the rspack options from the resolved file.
*/
config.entry ??= {};
config.resolve ??= {};
config.module ??= {};
config.plugins ??= [];
config.externals ??= [];
}
function applyNxDependentConfig(
options: NormalizedNxAppRspackPluginOptions,
config: Partial<RspackOptionsNormalized | Configuration>,
{ useNormalizedEntry }: { useNormalizedEntry?: boolean } = {}
): void {
const tsConfig = options.tsConfig ?? getRootTsConfigPath();
const plugins: RspackPluginInstance[] = [];
const executorContext: Partial<ExecutorContext> = {
projectName: options.projectName,
targetName: options.targetName,
projectGraph: options.projectGraph,
configurationName: options.configurationName,
root: options.root,
};
plugins.push(new NxTsconfigPathsRspackPlugin({ ...options, tsConfig }));
if (!options?.skipTypeChecking) {
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
plugins.push(
new ForkTsCheckerWebpackPlugin({
typescript: {
configFile: path.isAbsolute(tsConfig)
? tsConfig
: path.join(options.root, tsConfig),
memoryLimit: options.memoryLimit || 2018,
},
})
);
}
const entries: Array<{ name: string; import: string[] }> = [];
if (options.main) {
const mainEntry = options.outputFileName
? path.parse(options.outputFileName).name
: 'main';
entries.push({
name: mainEntry,
import: [path.resolve(options.root, options.main)],
});
}
if (options.additionalEntryPoints) {
for (const { entryName, entryPath } of options.additionalEntryPoints) {
entries.push({
name: entryName,
import: [path.resolve(options.root, entryPath)],
});
}
}
if (options.polyfills) {
entries.push({
name: 'polyfills',
import: [path.resolve(options.root, options.polyfills)],
});
}
config.entry ??= {};
entries.forEach((entry) => {
if (useNormalizedEntry) {
config.entry[entry.name] = { import: entry.import };
} else {
config.entry[entry.name] = entry.import;
}
});
if (options.progress) {
plugins.push(new ProgressPlugin({ profile: options.verbose }));
}
if (options.extractLicenses) {
plugins.push(
new LicenseWebpackPlugin({
stats: {
warnings: false,
errors: false,
},
perChunkOutput: false,
outputFilename: `3rdpartylicenses.txt`,
}) as unknown as RspackPluginInstance
);
}
if (Array.isArray(options.assets) && options.assets.length > 0) {
plugins.push(
new CopyRspackPlugin({
patterns: options.assets.map((asset) => {
return {
context: asset.input,
// Now we remove starting slash to make Webpack place it from the output root.
to: asset.output,
from: asset.glob,
globOptions: {
ignore: [
'.gitkeep',
'**/.DS_Store',
'**/Thumbs.db',
...(asset.ignore ?? []),
],
dot: true,
},
};
}),
})
);
}
if (options.generatePackageJson && executorContext) {
plugins.push(new GeneratePackageJsonPlugin({ ...options, tsConfig }));
}
if (options.statsJson) {
plugins.push(new StatsJsonPlugin());
}
const externals = [];
if (options.target === 'node' && options.externalDependencies === 'all') {
const modulesDir = `${options.root}/node_modules`;
externals.push(nodeExternals({ modulesDir }));
} else if (Array.isArray(options.externalDependencies)) {
externals.push(function (ctx, callback: Function) {
if (options.externalDependencies.includes(ctx.request)) {
// not bundled
return callback(null, `commonjs ${ctx.request}`);
}
// bundled
callback();
});
}
config.resolve = {
...config.resolve,
extensions: [...(config?.resolve?.extensions ?? []), ...extensions],
alias: {
...(config.resolve?.alias ?? {}),
...(options.fileReplacements?.reduce(
(aliases, replacement) => ({
...aliases,
[replacement.replace]: replacement.with,
}),
{}
) ?? {}),
},
mainFields: config.resolve?.mainFields ?? mainFields,
};
config.externals = externals;
// Enabled for performance
config.cache = true;
config.module = {
...config.module,
rules: [
...(config?.module?.rules ?? []),
options.sourceMap && {
test: /\.js$/,
enforce: 'pre' as const,
loader: require.resolve('source-map-loader'),
},
{
// There's an issue resolving paths without fully specified extensions
// See: https://github.com/graphql/graphql-js/issues/2721
// TODO(jack): Add a flag to turn this option on like Next.js does via experimental flag.
// See: https://github.com/vercel/next.js/pull/29880
test: /\.m?jsx?$/,
resolve: {
fullySpecified: false,
},
},
// There's an issue when using buildable libs and .js files (instead of .ts files),
// where the wrong type is used (commonjs vs esm) resulting in export-imports throwing errors.
// See: https://github.com/nrwl/nx/issues/10990
{
test: /\.js$/,
type: 'javascript/auto',
},
// Rspack's docs only suggest swc for TS compilation
//https://rspack.dev/guide/tech/typescript
{
test: /\.([jt])sx?$/,
loader: 'builtin:swc-loader',
exclude: /node_modules/,
type: 'javascript/auto',
options: {
jsc: {
parser: {
syntax: 'typescript',
decorators: true,
tsx: true,
},
transform: {
react: {
pragma: 'React.createElement',
pragmaFrag: 'React.Fragment',
throwIfNamespace: true,
// Config.mode is already set based on options.mode and `process.env.NODE_ENV`
development: config.mode === 'development',
useBuiltins: false,
},
},
},
},
},
].filter((r) => !!r),
};
config.plugins ??= [];
config.plugins.push(...plugins);
}

View File

@ -10,7 +10,6 @@ import {
} 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 {
@ -69,18 +68,17 @@ export function applyWebConfig(
plugins.push(
new HtmlRspackPlugin({
template: options.index,
sri: options.subresourceIntegrity ? 'sha256' : undefined,
sri: 'sha256',
...(options.baseHref ? { base: { href: options.baseHref } } : {}),
...(config.output?.scriptType === 'module'
? { scriptLoading: 'module' }
: {}),
})
);
}
if (options.subresourceIntegrity) {
plugins.push(new SubresourceIntegrityPlugin() as any);
}
const minimizer: RspackPluginInstance[] = [];
if (stylesOptimization) {
if (isProd && stylesOptimization) {
minimizer.push(
new LightningCssMinimizerRspackPlugin({
test: /\.(?:css|scss|sass|less|styl)$/,
@ -338,13 +336,11 @@ export function applyWebConfig(
config.output = {
...(config.output ?? {}),
assetModuleFilename: '[name].[contenthash:20][ext]',
crossOriginLoading: options.subresourceIntegrity
? ('anonymous' as const)
: (false as const),
assetModuleFilename: '[name].[contenthash:16][ext]',
crossOriginLoading: 'anonymous',
};
// In case users customize their webpack config with unsupported entry.
// In case users customize their rspack 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')
@ -360,41 +356,43 @@ export function applyWebConfig(
}
});
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,
config.optimization = !isProd
? undefined
: {
...(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[\\/]/,
},
},
},
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'];
@ -437,7 +435,7 @@ export function applyWebConfig(
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.
// injected into the application via DefinePlugin in rspack configuration.
const nxPublicKeyRegex = /^NX_PUBLIC_/i;
const raw = Object.keys(process.env)
@ -447,7 +445,7 @@ function getClientEnvironment(mode?: string) {
return env;
}, {});
// Stringify all values so we can feed into webpack DefinePlugin
// Stringify all values so we can feed into rspack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);

View File

@ -0,0 +1,36 @@
import * as path from 'path';
import * as fs from 'fs';
import browserslist = require('browserslist');
const VALID_BROWSERSLIST_FILES = ['.browserslistrc', 'browserslist'];
const ES5_BROWSERS = [
'ie 10',
'ie 11',
'safari 11',
'safari 11.1',
'safari 12',
'safari 12.1',
'safari 13',
'ios_saf 13.0',
'ios_saf 13.3',
];
export function getTerserEcmaVersion(projectRoot: string): 2020 | 5 {
let pathToBrowserslistFile = '';
for (const browserslistFile of VALID_BROWSERSLIST_FILES) {
const fullPathToFile = path.join(projectRoot, browserslistFile);
if (fs.existsSync(fullPathToFile)) {
pathToBrowserslistFile = fullPathToFile;
break;
}
}
if (!pathToBrowserslistFile) {
return 2020;
}
const env = browserslist.loadConfig({ path: pathToBrowserslistFile });
const browsers = browserslist(env);
return browsers.some((b) => ES5_BROWSERS.includes(b)) ? 5 : 2020;
}

View File

@ -1,3 +1,5 @@
import { logger } from '@nx/devkit';
export interface HashFormat {
chunk: string;
extract: string;
@ -5,7 +7,18 @@ export interface HashFormat {
script: string;
}
export function getOutputHashFormat(option: string, length = 20): HashFormat {
const MAX_HASH_LENGTH = 16;
export function getOutputHashFormat(
option: string,
length = MAX_HASH_LENGTH
): HashFormat {
if (length > MAX_HASH_LENGTH) {
logger.warn(
`Hash format length cannot be longer than ${MAX_HASH_LENGTH}. Using default of ${MAX_HASH_LENGTH}.`
);
length = MAX_HASH_LENGTH;
}
const hashFormats: { [option: string]: HashFormat } = {
none: { chunk: '', extract: '', file: '', script: '' },
media: { chunk: '', extract: '', file: `.[hash:${length}]`, script: '' },

View File

@ -105,7 +105,7 @@ function postcssOptionsCreator(
}
) {
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:
// PostCSS options depend on the rspack 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) => ({

View File

@ -130,11 +130,11 @@ export interface NxAppRspackPluginOptions {
*/
outputHashing?: any;
/**
* Override `output.path` in webpack configuration. This setting is not recommended and exists for backwards compatibility.
* Override `output.path` in rspack 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.
* Override `watchOptions.poll` in rspack configuration. This setting is not recommended and exists for backwards compatibility.
*/
poll?: number;
/**
@ -150,7 +150,7 @@ export interface NxAppRspackPluginOptions {
*/
progress?: boolean;
/**
* Add an additional chunk for the Webpack runtime. Defaults to `true` when `target === 'web'`.
* Add an additional chunk for the rspack runtime. Defaults to `true` when `target === 'web'`.
*/
runtimeChunk?: boolean;
/**
@ -194,11 +194,7 @@ export interface NxAppRspackPluginOptions {
*/
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.
* Override the `target` option in rspack configuration. This setting is not recommended and exists for backwards compatibility.
*/
target?: string | string[];
/**

View File

@ -0,0 +1,97 @@
import {
type Compiler,
sources,
type RspackPluginInstance,
} from '@rspack/core';
import {
createLockFile,
createPackageJson,
getHelperDependenciesFromProjectGraph,
getLockFileName,
HelperDependency,
readTsConfig,
} from '@nx/js';
import {
detectPackageManager,
type ProjectGraph,
serializeJson,
} from '@nx/devkit';
const pluginName = 'GeneratePackageJsonPlugin';
export class GeneratePackageJsonPlugin implements RspackPluginInstance {
constructor(
private readonly options: {
skipPackageManager?: boolean;
tsConfig: string;
outputFileName: string;
root: string;
projectName: string;
targetName: string;
projectGraph: ProjectGraph;
}
) {}
apply(compiler: Compiler): void {
compiler.hooks.thisCompilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tap(
{
name: pluginName,
stage: compiler.rspack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
() => {
const helperDependencies = getHelperDependenciesFromProjectGraph(
this.options.root,
this.options.projectName,
this.options.projectGraph
);
const importHelpers = !!readTsConfig(this.options.tsConfig).options
.importHelpers;
const shouldAddHelperDependency =
importHelpers &&
helperDependencies.every(
(dep) => dep.target !== HelperDependency.tsc
);
if (shouldAddHelperDependency) {
helperDependencies.push({
type: 'static',
source: this.options.projectName,
target: HelperDependency.tsc,
});
}
const packageJson = createPackageJson(
this.options.projectName,
this.options.projectGraph,
{
target: this.options.targetName,
root: this.options.root,
isProduction: true,
helperDependencies: helperDependencies.map((dep) => dep.target),
skipPackageManager: this.options.skipPackageManager,
}
);
packageJson.main = packageJson.main ?? this.options.outputFileName;
compilation.emitAsset(
'package.json',
new sources.RawSource(serializeJson(packageJson))
);
const packageManager = detectPackageManager(this.options.root);
compilation.emitAsset(
getLockFileName(packageManager),
new sources.RawSource(
createLockFile(
packageJson,
this.options.projectGraph,
packageManager
)
)
);
}
);
});
}
}

View File

@ -0,0 +1,235 @@
import { basename, dirname, join, parse, relative, resolve } from 'path';
import { statSync } from 'fs';
import {
normalizePath,
parseTargetString,
readCachedProjectGraph,
workspaceRoot,
} from '@nx/devkit';
import {
AssetGlobPattern,
FileReplacement,
NxAppRspackPluginOptions,
NormalizedNxAppRspackPluginOptions,
} from '../models';
export function normalizeOptions(
options: NxAppRspackPluginOptions
): NormalizedNxAppRspackPluginOptions {
const combinedPluginAndMaybeExecutorOptions: Partial<NormalizedNxAppRspackPluginOptions> =
{};
const isProd = process.env.NODE_ENV === 'production';
// Since this is invoked by the executor, the graph has already been created and cached.
const projectGraph = readCachedProjectGraph();
const taskDetailsFromBuildTarget = process.env.NX_BUILD_TARGET
? parseTargetString(process.env.NX_BUILD_TARGET, projectGraph)
: undefined;
const projectName = taskDetailsFromBuildTarget
? taskDetailsFromBuildTarget.project
: process.env.NX_TASK_TARGET_PROJECT;
const targetName = taskDetailsFromBuildTarget
? taskDetailsFromBuildTarget.target
: process.env.NX_TASK_TARGET_TARGET;
const configurationName = taskDetailsFromBuildTarget
? taskDetailsFromBuildTarget.configuration
: process.env.NX_TASK_TARGET_CONFIGURATION;
const projectNode = projectGraph.nodes[projectName];
const targetConfig = projectNode.data.targets[targetName];
normalizeRelativePaths(projectNode.data.root, options);
// Merge options from `@nx/rspack:rspack` into plugin options.
// Options from `@nx/rspack:rspack` take precedence.
const originalTargetOptions = targetConfig.options;
if (configurationName) {
Object.assign(
originalTargetOptions,
targetConfig.configurations?.[configurationName]
);
}
// This could be called from dev-server which means we need to read `buildTarget` to get actual build options.
// Otherwise, the options are passed from the `@nx/rspack:rspack` executor.
if (originalTargetOptions.buildTarget) {
const buildTargetOptions = targetConfig.options;
if (configurationName) {
Object.assign(
buildTargetOptions,
targetConfig.configurations?.[configurationName]
);
}
Object.assign(
combinedPluginAndMaybeExecutorOptions,
options,
// executor options take precedence (especially for overriding with CLI args)
buildTargetOptions
);
} else {
Object.assign(
combinedPluginAndMaybeExecutorOptions,
options,
// executor options take precedence (especially for overriding with CLI args)
originalTargetOptions
);
}
const sourceRoot = projectNode.data.sourceRoot ?? projectNode.data.root;
if (!combinedPluginAndMaybeExecutorOptions.main) {
throw new Error(
`Missing "main" option for the entry file. Set this option in your Nx rspack plugin.`
);
}
return {
...combinedPluginAndMaybeExecutorOptions,
assets: combinedPluginAndMaybeExecutorOptions.assets
? normalizeAssets(
combinedPluginAndMaybeExecutorOptions.assets,
workspaceRoot,
sourceRoot,
projectNode.data.root
)
: [],
baseHref: combinedPluginAndMaybeExecutorOptions.baseHref ?? '/',
buildLibsFromSource:
combinedPluginAndMaybeExecutorOptions.buildLibsFromSource ?? true,
commonChunk: combinedPluginAndMaybeExecutorOptions.commonChunk ?? true,
configurationName,
deleteOutputPath:
combinedPluginAndMaybeExecutorOptions.deleteOutputPath ?? true,
extractCss: combinedPluginAndMaybeExecutorOptions.extractCss ?? true,
fileReplacements: normalizeFileReplacements(
workspaceRoot,
combinedPluginAndMaybeExecutorOptions.fileReplacements
),
generateIndexHtml:
combinedPluginAndMaybeExecutorOptions.generateIndexHtml ?? true,
main: combinedPluginAndMaybeExecutorOptions.main,
namedChunks: combinedPluginAndMaybeExecutorOptions.namedChunks ?? !isProd,
optimization: combinedPluginAndMaybeExecutorOptions.optimization ?? isProd,
outputFileName:
combinedPluginAndMaybeExecutorOptions.outputFileName ?? 'main.js',
outputHashing:
combinedPluginAndMaybeExecutorOptions.outputHashing ??
(isProd ? 'all' : 'none'),
outputPath: combinedPluginAndMaybeExecutorOptions.outputPath,
projectGraph,
projectName,
projectRoot: projectNode.data.root,
root: workspaceRoot,
runtimeChunk: combinedPluginAndMaybeExecutorOptions.runtimeChunk ?? true,
scripts: combinedPluginAndMaybeExecutorOptions.scripts ?? [],
sourceMap: combinedPluginAndMaybeExecutorOptions.sourceMap ?? !isProd,
sourceRoot,
styles: combinedPluginAndMaybeExecutorOptions.styles ?? [],
target: combinedPluginAndMaybeExecutorOptions.target,
targetName,
vendorChunk: combinedPluginAndMaybeExecutorOptions.vendorChunk ?? !isProd,
};
}
export function normalizeAssets(
assets: any[],
root: string,
sourceRoot: string,
projectRoot: string,
resolveRelativePathsToProjectRoot = true
): AssetGlobPattern[] {
return assets.map((asset) => {
if (typeof asset === 'string') {
const assetPath = normalizePath(asset);
const resolvedAssetPath = resolve(root, assetPath);
const resolvedSourceRoot = resolve(root, sourceRoot);
if (!resolvedAssetPath.startsWith(resolvedSourceRoot)) {
throw new Error(
`The ${resolvedAssetPath} asset path must start with the project source root: ${sourceRoot}`
);
}
const isDirectory = statSync(resolvedAssetPath).isDirectory();
const input = isDirectory
? resolvedAssetPath
: dirname(resolvedAssetPath);
const output = relative(resolvedSourceRoot, resolve(root, input));
const glob = isDirectory ? '**/*' : basename(resolvedAssetPath);
return {
input,
output,
glob,
};
} else {
if (asset.output.startsWith('..')) {
throw new Error(
'An asset cannot be written to a location outside of the output path.'
);
}
const assetPath = normalizePath(asset.input);
let resolvedAssetPath = resolve(root, assetPath);
if (resolveRelativePathsToProjectRoot && asset.input.startsWith('.')) {
const resolvedProjectRoot = resolve(root, projectRoot);
resolvedAssetPath = resolve(resolvedProjectRoot, assetPath);
}
return {
...asset,
input: resolvedAssetPath,
// Now we remove starting slash to make rspack place it from the output root.
output: asset.output.replace(/^\//, ''),
};
}
});
}
export function normalizeFileReplacements(
root: string,
fileReplacements: FileReplacement[]
): FileReplacement[] {
return fileReplacements
? fileReplacements.map((fileReplacement) => ({
replace: resolve(root, fileReplacement.replace),
with: resolve(root, fileReplacement.with),
}))
: [];
}
function normalizeRelativePaths(
projectRoot: string,
options: NxAppRspackPluginOptions
): void {
for (const [fieldName, fieldValue] of Object.entries(options)) {
if (isRelativePath(fieldValue)) {
options[fieldName] = join(projectRoot, fieldValue);
} else if (fieldName === 'additionalEntryPoints') {
for (let i = 0; i < fieldValue.length; i++) {
const v = fieldValue[i];
if (isRelativePath(v)) {
fieldValue[i] = {
entryName: parse(v).name,
entryPath: join(projectRoot, v),
};
} else if (isRelativePath(v.entryPath)) {
v.entryPath = join(projectRoot, v.entryPath);
}
}
} else if (Array.isArray(fieldValue)) {
for (let i = 0; i < fieldValue.length; i++) {
if (isRelativePath(fieldValue[i])) {
fieldValue[i] = join(projectRoot, fieldValue[i]);
}
}
}
}
}
function isRelativePath(val: unknown): boolean {
return (
typeof val === 'string' &&
(val.startsWith('./') ||
// Windows
val.startsWith('.\\'))
);
}

View File

@ -0,0 +1,98 @@
import * as path from 'path';
import {
Compiler,
type Configuration,
type RspackOptionsNormalized,
} from '@rspack/core';
import { workspaceRoot } from '@nx/devkit';
import {
calculateProjectBuildableDependencies,
createTmpTsConfig,
} from '@nx/js/src/utils/buildable-libs-utils';
import { NormalizedNxAppRspackPluginOptions } from '../models';
import { RspackNxBuildCoordinationPlugin } from './rspack-nx-build-coordination-plugin';
import { unlinkSync } from 'fs';
export class NxTsconfigPathsRspackPlugin {
private tmpTsConfigPath: string;
constructor(private options: NormalizedNxAppRspackPluginOptions) {
if (!this.options.tsConfig)
throw new Error(
`Missing "tsConfig" option. Set this option in your Nx rspack plugin.`
);
}
apply(compiler: Compiler): void {
// TODO(Colum): Investigate the best way to handle this, currently it is not working and affecting HMR
// // If we are not building libs from source, we need to remap paths so tsconfig may be updated.
// this.handleBuildLibsFromSource(compiler.options, this.options);
const pathToTsconfig = !path.isAbsolute(this.options.tsConfig)
? path.join(workspaceRoot, this.options.tsConfig)
: this.options.tsConfig;
const extensions = new Set([
...['.ts', '.tsx', '.mjs', '.js', '.jsx'],
...(compiler.options?.resolve?.extensions ?? []),
]);
compiler.options.resolve = {
...compiler.options.resolve,
extensions: [...extensions],
tsConfig: { configFile: pathToTsconfig },
};
}
cleanupTmpTsConfigFile() {
if (this.tmpTsConfigPath) {
try {
if (this.tmpTsConfigPath) {
unlinkSync(this.tmpTsConfigPath);
}
} catch (e) {}
}
}
handleBuildLibsFromSource(
config: Partial<RspackOptionsNormalized | Configuration>,
options
): void {
if (!options.buildLibsFromSource && options.targetName) {
const remappedTarget =
options.targetName === 'serve' ? 'build' : options.targetName;
const { target, dependencies } = calculateProjectBuildableDependencies(
undefined,
options.projectGraph,
options.root,
options.projectName,
remappedTarget,
options.configurationName
);
options.tsConfig = createTmpTsConfig(
options.tsConfig,
options.root,
target.data.root,
dependencies
);
this.tmpTsConfigPath = options.tsConfig;
if (options.targetName === 'serve') {
const buildableDependencies = dependencies
.filter((dependency) => dependency.node.type === 'lib')
.map((dependency) => dependency.node.name)
.join(',');
const buildCommand = `nx run-many --target=build --projects=${buildableDependencies}`;
if (buildableDependencies && buildableDependencies.length > 0) {
config.plugins.push(
new RspackNxBuildCoordinationPlugin(buildCommand)
);
}
}
}
}
}

View File

@ -0,0 +1,105 @@
import { exec } from 'child_process';
import type { Compiler } from '@rspack/core';
import { daemonClient, isDaemonEnabled } from 'nx/src/daemon/client/client';
import { BatchFunctionRunner } from 'nx/src/command-line/watch/watch';
import { output } from 'nx/src/utils/output';
export class RspackNxBuildCoordinationPlugin {
private currentlyRunning: 'none' | 'nx-build' | 'rspack-build' = 'none';
private buildCmdProcess: ReturnType<typeof exec> | null = null;
constructor(private readonly buildCmd: string, skipInitialBuild?: boolean) {
if (!skipInitialBuild) {
this.buildChangedProjects();
}
if (isDaemonEnabled()) {
this.startWatchingBuildableLibs();
} else {
output.warn({
title:
'Nx Daemon is not enabled. Buildable libs will not be rebuilt on file changes.',
});
}
}
apply(compiler: Compiler) {
compiler.hooks.beforeCompile.tapPromise(
'IncrementalDevServerPlugin',
async () => {
while (this.currentlyRunning === 'nx-build') {
await sleep(50);
}
this.currentlyRunning = 'rspack-build';
}
);
compiler.hooks.done.tapPromise('IncrementalDevServerPlugin', async () => {
this.currentlyRunning = 'none';
});
}
async startWatchingBuildableLibs() {
const unregisterFileWatcher = await this.createFileWatcher();
process.on('exit', () => {
unregisterFileWatcher();
});
}
async buildChangedProjects() {
while (this.currentlyRunning === 'rspack-build') {
await sleep(50);
}
this.currentlyRunning = 'nx-build';
try {
return await new Promise<void>((res) => {
this.buildCmdProcess = exec(this.buildCmd, {
windowsHide: false,
});
this.buildCmdProcess.stdout.pipe(process.stdout);
this.buildCmdProcess.stderr.pipe(process.stderr);
this.buildCmdProcess.on('exit', () => {
res();
});
this.buildCmdProcess.on('error', () => {
res();
});
});
} finally {
this.currentlyRunning = 'none';
this.buildCmdProcess = null;
}
}
private createFileWatcher() {
const runner = new BatchFunctionRunner(() => this.buildChangedProjects());
return daemonClient.registerFileWatcher(
{
watchProjects: 'all',
},
(err, { changedProjects, changedFiles }) => {
if (err === 'closed') {
output.error({
title: 'Watch connection closed',
bodyLines: [
'The daemon has closed the connection to this watch process.',
'Please restart your watch command.',
],
});
process.exit(1);
}
if (this.buildCmdProcess) {
this.buildCmdProcess.kill(2);
this.buildCmdProcess = null;
}
// Queue a build
runner.enqueue(changedProjects, changedFiles);
}
);
}
}
function sleep(time: number) {
return new Promise((resolve) => setTimeout(resolve, time));
}

View File

@ -0,0 +1,10 @@
import { Compiler, sources } from '@rspack/core';
export class StatsJsonPlugin {
apply(compiler: Compiler) {
compiler.hooks.emit.tap('StatsJsonPlugin', (compilation) => {
const data = JSON.stringify(compilation.getStats().toJson('verbose'));
compilation.assets[`stats.json`] = new sources.RawSource(data);
});
}
}

View File

@ -47,10 +47,10 @@ export function composePlugins(
): 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.
// This is to enable `@nx/rspack/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,
// Build this from scratch since what rspack passes us is the env, not config,
// and `withNX()` creates a new config object anyway.
config = {};
}
@ -97,6 +97,7 @@ function ensureNxRspackExecutionContext(ctx: NxRspackExecutionContext): void {
// These aren't actually needed since NxRspackPlugin and withNx both support them being undefined.
assets: undefined,
outputFileName: undefined,
outputPath: undefined,
rspackConfig: undefined,
};
ctx.context ??= {

View File

@ -15,7 +15,7 @@ export async function createCompiler(
},
context: ExecutorContext
): Promise<Compiler | MultiCompiler> {
const pathToConfig = path.join(context.root, options.rspackConfig);
const pathToConfig = options.rspackConfig;
let userDefinedConfig: any = {};
if (options.tsConfig) {
userDefinedConfig = resolveUserDefinedRspackConfig(

View File

@ -2,7 +2,7 @@ export function getCopyPatterns(assets: any[]) {
return assets.map((asset) => {
return {
context: asset.input,
// Now we remove starting slash to make Webpack place it from the output root.
// Now we remove starting slash to make rspack place it from the output root.
to: asset.output,
from: asset.glob,
globOptions: {

View File

@ -82,7 +82,7 @@ export function shareWorkspaceLibraries(
pathMappings.reduce(
(aliases, library) => ({
...aliases,
// If the library path ends in a wildcard, remove it as webpack can't handle this in resolve.alias
// If the library path ends in a wildcard, remove it as rspack can't handle this in resolve.alias
// e.g. path/to/my/lib/* -> path/to/my/lib
[library.name]: library.path.replace(/\/\*$/, ''),
}),
@ -155,7 +155,7 @@ export function shareWorkspaceLibraries(
* library.path is usually in the form of "/Users/username/path/to/Workspace/path/to/library"
*
* When a wildcard is used in the TS path mappings, we want to get everything after the import to
* re-route the request correctly inline with the webpack resolve.alias
* re-route the request correctly inline with the rspack resolve.alias
*/
join(
library.name,

View File

@ -40,7 +40,7 @@ export function getFunctionDeterminateRemoteUrl(isServer = false) {
if (!serveTarget) {
throw new Error(
`Cannot automatically determine URL of remote (${remote}). Looked for property "host" in the project's "${serveTarget}" target.\n
You can also use the tuple syntax in your webpack config to configure your remotes. e.g. \`remotes: [['remote1', 'http://localhost:4201']]\``
You can also use the tuple syntax in your rspack config to configure your remotes. e.g. \`remotes: [['remote1', 'http://localhost:4201']]\``
);
}

View File

@ -42,7 +42,7 @@ export function normalizeAssets(
return {
...asset,
input: resolvedAssetPath,
// Now we remove starting slash to make Webpack place it from the output root.
// Now we remove starting slash to make rspack place it from the output root.
output: asset.output.replace(/^\//, ''),
};
}

View File

@ -1,233 +1,50 @@
import {
Configuration,
ExternalItem,
ResolveAlias,
RspackPluginInstance,
rspack,
} from '@rspack/core';
import { existsSync, readFileSync } from 'fs';
import { LicenseWebpackPlugin } from 'license-webpack-plugin';
import * as path from 'path';
import { join } from 'path';
import { GeneratePackageJsonPlugin } from '../plugins/generate-package-json-plugin';
import { getCopyPatterns } from './get-copy-patterns';
import { type Configuration } from '@rspack/core';
import { normalizeAssets } from './normalize-assets';
import { NxRspackExecutionContext } from './config';
import { NxAppRspackPluginOptions } from '../plugins/utils/models';
import { applyBaseConfig } from '../plugins/utils/apply-base-config';
import { NxRspackExecutionContext, NxComposableRspackPlugin } from './config';
export function withNx(_opts = {}) {
const processed = new Set();
export type WithNxOptions = Partial<NxAppRspackPluginOptions>;
/**
* @param {WithNxOptions} pluginOptions
* @returns {NxComposableRspackPlugin}
*/
export function withNx(
pluginOptions: WithNxOptions = {}
): NxComposableRspackPlugin {
return function makeConfig(
config: Configuration,
{ options, context }: NxRspackExecutionContext
): Configuration {
const isProd =
process.env.NODE_ENV === 'production' || options.mode === 'production';
if (processed.has(config)) return config;
const project = context.projectGraph.nodes[context.projectName];
const sourceRoot = path.join(context.root, project.data.sourceRoot);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const tsconfigPaths = require('tsconfig-paths');
const { paths } = tsconfigPaths.loadConfig(options.tsConfig);
const alias: ResolveAlias = Object.keys(paths).reduce((acc, k) => {
acc[k] = path.join(context.root, paths[k][0]);
return acc;
}, {});
const plugins = config.plugins ?? [];
if (options.extractLicenses) {
/**
* Needed to prevent an issue with Rspack and Workspaces where the
* workspace's root package.json file is added to the dependency tree
*/
let rootPackageJsonName;
const pathToRootPackageJson = join(context.root, 'package.json');
if (existsSync(pathToRootPackageJson)) {
try {
const rootPackageJson = JSON.parse(
readFileSync(pathToRootPackageJson, 'utf-8')
);
rootPackageJsonName = rootPackageJson.name;
} catch {
// do nothing
}
}
plugins.push(
new LicenseWebpackPlugin({
stats: {
warnings: false,
errors: false,
},
outputFilename: `3rdpartylicenses.txt`,
/**
* Needed to prevent an issue with Rspack and Workspaces where the
* workspace's root package.json file is added to the dependency tree
*/
excludedPackageTest: (packageName) => {
if (!rootPackageJsonName) {
return false;
}
return packageName === rootPackageJsonName;
},
}) as unknown as RspackPluginInstance
);
}
if (options.generatePackageJson) {
const mainOutputFile =
options.main.split('/').pop().split('.')[0] + '.js';
plugins.push(
new GeneratePackageJsonPlugin(
{
tsConfig: options.tsConfig,
outputFileName: options.outputFileName ?? mainOutputFile,
},
context
)
);
}
plugins.push(
new rspack.CopyRspackPlugin({
patterns: getCopyPatterns(
normalizeAssets(options.assets, context.root, sourceRoot)
),
})
applyBaseConfig(
{
...options,
...pluginOptions,
target: options.target ?? 'web',
assets: options.assets
? options.assets
: pluginOptions.assets
? normalizeAssets(
pluginOptions.assets,
options.root,
options.sourceRoot
)
: [],
root: context.root,
projectName: context.projectName,
targetName: context.targetName,
configurationName: context.configurationName,
projectGraph: context.projectGraph,
},
config
);
plugins.push(new rspack.ProgressPlugin());
options.fileReplacements.forEach((item) => {
alias[item.replace] = item.with;
});
const externals: ExternalItem = {};
let externalsType: Configuration['externalsType'];
if (options.target === 'node') {
const projectDeps =
context.projectGraph.dependencies[context.projectName];
for (const dep of Object.values(projectDeps)) {
const externalNode = context.projectGraph.externalNodes[dep.target];
if (externalNode) {
externals[externalNode.data.packageName] =
externalNode.data.packageName;
}
}
externalsType = 'commonjs';
}
const updated: Configuration = {
...config,
target: options.target,
mode: options.mode,
entry: {},
context: join(
context.root,
context.projectGraph.nodes[context.projectName].data.root
),
devtool:
options.sourceMap === 'hidden'
? ('hidden-source-map' as const)
: options.sourceMap
? ('source-map' as const)
: (false as const),
output: {
path: path.join(context.root, options.outputPath),
publicPath: '/',
filename:
isProd && options.target !== 'node'
? '[name].[contenthash:8].js'
: '[name].js',
chunkFilename:
isProd && options.target !== 'node'
? '[name].[contenthash:8].js'
: '[name].js',
cssFilename:
isProd && options.target !== 'node'
? '[name].[contenthash:8].css'
: '[name].css',
cssChunkFilename:
isProd && options.target !== 'node'
? '[name].[contenthash:8].css'
: '[name].css',
assetModuleFilename:
isProd && options.target !== 'node'
? '[name].[contenthash:8][ext]'
: '[name][ext]',
},
devServer: {
...(config.devServer ?? {}),
port: config.devServer?.port ?? 4200,
hot: config.devServer?.hot ?? true,
devMiddleware: {
...(config.devServer?.devMiddleware ?? {}),
stats: true,
},
} as any,
module: {
rules: [
{
test: /\.js$/,
loader: 'builtin:swc-loader',
exclude: /node_modules/,
options: {
jsc: {
parser: {
syntax: 'ecmascript',
},
externalHelpers: true,
},
},
type: 'javascript/auto',
},
{
test: /\.ts$/,
loader: 'builtin:swc-loader',
exclude: /node_modules/,
options: {
jsc: {
parser: {
syntax: 'typescript',
decorators: true,
},
transform: {
legacyDecorator: true,
decoratorMetadata: true,
},
externalHelpers: true,
},
},
type: 'javascript/auto',
},
],
},
plugins: plugins,
resolve: {
// There are some issues resolving workspace libs in a monorepo.
// It looks to be an issue with rspack itself, but will check back after Nx 16 release
// once I can reproduce a small example repo with rspack only.
alias,
// We need to define the extensions that rspack can resolve
extensions: ['...', '.ts', '.tsx', '.jsx'],
// tsConfigPath: path.join(context.root, options.tsConfig),
},
infrastructureLogging: {
debug: false,
},
externals,
externalsType,
stats: {
colors: true,
preset: 'normal',
},
};
const mainEntry = options.main
? options.outputFileName
? path.parse(options.outputFileName).name
: 'main'
: 'main';
updated.entry[mainEntry] = path.resolve(context.root, options.main);
return updated;
processed.add(config);
return config;
};
}

View File

@ -12,7 +12,6 @@ export interface WithWebOptions {
postcssConfig?: string;
scripts?: Array<ExtraEntryPointClass | string>;
styles?: Array<ExtraEntryPointClass | string>;
subresourceIntegrity?: boolean;
stylePreprocessorOptions?: {
includePaths?: string[];
};
@ -51,7 +50,7 @@ export function withWeb(pluginOptions: WithWebOptions = {}) {
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.
// injected into the application via DefinePlugin in rspack configuration.
const nxPublicKeyRegex = /^NX_PUBLIC_/i;
const raw = Object.keys(process.env)
@ -61,7 +60,7 @@ function getClientEnvironment(mode?: string) {
return env;
}, {});
// Stringify all values so we can feed into webpack DefinePlugin
// Stringify all values so we can feed into rspack DefinePlugin
const stringified = {
'process.env': Object.keys(raw).reduce((env, key) => {
env[key] = JSON.stringify(raw[key]);

View File

@ -51,7 +51,7 @@ export function applyBaseConfig(
applyNxIndependentConfig(options, config);
// Some of the options only work during actual tasks, not when reading the webpack config during CreateNodes.
if (!process.env['NX_TASK_TARGET_PROJECT']) return;
if (global.NX_GRAPH_CREATION) return;
applyNxDependentConfig(options, config, { useNormalizedEntry });
}