Skip to content

devtool: 'eval' gives modules with identical content the wrong sourceURL, and makes builds nondeterministic #14774

Description

@stormslowly

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions