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:
parent
cd121bd5ee
commit
048f7c61af
@ -86,13 +86,15 @@ describe('React Applications', () => {
|
|||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
runCLI(`build ${appName}`);
|
runCLI(`build ${appName}`, { verbose: true });
|
||||||
|
|
||||||
checkFilesExist(`dist/${appName}/index.html`);
|
checkFilesExist(`dist/${appName}/index.html`);
|
||||||
|
|
||||||
if (runE2ETests()) {
|
if (runE2ETests()) {
|
||||||
// TODO(Colum): investigate why webkit is failing
|
// 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(e2eResults).toContain('Successfully ran target e2e for project');
|
||||||
expect(await killPorts()).toBeTruthy();
|
expect(await killPorts()).toBeTruthy();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,11 +139,11 @@ describe('rspack e2e', () => {
|
|||||||
result = runCLI(`build ${app3}`);
|
result = runCLI(`build ${app3}`);
|
||||||
expect(result).toContain('Successfully ran target build');
|
expect(result).toContain('Successfully ran target build');
|
||||||
// Make sure expected files are present.
|
// Make sure expected files are present.
|
||||||
expect(listFiles(`dist/${app3}`)).toHaveLength(2);
|
expect(listFiles(`dist/${app3}`)).toHaveLength(3);
|
||||||
|
|
||||||
result = runCLI(`build ${app3} --generatePackageJson=true`);
|
result = runCLI(`build ${app3} --generatePackageJson=true`);
|
||||||
expect(result).toContain('Successfully ran target build');
|
expect(result).toContain('Successfully ran target build');
|
||||||
// Make sure expected files are present.
|
// Make sure expected files are present.
|
||||||
expect(listFiles(`dist/${app3}`)).toHaveLength(4);
|
expect(listFiles(`dist/${app3}`)).toHaveLength(5);
|
||||||
}, 200_000);
|
}, 200_000);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -32,23 +32,25 @@
|
|||||||
"@rspack/dev-server": "^1.0.4",
|
"@rspack/dev-server": "^1.0.4",
|
||||||
"@rspack/plugin-react-refresh": "^1.0.0",
|
"@rspack/plugin-react-refresh": "^1.0.0",
|
||||||
"autoprefixer": "^10.4.9",
|
"autoprefixer": "^10.4.9",
|
||||||
|
"browserslist": "^4.21.4",
|
||||||
"chalk": "~4.1.0",
|
"chalk": "~4.1.0",
|
||||||
"css-loader": "^6.4.0",
|
"css-loader": "^6.4.0",
|
||||||
"enquirer": "~2.3.6",
|
"enquirer": "~2.3.6",
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
|
"fork-ts-checker-webpack-plugin": "7.2.13",
|
||||||
"http-proxy-middleware": "^3.0.3",
|
"http-proxy-middleware": "^3.0.3",
|
||||||
"less-loader": "11.1.0",
|
"less-loader": "11.1.0",
|
||||||
"license-webpack-plugin": "^4.0.2",
|
"license-webpack-plugin": "^4.0.2",
|
||||||
"loader-utils": "^2.0.3",
|
"loader-utils": "^2.0.3",
|
||||||
"sass": "^1.42.1",
|
"sass": "^1.42.1",
|
||||||
"sass-loader": "^12.2.0",
|
"sass-loader": "^12.2.0",
|
||||||
|
"source-map-loader": "^5.0.0",
|
||||||
"style-loader": "^3.3.0",
|
"style-loader": "^3.3.0",
|
||||||
"postcss-import": "~14.1.0",
|
"postcss-import": "~14.1.0",
|
||||||
"postcss-loader": "^8.1.1",
|
"postcss-loader": "^8.1.1",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"tsconfig-paths": "^4.1.2",
|
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"webpack-subresource-integrity": "^5.1.0"
|
"webpack-node-externals": "^3.0.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@module-federation/enhanced": "~0.6.0",
|
"@module-federation/enhanced": "~0.6.0",
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { createCompiler, isMultiCompiler } from '../../utils/create-compiler';
|
|||||||
import { isMode } from '../../utils/mode-utils';
|
import { isMode } from '../../utils/mode-utils';
|
||||||
import { getDevServerOptions } from './lib/get-dev-server-config';
|
import { getDevServerOptions } from './lib/get-dev-server-config';
|
||||||
import { DevServerExecutorSchema } from './schema';
|
import { DevServerExecutorSchema } from './schema';
|
||||||
|
import { normalizeOptions } from '../rspack/lib/normalize-options';
|
||||||
|
|
||||||
type DevServer = Configuration['devServer'];
|
type DevServer = Configuration['devServer'];
|
||||||
export default async function* runExecutor(
|
export default async function* runExecutor(
|
||||||
@ -30,14 +31,28 @@ export default async function* runExecutor(
|
|||||||
|
|
||||||
const buildOptions = readTargetOptions(buildTarget, context);
|
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(
|
let devServerConfig: DevServer = getDevServerOptions(
|
||||||
context.root,
|
context.root,
|
||||||
options,
|
options,
|
||||||
buildOptions
|
normalizedBuildOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
const compiler = await createCompiler(
|
const compiler = await createCompiler(
|
||||||
{ ...buildOptions, devServer: devServerConfig, mode: options.mode },
|
{
|
||||||
|
...normalizedBuildOptions,
|
||||||
|
devServer: devServerConfig,
|
||||||
|
mode: options.mode,
|
||||||
|
},
|
||||||
context
|
context
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@ export function getDevServerOptions(
|
|||||||
|
|
||||||
const config: RspackDevServerConfiguration = {
|
const config: RspackDevServerConfiguration = {
|
||||||
host: serveOptions.host,
|
host: serveOptions.host,
|
||||||
port: serveOptions.port,
|
port: serveOptions.port ?? 4200,
|
||||||
headers: { 'Access-Control-Allow-Origin': '*' },
|
headers: { 'Access-Control-Allow-Origin': '*' },
|
||||||
historyApiFallback: {
|
historyApiFallback: {
|
||||||
index:
|
index:
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ import * as path from 'path';
|
|||||||
import { createCompiler, isMultiCompiler } from '../../utils/create-compiler';
|
import { createCompiler, isMultiCompiler } from '../../utils/create-compiler';
|
||||||
import { isMode } from '../../utils/mode-utils';
|
import { isMode } from '../../utils/mode-utils';
|
||||||
import { RspackExecutorSchema } from './schema';
|
import { RspackExecutorSchema } from './schema';
|
||||||
|
import { normalizeOptions } from './lib/normalize-options';
|
||||||
|
|
||||||
export default async function* runExecutor(
|
export default async function* runExecutor(
|
||||||
options: RspackExecutorSchema,
|
options: RspackExecutorSchema,
|
||||||
@ -28,8 +29,16 @@ export default async function* runExecutor(
|
|||||||
force: true,
|
force: true,
|
||||||
recursive: 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<{
|
const iterable = createAsyncIterable<{
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@ -58,7 +67,11 @@ export default async function* runExecutor(
|
|||||||
}
|
}
|
||||||
next({
|
next({
|
||||||
success: !stats.hasErrors(),
|
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({
|
next({
|
||||||
success: !stats.hasErrors(),
|
success: !stats.hasErrors(),
|
||||||
outfile: path.resolve(context.root, options.outputPath, 'main.js'),
|
outfile: path.resolve(
|
||||||
|
context.root,
|
||||||
|
normalizedOptions.outputPath,
|
||||||
|
'main.js'
|
||||||
|
),
|
||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|||||||
423
packages/rspack/src/plugins/utils/apply-base-config.ts
Normal file
423
packages/rspack/src/plugins/utils/apply-base-config.ts
Normal 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);
|
||||||
|
}
|
||||||
@ -10,7 +10,6 @@ import {
|
|||||||
} from '@rspack/core';
|
} from '@rspack/core';
|
||||||
import { instantiateScriptPlugins } from './instantiate-script-plugins';
|
import { instantiateScriptPlugins } from './instantiate-script-plugins';
|
||||||
import { join, resolve } from 'path';
|
import { join, resolve } from 'path';
|
||||||
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
|
|
||||||
import { getOutputHashFormat } from './hash-format';
|
import { getOutputHashFormat } from './hash-format';
|
||||||
import { normalizeExtraEntryPoints } from './normalize-entry';
|
import { normalizeExtraEntryPoints } from './normalize-entry';
|
||||||
import {
|
import {
|
||||||
@ -69,18 +68,17 @@ export function applyWebConfig(
|
|||||||
plugins.push(
|
plugins.push(
|
||||||
new HtmlRspackPlugin({
|
new HtmlRspackPlugin({
|
||||||
template: options.index,
|
template: options.index,
|
||||||
sri: options.subresourceIntegrity ? 'sha256' : undefined,
|
sri: 'sha256',
|
||||||
...(options.baseHref ? { base: { href: options.baseHref } } : {}),
|
...(options.baseHref ? { base: { href: options.baseHref } } : {}),
|
||||||
|
...(config.output?.scriptType === 'module'
|
||||||
|
? { scriptLoading: 'module' }
|
||||||
|
: {}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.subresourceIntegrity) {
|
|
||||||
plugins.push(new SubresourceIntegrityPlugin() as any);
|
|
||||||
}
|
|
||||||
|
|
||||||
const minimizer: RspackPluginInstance[] = [];
|
const minimizer: RspackPluginInstance[] = [];
|
||||||
if (stylesOptimization) {
|
if (isProd && stylesOptimization) {
|
||||||
minimizer.push(
|
minimizer.push(
|
||||||
new LightningCssMinimizerRspackPlugin({
|
new LightningCssMinimizerRspackPlugin({
|
||||||
test: /\.(?:css|scss|sass|less|styl)$/,
|
test: /\.(?:css|scss|sass|less|styl)$/,
|
||||||
@ -338,13 +336,11 @@ export function applyWebConfig(
|
|||||||
|
|
||||||
config.output = {
|
config.output = {
|
||||||
...(config.output ?? {}),
|
...(config.output ?? {}),
|
||||||
assetModuleFilename: '[name].[contenthash:20][ext]',
|
assetModuleFilename: '[name].[contenthash:16][ext]',
|
||||||
crossOriginLoading: options.subresourceIntegrity
|
crossOriginLoading: 'anonymous',
|
||||||
? ('anonymous' as const)
|
|
||||||
: (false as const),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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')
|
if (typeof config.entry === 'function')
|
||||||
throw new Error('Entry function is not supported. Use an object.');
|
throw new Error('Entry function is not supported. Use an object.');
|
||||||
if (typeof config.entry === 'string')
|
if (typeof config.entry === 'string')
|
||||||
@ -360,41 +356,43 @@ export function applyWebConfig(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
config.optimization = {
|
config.optimization = !isProd
|
||||||
...(config.optimization ?? {}),
|
? undefined
|
||||||
minimizer: [...(config.optimization?.minimizer ?? []), ...minimizer],
|
: {
|
||||||
emitOnErrors: false,
|
...(config.optimization ?? {}),
|
||||||
moduleIds: 'deterministic' as const,
|
minimizer: [...(config.optimization?.minimizer ?? []), ...minimizer],
|
||||||
runtimeChunk: options.runtimeChunk ? { name: 'runtime' } : false,
|
emitOnErrors: false,
|
||||||
splitChunks: {
|
moduleIds: 'deterministic' as const,
|
||||||
defaultSizeTypes:
|
runtimeChunk: options.runtimeChunk ? { name: 'runtime' } : false,
|
||||||
config.optimization?.splitChunks !== false
|
splitChunks: {
|
||||||
? config.optimization?.splitChunks?.defaultSizeTypes
|
defaultSizeTypes:
|
||||||
: ['...'],
|
config.optimization?.splitChunks !== false
|
||||||
maxAsyncRequests: Infinity,
|
? config.optimization?.splitChunks?.defaultSizeTypes
|
||||||
cacheGroups: {
|
: ['...'],
|
||||||
default: !!options.commonChunk && {
|
maxAsyncRequests: Infinity,
|
||||||
chunks: 'async' as const,
|
cacheGroups: {
|
||||||
minChunks: 2,
|
default: !!options.commonChunk && {
|
||||||
priority: 10,
|
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'];
|
config.resolve.mainFields = ['browser', 'module', 'main'];
|
||||||
|
|
||||||
@ -437,7 +435,7 @@ export function applyWebConfig(
|
|||||||
|
|
||||||
function getClientEnvironment(mode?: string) {
|
function getClientEnvironment(mode?: string) {
|
||||||
// Grab NODE_ENV and NX_PUBLIC_* environment variables and prepare them to be
|
// 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 nxPublicKeyRegex = /^NX_PUBLIC_/i;
|
||||||
|
|
||||||
const raw = Object.keys(process.env)
|
const raw = Object.keys(process.env)
|
||||||
@ -447,7 +445,7 @@ function getClientEnvironment(mode?: string) {
|
|||||||
return env;
|
return env;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Stringify all values so we can feed into webpack DefinePlugin
|
// Stringify all values so we can feed into rspack DefinePlugin
|
||||||
const stringified = {
|
const stringified = {
|
||||||
'process.env': Object.keys(raw).reduce((env, key) => {
|
'process.env': Object.keys(raw).reduce((env, key) => {
|
||||||
env[key] = JSON.stringify(raw[key]);
|
env[key] = JSON.stringify(raw[key]);
|
||||||
|
|||||||
36
packages/rspack/src/plugins/utils/get-terser-ecma-version.ts
Normal file
36
packages/rspack/src/plugins/utils/get-terser-ecma-version.ts
Normal 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;
|
||||||
|
}
|
||||||
@ -1,3 +1,5 @@
|
|||||||
|
import { logger } from '@nx/devkit';
|
||||||
|
|
||||||
export interface HashFormat {
|
export interface HashFormat {
|
||||||
chunk: string;
|
chunk: string;
|
||||||
extract: string;
|
extract: string;
|
||||||
@ -5,7 +7,18 @@ export interface HashFormat {
|
|||||||
script: string;
|
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 } = {
|
const hashFormats: { [option: string]: HashFormat } = {
|
||||||
none: { chunk: '', extract: '', file: '', script: '' },
|
none: { chunk: '', extract: '', file: '', script: '' },
|
||||||
media: { chunk: '', extract: '', file: `.[hash:${length}]`, script: '' },
|
media: { chunk: '', extract: '', file: `.[hash:${length}]`, script: '' },
|
||||||
|
|||||||
@ -105,7 +105,7 @@ function postcssOptionsCreator(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
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
|
// https://github.com/webpack-contrib/postcss-loader/blob/0d342b1/src/utils.js#L36
|
||||||
|
|
||||||
const postcssOptions: PostcssOptions = (loader) => ({
|
const postcssOptions: PostcssOptions = (loader) => ({
|
||||||
|
|||||||
@ -130,11 +130,11 @@ export interface NxAppRspackPluginOptions {
|
|||||||
*/
|
*/
|
||||||
outputHashing?: any;
|
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;
|
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;
|
poll?: number;
|
||||||
/**
|
/**
|
||||||
@ -150,7 +150,7 @@ export interface NxAppRspackPluginOptions {
|
|||||||
*/
|
*/
|
||||||
progress?: boolean;
|
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;
|
runtimeChunk?: boolean;
|
||||||
/**
|
/**
|
||||||
@ -194,11 +194,7 @@ export interface NxAppRspackPluginOptions {
|
|||||||
*/
|
*/
|
||||||
styles?: Array<ExtraEntryPointClass | string>;
|
styles?: Array<ExtraEntryPointClass | string>;
|
||||||
/**
|
/**
|
||||||
* Enables the use of subresource integrity validation.
|
* Override the `target` option in rspack configuration. This setting is not recommended and exists for backwards compatibility.
|
||||||
*/
|
|
||||||
subresourceIntegrity?: boolean;
|
|
||||||
/**
|
|
||||||
* Override the `target` option in webpack configuration. This setting is not recommended and exists for backwards compatibility.
|
|
||||||
*/
|
*/
|
||||||
target?: string | string[];
|
target?: string | string[];
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
235
packages/rspack/src/plugins/utils/plugins/normalize-options.ts
Normal file
235
packages/rspack/src/plugins/utils/plugins/normalize-options.ts
Normal 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('.\\'))
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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));
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -47,10 +47,10 @@ export function composePlugins(
|
|||||||
): Promise<Configuration> {
|
): Promise<Configuration> {
|
||||||
// Rspack may be calling us as a standard config function.
|
// Rspack may be calling us as a standard config function.
|
||||||
// Build up Nx context from environment variables.
|
// 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']) {
|
if (ctx['env']) {
|
||||||
ensureNxRspackExecutionContext(ctx);
|
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.
|
// and `withNX()` creates a new config object anyway.
|
||||||
config = {};
|
config = {};
|
||||||
}
|
}
|
||||||
@ -97,6 +97,7 @@ function ensureNxRspackExecutionContext(ctx: NxRspackExecutionContext): void {
|
|||||||
// These aren't actually needed since NxRspackPlugin and withNx both support them being undefined.
|
// These aren't actually needed since NxRspackPlugin and withNx both support them being undefined.
|
||||||
assets: undefined,
|
assets: undefined,
|
||||||
outputFileName: undefined,
|
outputFileName: undefined,
|
||||||
|
outputPath: undefined,
|
||||||
rspackConfig: undefined,
|
rspackConfig: undefined,
|
||||||
};
|
};
|
||||||
ctx.context ??= {
|
ctx.context ??= {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ export async function createCompiler(
|
|||||||
},
|
},
|
||||||
context: ExecutorContext
|
context: ExecutorContext
|
||||||
): Promise<Compiler | MultiCompiler> {
|
): Promise<Compiler | MultiCompiler> {
|
||||||
const pathToConfig = path.join(context.root, options.rspackConfig);
|
const pathToConfig = options.rspackConfig;
|
||||||
let userDefinedConfig: any = {};
|
let userDefinedConfig: any = {};
|
||||||
if (options.tsConfig) {
|
if (options.tsConfig) {
|
||||||
userDefinedConfig = resolveUserDefinedRspackConfig(
|
userDefinedConfig = resolveUserDefinedRspackConfig(
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export function getCopyPatterns(assets: any[]) {
|
|||||||
return assets.map((asset) => {
|
return assets.map((asset) => {
|
||||||
return {
|
return {
|
||||||
context: asset.input,
|
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,
|
to: asset.output,
|
||||||
from: asset.glob,
|
from: asset.glob,
|
||||||
globOptions: {
|
globOptions: {
|
||||||
|
|||||||
@ -82,7 +82,7 @@ export function shareWorkspaceLibraries(
|
|||||||
pathMappings.reduce(
|
pathMappings.reduce(
|
||||||
(aliases, library) => ({
|
(aliases, library) => ({
|
||||||
...aliases,
|
...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
|
// e.g. path/to/my/lib/* -> path/to/my/lib
|
||||||
[library.name]: library.path.replace(/\/\*$/, ''),
|
[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"
|
* 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
|
* 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(
|
join(
|
||||||
library.name,
|
library.name,
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export function getFunctionDeterminateRemoteUrl(isServer = false) {
|
|||||||
if (!serveTarget) {
|
if (!serveTarget) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Cannot automatically determine URL of remote (${remote}). Looked for property "host" in the project's "${serveTarget}" target.\n
|
`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']]\``
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,7 @@ export function normalizeAssets(
|
|||||||
return {
|
return {
|
||||||
...asset,
|
...asset,
|
||||||
input: resolvedAssetPath,
|
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(/^\//, ''),
|
output: asset.output.replace(/^\//, ''),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,233 +1,50 @@
|
|||||||
import {
|
import { type Configuration } from '@rspack/core';
|
||||||
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 { normalizeAssets } from './normalize-assets';
|
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(
|
return function makeConfig(
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
{ options, context }: NxRspackExecutionContext
|
{ options, context }: NxRspackExecutionContext
|
||||||
): Configuration {
|
): Configuration {
|
||||||
const isProd =
|
if (processed.has(config)) return config;
|
||||||
process.env.NODE_ENV === 'production' || options.mode === 'production';
|
|
||||||
|
|
||||||
const project = context.projectGraph.nodes[context.projectName];
|
applyBaseConfig(
|
||||||
const sourceRoot = path.join(context.root, project.data.sourceRoot);
|
{
|
||||||
|
...options,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
...pluginOptions,
|
||||||
const tsconfigPaths = require('tsconfig-paths');
|
target: options.target ?? 'web',
|
||||||
const { paths } = tsconfigPaths.loadConfig(options.tsConfig);
|
assets: options.assets
|
||||||
const alias: ResolveAlias = Object.keys(paths).reduce((acc, k) => {
|
? options.assets
|
||||||
acc[k] = path.join(context.root, paths[k][0]);
|
: pluginOptions.assets
|
||||||
return acc;
|
? normalizeAssets(
|
||||||
}, {});
|
pluginOptions.assets,
|
||||||
|
options.root,
|
||||||
const plugins = config.plugins ?? [];
|
options.sourceRoot
|
||||||
if (options.extractLicenses) {
|
)
|
||||||
/**
|
: [],
|
||||||
* Needed to prevent an issue with Rspack and Workspaces where the
|
root: context.root,
|
||||||
* workspace's root package.json file is added to the dependency tree
|
projectName: context.projectName,
|
||||||
*/
|
targetName: context.targetName,
|
||||||
let rootPackageJsonName;
|
configurationName: context.configurationName,
|
||||||
const pathToRootPackageJson = join(context.root, 'package.json');
|
projectGraph: context.projectGraph,
|
||||||
if (existsSync(pathToRootPackageJson)) {
|
},
|
||||||
try {
|
config
|
||||||
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)
|
|
||||||
),
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
plugins.push(new rspack.ProgressPlugin());
|
|
||||||
|
|
||||||
options.fileReplacements.forEach((item) => {
|
processed.add(config);
|
||||||
alias[item.replace] = item.with;
|
return config;
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ export interface WithWebOptions {
|
|||||||
postcssConfig?: string;
|
postcssConfig?: string;
|
||||||
scripts?: Array<ExtraEntryPointClass | string>;
|
scripts?: Array<ExtraEntryPointClass | string>;
|
||||||
styles?: Array<ExtraEntryPointClass | string>;
|
styles?: Array<ExtraEntryPointClass | string>;
|
||||||
subresourceIntegrity?: boolean;
|
|
||||||
stylePreprocessorOptions?: {
|
stylePreprocessorOptions?: {
|
||||||
includePaths?: string[];
|
includePaths?: string[];
|
||||||
};
|
};
|
||||||
@ -51,7 +50,7 @@ export function withWeb(pluginOptions: WithWebOptions = {}) {
|
|||||||
|
|
||||||
function getClientEnvironment(mode?: string) {
|
function getClientEnvironment(mode?: string) {
|
||||||
// Grab NODE_ENV and NX_PUBLIC_* environment variables and prepare them to be
|
// 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 nxPublicKeyRegex = /^NX_PUBLIC_/i;
|
||||||
|
|
||||||
const raw = Object.keys(process.env)
|
const raw = Object.keys(process.env)
|
||||||
@ -61,7 +60,7 @@ function getClientEnvironment(mode?: string) {
|
|||||||
return env;
|
return env;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Stringify all values so we can feed into webpack DefinePlugin
|
// Stringify all values so we can feed into rspack DefinePlugin
|
||||||
const stringified = {
|
const stringified = {
|
||||||
'process.env': Object.keys(raw).reduce((env, key) => {
|
'process.env': Object.keys(raw).reduce((env, key) => {
|
||||||
env[key] = JSON.stringify(raw[key]);
|
env[key] = JSON.stringify(raw[key]);
|
||||||
|
|||||||
@ -51,7 +51,7 @@ export function applyBaseConfig(
|
|||||||
applyNxIndependentConfig(options, config);
|
applyNxIndependentConfig(options, config);
|
||||||
|
|
||||||
// Some of the options only work during actual tasks, not when reading the webpack config during CreateNodes.
|
// 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 });
|
applyNxDependentConfig(options, config, { useNormalizedEntry });
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user