Summary
With devtool: 'eval', two modules whose generated code is byte-identical share one cache entry in EvalDevToolModulePlugin. The cached entry already has //# sourceURL=<path> baked in, so the second module gets the first module's path. Which module wins depends on the parallel codegen order, so the bundle is not deterministic across builds.
Reproduce
300 modules, where each even/odd pair has byte-identical content:
// build-fixture.js
const fs = require("fs");
fs.mkdirSync("src", { recursive: true });
for (let i = 0; i < 300; i++) {
// m0/m1, m2/m3 ... are byte-identical
fs.writeFileSync(`src/m${i}.js`,
`export const value = ${i >> 1};\nexport function fn() { return value * 2; }\n`);
}
fs.writeFileSync("src/index.js",
Array.from({ length: 300 }, (_, i) => `import { fn as fn${i} } from './m${i}.js';`).join("\n") +
`\nconsole.log(${Array.from({ length: 300 }, (_, i) => `fn${i}()`).join(", ")});\n`);
// rspack.config.js
module.exports = {
mode: "development",
devtool: "eval",
entry: "./src/index.js",
output: { filename: "main.js" },
};
node build-fixture.js
for i in 1 2 3 4 5; do rspack build >/dev/null && md5 -q dist/main.js; done
Actual — five builds, five different bundles:
3bd3a4d2fd48cd852ae2ba59db0607bc
730469dcb04747fac870ff7405ebb520
a917870f983bb841329dc4f494457109
abd953f5e781b1554d98de743ca69ffb
d8adbd2d4b691601c17ea338f56c4a07
Expected — identical bundles.
Diffing two builds shows only sourceURL lines move around (266 of them), and they point at the wrong module:
< sourceURL=webpack://repro/./src/m11.js?2f73
---
> sourceURL=webpack://repro/./src/m10.js?8c8d
So in the devtools Sources panel, m11.js is shown under the name m10.js.
Two controls confirm the cause:
- Make every module's content unique → 5/5 builds byte-identical.
- Strip the
sourceURL comments before hashing → the rest of the bundle is identical. Nothing but the sourceURL is affected.
devtool: 'eval-source-map' → deterministic (it keys its cache by module hash, not by source).
Root cause
crates/rspack_plugin_devtool/src/eval_dev_tool_module_plugin.rs
cache: FxDashMap<BoxSource, BoxSource>, // L42 — key is the module's rendered source
...
let origin_source = render_source.source.clone();
if let Some(cached_source) = self.cache.get(&origin_source) { // L92
render_source.source = cached_source.value().clone(); // wrong sourceURL for this module
return Ok(());
}
...
self.cache.insert(origin_source, source.clone()); // L177
BoxSource's Hash/Eq compare content, but the cached value embeds this module's own sourceURL, which is derived from the module identifier. Identical content ⇒ collision ⇒ the wrong path.
webpack keys the same cache by source identity, not content, so it never collides:
// lib/EvalDevToolModulePlugin.js
const cache = new WeakMap();
The sibling eval_source_map_dev_tool_plugin.rs already keys by RspackHashDigest (module hash) and is not affected.
Impact
- Wrong file names in the devtools Sources panel whenever a project has duplicate-content modules (barrel re-exports, generated files,
three.js-style shader chunks — the threejs benchcase in this repo has color_pars_vertex.glsl.js and color_pars_fragment.glsl.js byte-identical).
- Non-reproducible dev builds.
- rspack's own Rust
Compiler::builder() defaults to eval, so the bundle output of the bundle@*-development benchmarks is nondeterministic today.
Suggested fix
Key the cache by module identifier (or by module hash, as eval_source_map_dev_tool_plugin does), or drop the cache — the only part of the result that is content-derived is the eval wrapper; the sourceURL is not.
Environment
@rspack/core 2.1.3, macOS arm64, Node 22
Summary
With
devtool: 'eval', two modules whose generated code is byte-identical share one cache entry inEvalDevToolModulePlugin. The cached entry already has//# sourceURL=<path>baked in, so the second module gets the first module's path. Which module wins depends on the parallel codegen order, so the bundle is not deterministic across builds.Reproduce
300 modules, where each even/odd pair has byte-identical content:
Actual — five builds, five different bundles:
Expected — identical bundles.
Diffing two builds shows only
sourceURLlines move around (266 of them), and they point at the wrong module:So in the devtools Sources panel,
m11.jsis shown under the namem10.js.Two controls confirm the cause:
sourceURLcomments before hashing → the rest of the bundle is identical. Nothing but the sourceURL is affected.devtool: 'eval-source-map'→ deterministic (it keys its cache by module hash, not by source).Root cause
crates/rspack_plugin_devtool/src/eval_dev_tool_module_plugin.rsBoxSource'sHash/Eqcompare content, but the cached value embeds this module's ownsourceURL, which is derived from the module identifier. Identical content ⇒ collision ⇒ the wrong path.webpack keys the same cache by source identity, not content, so it never collides:
The sibling
eval_source_map_dev_tool_plugin.rsalready keys byRspackHashDigest(module hash) and is not affected.Impact
three.js-style shader chunks — thethreejsbenchcase in this repo hascolor_pars_vertex.glsl.jsandcolor_pars_fragment.glsl.jsbyte-identical).Compiler::builder()defaults toeval, so the bundle output of thebundle@*-developmentbenchmarks is nondeterministic today.Suggested fix
Key the cache by module identifier (or by module hash, as
eval_source_map_dev_tool_plugindoes), or drop the cache — the only part of the result that is content-derived is the eval wrapper; thesourceURLis not.Environment
@rspack/core2.1.3, macOS arm64, Node 22