[WIP] Migrate webpack to rspack - node22#182
Conversation
liaoyu
commented
May 29, 2026
- node 版本要求 >=22.12.0
迁移 generate/serve 与 webpack 配置到 rspack,新增 ESM 动态加载适配层,并更新相关依赖与 Node 版本要求。
There was a problem hiding this comment.
Code Review
This pull request migrates the builder from Webpack to Rspack v2+, updating dependencies, dev server configurations, and plugins to their Rspack equivalents. It also introduces a dynamic ESM loader utility to bridge CommonJS and ESM modules. The review feedback highlights several critical issues: a configuration error in css-loader where localIdentHashDigest was used instead of localIdentHashFunction, an incorrectly structured quality option in ImageMinimizerPlugin, a fragile dynamic file-resolution implementation for webpackbar, and the use of the deprecated url.parse() API instead of the modern new URL() constructor.
| // Node 17+ / OpenSSL 3 不再支持 MD4,需使用 xxhash64 | ||
| localIdentHashDigest: 'xxhash64', |
There was a problem hiding this comment.
xxhash64 is a hash function, not a digest. In css-loader, the hash function is configured via localIdentHashFunction, while localIdentHashDigest is used for the digest type (e.g., base64).
If you set localIdentHashDigest: 'xxhash64', it will be treated as an invalid digest, and css-loader will still default to using md4 as the hash function, which will crash on Node 17+ / OpenSSL 3.
Please change this to localIdentHashFunction: 'xxhash64'.
| // Node 17+ / OpenSSL 3 不再支持 MD4,需使用 xxhash64 | |
| localIdentHashDigest: 'xxhash64', | |
| // Node 17+ / OpenSSL 3 不再支持 MD4,需使用 xxhash64 | |
| localIdentHashFunction: 'xxhash64', |
| new ImageMinimizerPlugin({ | ||
| use: 'jpeg', | ||
| test: /\.(?:jpg|jpeg|jpe)$/i, | ||
| quality: 65 | ||
| }), |
There was a problem hiding this comment.
In @rsbuild/plugin-image-compress's ImageMinimizerPlugin, the minimizer-specific options (such as quality for JPEG) must be wrapped inside the options property. Passing quality as a top-level option will have no effect.
new ImageMinimizerPlugin({
use: 'jpeg',
test: /\.(?:jpg|jpeg|jpe)$/i,
options: {
quality: 65
}
}),| import fs from 'fs' | ||
| import path from 'path' | ||
| import type { rspack as Rspack, RspackPluginInstance } from '@rspack/core' | ||
| import type { WebpackBarOptions } from 'webpackbar' | ||
|
|
||
| type RspackProgressInfo = { | ||
| builtModules?: number | ||
| moduleIdentifier?: string | ||
| } | ||
|
|
||
| type WebpackBarInstance = { | ||
| apply(compiler: unknown): void | ||
| updateProgress(percent: number, message: string, details: string[]): void | ||
| } | ||
|
|
||
| type WebpackBarConstructor = new (options?: WebpackBarOptions) => WebpackBarInstance | ||
|
|
||
| function loadWebpackBarClass(): WebpackBarConstructor { | ||
| const sharedDir = path.join( | ||
| path.dirname(require.resolve('webpackbar/rspack')), | ||
| 'shared' | ||
| ) | ||
| const sharedBundle = fs.readdirSync(sharedDir).find( | ||
| name => name.startsWith('webpackbar.') && name.endsWith('.cjs') | ||
| ) | ||
| if (!sharedBundle) { | ||
| throw new Error('Cannot find webpackbar shared bundle') | ||
| } | ||
| // eslint-disable-next-line @typescript-eslint/no-var-requires | ||
| return require(path.join(sharedDir, sharedBundle)).WebpackBar | ||
| } | ||
|
|
||
| function toProgressDetails(info: unknown): string[] { | ||
| if (typeof info === 'string') { | ||
| return [info] | ||
| } | ||
| if (Array.isArray(info)) { | ||
| return info.filter((item): item is string => typeof item === 'string') | ||
| } | ||
| if (info != null && typeof info === 'object' && 'moduleIdentifier' in info) { | ||
| const { moduleIdentifier } = info as RspackProgressInfo | ||
| return typeof moduleIdentifier === 'string' ? [moduleIdentifier] : [] | ||
| } | ||
| return [] | ||
| } | ||
|
|
||
| /** webpackbar/rspack 与 Rspack 2 ProgressPlugin 的 info 对象格式不兼容,在此适配 */ | ||
| export function createWebpackBarRspackPlugin( | ||
| rspack: typeof Rspack, | ||
| options?: WebpackBarOptions | ||
| ): RspackPluginInstance { | ||
| const WebpackBar = loadWebpackBarClass() | ||
| const webpackbar = new WebpackBar(options) | ||
| const progressPlugin = new rspack.ProgressPlugin((percent, message, info) => { | ||
| webpackbar.updateProgress(percent, message, toProgressDetails(info)) | ||
| }) | ||
|
|
||
| return { | ||
| apply(compiler) { | ||
| progressPlugin.apply(compiler) | ||
| webpackbar.apply(compiler) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation of loadWebpackBarClass dynamically scans the node_modules/webpackbar directory to find a .cjs file in the shared folder. This is extremely fragile and highly likely to break with minor or patch updates of webpackbar if its internal packaging structure changes.
Since WebpackBar is exported as the default export of the webpackbar package, you can import it directly instead of resolving it through internal paths. This also allows you to remove the unused fs and path imports.
import type { rspack as Rspack, RspackPluginInstance } from '@rspack/core'
import WebpackBar, { type WebpackBarOptions } from 'webpackbar'
type RspackProgressInfo = {
builtModules?: number
moduleIdentifier?: string
}
function toProgressDetails(info: unknown): string[] {
if (typeof info === 'string') {
return [info]
}
if (Array.isArray(info)) {
return info.filter((item): item is string => typeof item === 'string')
}
if (info != null && typeof info === 'object' && 'moduleIdentifier' in info) {
const { moduleIdentifier } = info as RspackProgressInfo
return typeof moduleIdentifier === 'string' ? [moduleIdentifier] : []
}
return []
}
/** webpackbar/rspack 与 Rspack 2 ProgressPlugin 的 info 对象格式不兼容,在此适配 */
export function createWebpackBarRspackPlugin(
rspack: typeof Rspack,
options?: WebpackBarOptions
): RspackPluginInstance {
const webpackbar = new WebpackBar(options)
const progressPlugin = new rspack.ProgressPlugin((percent, message, info) => {
webpackbar.updateProgress(percent, message, toProgressDetails(info))
})
return {
apply(compiler) {
progressPlugin.apply(compiler)
webpackbar.apply(compiler)
}
}
}| proxyReq(proxyReq) { | ||
| // add header `X-Real-IP` | ||
| const origin = proxyReq.getHeader('origin') as (string | undefined) | ||
| if (origin) { | ||
| proxyReq.setHeader( | ||
| "X-Real-IP", | ||
| url.parse(origin).hostname! | ||
| ) | ||
| ) | ||
| } | ||
| }, | ||
|
|
||
| onProxyRes(proxyRes) { | ||
| // 干掉 set-cookie 中的 secure 设置,因为本地开发 server 是 http 的 | ||
| // TODO: 考虑支持 https dev server? | ||
| if (proxyRes.headers['set-cookie']) { | ||
| proxyRes.headers['set-cookie'] = proxyRes.headers['set-cookie'].map( | ||
| cookie => cookie.replace('; Secure', '') | ||
| ) | ||
| } | ||
|
|
||
| // fix `referer` to avoid csrf detect | ||
| const referer = proxyReq.getHeader('referer') as (string | undefined) | ||
| if (referer) { | ||
| proxyReq.setHeader( | ||
| 'referer', | ||
| referer.replace( | ||
| url.parse(referer).host!, | ||
| proxyReq.getHeader('host') as string | ||
| ) | ||
| ) | ||
| } | ||
| }, |
There was a problem hiding this comment.
Since the Node.js engine requirement has been upgraded to >=22.12.0 in this PR, you should use the standard, modern new URL() API instead of the legacy and deprecated url.parse().
Additionally, wrapping the URL parsing in a try-catch block ensures that invalid or malformed headers do not crash the dev server.
proxyReq(proxyReq) {
// add header `X-Real-IP`
const origin = proxyReq.getHeader('origin') as (string | undefined)
if (origin) {
try {
const originUrl = new URL(origin)
proxyReq.setHeader("X-Real-IP", originUrl.hostname)
} catch {
// ignore invalid URL
}
}
// fix `referer` to avoid csrf detect
const referer = proxyReq.getHeader('referer') as (string | undefined)
if (referer) {
try {
const refererUrl = new URL(referer)
proxyReq.setHeader(
'referer',
referer.replace(
refererUrl.host,
proxyReq.getHeader('host') as string
)
)
} catch {
// ignore invalid URL
}
}
},| ? { | ||
| localIdentName: '[local]_[hash:base64:5]', | ||
| // Node 17+ / OpenSSL 3 不再支持 MD4,需使用 xxhash64 | ||
| localIdentHashDigest: 'xxhash64', |
There was a problem hiding this comment.
localIdentHashDigest controls the digest encoding (for example hex/base64), not the hash algorithm. Setting it to xxhash64 leaves the hash function on the default md4 and then asks the digest step to use an invalid encoding, so CSS Modules builds can still fail under OpenSSL 3. This should be localIdentHashFunction: 'xxhash64' (and keep the digest as base64/default as needed).
| quality: 65 | ||
| }), | ||
| new ImageMinimizerPlugin({ | ||
| use: 'svg', |
There was a problem hiding this comment.
The migration now only installs JPEG and SVG image minimizers, but optimization.compressImage previously also compressed GIFs and the user-facing docs still advertise GIF compression. If GIF support is no longer available with the new plugin, the docs/config behavior should be updated; otherwise add a GIF minimizer equivalent so existing projects don't silently lose that optimization.