feat(rspack): use custom WriteIndexHtmlPlugin to handle variable interpolation (#30805)
## Current Behavior The `HtmlRspackPlugin` does not support interpolation of %VAR% in the index.html. This is supported with a custom Webpack Plugin for `@nx/webpack` for generating index.html files. ## Expected Behavior The `@nx/rspack` plugin should support the same feature set as webpack for seamless migration. Add a new `WriteIndexHtmlPlugin` for Rspack to support this. It should only be used when `useLegacyHtmlPlugin` is set to true
This commit is contained in:
parent
851196aaa5
commit
a65f0f421b
@ -42,6 +42,7 @@
|
|||||||
"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",
|
||||||
|
"parse5": "4.0.0",
|
||||||
"sass": "^1.85.0",
|
"sass": "^1.85.0",
|
||||||
"sass-embedded": "^1.83.4",
|
"sass-embedded": "^1.83.4",
|
||||||
"sass-loader": "^16.0.4",
|
"sass-loader": "^16.0.4",
|
||||||
|
|||||||
@ -34,6 +34,7 @@ describe('Convert webpack', () => {
|
|||||||
module.exports = composePlugins(
|
module.exports = composePlugins(
|
||||||
withNx(),
|
withNx(),
|
||||||
withReact({
|
withReact({
|
||||||
|
useLegacyHtmlPlugin: true,
|
||||||
// Uncomment this line if you don't want to use SVGR
|
// Uncomment this line if you don't want to use SVGR
|
||||||
// See: https://react-svgr.com/
|
// See: https://react-svgr.com/
|
||||||
// svgr: false
|
// svgr: false
|
||||||
@ -162,7 +163,7 @@ describe('Convert webpack', () => {
|
|||||||
*/
|
*/
|
||||||
export default composePlugins(
|
export default composePlugins(
|
||||||
withNx(),
|
withNx(),
|
||||||
withReact(),
|
withReact({ useLegacyHtmlPlugin: true }),
|
||||||
withModuleFederation(config, { dts: false })
|
withModuleFederation(config, { dts: false })
|
||||||
);
|
);
|
||||||
"
|
"
|
||||||
@ -262,7 +263,7 @@ describe('Convert webpack', () => {
|
|||||||
*/
|
*/
|
||||||
export default composePlugins(
|
export default composePlugins(
|
||||||
withNx(),
|
withNx(),
|
||||||
withReact(),
|
withReact({ useLegacyHtmlPlugin: true }),
|
||||||
withModuleFederation(config, { dts: false })
|
withModuleFederation(config, { dts: false })
|
||||||
);
|
);
|
||||||
"
|
"
|
||||||
@ -368,7 +369,7 @@ describe('Convert webpack', () => {
|
|||||||
*/
|
*/
|
||||||
export default composePlugins(
|
export default composePlugins(
|
||||||
withNx(),
|
withNx(),
|
||||||
withReact(),
|
withReact({ useLegacyHtmlPlugin: true }),
|
||||||
withModuleFederation(config, { dts: false })
|
withModuleFederation(config, { dts: false })
|
||||||
);
|
);
|
||||||
"
|
"
|
||||||
|
|||||||
@ -11,6 +11,111 @@ export function transformCjsConfigFile(tree: Tree, configPath: string) {
|
|||||||
transformWithModuleFederation(tree, configPath, scope);
|
transformWithModuleFederation(tree, configPath, scope);
|
||||||
transformWithModuleFederationSSR(tree, configPath, scope);
|
transformWithModuleFederationSSR(tree, configPath, scope);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add useLegacyHtmlPlugin: true to withWeb() calls
|
||||||
|
transformWithWebCalls(tree, configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformWithWebCalls(tree: Tree, configPath: string) {
|
||||||
|
const configContents = tree.read(configPath, 'utf-8');
|
||||||
|
const ast = tsquery.ast(configContents);
|
||||||
|
|
||||||
|
// Find withWeb() calls
|
||||||
|
const withWebCallNodes = tsquery(
|
||||||
|
ast,
|
||||||
|
'CallExpression > Identifier[name=withWeb]'
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there are withWeb calls, update them
|
||||||
|
if (withWebCallNodes.length > 0) {
|
||||||
|
let newContents = configContents;
|
||||||
|
|
||||||
|
for (const node of withWebCallNodes) {
|
||||||
|
const callExpr = node.parent;
|
||||||
|
if (!callExpr) continue;
|
||||||
|
|
||||||
|
const startPos = callExpr.getStart();
|
||||||
|
const endPos = callExpr.getEnd();
|
||||||
|
const callText = configContents.substring(startPos, endPos);
|
||||||
|
|
||||||
|
// Skip if useLegacyHtmlPlugin is already present
|
||||||
|
if (callText.includes('useLegacyHtmlPlugin')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's already withWeb({ ... }), add useLegacyHtmlPlugin: true to the options
|
||||||
|
if (callText.includes('{') && callText.includes('}')) {
|
||||||
|
const newCallText = callText.replace(
|
||||||
|
/\{\s*/,
|
||||||
|
'{ useLegacyHtmlPlugin: true,\n '
|
||||||
|
);
|
||||||
|
newContents =
|
||||||
|
newContents.substring(0, startPos) +
|
||||||
|
newCallText +
|
||||||
|
newContents.substring(endPos);
|
||||||
|
} else {
|
||||||
|
// If it's just withWeb(), replace with withWeb({ useLegacyHtmlPlugin: true })
|
||||||
|
const newCallText = 'withWeb({ useLegacyHtmlPlugin: true })';
|
||||||
|
newContents =
|
||||||
|
newContents.substring(0, startPos) +
|
||||||
|
newCallText +
|
||||||
|
newContents.substring(endPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContents !== configContents) {
|
||||||
|
tree.write(configPath, newContents);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no withWeb calls, check for withReact calls
|
||||||
|
const withReactCallNodes = tsquery(
|
||||||
|
ast,
|
||||||
|
'CallExpression > Identifier[name=withReact]'
|
||||||
|
);
|
||||||
|
if (withReactCallNodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newContents = configContents;
|
||||||
|
|
||||||
|
for (const node of withReactCallNodes) {
|
||||||
|
const callExpr = node.parent;
|
||||||
|
if (!callExpr) continue;
|
||||||
|
|
||||||
|
const startPos = callExpr.getStart();
|
||||||
|
const endPos = callExpr.getEnd();
|
||||||
|
const callText = configContents.substring(startPos, endPos);
|
||||||
|
|
||||||
|
// Skip if useLegacyHtmlPlugin is already present
|
||||||
|
if (callText.includes('useLegacyHtmlPlugin')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's already withReact({ ... }), add useLegacyHtmlPlugin: true to the options
|
||||||
|
if (callText.includes('{') && callText.includes('}')) {
|
||||||
|
const newCallText = callText.replace(
|
||||||
|
/\{\s*/,
|
||||||
|
'{ useLegacyHtmlPlugin: true,\n '
|
||||||
|
);
|
||||||
|
newContents =
|
||||||
|
newContents.substring(0, startPos) +
|
||||||
|
newCallText +
|
||||||
|
newContents.substring(endPos);
|
||||||
|
} else {
|
||||||
|
// If it's just withReact(), replace with withReact({ useLegacyHtmlPlugin: true })
|
||||||
|
const newCallText = 'withReact({ useLegacyHtmlPlugin: true })';
|
||||||
|
newContents =
|
||||||
|
newContents.substring(0, startPos) +
|
||||||
|
newCallText +
|
||||||
|
newContents.substring(endPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContents !== configContents) {
|
||||||
|
tree.write(configPath, newContents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformComposePlugins(
|
function transformComposePlugins(
|
||||||
|
|||||||
@ -11,6 +11,111 @@ export function transformEsmConfigFile(tree: Tree, configPath: string) {
|
|||||||
transformWithModuleFederation(tree, configPath, scope);
|
transformWithModuleFederation(tree, configPath, scope);
|
||||||
transformWithModuleFederationSSR(tree, configPath, scope);
|
transformWithModuleFederationSSR(tree, configPath, scope);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add useLegacyHtmlPlugin: true to withWeb() calls
|
||||||
|
transformWithWebCalls(tree, configPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
function transformWithWebCalls(tree: Tree, configPath: string) {
|
||||||
|
const configContents = tree.read(configPath, 'utf-8');
|
||||||
|
const ast = tsquery.ast(configContents);
|
||||||
|
|
||||||
|
// Find withWeb() calls
|
||||||
|
const withWebCallNodes = tsquery(
|
||||||
|
ast,
|
||||||
|
'CallExpression > Identifier[name=withWeb]'
|
||||||
|
);
|
||||||
|
|
||||||
|
// If there are withWeb calls, update them
|
||||||
|
if (withWebCallNodes.length > 0) {
|
||||||
|
let newContents = configContents;
|
||||||
|
|
||||||
|
for (const node of withWebCallNodes) {
|
||||||
|
const callExpr = node.parent;
|
||||||
|
if (!callExpr) continue;
|
||||||
|
|
||||||
|
const startPos = callExpr.getStart();
|
||||||
|
const endPos = callExpr.getEnd();
|
||||||
|
const callText = configContents.substring(startPos, endPos);
|
||||||
|
|
||||||
|
// Skip if useLegacyHtmlPlugin is already present
|
||||||
|
if (callText.includes('useLegacyHtmlPlugin')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's already withWeb({ ... }), add useLegacyHtmlPlugin: true to the options
|
||||||
|
if (callText.includes('{') && callText.includes('}')) {
|
||||||
|
const newCallText = callText.replace(
|
||||||
|
/\{\s*/,
|
||||||
|
'{ useLegacyHtmlPlugin: true,\n '
|
||||||
|
);
|
||||||
|
newContents =
|
||||||
|
newContents.substring(0, startPos) +
|
||||||
|
newCallText +
|
||||||
|
newContents.substring(endPos);
|
||||||
|
} else {
|
||||||
|
// If it's just withWeb(), replace with withWeb({ useLegacyHtmlPlugin: true })
|
||||||
|
const newCallText = 'withWeb({ useLegacyHtmlPlugin: true })';
|
||||||
|
newContents =
|
||||||
|
newContents.substring(0, startPos) +
|
||||||
|
newCallText +
|
||||||
|
newContents.substring(endPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContents !== configContents) {
|
||||||
|
tree.write(configPath, newContents);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no withWeb calls, check for withReact calls
|
||||||
|
const withReactCallNodes = tsquery(
|
||||||
|
ast,
|
||||||
|
'CallExpression > Identifier[name=withReact]'
|
||||||
|
);
|
||||||
|
if (withReactCallNodes.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newContents = configContents;
|
||||||
|
|
||||||
|
for (const node of withReactCallNodes) {
|
||||||
|
const callExpr = node.parent;
|
||||||
|
if (!callExpr) continue;
|
||||||
|
|
||||||
|
const startPos = callExpr.getStart();
|
||||||
|
const endPos = callExpr.getEnd();
|
||||||
|
const callText = configContents.substring(startPos, endPos);
|
||||||
|
|
||||||
|
// Skip if useLegacyHtmlPlugin is already present
|
||||||
|
if (callText.includes('useLegacyHtmlPlugin')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's already withReact({ ... }), add useLegacyHtmlPlugin: true to the options
|
||||||
|
if (callText.includes('{') && callText.includes('}')) {
|
||||||
|
const newCallText = callText.replace(
|
||||||
|
/\{\s*/,
|
||||||
|
'{ useLegacyHtmlPlugin: true,\n '
|
||||||
|
);
|
||||||
|
newContents =
|
||||||
|
newContents.substring(0, startPos) +
|
||||||
|
newCallText +
|
||||||
|
newContents.substring(endPos);
|
||||||
|
} else {
|
||||||
|
// If it's just withReact(), replace with withReact({ useLegacyHtmlPlugin: true })
|
||||||
|
const newCallText = 'withReact({ useLegacyHtmlPlugin: true })';
|
||||||
|
newContents =
|
||||||
|
newContents.substring(0, startPos) +
|
||||||
|
newCallText +
|
||||||
|
newContents.substring(endPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContents !== configContents) {
|
||||||
|
tree.write(configPath, newContents);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformComposePlugins(
|
function transformComposePlugins(
|
||||||
|
|||||||
@ -4,11 +4,12 @@ import {
|
|||||||
type RuleSetRule,
|
type RuleSetRule,
|
||||||
LightningCssMinimizerRspackPlugin,
|
LightningCssMinimizerRspackPlugin,
|
||||||
DefinePlugin,
|
DefinePlugin,
|
||||||
HtmlRspackPlugin,
|
|
||||||
CssExtractRspackPlugin,
|
CssExtractRspackPlugin,
|
||||||
EnvironmentPlugin,
|
EnvironmentPlugin,
|
||||||
RspackOptionsNormalized,
|
RspackOptionsNormalized,
|
||||||
|
HtmlRspackPlugin,
|
||||||
} from '@rspack/core';
|
} from '@rspack/core';
|
||||||
|
import { WriteIndexHtmlPlugin } from '../write-index-html-plugin';
|
||||||
import { instantiateScriptPlugins } from './instantiate-script-plugins';
|
import { instantiateScriptPlugins } from './instantiate-script-plugins';
|
||||||
import { join, resolve } from 'path';
|
import { join, resolve } from 'path';
|
||||||
import { getOutputHashFormat } from './hash-format';
|
import { getOutputHashFormat } from './hash-format';
|
||||||
@ -66,6 +67,21 @@ export function applyWebConfig(
|
|||||||
plugins.push(...instantiateScriptPlugins(options));
|
plugins.push(...instantiateScriptPlugins(options));
|
||||||
}
|
}
|
||||||
if (options.index && options.generateIndexHtml) {
|
if (options.index && options.generateIndexHtml) {
|
||||||
|
if (options.useLegacyHtmlPlugin) {
|
||||||
|
plugins.push(
|
||||||
|
new WriteIndexHtmlPlugin({
|
||||||
|
indexPath: options.index,
|
||||||
|
outputPath: 'index.html',
|
||||||
|
baseHref:
|
||||||
|
typeof options.baseHref === 'string' ? options.baseHref : undefined,
|
||||||
|
sri: options.subresourceIntegrity,
|
||||||
|
scripts: options.scripts,
|
||||||
|
styles: options.styles,
|
||||||
|
crossOrigin:
|
||||||
|
config.output?.scriptType === 'module' ? 'anonymous' : undefined,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
plugins.push(
|
plugins.push(
|
||||||
new HtmlRspackPlugin({
|
new HtmlRspackPlugin({
|
||||||
template: options.index,
|
template: options.index,
|
||||||
@ -77,6 +93,7 @@ export function applyWebConfig(
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const minimizer: RspackPluginInstance[] = [];
|
const minimizer: RspackPluginInstance[] = [];
|
||||||
if (isProd && stylesOptimization) {
|
if (isProd && stylesOptimization) {
|
||||||
|
|||||||
@ -247,6 +247,10 @@ export interface NxAppRspackPluginOptions {
|
|||||||
* Whether to rebase absolute path for assets in postcss cli resources.
|
* Whether to rebase absolute path for assets in postcss cli resources.
|
||||||
*/
|
*/
|
||||||
rebaseRootRelative?: boolean;
|
rebaseRootRelative?: boolean;
|
||||||
|
/**
|
||||||
|
* Use the legacy WriteIndexHtmlPlugin instead of the built-in HtmlRspackPlugin.
|
||||||
|
*/
|
||||||
|
useLegacyHtmlPlugin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NormalizedNxAppRspackPluginOptions
|
export interface NormalizedNxAppRspackPluginOptions
|
||||||
@ -260,4 +264,5 @@ export interface NormalizedNxAppRspackPluginOptions
|
|||||||
projectGraph: ProjectGraph;
|
projectGraph: ProjectGraph;
|
||||||
outputFileName: string;
|
outputFileName: string;
|
||||||
assets: AssetGlobPattern[];
|
assets: AssetGlobPattern[];
|
||||||
|
useLegacyHtmlPlugin: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -106,6 +106,8 @@ export function normalizeOptions(
|
|||||||
),
|
),
|
||||||
generateIndexHtml:
|
generateIndexHtml:
|
||||||
combinedPluginAndMaybeExecutorOptions.generateIndexHtml ?? true,
|
combinedPluginAndMaybeExecutorOptions.generateIndexHtml ?? true,
|
||||||
|
useLegacyHtmlPlugin:
|
||||||
|
combinedPluginAndMaybeExecutorOptions.useLegacyHtmlPlugin ?? false,
|
||||||
main: combinedPluginAndMaybeExecutorOptions.main,
|
main: combinedPluginAndMaybeExecutorOptions.main,
|
||||||
namedChunks: combinedPluginAndMaybeExecutorOptions.namedChunks ?? !isProd,
|
namedChunks: combinedPluginAndMaybeExecutorOptions.namedChunks ?? !isProd,
|
||||||
optimization: combinedPluginAndMaybeExecutorOptions.optimization ?? isProd,
|
optimization: combinedPluginAndMaybeExecutorOptions.optimization ?? isProd,
|
||||||
|
|||||||
378
packages/rspack/src/plugins/write-index-html-plugin.ts
Normal file
378
packages/rspack/src/plugins/write-index-html-plugin.ts
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
import * as rspack from '@rspack/core';
|
||||||
|
import { Compiler } from '@rspack/core';
|
||||||
|
import { createHash } from 'crypto';
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
import { EmittedFile, ExtraEntryPoint } from '../utils/model';
|
||||||
|
import { interpolateEnvironmentVariablesToIndex } from '../utils/webpack/interpolate-env-variables-to-index';
|
||||||
|
import { generateEntryPoints } from '../utils/webpack/package-chunk-sort';
|
||||||
|
import { extname } from 'path';
|
||||||
|
|
||||||
|
const parse5 = require('parse5');
|
||||||
|
|
||||||
|
export interface WriteIndexHtmlOptions {
|
||||||
|
indexPath: string;
|
||||||
|
outputPath: string;
|
||||||
|
baseHref?: string;
|
||||||
|
deployUrl?: string;
|
||||||
|
sri?: boolean;
|
||||||
|
scripts?: ExtraEntryPoint[];
|
||||||
|
styles?: ExtraEntryPoint[];
|
||||||
|
crossOrigin?: 'none' | 'anonymous' | 'use-credentials';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WriteIndexHtmlPlugin {
|
||||||
|
constructor(private readonly options: WriteIndexHtmlOptions) {}
|
||||||
|
|
||||||
|
apply(compiler: Compiler) {
|
||||||
|
const {
|
||||||
|
outputPath,
|
||||||
|
indexPath,
|
||||||
|
baseHref,
|
||||||
|
deployUrl,
|
||||||
|
sri = false,
|
||||||
|
scripts = [],
|
||||||
|
styles = [],
|
||||||
|
crossOrigin,
|
||||||
|
} = this.options;
|
||||||
|
compiler.hooks.thisCompilation.tap(
|
||||||
|
'WriteIndexHtmlPlugin',
|
||||||
|
(compilation) => {
|
||||||
|
compilation.hooks.processAssets.tap(
|
||||||
|
{
|
||||||
|
name: 'WriteIndexHtmlPlugin',
|
||||||
|
// After minification and sourcemaps are done
|
||||||
|
stage: rspack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE,
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
const moduleFiles = this.getEmittedFiles(compilation);
|
||||||
|
const files = moduleFiles.filter((x) => x.extension === '.css');
|
||||||
|
let content = readFileSync(indexPath).toString();
|
||||||
|
content = this.stripBom(content);
|
||||||
|
compilation.assets[outputPath] = this.augmentIndexHtml({
|
||||||
|
input: outputPath,
|
||||||
|
inputContent: interpolateEnvironmentVariablesToIndex(
|
||||||
|
content,
|
||||||
|
deployUrl
|
||||||
|
),
|
||||||
|
baseHref,
|
||||||
|
deployUrl,
|
||||||
|
crossOrigin,
|
||||||
|
sri,
|
||||||
|
entrypoints: generateEntryPoints({ scripts, styles }),
|
||||||
|
files: this.filterAndMapBuildFiles(files, ['.js', '.css']),
|
||||||
|
moduleFiles: this.filterAndMapBuildFiles(moduleFiles, ['.js']),
|
||||||
|
loadOutputFile: (filePath) =>
|
||||||
|
compilation.assets[filePath].source().toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getEmittedFiles(compilation: rspack.Compilation): EmittedFile[] {
|
||||||
|
const files: EmittedFile[] = [];
|
||||||
|
// adds all chunks to the list of emitted files such as lazy loaded modules
|
||||||
|
for (const chunk of compilation.chunks) {
|
||||||
|
for (const file of chunk.files) {
|
||||||
|
files.push({
|
||||||
|
// The id is guaranteed to exist at this point in the compilation process
|
||||||
|
id: chunk.id.toString(),
|
||||||
|
name: chunk.name,
|
||||||
|
file,
|
||||||
|
extension: extname(file),
|
||||||
|
initial: chunk.isOnlyInitial(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// other all files
|
||||||
|
for (const file of Object.keys(compilation.assets)) {
|
||||||
|
files.push({
|
||||||
|
file,
|
||||||
|
extension: extname(file),
|
||||||
|
initial: false,
|
||||||
|
asset: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// dedupe
|
||||||
|
return files.filter(
|
||||||
|
({ file, name }, index) =>
|
||||||
|
files.findIndex(
|
||||||
|
(f) => f.file === file && (!name || name === f.name)
|
||||||
|
) === index
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private stripBom(data: string) {
|
||||||
|
return data.replace(/^\uFEFF/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private augmentIndexHtml(params: {
|
||||||
|
/* Input file name (e. g. index.html) */
|
||||||
|
input: string;
|
||||||
|
/* Input contents */
|
||||||
|
inputContent: string;
|
||||||
|
baseHref?: string;
|
||||||
|
deployUrl?: string;
|
||||||
|
sri: boolean;
|
||||||
|
/** crossorigin attribute setting of elements that provide CORS support */
|
||||||
|
crossOrigin?: 'none' | 'anonymous' | 'use-credentials';
|
||||||
|
/*
|
||||||
|
* Files emitted by the build.
|
||||||
|
*/
|
||||||
|
files: {
|
||||||
|
file: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
}[];
|
||||||
|
/** Files that should be added using 'module'. */
|
||||||
|
moduleFiles?: {
|
||||||
|
file: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
}[];
|
||||||
|
/*
|
||||||
|
* Function that loads a file used.
|
||||||
|
* This allows us to use different routines within the IndexHtmlWebpackPlugin and
|
||||||
|
* when used without this plugin.
|
||||||
|
*/
|
||||||
|
loadOutputFile: (file: string) => string;
|
||||||
|
/** Used to sort the inseration of files in the HTML file */
|
||||||
|
entrypoints: string[];
|
||||||
|
}): rspack.sources.Source {
|
||||||
|
const { loadOutputFile, files, moduleFiles = [], entrypoints } = params;
|
||||||
|
|
||||||
|
let { crossOrigin = 'none' } = params;
|
||||||
|
if (params.sri && crossOrigin === 'none') {
|
||||||
|
crossOrigin = 'anonymous';
|
||||||
|
}
|
||||||
|
|
||||||
|
const stylesheets = new Set<string>();
|
||||||
|
const scripts = new Set<string>();
|
||||||
|
|
||||||
|
// Sort files in the order we want to insert them by entrypoint and dedupes duplicates
|
||||||
|
const mergedFiles = [...moduleFiles, ...files];
|
||||||
|
for (const entrypoint of entrypoints) {
|
||||||
|
for (const { extension, file, name } of mergedFiles) {
|
||||||
|
if (name !== entrypoint) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (extension) {
|
||||||
|
case '.js':
|
||||||
|
scripts.add(file);
|
||||||
|
break;
|
||||||
|
case '.css':
|
||||||
|
stylesheets.add(file);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the head and body elements
|
||||||
|
const treeAdapter = parse5.treeAdapters.default;
|
||||||
|
const document = parse5.parse(params.inputContent, {
|
||||||
|
treeAdapter,
|
||||||
|
locationInfo: true,
|
||||||
|
});
|
||||||
|
let headElement;
|
||||||
|
let bodyElement;
|
||||||
|
for (const docChild of document.childNodes) {
|
||||||
|
if (docChild.tagName === 'html') {
|
||||||
|
for (const htmlChild of docChild.childNodes) {
|
||||||
|
if (htmlChild.tagName === 'head') {
|
||||||
|
headElement = htmlChild;
|
||||||
|
} else if (htmlChild.tagName === 'body') {
|
||||||
|
bodyElement = htmlChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!headElement || !bodyElement) {
|
||||||
|
throw new Error('Missing head and/or body elements');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine script insertion point
|
||||||
|
let scriptInsertionPoint;
|
||||||
|
if (bodyElement.__location && bodyElement.__location.endTag) {
|
||||||
|
scriptInsertionPoint = bodyElement.__location.endTag.startOffset;
|
||||||
|
} else {
|
||||||
|
// Less accurate fallback
|
||||||
|
// parse5 4.x does not provide locations if malformed html is present
|
||||||
|
scriptInsertionPoint = params.inputContent.indexOf('</body>');
|
||||||
|
}
|
||||||
|
|
||||||
|
let styleInsertionPoint;
|
||||||
|
if (headElement.__location && headElement.__location.endTag) {
|
||||||
|
styleInsertionPoint = headElement.__location.endTag.startOffset;
|
||||||
|
} else {
|
||||||
|
// Less accurate fallback
|
||||||
|
// parse5 4.x does not provide locations if malformed html is present
|
||||||
|
styleInsertionPoint = params.inputContent.indexOf('</head>');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject into the html
|
||||||
|
const indexSource = new rspack.sources.ReplaceSource(
|
||||||
|
new rspack.sources.RawSource(params.inputContent),
|
||||||
|
params.input
|
||||||
|
);
|
||||||
|
|
||||||
|
let scriptElements = '';
|
||||||
|
for (const script of scripts) {
|
||||||
|
const attrs: { name: string; value: string | null }[] = [
|
||||||
|
{ name: 'src', value: (params.deployUrl || '') + script },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (crossOrigin !== 'none') {
|
||||||
|
attrs.push({ name: 'crossorigin', value: crossOrigin });
|
||||||
|
}
|
||||||
|
|
||||||
|
// We want to include nomodule or module when a file is not common amongs all
|
||||||
|
// such as runtime.js
|
||||||
|
const scriptPredictor = ({
|
||||||
|
file,
|
||||||
|
}: {
|
||||||
|
file: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
}): boolean => file === script;
|
||||||
|
if (!files.some(scriptPredictor)) {
|
||||||
|
// in some cases for differential loading file with the same name is avialable in both
|
||||||
|
// nomodule and module such as scripts.js
|
||||||
|
// we shall not add these attributes if that's the case
|
||||||
|
const isModuleType = moduleFiles.some(scriptPredictor);
|
||||||
|
|
||||||
|
if (isModuleType) {
|
||||||
|
attrs.push({ name: 'type', value: 'module' });
|
||||||
|
} else {
|
||||||
|
attrs.push({ name: 'defer', value: null });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attrs.push({ name: 'type', value: 'module' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.sri) {
|
||||||
|
const content = loadOutputFile(script);
|
||||||
|
attrs.push(...this.generateSriAttributes(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
const attributes = attrs
|
||||||
|
.map((attr) =>
|
||||||
|
attr.value === null ? attr.name : `${attr.name}="${attr.value}"`
|
||||||
|
)
|
||||||
|
.join(' ');
|
||||||
|
scriptElements += `<script ${attributes}></script>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
indexSource.insert(scriptInsertionPoint, scriptElements);
|
||||||
|
|
||||||
|
// Adjust base href if specified
|
||||||
|
if (typeof params.baseHref == 'string') {
|
||||||
|
let baseElement;
|
||||||
|
for (const headChild of headElement.childNodes) {
|
||||||
|
if (headChild.tagName === 'base') {
|
||||||
|
baseElement = headChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseFragment = treeAdapter.createDocumentFragment();
|
||||||
|
|
||||||
|
if (!baseElement) {
|
||||||
|
baseElement = treeAdapter.createElement('base', undefined, [
|
||||||
|
{ name: 'href', value: params.baseHref },
|
||||||
|
]);
|
||||||
|
|
||||||
|
treeAdapter.appendChild(baseFragment, baseElement);
|
||||||
|
indexSource.insert(
|
||||||
|
headElement.__location.startTag.endOffset,
|
||||||
|
parse5.serialize(baseFragment, { treeAdapter })
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
let hrefAttribute;
|
||||||
|
for (const attribute of baseElement.attrs) {
|
||||||
|
if (attribute.name === 'href') {
|
||||||
|
hrefAttribute = attribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (hrefAttribute) {
|
||||||
|
hrefAttribute.value = params.baseHref;
|
||||||
|
} else {
|
||||||
|
baseElement.attrs.push({ name: 'href', value: params.baseHref });
|
||||||
|
}
|
||||||
|
|
||||||
|
treeAdapter.appendChild(baseFragment, baseElement);
|
||||||
|
indexSource.replace(
|
||||||
|
baseElement.__location.startOffset,
|
||||||
|
baseElement.__location.endOffset,
|
||||||
|
parse5.serialize(baseFragment, { treeAdapter })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleElements = treeAdapter.createDocumentFragment();
|
||||||
|
for (const stylesheet of stylesheets) {
|
||||||
|
const attrs = [
|
||||||
|
{ name: 'rel', value: 'stylesheet' },
|
||||||
|
{ name: 'href', value: (params.deployUrl || '') + stylesheet },
|
||||||
|
];
|
||||||
|
|
||||||
|
if (crossOrigin !== 'none') {
|
||||||
|
attrs.push({ name: 'crossorigin', value: crossOrigin });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.sri) {
|
||||||
|
const content = loadOutputFile(stylesheet);
|
||||||
|
attrs.push(...this.generateSriAttributes(content));
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = treeAdapter.createElement('link', undefined, attrs);
|
||||||
|
treeAdapter.appendChild(styleElements, element);
|
||||||
|
}
|
||||||
|
|
||||||
|
indexSource.insert(
|
||||||
|
styleInsertionPoint,
|
||||||
|
parse5.serialize(styleElements, { treeAdapter })
|
||||||
|
);
|
||||||
|
|
||||||
|
return indexSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateSriAttributes(content: string) {
|
||||||
|
const algo = 'sha384';
|
||||||
|
const hash = createHash(algo).update(content, 'utf8').digest('base64');
|
||||||
|
|
||||||
|
return [{ name: 'integrity', value: `${algo}-${hash}` }];
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterAndMapBuildFiles(
|
||||||
|
files: EmittedFile[],
|
||||||
|
extensionFilter: string[]
|
||||||
|
): {
|
||||||
|
file: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
}[] {
|
||||||
|
const filteredFiles: {
|
||||||
|
file: string;
|
||||||
|
name: string;
|
||||||
|
extension: string;
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
// This test excludes files generated by HMR (e.g. main.hot-update.js).
|
||||||
|
const hotUpdateAsset = /hot-update\.[cm]?js$/;
|
||||||
|
|
||||||
|
for (const { file, name, extension, initial } of files) {
|
||||||
|
if (
|
||||||
|
name &&
|
||||||
|
initial &&
|
||||||
|
extensionFilter.includes(extension) &&
|
||||||
|
!hotUpdateAsset.test(file)
|
||||||
|
) {
|
||||||
|
filteredFiles.push({ file, extension, name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
export function interpolateEnvironmentVariablesToIndex(
|
||||||
|
contents: string,
|
||||||
|
deployUrl?: string
|
||||||
|
): string {
|
||||||
|
const environmentVariables = getClientEnvironment(deployUrl || '');
|
||||||
|
return interpolateEnvironmentVariables(contents, environmentVariables as any);
|
||||||
|
}
|
||||||
|
|
||||||
|
const NX_PREFIX = /^NX_PUBLIC_/i;
|
||||||
|
|
||||||
|
function isNxEnvironmentKey(x: string): boolean {
|
||||||
|
return NX_PREFIX.test(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClientEnvironment(deployUrl: string) {
|
||||||
|
return Object.keys(process.env)
|
||||||
|
.filter(isNxEnvironmentKey)
|
||||||
|
.reduce(
|
||||||
|
(env, key) => {
|
||||||
|
env[key] = process.env[key];
|
||||||
|
return env;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||||
|
DEPLOY_URL: deployUrl || process.env.DEPLOY_URL || '',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateEnvironmentVariables(
|
||||||
|
documentContents: string,
|
||||||
|
environmentVariables: Record<string, string>
|
||||||
|
): string {
|
||||||
|
let temp = documentContents;
|
||||||
|
for (const [key, value] of Object.entries(environmentVariables)) {
|
||||||
|
temp = temp.replace(new RegExp(`%${key}%`, 'g'), value);
|
||||||
|
}
|
||||||
|
return temp;
|
||||||
|
}
|
||||||
30
packages/rspack/src/utils/webpack/normalize-entry.ts
Normal file
30
packages/rspack/src/utils/webpack/normalize-entry.ts
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import { ExtraEntryPoint, NormalizedEntryPoint } from '../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;
|
||||||
|
});
|
||||||
|
}
|
||||||
53
packages/rspack/src/utils/webpack/package-chunk-sort.ts
Normal file
53
packages/rspack/src/utils/webpack/package-chunk-sort.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { ExtraEntryPoint } from '../model';
|
||||||
|
import { normalizeExtraEntryPoints } from './normalize-entry';
|
||||||
|
|
||||||
|
export function generateEntryPoints(appConfig: {
|
||||||
|
styles: ExtraEntryPoint[];
|
||||||
|
scripts: ExtraEntryPoint[];
|
||||||
|
}) {
|
||||||
|
// Add all styles/scripts, except lazy-loaded ones.
|
||||||
|
const extraEntryPoints = (
|
||||||
|
extraEntryPoints: ExtraEntryPoint[],
|
||||||
|
defaultBundleName: string
|
||||||
|
): string[] => {
|
||||||
|
const entryPoints = normalizeExtraEntryPoints(
|
||||||
|
extraEntryPoints,
|
||||||
|
defaultBundleName
|
||||||
|
).map((entry) => entry.bundleName);
|
||||||
|
|
||||||
|
// remove duplicates
|
||||||
|
return [...new Set(entryPoints)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const styleEntryPoints = appConfig.styles.filter(
|
||||||
|
(style) => !(typeof style !== 'string' && !style.inject)
|
||||||
|
);
|
||||||
|
const scriptEntryPoints = appConfig.scripts.filter(
|
||||||
|
(script) => !(typeof script !== 'string' && !script.inject)
|
||||||
|
);
|
||||||
|
const entryPoints = [
|
||||||
|
'runtime',
|
||||||
|
'polyfills',
|
||||||
|
'sw-register',
|
||||||
|
...extraEntryPoints(styleEntryPoints, 'styles'),
|
||||||
|
...extraEntryPoints(scriptEntryPoints, 'scripts'),
|
||||||
|
'vendor',
|
||||||
|
'main',
|
||||||
|
];
|
||||||
|
|
||||||
|
const duplicates = [
|
||||||
|
...new Set(
|
||||||
|
entryPoints.filter(
|
||||||
|
(x) => entryPoints.indexOf(x) !== entryPoints.lastIndexOf(x)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (duplicates.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
`Multiple bundles have been named the same: '${duplicates.join(`', '`)}'.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entryPoints;
|
||||||
|
}
|
||||||
@ -40,6 +40,7 @@ export function withNx(
|
|||||||
targetName: context.targetName,
|
targetName: context.targetName,
|
||||||
configurationName: context.configurationName,
|
configurationName: context.configurationName,
|
||||||
projectGraph: context.projectGraph,
|
projectGraph: context.projectGraph,
|
||||||
|
useLegacyHtmlPlugin: pluginOptions.useLegacyHtmlPlugin ?? false,
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
|||||||
@ -20,6 +20,10 @@ export interface WithWebOptions {
|
|||||||
};
|
};
|
||||||
cssModules?: boolean;
|
cssModules?: boolean;
|
||||||
ssr?: boolean;
|
ssr?: boolean;
|
||||||
|
/**
|
||||||
|
* Use the legacy WriteIndexHtmlPlugin instead of the built-in HtmlRspackPlugin.
|
||||||
|
*/
|
||||||
|
useLegacyHtmlPlugin?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const processed = new Set();
|
const processed = new Set();
|
||||||
@ -42,6 +46,7 @@ export function withWeb(pluginOptions: WithWebOptions = {}) {
|
|||||||
targetName: context.targetName,
|
targetName: context.targetName,
|
||||||
configurationName: context.configurationName,
|
configurationName: context.configurationName,
|
||||||
projectGraph: context.projectGraph,
|
projectGraph: context.projectGraph,
|
||||||
|
useLegacyHtmlPlugin: pluginOptions.useLegacyHtmlPlugin ?? false,
|
||||||
},
|
},
|
||||||
config
|
config
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user