feat(rspack): move logic for withWeb to applyWebConfig and bring in line with webpack (#28803)
<!-- Please make sure you have read the submission guidelines before posting an PR --> <!-- https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr --> <!-- Please make sure that your commit message follows our format --> <!-- Example: `fix(nx): must begin with lowercase` --> <!-- If this is a particularly complex change or feature addition, you can request a dedicated Nx release for this pull request branch. Mention someone from the Nx team or the `@nrwl/nx-pipelines-reviewers` and they will confirm if the PR warrants its own release for testing purposes, and generate it for you if appropriate. --> ## Current Behavior <!-- This is the behavior we have today --> `withWeb` from `@nx/rspack` is not reflective of what `@nx/webpack` does. ## Expected Behavior <!-- This is the behavior we should expect with the changes in this PR --> Bring `withWeb` in line with `@nx/webpack` ## Related Issue(s) <!-- Please link the issue being fixed so it gets closed when this is merged. --> Fixes #
This commit is contained in:
parent
2c9fc572f0
commit
fd2e8d0f55
@ -26,15 +26,13 @@
|
||||
"type": "string",
|
||||
"description": "The tsconfig file to build the project."
|
||||
},
|
||||
"typeCheck": {
|
||||
"skipTypeChecking": {
|
||||
"alias": "typeCheck",
|
||||
"type": "boolean",
|
||||
"description": "Skip the type checking."
|
||||
},
|
||||
"indexHtml": {
|
||||
"type": "string",
|
||||
"description": "The path to the index.html file."
|
||||
"description": "Skip the type checking. Default is `false`."
|
||||
},
|
||||
"index": {
|
||||
"alias": "indexHtml",
|
||||
"type": "string",
|
||||
"description": "HTML File which will be contain the application.",
|
||||
"x-completion-type": "file",
|
||||
|
||||
@ -65,7 +65,11 @@ describe('rspack e2e', () => {
|
||||
});
|
||||
expect(result).toContain('Successfully ran target build');
|
||||
// Make sure expected files are present.
|
||||
expect(listFiles(`dist/${project}`)).toHaveLength(5);
|
||||
/**
|
||||
* The files that are generated are:
|
||||
* ["3rdpartylicenses.txt", "assets", "favicon.ico", "index.html", "main.bf7851e6.js", "runtime.e4294127.js"]
|
||||
*/
|
||||
expect(listFiles(`dist/${project}`)).toHaveLength(6);
|
||||
|
||||
result = runCLI(`test ${project}`);
|
||||
expect(result).toContain('Successfully ran target test');
|
||||
@ -83,7 +87,7 @@ describe('rspack e2e', () => {
|
||||
env: { NODE_ENV: 'production' },
|
||||
});
|
||||
expect(result).toContain('Successfully ran target build');
|
||||
expect(listFiles(`dist/${project}`)).toHaveLength(5); // same length as before
|
||||
expect(listFiles(`dist/${project}`)).toHaveLength(6); // same length as before
|
||||
|
||||
// Generate a new app and check that the files are correct
|
||||
const app2 = uniq('app2');
|
||||
@ -116,7 +120,7 @@ describe('rspack e2e', () => {
|
||||
});
|
||||
expect(result).toContain('Successfully ran target build');
|
||||
// Make sure expected files are present.
|
||||
expect(listFiles(`dist/${app2}`)).toHaveLength(5);
|
||||
expect(listFiles(`dist/${app2}`)).toHaveLength(6);
|
||||
|
||||
result = runCLI(`test ${app2}`);
|
||||
expect(result).toContain('Successfully ran target test');
|
||||
|
||||
@ -39,7 +39,8 @@
|
||||
"@nx/workspace",
|
||||
// Imported types only
|
||||
"@module-federation/sdk",
|
||||
"@module-federation/enhanced"
|
||||
"@module-federation/enhanced",
|
||||
"css-loader"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -28,20 +28,27 @@
|
||||
"@nx/devkit": "file:../devkit",
|
||||
"@nx/web": "file:../web",
|
||||
"@phenomnomnominal/tsquery": "~5.0.1",
|
||||
"@rspack/core": "^1.0.4",
|
||||
"@rspack/dev-server": "^1.0.4",
|
||||
"@rspack/plugin-react-refresh": "^1.0.0",
|
||||
"autoprefixer": "^10.4.9",
|
||||
"chalk": "~4.1.0",
|
||||
"css-loader": "^6.4.0",
|
||||
"enquirer": "~2.3.6",
|
||||
"express": "^4.19.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"less-loader": "11.1.0",
|
||||
"license-webpack-plugin": "^4.0.2",
|
||||
"loader-utils": "^2.0.3",
|
||||
"sass": "^1.42.1",
|
||||
"sass-loader": "^12.2.0",
|
||||
"stylus-loader": "^7.1.0",
|
||||
"style-loader": "^3.3.0",
|
||||
"postcss-import": "~14.1.0",
|
||||
"postcss-loader": "^8.1.1",
|
||||
"@rspack/core": "^1.0.4",
|
||||
"@rspack/dev-server": "^1.0.4",
|
||||
"@rspack/plugin-react-refresh": "^1.0.0",
|
||||
"chalk": "~4.1.0",
|
||||
"postcss": "^8.4.38",
|
||||
"tsconfig-paths": "^4.1.2",
|
||||
"tslib": "^2.3.0"
|
||||
"tslib": "^2.3.0",
|
||||
"webpack-subresource-integrity": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@module-federation/enhanced": "~0.6.0",
|
||||
|
||||
17
packages/rspack/src/executors/rspack/schema.d.ts
vendored
17
packages/rspack/src/executors/rspack/schema.d.ts
vendored
@ -6,8 +6,10 @@ export interface RspackExecutorSchema {
|
||||
index?: string;
|
||||
tsConfig?: string;
|
||||
typeCheck?: boolean;
|
||||
skipTypeChecking?: boolean;
|
||||
outputPath?: string;
|
||||
outputFileName?: string;
|
||||
index?: string;
|
||||
indexHtml?: string;
|
||||
mode?: Mode;
|
||||
watch?: boolean;
|
||||
@ -23,6 +25,13 @@ export interface RspackExecutorSchema {
|
||||
generatePackageJson?: boolean;
|
||||
}
|
||||
|
||||
export interface AssetGlobPattern {
|
||||
glob: string;
|
||||
input: string;
|
||||
output: string;
|
||||
ignore?: string[];
|
||||
}
|
||||
|
||||
export interface FileReplacement {
|
||||
replace: string;
|
||||
with: string;
|
||||
@ -32,3 +41,11 @@ export interface OptimizationOptions {
|
||||
scripts: boolean;
|
||||
styles: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedRspackExecutorSchema extends RspackExecutorSchema {
|
||||
outputFileName: string;
|
||||
assets: AssetGlobPattern[];
|
||||
root: string;
|
||||
projectRoot: string;
|
||||
sourceRoot: string;
|
||||
}
|
||||
|
||||
@ -26,15 +26,13 @@
|
||||
"type": "string",
|
||||
"description": "The tsconfig file to build the project."
|
||||
},
|
||||
"typeCheck": {
|
||||
"skipTypeChecking": {
|
||||
"alias": "typeCheck",
|
||||
"type": "boolean",
|
||||
"description": "Skip the type checking."
|
||||
},
|
||||
"indexHtml": {
|
||||
"type": "string",
|
||||
"description": "The path to the index.html file."
|
||||
"description": "Skip the type checking. Default is `false`."
|
||||
},
|
||||
"index": {
|
||||
"alias": "indexHtml",
|
||||
"type": "string",
|
||||
"description": "HTML File which will be contain the application.",
|
||||
"x-completion-type": "file",
|
||||
|
||||
459
packages/rspack/src/plugins/utils/apply-web-config.ts
Normal file
459
packages/rspack/src/plugins/utils/apply-web-config.ts
Normal file
@ -0,0 +1,459 @@
|
||||
import {
|
||||
type RspackPluginInstance,
|
||||
type Configuration,
|
||||
type RuleSetRule,
|
||||
LightningCssMinimizerRspackPlugin,
|
||||
DefinePlugin,
|
||||
HtmlRspackPlugin,
|
||||
CssExtractRspackPlugin,
|
||||
EnvironmentPlugin,
|
||||
} from '@rspack/core';
|
||||
import { instantiateScriptPlugins } from './instantiate-script-plugins';
|
||||
import { join, resolve } from 'path';
|
||||
import { SubresourceIntegrityPlugin } from 'webpack-subresource-integrity';
|
||||
import { getOutputHashFormat } from './hash-format';
|
||||
import { normalizeExtraEntryPoints } from './normalize-entry';
|
||||
import {
|
||||
getCommonLoadersForCssModules,
|
||||
getCommonLoadersForGlobalCss,
|
||||
getCommonLoadersForGlobalStyle,
|
||||
} from './loaders/stylesheet-loaders';
|
||||
import { NormalizedNxAppRspackPluginOptions } from './models';
|
||||
|
||||
export function applyWebConfig(
|
||||
options: NormalizedNxAppRspackPluginOptions,
|
||||
config: Configuration = {},
|
||||
{
|
||||
useNormalizedEntry,
|
||||
}: {
|
||||
// rspack.Configuration allows arrays to be set on a single entry
|
||||
// rspack then normalizes them to { import: "..." } objects
|
||||
// This option allows use to preserve existing composePlugins behavior where entry.main is an array.
|
||||
useNormalizedEntry?: boolean;
|
||||
} = {}
|
||||
): void {
|
||||
if (global.NX_GRAPH_CREATION) return;
|
||||
|
||||
// Defaults that was applied from executor schema previously.
|
||||
options.runtimeChunk ??= true; // need this for HMR and other things to work
|
||||
options.extractCss ??= true;
|
||||
options.generateIndexHtml ??= true;
|
||||
options.index = options.index
|
||||
? join(options.root, options.index)
|
||||
: join(
|
||||
options.root,
|
||||
options.projectGraph.nodes[options.projectName].data.sourceRoot,
|
||||
'index.html'
|
||||
);
|
||||
options.styles ??= [];
|
||||
options.scripts ??= [];
|
||||
|
||||
const isProd =
|
||||
process.env.NODE_ENV === 'production' || options.mode === 'production';
|
||||
|
||||
const plugins: RspackPluginInstance[] = [
|
||||
new EnvironmentPlugin({
|
||||
NODE_ENV: isProd ? 'production' : 'development',
|
||||
}),
|
||||
];
|
||||
|
||||
const stylesOptimization =
|
||||
typeof options.optimization === 'object'
|
||||
? options.optimization.styles
|
||||
: options.optimization;
|
||||
|
||||
if (Array.isArray(options.scripts)) {
|
||||
plugins.push(...instantiateScriptPlugins(options));
|
||||
}
|
||||
if (options.index && options.generateIndexHtml) {
|
||||
plugins.push(
|
||||
new HtmlRspackPlugin({
|
||||
template: options.index,
|
||||
sri: options.subresourceIntegrity ? 'sha256' : undefined,
|
||||
...(options.baseHref ? { base: { href: options.baseHref } } : {}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
if (options.subresourceIntegrity) {
|
||||
plugins.push(new SubresourceIntegrityPlugin() as any);
|
||||
}
|
||||
|
||||
const minimizer: RspackPluginInstance[] = [];
|
||||
if (stylesOptimization) {
|
||||
minimizer.push(
|
||||
new LightningCssMinimizerRspackPlugin({
|
||||
test: /\.(?:css|scss|sass|less|styl)$/,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!options.ssr) {
|
||||
plugins.push(
|
||||
new DefinePlugin(getClientEnvironment(process.env.NODE_ENV).stringified)
|
||||
);
|
||||
}
|
||||
|
||||
const entries: { [key: string]: { import: string[] } } = {};
|
||||
const globalStylePaths: string[] = [];
|
||||
|
||||
// Determine hashing format.
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
|
||||
const includePaths: string[] = [];
|
||||
if (options?.stylePreprocessorOptions?.includePaths?.length > 0) {
|
||||
options.stylePreprocessorOptions.includePaths.forEach(
|
||||
(includePath: string) =>
|
||||
includePaths.push(resolve(options.root, includePath))
|
||||
);
|
||||
}
|
||||
|
||||
let lessPathOptions: { paths?: string[] } = {};
|
||||
|
||||
if (includePaths.length > 0) {
|
||||
lessPathOptions = {
|
||||
paths: includePaths,
|
||||
};
|
||||
}
|
||||
|
||||
// Process global styles.
|
||||
if (options.styles.length > 0) {
|
||||
normalizeExtraEntryPoints(options.styles, 'styles').forEach((style) => {
|
||||
const resolvedPath = style.input.startsWith('.')
|
||||
? style.input
|
||||
: resolve(options.root, style.input);
|
||||
// Add style entry points.
|
||||
if (entries[style.bundleName]) {
|
||||
entries[style.bundleName].import.push(resolvedPath);
|
||||
} else {
|
||||
entries[style.bundleName] = { import: [resolvedPath] };
|
||||
}
|
||||
|
||||
// Add global css paths.
|
||||
globalStylePaths.push(resolvedPath);
|
||||
});
|
||||
}
|
||||
|
||||
const cssModuleRules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.module\.css$/,
|
||||
exclude: globalStylePaths,
|
||||
use: getCommonLoadersForCssModules(options, includePaths),
|
||||
},
|
||||
{
|
||||
test: /\.module\.(scss|sass)$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForCssModules(options, includePaths),
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
precision: 8,
|
||||
includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.less$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForCssModules(options, includePaths),
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
options: {
|
||||
lessOptions: {
|
||||
paths: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.module\.styl$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForCssModules(options, includePaths),
|
||||
{
|
||||
loader: join(
|
||||
__dirname,
|
||||
'../../../utils/webpack/deprecated-stylus-loader.js'
|
||||
),
|
||||
options: {
|
||||
stylusOptions: {
|
||||
include: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const globalCssRules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: globalStylePaths,
|
||||
use: getCommonLoadersForGlobalCss(options, includePaths),
|
||||
},
|
||||
{
|
||||
test: /\.scss$|\.sass$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalCss(options, includePaths),
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sourceMap: !!options.sourceMap,
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
// bootstrap-sass requires a minimum precision of 8
|
||||
precision: 8,
|
||||
includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalCss(options, includePaths),
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
options: {
|
||||
sourceMap: !!options.sourceMap,
|
||||
lessOptions: {
|
||||
javascriptEnabled: true,
|
||||
...lessPathOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.styl$/,
|
||||
exclude: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalCss(options, includePaths),
|
||||
{
|
||||
loader: join(
|
||||
__dirname,
|
||||
'../../../utils/webpack/deprecated-stylus-loader.js'
|
||||
),
|
||||
options: {
|
||||
sourceMap: !!options.sourceMap,
|
||||
stylusOptions: {
|
||||
include: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const globalStyleRules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.css$/,
|
||||
include: globalStylePaths,
|
||||
use: getCommonLoadersForGlobalStyle(options, includePaths),
|
||||
},
|
||||
{
|
||||
test: /\.scss$|\.sass$/,
|
||||
include: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalStyle(options, includePaths),
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
implementation: require('sass'),
|
||||
sourceMap: !!options.sourceMap,
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
// bootstrap-sass requires a minimum precision of 8
|
||||
precision: 8,
|
||||
includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.less$/,
|
||||
include: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalStyle(options, includePaths),
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
options: {
|
||||
sourceMap: !!options.sourceMap,
|
||||
lessOptions: {
|
||||
javascriptEnabled: true,
|
||||
...lessPathOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.styl$/,
|
||||
include: globalStylePaths,
|
||||
use: [
|
||||
...getCommonLoadersForGlobalStyle(options, includePaths),
|
||||
{
|
||||
loader: join(
|
||||
__dirname,
|
||||
'../../../utils/webpack/deprecated-stylus-loader.js'
|
||||
),
|
||||
options: {
|
||||
sourceMap: !!options.sourceMap,
|
||||
stylusOptions: {
|
||||
include: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const rules: RuleSetRule[] = [
|
||||
{
|
||||
test: /\.css$|\.scss$|\.sass$|\.less$|\.styl$/,
|
||||
oneOf: [...cssModuleRules, ...globalCssRules, ...globalStyleRules],
|
||||
},
|
||||
];
|
||||
|
||||
if (options.extractCss) {
|
||||
plugins.push(
|
||||
// extract global css from js files into own css file
|
||||
new CssExtractRspackPlugin({
|
||||
filename: `[name]${hashFormat.extract}.css`,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
config.output = {
|
||||
...(config.output ?? {}),
|
||||
assetModuleFilename: '[name].[contenthash:20][ext]',
|
||||
crossOriginLoading: options.subresourceIntegrity
|
||||
? ('anonymous' as const)
|
||||
: (false as const),
|
||||
};
|
||||
|
||||
// In case users customize their webpack config with unsupported entry.
|
||||
if (typeof config.entry === 'function')
|
||||
throw new Error('Entry function is not supported. Use an object.');
|
||||
if (typeof config.entry === 'string')
|
||||
throw new Error('Entry string is not supported. Use an object.');
|
||||
if (Array.isArray(config.entry))
|
||||
throw new Error('Entry array is not supported. Use an object.');
|
||||
|
||||
Object.entries(entries).forEach(([entryName, entryData]) => {
|
||||
if (useNormalizedEntry) {
|
||||
config.entry[entryName] = { import: entryData.import };
|
||||
} else {
|
||||
config.entry[entryName] = entryData.import;
|
||||
}
|
||||
});
|
||||
|
||||
config.optimization = {
|
||||
...(config.optimization ?? {}),
|
||||
minimizer: [...(config.optimization?.minimizer ?? []), ...minimizer],
|
||||
emitOnErrors: false,
|
||||
moduleIds: 'deterministic' as const,
|
||||
runtimeChunk: options.runtimeChunk ? { name: 'runtime' } : false,
|
||||
splitChunks: {
|
||||
defaultSizeTypes:
|
||||
config.optimization?.splitChunks !== false
|
||||
? config.optimization?.splitChunks?.defaultSizeTypes
|
||||
: ['...'],
|
||||
maxAsyncRequests: Infinity,
|
||||
cacheGroups: {
|
||||
default: !!options.commonChunk && {
|
||||
chunks: 'async' as const,
|
||||
minChunks: 2,
|
||||
priority: 10,
|
||||
},
|
||||
common: !!options.commonChunk && {
|
||||
name: 'common',
|
||||
chunks: 'async' as const,
|
||||
minChunks: 2,
|
||||
enforce: true,
|
||||
priority: 5,
|
||||
},
|
||||
vendors: false as const,
|
||||
vendor: !!options.vendorChunk && {
|
||||
name: 'vendor',
|
||||
chunks: (chunk) => chunk.name === 'main',
|
||||
enforce: true,
|
||||
test: /[\\/]node_modules[\\/]/,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
config.resolve.mainFields = ['browser', 'module', 'main'];
|
||||
|
||||
config.module = {
|
||||
...(config.module ?? {}),
|
||||
rules: [
|
||||
...(config.module.rules ?? []),
|
||||
// Images: Inline small images, and emit a separate file otherwise.
|
||||
{
|
||||
test: /\.(avif|bmp|gif|ico|jpe?g|png|webp)$/,
|
||||
type: 'asset',
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 10_000, // 10 kB
|
||||
},
|
||||
},
|
||||
},
|
||||
// SVG: same as image but we need to separate it so it can be swapped for SVGR in the React plugin.
|
||||
{
|
||||
test: /\.svg$/,
|
||||
type: 'asset',
|
||||
parser: {
|
||||
dataUrlCondition: {
|
||||
maxSize: 10_000, // 10 kB
|
||||
},
|
||||
},
|
||||
},
|
||||
// Fonts: Emit separate file and export the URL.
|
||||
{
|
||||
test: /\.(eot|otf|ttf|woff|woff2)$/,
|
||||
type: 'asset/resource',
|
||||
},
|
||||
...rules,
|
||||
],
|
||||
};
|
||||
|
||||
config.plugins ??= [];
|
||||
config.plugins.push(...plugins);
|
||||
}
|
||||
|
||||
function getClientEnvironment(mode?: string) {
|
||||
// Grab NODE_ENV and NX_PUBLIC_* environment variables and prepare them to be
|
||||
// injected into the application via DefinePlugin in webpack configuration.
|
||||
const nxPublicKeyRegex = /^NX_PUBLIC_/i;
|
||||
|
||||
const raw = Object.keys(process.env)
|
||||
.filter((key) => nxPublicKeyRegex.test(key))
|
||||
.reduce((env, key) => {
|
||||
env[key] = process.env[key];
|
||||
return env;
|
||||
}, {});
|
||||
|
||||
// Stringify all values so we can feed into webpack DefinePlugin
|
||||
const stringified = {
|
||||
'process.env': Object.keys(raw).reduce((env, key) => {
|
||||
env[key] = JSON.stringify(raw[key]);
|
||||
return env;
|
||||
}, {}),
|
||||
};
|
||||
|
||||
return { stringified };
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import { posix } from 'path';
|
||||
import { getHashDigest, interpolateName } from 'loader-utils';
|
||||
|
||||
export function getCSSModuleLocalIdent(
|
||||
ctx,
|
||||
localIdentName,
|
||||
localName,
|
||||
options
|
||||
) {
|
||||
// Use the filename or folder name, based on some uses the index.js / index.module.(css|scss|sass) project style
|
||||
const fileNameOrFolder = ctx.resourcePath.match(
|
||||
/index\.module\.(css|scss|sass)$/
|
||||
)
|
||||
? '[folder]'
|
||||
: '[name]';
|
||||
// Create a hash based on a the file location and class name. Will be unique across a project, and close to globally unique.
|
||||
const hash = getHashDigest(
|
||||
posix.relative(ctx.rootContext, ctx.resourcePath) + localName,
|
||||
'md5',
|
||||
'base64',
|
||||
5
|
||||
);
|
||||
// Use loaderUtils to find the file or folder name
|
||||
const className = interpolateName(
|
||||
ctx,
|
||||
`${fileNameOrFolder}_${localName}__${hash}`,
|
||||
options
|
||||
);
|
||||
// Remove the .module that appears in every classname when based on the file and replace all "." with "_".
|
||||
return className.replace('.module_', '_').replace(/\./g, '_');
|
||||
}
|
||||
26
packages/rspack/src/plugins/utils/hash-format.ts
Normal file
26
packages/rspack/src/plugins/utils/hash-format.ts
Normal file
@ -0,0 +1,26 @@
|
||||
export interface HashFormat {
|
||||
chunk: string;
|
||||
extract: string;
|
||||
file: string;
|
||||
script: string;
|
||||
}
|
||||
|
||||
export function getOutputHashFormat(option: string, length = 20): HashFormat {
|
||||
const hashFormats: { [option: string]: HashFormat } = {
|
||||
none: { chunk: '', extract: '', file: '', script: '' },
|
||||
media: { chunk: '', extract: '', file: `.[hash:${length}]`, script: '' },
|
||||
bundles: {
|
||||
chunk: `.[chunkhash:${length}]`,
|
||||
extract: `.[contenthash:${length}]`,
|
||||
file: '',
|
||||
script: `.[contenthash:${length}]`,
|
||||
},
|
||||
all: {
|
||||
chunk: `.[chunkhash:${length}]`,
|
||||
extract: `.[contenthash:${length}]`,
|
||||
file: `.[contenthash:${length}]`,
|
||||
script: `.[contenthash:${length}]`,
|
||||
},
|
||||
};
|
||||
return hashFormats[option] || hashFormats['none'];
|
||||
}
|
||||
@ -0,0 +1,55 @@
|
||||
import * as path from 'path';
|
||||
import type { RspackPluginInstance } from '@rspack/core';
|
||||
|
||||
import { getOutputHashFormat } from './hash-format';
|
||||
import { ScriptsRspackPlugin } from './plugins/scripts-rspack-plugin';
|
||||
import { normalizeExtraEntryPoints } from './normalize-entry';
|
||||
|
||||
export function instantiateScriptPlugins(options: any): RspackPluginInstance[] {
|
||||
// process global scripts
|
||||
const globalScriptsByBundleName = normalizeExtraEntryPoints(
|
||||
options.scripts || [],
|
||||
'scripts'
|
||||
).reduce(
|
||||
(
|
||||
prev: { inject: boolean; bundleName: string; paths: string[] }[],
|
||||
curr
|
||||
) => {
|
||||
const bundleName = curr.bundleName;
|
||||
const resolvedPath = path.resolve(options.root, curr.input);
|
||||
const existingEntry = prev.find((el) => el.bundleName === bundleName);
|
||||
if (existingEntry) {
|
||||
existingEntry.paths.push(resolvedPath);
|
||||
} else {
|
||||
prev.push({
|
||||
inject: curr.inject,
|
||||
bundleName,
|
||||
paths: [resolvedPath],
|
||||
});
|
||||
}
|
||||
|
||||
return prev;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
const plugins = [];
|
||||
// Add a new asset for each entry.
|
||||
globalScriptsByBundleName.forEach((script) => {
|
||||
const hash = script.inject ? hashFormat.script : '';
|
||||
const bundleName = script.bundleName;
|
||||
|
||||
plugins.push(
|
||||
new ScriptsRspackPlugin({
|
||||
name: bundleName,
|
||||
sourceMap: !!options.sourceMap,
|
||||
filename: `${path.basename(bundleName)}${hash}.js`,
|
||||
scripts: script.paths,
|
||||
basePath: options.sourceRoot,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
return plugins;
|
||||
}
|
||||
145
packages/rspack/src/plugins/utils/loaders/stylesheet-loaders.ts
Normal file
145
packages/rspack/src/plugins/utils/loaders/stylesheet-loaders.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import * as path from 'path';
|
||||
import autoprefixer = require('autoprefixer');
|
||||
import postcssImports = require('postcss-import');
|
||||
import { CssExtractRspackPlugin } from '@rspack/core';
|
||||
|
||||
import { getCSSModuleLocalIdent } from '../get-css-module-local-ident';
|
||||
import { getOutputHashFormat } from '../hash-format';
|
||||
import { PostcssCliResources } from '../plugins/postcss-cli-resources';
|
||||
|
||||
interface PostcssOptions {
|
||||
(loader: any): any;
|
||||
|
||||
config?: string;
|
||||
}
|
||||
|
||||
export function getCommonLoadersForCssModules(
|
||||
options: any,
|
||||
includePaths: string[]
|
||||
) {
|
||||
// load component css as raw strings
|
||||
return [
|
||||
{
|
||||
loader: options.extractCss
|
||||
? CssExtractRspackPlugin.loader
|
||||
: require.resolve('style-loader'),
|
||||
},
|
||||
{
|
||||
loader: require.resolve('css-loader'),
|
||||
options: {
|
||||
modules: {
|
||||
mode: 'local',
|
||||
getLocalIdent: getCSSModuleLocalIdent,
|
||||
},
|
||||
importLoaders: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, {
|
||||
includePaths,
|
||||
forCssModules: true,
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getCommonLoadersForGlobalCss(
|
||||
options: any,
|
||||
includePaths: string[]
|
||||
) {
|
||||
return [
|
||||
{
|
||||
loader: options.extractCss
|
||||
? CssExtractRspackPlugin.loader
|
||||
: require.resolve('style-loader'),
|
||||
},
|
||||
{ loader: require.resolve('css-loader'), options: { url: false } },
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, {
|
||||
includePaths,
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getCommonLoadersForGlobalStyle(
|
||||
options: any,
|
||||
includePaths: string[]
|
||||
) {
|
||||
return [
|
||||
{
|
||||
loader: options.extractCss
|
||||
? CssExtractRspackPlugin.loader
|
||||
: require.resolve('style-loader'),
|
||||
options: { esModule: true },
|
||||
},
|
||||
{ loader: require.resolve('css-loader'), options: { url: false } },
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
options: {
|
||||
implementation: require('postcss'),
|
||||
postcssOptions: postcssOptionsCreator(options, {
|
||||
includePaths,
|
||||
}),
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function postcssOptionsCreator(
|
||||
options: any,
|
||||
{
|
||||
includePaths,
|
||||
forCssModules = false,
|
||||
}: {
|
||||
includePaths: string[];
|
||||
forCssModules?: boolean;
|
||||
}
|
||||
) {
|
||||
const hashFormat = getOutputHashFormat(options.outputHashing as string);
|
||||
// PostCSS options depend on the webpack loader, but we need to set the `config` path as a string due to this check:
|
||||
// https://github.com/webpack-contrib/postcss-loader/blob/0d342b1/src/utils.js#L36
|
||||
|
||||
const postcssOptions: PostcssOptions = (loader) => ({
|
||||
map: options.sourceMap &&
|
||||
options.sourceMap !== 'hidden' && {
|
||||
inline: true,
|
||||
annotation: false,
|
||||
},
|
||||
plugins: [
|
||||
postcssImports({
|
||||
addModulesDirectories: includePaths,
|
||||
resolve: (url: string) => (url.startsWith('~') ? url.slice(1) : url),
|
||||
}),
|
||||
...(forCssModules
|
||||
? []
|
||||
: [
|
||||
PostcssCliResources({
|
||||
baseHref: options.baseHref,
|
||||
deployUrl: options.deployUrl,
|
||||
loader,
|
||||
filename: `[name]${hashFormat.file}.[ext]`,
|
||||
publicPath: options.publicPath,
|
||||
rebaseRootRelative: options.rebaseRootRelative,
|
||||
}),
|
||||
autoprefixer(),
|
||||
]),
|
||||
],
|
||||
});
|
||||
|
||||
// If a path to postcssConfig is passed in, set it for app and all libs, otherwise
|
||||
// use automatic detection.
|
||||
if (typeof options.postcssConfig === 'string') {
|
||||
postcssOptions.config = path.join(options.root, options.postcssConfig);
|
||||
}
|
||||
|
||||
return postcssOptions;
|
||||
}
|
||||
241
packages/rspack/src/plugins/utils/models.ts
Normal file
241
packages/rspack/src/plugins/utils/models.ts
Normal file
@ -0,0 +1,241 @@
|
||||
import type { Mode } from '@rspack/core';
|
||||
import type { ProjectGraph } from '@nx/devkit';
|
||||
import type { AssetGlob } from '@nx/js/src/utils/assets/assets';
|
||||
|
||||
export interface AssetGlobPattern {
|
||||
glob: string;
|
||||
input: string;
|
||||
output: string;
|
||||
ignore?: string[];
|
||||
}
|
||||
|
||||
export interface ExtraEntryPointClass {
|
||||
bundleName?: string;
|
||||
inject?: boolean;
|
||||
input: string;
|
||||
lazy?: boolean;
|
||||
}
|
||||
|
||||
export interface FileReplacement {
|
||||
replace: string;
|
||||
with: string;
|
||||
}
|
||||
|
||||
export interface AdditionalEntryPoint {
|
||||
entryName: string;
|
||||
entryPath: string;
|
||||
}
|
||||
|
||||
export interface TransformerPlugin {
|
||||
name: string;
|
||||
options: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type TransformerEntry = string | TransformerPlugin;
|
||||
|
||||
export interface OptimizationOptions {
|
||||
scripts: boolean;
|
||||
styles: boolean;
|
||||
}
|
||||
|
||||
export interface NxAppRspackPluginOptions {
|
||||
/**
|
||||
* The tsconfig file for the project. e.g. `tsconfig.json`
|
||||
*/
|
||||
tsConfig?: string;
|
||||
/**
|
||||
* The entry point for the bundle. e.g. `src/main.ts`
|
||||
*/
|
||||
main?: string;
|
||||
/**
|
||||
* Secondary entry points for the bundle.
|
||||
*/
|
||||
additionalEntryPoints?: AdditionalEntryPoint[];
|
||||
/**
|
||||
* Assets to be copied over to the output path.
|
||||
*/
|
||||
assets?: Array<AssetGlob | string>;
|
||||
/**
|
||||
* Set <base href> for the resulting index.html.
|
||||
*/
|
||||
baseHref?: string;
|
||||
/**
|
||||
* Build the libraries from source. Default is `true`.
|
||||
*/
|
||||
buildLibsFromSource?: boolean;
|
||||
|
||||
commonChunk?: boolean;
|
||||
|
||||
/**
|
||||
* Delete the output path before building.
|
||||
*/
|
||||
deleteOutputPath?: boolean;
|
||||
/**
|
||||
* The deploy path for the application. e.g. `/my-app/`
|
||||
*/
|
||||
deployUrl?: string;
|
||||
/**
|
||||
* Define external packages that will not be bundled.
|
||||
* Use `all` to exclude all 3rd party packages, and `none` to bundle all packages.
|
||||
* Use an array to exclude specific packages from the bundle.
|
||||
* Default is `none`.
|
||||
*/
|
||||
externalDependencies?: 'all' | 'none' | string[];
|
||||
/**
|
||||
* Extract CSS as an external file. Default is `true`.
|
||||
*/
|
||||
extractCss?: boolean;
|
||||
/**
|
||||
* Extract licenses from 3rd party modules and add them to the output.
|
||||
*/
|
||||
extractLicenses?: boolean;
|
||||
/**
|
||||
* Replace files at build time. e.g. `[{ "replace": "src/a.dev.ts", "with": "src/a.prod.ts" }]`
|
||||
*/
|
||||
fileReplacements?: FileReplacement[];
|
||||
/**
|
||||
* Generate an `index.html` file if `index.html` is passed. Default is `true`
|
||||
*/
|
||||
generateIndexHtml?: boolean;
|
||||
/**
|
||||
* Generate a `package.json` file for the bundle. Useful for Node applications.
|
||||
*/
|
||||
generatePackageJson?: boolean;
|
||||
/**
|
||||
* Path to the `index.html`.
|
||||
*/
|
||||
index?: string;
|
||||
/**
|
||||
* Mode to run the build in.
|
||||
*/
|
||||
mode?: Mode;
|
||||
/**
|
||||
* Set the memory limit for the type-checking process. Default is `2048`.
|
||||
*/
|
||||
memoryLimit?: number;
|
||||
/**
|
||||
* Use the source file name in output chunks. Useful for development or for Node.
|
||||
*/
|
||||
namedChunks?: boolean;
|
||||
/**
|
||||
* Optimize the bundle using Terser.
|
||||
*/
|
||||
optimization?: boolean | OptimizationOptions;
|
||||
/**
|
||||
* Specify the output filename for the bundle. Useful for Node applications that use `@nx/js:node` to serve.
|
||||
*/
|
||||
outputFileName?: string;
|
||||
/**
|
||||
* Use file hashes in the output filenames. Recommended for production web applications.
|
||||
*/
|
||||
outputHashing?: any;
|
||||
/**
|
||||
* Override `output.path` in webpack configuration. This setting is not recommended and exists for backwards compatibility.
|
||||
*/
|
||||
outputPath?: string;
|
||||
/**
|
||||
* Override `watchOptions.poll` in webpack configuration. This setting is not recommended and exists for backwards compatibility.
|
||||
*/
|
||||
poll?: number;
|
||||
/**
|
||||
* The polyfill file to use. Useful for supporting legacy browsers. e.g. `src/polyfills.ts`
|
||||
*/
|
||||
polyfills?: string;
|
||||
/**
|
||||
* Manually set the PostCSS configuration file. By default, PostCSS will look for `postcss.config.js` in the directory.
|
||||
*/
|
||||
postcssConfig?: string;
|
||||
/**
|
||||
* Display build progress in the terminal.
|
||||
*/
|
||||
progress?: boolean;
|
||||
/**
|
||||
* Add an additional chunk for the Webpack runtime. Defaults to `true` when `target === 'web'`.
|
||||
*/
|
||||
runtimeChunk?: boolean;
|
||||
/**
|
||||
* External scripts that will be included before the main application entry.
|
||||
*/
|
||||
scripts?: Array<ExtraEntryPointClass | string>;
|
||||
/**
|
||||
* Do not add a `overrides` and `resolutions` entries to the generated package.json file. Only works in conjunction with `generatePackageJson` option.
|
||||
*/
|
||||
skipOverrides?: boolean;
|
||||
/**
|
||||
* Do not add a `packageManager` entry to the generated package.json file. Only works in conjunction with `generatePackageJson` option.
|
||||
*/
|
||||
skipPackageManager?: boolean;
|
||||
/**
|
||||
* Skip type checking. Default is `false`.
|
||||
*/
|
||||
skipTypeChecking?: boolean;
|
||||
/**
|
||||
* Skip type checking. Default is `false`.
|
||||
*/
|
||||
typeCheck?: boolean;
|
||||
/**
|
||||
* Generate source maps.
|
||||
*/
|
||||
sourceMap?: boolean | string;
|
||||
/**
|
||||
* When `true`, `process.env.NODE_ENV` will be excluded from the bundle. Useful for building a web application to run in a Node environment.
|
||||
*/
|
||||
ssr?: boolean;
|
||||
/**
|
||||
* Generate a `stats.json` file which can be analyzed using tools such as `webpack-bundle-analyzer`.
|
||||
*/
|
||||
statsJson?: boolean;
|
||||
/**
|
||||
* Options for the style preprocessor. e.g. `{ "includePaths": [] }` for SASS.
|
||||
*/
|
||||
stylePreprocessorOptions?: any;
|
||||
/**
|
||||
* External stylesheets that will be included with the application.
|
||||
*/
|
||||
styles?: Array<ExtraEntryPointClass | string>;
|
||||
/**
|
||||
* Enables the use of subresource integrity validation.
|
||||
*/
|
||||
subresourceIntegrity?: boolean;
|
||||
/**
|
||||
* Override the `target` option in webpack configuration. This setting is not recommended and exists for backwards compatibility.
|
||||
*/
|
||||
target?: string | string[];
|
||||
/**
|
||||
* List of TypeScript Compiler Transformers Plugins.
|
||||
*/
|
||||
transformers?: TransformerEntry[];
|
||||
/**
|
||||
* Generate a separate vendor chunk for 3rd party packages.
|
||||
*/
|
||||
vendorChunk?: boolean;
|
||||
/**
|
||||
* Log additional information for debugging purposes.
|
||||
*/
|
||||
verbose?: boolean;
|
||||
/**
|
||||
* Watch for file changes.
|
||||
*/
|
||||
watch?: boolean;
|
||||
/**
|
||||
* Set a public path for assets resources with absolute paths.
|
||||
*/
|
||||
publicPath?: string;
|
||||
/**
|
||||
* Whether to rebase absolute path for assets in postcss cli resources.
|
||||
*/
|
||||
rebaseRootRelative?: boolean;
|
||||
}
|
||||
|
||||
export interface NormalizedNxAppRspackPluginOptions
|
||||
extends NxAppRspackPluginOptions {
|
||||
projectName: string;
|
||||
root: string;
|
||||
projectRoot: string;
|
||||
sourceRoot: string;
|
||||
configurationName: string;
|
||||
targetName: string;
|
||||
projectGraph: ProjectGraph;
|
||||
outputFileName: string;
|
||||
assets: AssetGlobPattern[];
|
||||
}
|
||||
30
packages/rspack/src/plugins/utils/normalize-entry.ts
Normal file
30
packages/rspack/src/plugins/utils/normalize-entry.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { ExtraEntryPoint, NormalizedEntryPoint } from '../../utils/model';
|
||||
|
||||
export function normalizeExtraEntryPoints(
|
||||
extraEntryPoints: ExtraEntryPoint[],
|
||||
defaultBundleName: string
|
||||
): NormalizedEntryPoint[] {
|
||||
return extraEntryPoints.map((entry) => {
|
||||
let normalizedEntry;
|
||||
if (typeof entry === 'string') {
|
||||
normalizedEntry = {
|
||||
input: entry,
|
||||
inject: true,
|
||||
bundleName: defaultBundleName,
|
||||
};
|
||||
} else {
|
||||
const { inject = true, ...newEntry } = entry;
|
||||
let bundleName;
|
||||
|
||||
if (entry.bundleName) {
|
||||
bundleName = entry.bundleName;
|
||||
} else {
|
||||
bundleName = defaultBundleName;
|
||||
}
|
||||
|
||||
normalizedEntry = { ...newEntry, bundleName };
|
||||
}
|
||||
|
||||
return normalizedEntry;
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,198 @@
|
||||
import { interpolateName } from 'loader-utils';
|
||||
import * as path from 'path';
|
||||
import type { Declaration } from 'postcss';
|
||||
import * as url from 'node:url';
|
||||
import type { LoaderContext } from '@rspack/core';
|
||||
|
||||
function wrapUrl(url: string): string {
|
||||
let wrappedUrl;
|
||||
const hasSingleQuotes = url.indexOf("'") >= 0;
|
||||
if (hasSingleQuotes) {
|
||||
wrappedUrl = `"${url}"`;
|
||||
} else {
|
||||
wrappedUrl = `'${url}'`;
|
||||
}
|
||||
return `url(${wrappedUrl})`;
|
||||
}
|
||||
|
||||
export interface PostcssCliResourcesOptions {
|
||||
baseHref?: string;
|
||||
deployUrl?: string;
|
||||
resourcesOutputPath?: string;
|
||||
rebaseRootRelative?: boolean;
|
||||
filename: string;
|
||||
loader: LoaderContext<unknown>;
|
||||
publicPath: string;
|
||||
}
|
||||
|
||||
async function resolve(
|
||||
file: string,
|
||||
base: string,
|
||||
resolver: (file: string, base: string) => Promise<boolean | string>
|
||||
): Promise<boolean | string> {
|
||||
try {
|
||||
return await resolver(`./${file}`, base);
|
||||
} catch {
|
||||
return resolver(file, base);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports.postcss = true;
|
||||
|
||||
export function PostcssCliResources(options: PostcssCliResourcesOptions) {
|
||||
const {
|
||||
deployUrl = '',
|
||||
baseHref = '',
|
||||
resourcesOutputPath = '',
|
||||
rebaseRootRelative = false,
|
||||
filename,
|
||||
loader,
|
||||
publicPath = '',
|
||||
} = options;
|
||||
const dedupeSlashes = (url: string) => url.replace(/\/\/+/g, '/');
|
||||
const process = async (
|
||||
inputUrl: string,
|
||||
context: string,
|
||||
resourceCache: Map<string, string>
|
||||
) => {
|
||||
// If root-relative, absolute or protocol relative url, leave as is
|
||||
if (/^((?:\w+:)?\/\/|data:|chrome:|#)/.test(inputUrl)) {
|
||||
return inputUrl;
|
||||
}
|
||||
if (!rebaseRootRelative && /^\//.test(inputUrl)) {
|
||||
return inputUrl;
|
||||
}
|
||||
// If starts with a caret, remove and return remainder
|
||||
// this supports bypassing asset processing
|
||||
if (inputUrl.startsWith('^')) {
|
||||
return inputUrl.slice(1);
|
||||
}
|
||||
const cacheKey = path.resolve(context, inputUrl);
|
||||
const cachedUrl = resourceCache.get(cacheKey);
|
||||
if (cachedUrl) {
|
||||
return cachedUrl;
|
||||
}
|
||||
if (inputUrl.startsWith('~')) {
|
||||
inputUrl = inputUrl.slice(1);
|
||||
}
|
||||
if (inputUrl.startsWith('/')) {
|
||||
let outputUrl = '';
|
||||
if (deployUrl.match(/:\/\//) || deployUrl.startsWith('/')) {
|
||||
// If deployUrl is absolute or root relative, ignore baseHref & use deployUrl as is.
|
||||
outputUrl = `${deployUrl.replace(/\/$/, '')}${inputUrl}`;
|
||||
} else if (baseHref.match(/:\/\//)) {
|
||||
// If baseHref contains a scheme, include it as is.
|
||||
outputUrl =
|
||||
baseHref.replace(/\/$/, '') +
|
||||
dedupeSlashes(`/${deployUrl}/${inputUrl}`);
|
||||
} else {
|
||||
// Join together base-href, deploy-url and the original URL.
|
||||
outputUrl = dedupeSlashes(
|
||||
`/${baseHref}/${deployUrl}/${publicPath}/${inputUrl}`
|
||||
);
|
||||
}
|
||||
resourceCache.set(cacheKey, outputUrl);
|
||||
return outputUrl;
|
||||
}
|
||||
const { pathname, hash, search } = url.parse(inputUrl.replace(/\\/g, '/'));
|
||||
const resolver = (file: string, base: string) =>
|
||||
new Promise<boolean | string>((resolve, reject) => {
|
||||
loader.resolve(base, decodeURI(file), (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
const result = await resolve(pathname as string, context, resolver);
|
||||
return new Promise<boolean | string>((resolve, reject) => {
|
||||
loader.fs.readFile(result as string, (err: Error, content: Buffer) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
let outputPath = interpolateName(
|
||||
{ resourcePath: result } as LoaderContext<unknown>,
|
||||
filename,
|
||||
{ content }
|
||||
);
|
||||
if (resourcesOutputPath) {
|
||||
outputPath = path.posix.join(resourcesOutputPath, outputPath);
|
||||
}
|
||||
loader.addDependency(result as string);
|
||||
loader.emitFile(outputPath, content, undefined);
|
||||
let outputUrl = outputPath.replace(/\\/g, '/');
|
||||
if (hash || search) {
|
||||
outputUrl = url.format({ pathname: outputUrl, hash, search });
|
||||
}
|
||||
const loaderOptions: any = loader.loaders[loader.loaderIndex].options;
|
||||
if (deployUrl && loaderOptions.ident !== 'extracted') {
|
||||
outputUrl = url.resolve(deployUrl, outputUrl);
|
||||
}
|
||||
resourceCache.set(cacheKey, outputUrl);
|
||||
resolve(outputUrl);
|
||||
});
|
||||
});
|
||||
};
|
||||
return {
|
||||
postcssPlugin: 'postcss-cli-resources',
|
||||
Once(root) {
|
||||
const urlDeclarations: Array<Declaration> = [];
|
||||
/**
|
||||
* TODO: Explore if this can be rewritten using the new `Declaration()`
|
||||
* listener added in postcss v8
|
||||
*/
|
||||
root.walkDecls((decl) => {
|
||||
if (decl.value && decl.value.includes('url')) {
|
||||
urlDeclarations.push(decl);
|
||||
}
|
||||
});
|
||||
if (urlDeclarations.length === 0) {
|
||||
return;
|
||||
}
|
||||
const resourceCache = new Map<string, string>();
|
||||
return Promise.all(
|
||||
urlDeclarations.map(async (decl) => {
|
||||
const value = decl.value;
|
||||
const urlRegex = /url(?:\(\s*(['"]?))(.*?)(?:\1\s*\))/g;
|
||||
const segments: string[] = [];
|
||||
let match;
|
||||
let lastIndex = 0;
|
||||
let modified = false;
|
||||
// We want to load it relative to the file that imports
|
||||
const inputFile = decl.source && decl.source.input.file;
|
||||
const context =
|
||||
(inputFile && path.dirname(inputFile)) || loader.context;
|
||||
while ((match = urlRegex.exec(value))) {
|
||||
const originalUrl = match[2];
|
||||
let processedUrl;
|
||||
try {
|
||||
processedUrl = await process(originalUrl, context, resourceCache);
|
||||
} catch (err) {
|
||||
loader.emitError(decl.error(err.message, { word: originalUrl }));
|
||||
continue;
|
||||
}
|
||||
if (lastIndex < match.index) {
|
||||
segments.push(value.slice(lastIndex, match.index));
|
||||
}
|
||||
if (!processedUrl || originalUrl === processedUrl) {
|
||||
segments.push(match[0]);
|
||||
} else {
|
||||
segments.push(wrapUrl(processedUrl));
|
||||
modified = true;
|
||||
}
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
if (lastIndex < value.length) {
|
||||
segments.push(value.slice(lastIndex));
|
||||
}
|
||||
if (modified) {
|
||||
decl.value = segments.join('');
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
import { interpolateName } from 'loader-utils';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
sources,
|
||||
EntryPlugin,
|
||||
type Compiler,
|
||||
type Compilation,
|
||||
} from '@rspack/core';
|
||||
|
||||
export interface ScriptsRspackPluginOptions {
|
||||
name: string;
|
||||
sourceMap: boolean;
|
||||
scripts: string[];
|
||||
filename: string;
|
||||
basePath: string;
|
||||
}
|
||||
|
||||
interface ScriptOutput {
|
||||
filename: string;
|
||||
source: sources.Source;
|
||||
}
|
||||
|
||||
function addDependencies(compilation: any, scripts: string[]): void {
|
||||
for (const script of scripts) {
|
||||
compilation.fileDependencies.add(script);
|
||||
}
|
||||
}
|
||||
|
||||
function hook(
|
||||
compiler: any,
|
||||
action: (compilation: any, callback: (err?: Error) => void) => void
|
||||
) {
|
||||
compiler.hooks.thisCompilation.tap(
|
||||
'scripts-rspack-plugin',
|
||||
(compilation: any) => {
|
||||
compilation.hooks.additionalAssets.tapAsync(
|
||||
'scripts-rspack-plugin',
|
||||
(callback: (err?: Error) => void) => action(compilation, callback)
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export class ScriptsRspackPlugin {
|
||||
private _lastBuildTime?: number;
|
||||
private _cachedOutput?: ScriptOutput;
|
||||
|
||||
constructor(private options: Partial<ScriptsRspackPluginOptions> = {}) {}
|
||||
|
||||
private _insertOutput(
|
||||
compiler: Compiler,
|
||||
compilation: Compilation,
|
||||
{ filename, source }: ScriptOutput,
|
||||
cached = false
|
||||
) {
|
||||
new EntryPlugin(compiler.context, this.options.name).apply(compiler);
|
||||
|
||||
compilation.assets[filename] = source;
|
||||
}
|
||||
|
||||
apply(compiler: Compiler): void {
|
||||
if (!this.options.scripts || this.options.scripts.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const scripts = this.options.scripts
|
||||
.filter((script) => !!script)
|
||||
.map((script) => path.resolve(this.options.basePath || '', script));
|
||||
|
||||
hook(compiler, (compilation: Compilation, callback) => {
|
||||
const sourceGetters = scripts.map((fullPath) => {
|
||||
return new Promise<sources.Source>((resolve, reject) => {
|
||||
compilation.inputFileSystem.readFile(
|
||||
fullPath,
|
||||
(err: Error, data: Buffer) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const content = data.toString();
|
||||
|
||||
let source;
|
||||
if (this.options.sourceMap) {
|
||||
// TODO: Look for source map file (for '.min' scripts, etc.)
|
||||
|
||||
let adjustedPath = fullPath;
|
||||
if (this.options.basePath) {
|
||||
adjustedPath = path.relative(this.options.basePath, fullPath);
|
||||
}
|
||||
source = new sources.OriginalSource(content, adjustedPath);
|
||||
} else {
|
||||
source = new sources.RawSource(content);
|
||||
}
|
||||
|
||||
resolve(source);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(sourceGetters)
|
||||
.then((_sources) => {
|
||||
const concatSource = new sources.ConcatSource();
|
||||
_sources.forEach((source) => {
|
||||
concatSource.add(source);
|
||||
concatSource.add('\n;');
|
||||
});
|
||||
|
||||
const combinedSource = new sources.CachedSource(concatSource);
|
||||
const filename = interpolateName(
|
||||
{ resourcePath: 'scripts.js' },
|
||||
this.options.filename as string,
|
||||
{ content: combinedSource.source() }
|
||||
);
|
||||
|
||||
const output = { filename, source: combinedSource };
|
||||
this._insertOutput(compiler, compilation, output);
|
||||
this._cachedOutput = output;
|
||||
addDependencies(compilation, scripts);
|
||||
|
||||
callback();
|
||||
})
|
||||
.catch((err: Error) => callback(err));
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,13 @@
|
||||
import type { ExecutorContext } from '@nx/devkit';
|
||||
import {
|
||||
ExecutorContext,
|
||||
readCachedProjectGraph,
|
||||
readProjectsConfigurationFromProjectGraph,
|
||||
workspaceRoot,
|
||||
} from '@nx/devkit';
|
||||
import type { Configuration } from '@rspack/core';
|
||||
import { readNxJson } from 'nx/src/config/configuration';
|
||||
|
||||
import { SharedConfigContext } from './model';
|
||||
import { NormalizedRspackExecutorSchema } from '../executors/rspack/schema';
|
||||
|
||||
export const nxRspackComposablePlugin = 'nxRspackComposablePlugin';
|
||||
|
||||
@ -12,7 +18,7 @@ export function isNxRspackComposablePlugin(
|
||||
}
|
||||
|
||||
export interface NxRspackExecutionContext {
|
||||
options: unknown;
|
||||
options: NormalizedRspackExecutorSchema;
|
||||
context: ExecutorContext;
|
||||
configuration?: string;
|
||||
}
|
||||
@ -27,23 +33,82 @@ export interface AsyncNxComposableRspackPlugin {
|
||||
| Promise<Configuration>;
|
||||
}
|
||||
|
||||
export function composePlugins(...plugins: any[]) {
|
||||
return Object.defineProperty(
|
||||
export function composePlugins(
|
||||
...plugins: (
|
||||
| NxComposableRspackPlugin
|
||||
| AsyncNxComposableRspackPlugin
|
||||
| Promise<NxComposableRspackPlugin | AsyncNxComposableRspackPlugin>
|
||||
)[]
|
||||
) {
|
||||
return Object.assign(
|
||||
async function combined(
|
||||
config: Configuration,
|
||||
ctx: SharedConfigContext
|
||||
ctx: NxRspackExecutionContext
|
||||
): Promise<Configuration> {
|
||||
// Rspack may be calling us as a standard config function.
|
||||
// Build up Nx context from environment variables.
|
||||
// This is to enable `@nx/webpack/plugin` to work with existing projects.
|
||||
if (ctx['env']) {
|
||||
ensureNxRspackExecutionContext(ctx);
|
||||
// Build this from scratch since what webpack passes us is the env, not config,
|
||||
// and `withNX()` creates a new config object anyway.
|
||||
config = {};
|
||||
}
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const fn = await plugin;
|
||||
config = await fn(config, ctx);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
nxRspackComposablePlugin,
|
||||
{
|
||||
value: true,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
[nxRspackComposablePlugin]: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function composePluginsSync(...plugins: NxComposableRspackPlugin[]) {
|
||||
return Object.assign(
|
||||
function combined(
|
||||
config: Configuration,
|
||||
ctx: NxRspackExecutionContext
|
||||
): Configuration {
|
||||
for (const plugin of plugins) {
|
||||
config = plugin(config, ctx);
|
||||
}
|
||||
return config;
|
||||
},
|
||||
{
|
||||
[nxRspackComposablePlugin]: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function ensureNxRspackExecutionContext(ctx: NxRspackExecutionContext): void {
|
||||
const projectName = process.env.NX_TASK_TARGET_PROJECT;
|
||||
const targetName = process.env.NX_TASK_TARGET_TARGET;
|
||||
const configurationName = process.env.NX_TASK_TARGET_CONFIGURATION;
|
||||
const projectGraph = readCachedProjectGraph();
|
||||
const projectNode = projectGraph.nodes[projectName];
|
||||
ctx.options ??= {
|
||||
root: workspaceRoot,
|
||||
projectRoot: projectNode.data.root,
|
||||
sourceRoot: projectNode.data.sourceRoot ?? projectNode.data.root,
|
||||
// These aren't actually needed since NxRspackPlugin and withNx both support them being undefined.
|
||||
assets: undefined,
|
||||
outputFileName: undefined,
|
||||
rspackConfig: undefined,
|
||||
};
|
||||
ctx.context ??= {
|
||||
projectName,
|
||||
targetName,
|
||||
configurationName,
|
||||
projectsConfigurations:
|
||||
readProjectsConfigurationFromProjectGraph(projectGraph),
|
||||
nxJsonConfiguration: readNxJson(workspaceRoot),
|
||||
cwd: process.cwd(),
|
||||
root: workspaceRoot,
|
||||
isVerbose: process.env['NX_VERBOSE_LOGGING'] === 'true',
|
||||
projectGraph,
|
||||
};
|
||||
}
|
||||
|
||||
@ -172,6 +172,7 @@ export function addOrChangeBuildTarget(
|
||||
// If standalone project then use the project's name in dist.
|
||||
project.root === '.' ? project.name : project.root
|
||||
),
|
||||
index: joinPathFragments(project.root, 'src/index.html'),
|
||||
main: determineMain(tree, options),
|
||||
tsConfig: determineTsConfig(tree, options),
|
||||
rspackConfig: joinPathFragments(project.root, 'rspack.config.js'),
|
||||
|
||||
@ -1,7 +1,19 @@
|
||||
import { ExecutorContext } from '@nx/devkit';
|
||||
import { RspackExecutorSchema } from '../executors/rspack/schema';
|
||||
|
||||
export interface SharedConfigContext {
|
||||
options: RspackExecutorSchema;
|
||||
context: ExecutorContext;
|
||||
export interface ExtraEntryPointClass {
|
||||
bundleName?: string;
|
||||
inject?: boolean;
|
||||
input: string;
|
||||
lazy?: boolean;
|
||||
}
|
||||
|
||||
export type ExtraEntryPoint = ExtraEntryPointClass | string;
|
||||
|
||||
export type NormalizedEntryPoint = Required<ExtraEntryPointClass>;
|
||||
|
||||
export interface EmittedFile {
|
||||
id?: string;
|
||||
name?: string;
|
||||
file: string;
|
||||
extension: string;
|
||||
initial: boolean;
|
||||
asset?: boolean;
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { DefinePlugin } from '@rspack/core';
|
||||
import { SharedConfigContext } from '../../model';
|
||||
import {
|
||||
ModuleFederationConfig,
|
||||
NxModuleFederationConfigOverride,
|
||||
} from '../models';
|
||||
import { getModuleFederationConfig } from './utils';
|
||||
import { NxRspackExecutionContext } from '../../config';
|
||||
|
||||
export async function withModuleFederationForSSR(
|
||||
options: ModuleFederationConfig,
|
||||
@ -19,7 +19,7 @@ export async function withModuleFederationForSSR(
|
||||
isServer: true,
|
||||
});
|
||||
|
||||
return (config, { context }: SharedConfigContext) => {
|
||||
return (config, { context }: NxRspackExecutionContext) => {
|
||||
config.target = 'async-node';
|
||||
config.output.uniqueName = options.name;
|
||||
config.optimization = {
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import { ModuleFederationPlugin } from '@module-federation/enhanced/rspack';
|
||||
import type { Configuration } from '@rspack/core';
|
||||
import { DefinePlugin } from '@rspack/core';
|
||||
import { SharedConfigContext } from '../../model';
|
||||
import {
|
||||
ModuleFederationConfig,
|
||||
NxModuleFederationConfigOverride,
|
||||
} from '../models';
|
||||
import { getModuleFederationConfig } from './utils';
|
||||
import { NxRspackExecutionContext } from '../../config';
|
||||
|
||||
const isVarOrWindow = (libType?: string) =>
|
||||
libType === 'var' || libType === 'window';
|
||||
@ -31,7 +31,7 @@ export async function withModuleFederation(
|
||||
|
||||
return function makeConfig(
|
||||
config: Configuration,
|
||||
{ context }: SharedConfigContext
|
||||
{ context }: NxRspackExecutionContext
|
||||
): Configuration {
|
||||
config.output.uniqueName = options.name;
|
||||
config.output.publicPath = 'auto';
|
||||
|
||||
@ -25,6 +25,12 @@ export async function readRspackOptions(
|
||||
root: workspaceRoot,
|
||||
projectRoot: '',
|
||||
sourceRoot: '',
|
||||
outputFileName: '',
|
||||
assets: [],
|
||||
main: '',
|
||||
tsConfig: '',
|
||||
outputPath: '',
|
||||
rspackConfig: '',
|
||||
},
|
||||
context: {
|
||||
root: workspaceRoot,
|
||||
|
||||
@ -11,13 +11,13 @@ import * as path from 'path';
|
||||
import { join } from 'path';
|
||||
import { GeneratePackageJsonPlugin } from '../plugins/generate-package-json-plugin';
|
||||
import { getCopyPatterns } from './get-copy-patterns';
|
||||
import { SharedConfigContext } from './model';
|
||||
import { normalizeAssets } from './normalize-assets';
|
||||
import { NxRspackExecutionContext } from './config';
|
||||
|
||||
export function withNx(_opts = {}) {
|
||||
return function makeConfig(
|
||||
config: Configuration,
|
||||
{ options, context }: SharedConfigContext
|
||||
{ options, context }: NxRspackExecutionContext
|
||||
): Configuration {
|
||||
const isProd =
|
||||
process.env.NODE_ENV === 'production' || options.mode === 'production';
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Configuration } from '@rspack/core';
|
||||
import { SharedConfigContext } from './model';
|
||||
import { withWeb } from './with-web';
|
||||
import { NxRspackExecutionContext } from './config';
|
||||
|
||||
export function withReact(opts = {}) {
|
||||
return function makeConfig(
|
||||
config: Configuration,
|
||||
{ options, context }: SharedConfigContext
|
||||
{ options, context }: NxRspackExecutionContext
|
||||
): Configuration {
|
||||
const isDev =
|
||||
process.env.NODE_ENV === 'development' || options.mode === 'development';
|
||||
|
||||
@ -1,132 +1,51 @@
|
||||
import { Configuration, RuleSetRule, rspack } from '@rspack/core';
|
||||
import * as path from 'path';
|
||||
import { SharedConfigContext } from './model';
|
||||
import { Configuration } from '@rspack/core';
|
||||
import { ExtraEntryPointClass } from './model';
|
||||
import { applyWebConfig } from '../plugins/utils/apply-web-config';
|
||||
import { NxRspackExecutionContext } from './config';
|
||||
|
||||
export interface WithWebOptions {
|
||||
baseHref?: string;
|
||||
deployUrl?: string;
|
||||
extractCss?: boolean;
|
||||
generateIndexHtml?: boolean;
|
||||
index?: string;
|
||||
postcssConfig?: string;
|
||||
scripts?: Array<ExtraEntryPointClass | string>;
|
||||
styles?: Array<ExtraEntryPointClass | string>;
|
||||
subresourceIntegrity?: boolean;
|
||||
stylePreprocessorOptions?: {
|
||||
includePaths?: string[];
|
||||
};
|
||||
cssModules?: boolean;
|
||||
ssr?: boolean;
|
||||
}
|
||||
|
||||
export function withWeb(opts: WithWebOptions = {}) {
|
||||
const processed = new Set();
|
||||
|
||||
export function withWeb(pluginOptions: WithWebOptions = {}) {
|
||||
return function makeConfig(
|
||||
config: Configuration,
|
||||
{ options, context }: SharedConfigContext
|
||||
{ options, context }: NxRspackExecutionContext
|
||||
): Configuration {
|
||||
const isProd =
|
||||
process.env.NODE_ENV === 'production' || options.mode === 'production';
|
||||
if (processed.has(config)) {
|
||||
return config;
|
||||
}
|
||||
|
||||
const projectRoot = path.join(
|
||||
context.root,
|
||||
context.projectGraph.nodes[context.projectName].data.root
|
||||
applyWebConfig(
|
||||
{
|
||||
...options,
|
||||
...pluginOptions,
|
||||
root: context.root,
|
||||
projectName: context.projectName,
|
||||
targetName: context.targetName,
|
||||
configurationName: context.configurationName,
|
||||
projectGraph: context.projectGraph,
|
||||
},
|
||||
config
|
||||
);
|
||||
|
||||
const includePaths: string[] = [];
|
||||
if (opts?.stylePreprocessorOptions?.includePaths?.length > 0) {
|
||||
opts.stylePreprocessorOptions.includePaths.forEach(
|
||||
(includePath: string) =>
|
||||
includePaths.push(path.resolve(context.root, includePath))
|
||||
);
|
||||
}
|
||||
|
||||
let lessPathOptions: { paths?: string[] } = {};
|
||||
|
||||
if (includePaths.length > 0) {
|
||||
lessPathOptions = {
|
||||
paths: includePaths,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
target: config.target ?? 'web',
|
||||
experiments: {
|
||||
css: true,
|
||||
},
|
||||
module: {
|
||||
...config.module,
|
||||
rules: [
|
||||
...(config.module.rules || []),
|
||||
{
|
||||
test: /\.css$/,
|
||||
type: opts?.cssModules ? 'css/module' : undefined,
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
type: 'css',
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('postcss-loader'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.scss$|\.sass$/,
|
||||
type: opts?.cssModules ? 'css/module' : undefined,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('sass-loader'),
|
||||
options: {
|
||||
sourceMap: !!options.sourceMap,
|
||||
sassOptions: {
|
||||
fiber: false,
|
||||
// bootstrap-sass requires a minimum precision of 8
|
||||
precision: 8,
|
||||
includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /.less$/,
|
||||
type: opts?.cssModules ? 'css/module' : undefined,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('less-loader'),
|
||||
options: {
|
||||
sourceMap: !!options.sourceMap,
|
||||
lessOptions: {
|
||||
javascriptEnabled: true,
|
||||
...lessPathOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.styl$/,
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve('stylus-loader'),
|
||||
options: {
|
||||
sourceMap: !!options.sourceMap,
|
||||
stylusOptions: {
|
||||
include: includePaths,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
].filter((a): a is RuleSetRule => !!a),
|
||||
},
|
||||
plugins: [
|
||||
...config.plugins,
|
||||
new rspack.HtmlRspackPlugin({
|
||||
template: options.indexHtml
|
||||
? path.join(context.root, options.indexHtml)
|
||||
: path.join(projectRoot, 'src/index.html'),
|
||||
...(options.baseHref ? { base: { href: options.baseHref } } : {}),
|
||||
}),
|
||||
new rspack.EnvironmentPlugin({
|
||||
NODE_ENV: isProd ? 'production' : 'development',
|
||||
}),
|
||||
new rspack.DefinePlugin(
|
||||
getClientEnvironment(isProd ? 'production' : undefined).stringified
|
||||
),
|
||||
],
|
||||
};
|
||||
processed.add(config);
|
||||
return config;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -35,7 +35,7 @@ export function applyWebConfig(
|
||||
useNormalizedEntry?: boolean;
|
||||
} = {}
|
||||
): void {
|
||||
if (!process.env['NX_TASK_TARGET_PROJECT']) return;
|
||||
if (global.NX_GRAPH_CREATION) return;
|
||||
|
||||
// Defaults that was applied from executor schema previously.
|
||||
options.runtimeChunk ??= true; // need this for HMR and other things to work
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user