System Info
rspack version: 1.6.x
rsbuild version: 1.6.6
rspack_resolver version: 0.6.4
OS: macOS (also affects Linux)
Node.js: 20.x
Details
Summary
Chunk content hashes differ between builds (e.g. local vs CI) even with the same source and commit. The only difference in the built JS is the order of keys in the same export object (e.g. __webpack_require__.d(exports, { "PA8": ..., "HRB": ... }) vs { "HRB": ..., "PA8": ... }). Same code, different serialization order → different content hash.
Reproduction
- Build the project locally and note a chunk filename, e.g.
3916.35d2a9f7.js.
- Build the same commit on CI (or another machine) and get the same chunk with a different hash, e.g.
3916.41ba190a.js.
- Diff the two files: the only difference is the order of two (or more) export keys in one object (e.g. PA8 vs HRB), ~42 bytes.
No code or config change; the difference is purely the iteration order of the object keys when emitting the chunk.
Root cause (from source inspection)
In crates/rspack_core/src/concatenated_module.rs:
- Line ~1240:
exports_map is a HashMap<Atom, String> (e.g. FxHashMap).
- Lines ~1368–1376: The code builds the
__webpack_require__.d(exports, { ... }) object by iterating exports_map.iter() and joining definitions. HashMap iteration order is not guaranteed, so the same exports can be serialized in different order across builds → different content → different content hash.
By contrast:
crates/rspack_core/src/init_fragment.rs (ESMExportInitFragment, ~line 369): sorts export_map with sort_by(|a, b| a.0.cmp(&b.0)) before rendering, so that path is deterministic.
crates/rspack_plugin_javascript/src/runtime.rs (line 69): sorts modules by module_identifier before emitting the chunk module map, so module order in the chunk is deterministic.
So the only place that emits an export object without sorting keys is the ConcatenatedModule (scope hoisting) path in concatenated_module.rs.
Expected behavior
Builds with the same source and lockfile should produce the same content hashes for the same chunks (deterministic builds). Export key order in ConcatenatedModule output should be deterministic (e.g. sorted by key).
Suggested fix
Before building the definitions string from exports_map, sort entries by key (same approach as ESMExportInitFragment), e.g.:
// Define exports
if !exports_map.is_empty() {
let mut definitions: Vec<_> = exports_map
.iter()
.map(|(key, value)| {
(
key,
format!(
"\n {}: {}",
property_name(key).expect("should convert to property_name"),
returning_function(&compilation.options.output.environment, value, "")
),
)
})
.collect();
definitions.sort_by(|a, b| a.0.cmp(b.0));
let definitions: Vec<_> = definitions.into_iter().map(|(_, s)| s).collect();
// ... rest unchanged, use definitions.join(",")
}
This makes the export object key order deterministic and stabilizes content hashes for chunks that include ConcatenatedModule output (e.g. scope-hoisted bundles like iconResources.ts + 404 modules).
Additional context
- Observed with Rsbuild (rspack-based); chunk naming uses
[name].[contenthash:8].js.
- The chunk that showed the diff contained scope hoisting output (ConcatenatedModule); the same non-determinism would affect any chunk that includes a ConcatenatedModule’s export object.
Reproduce link
No response
Reproduce Steps
Reproduce Steps
System Info
rspack version: 1.6.x
rsbuild version: 1.6.6
rspack_resolver version: 0.6.4
OS: macOS (also affects Linux)
Node.js: 20.x
Details
Summary
Chunk content hashes differ between builds (e.g. local vs CI) even with the same source and commit. The only difference in the built JS is the order of keys in the same export object (e.g.
__webpack_require__.d(exports, { "PA8": ..., "HRB": ... })vs{ "HRB": ..., "PA8": ... }). Same code, different serialization order → different content hash.Reproduction
3916.35d2a9f7.js.3916.41ba190a.js.No code or config change; the difference is purely the iteration order of the object keys when emitting the chunk.
Root cause (from source inspection)
In
crates/rspack_core/src/concatenated_module.rs:exports_mapis aHashMap<Atom, String>(e.g.FxHashMap).__webpack_require__.d(exports, { ... })object by iteratingexports_map.iter()and joining definitions.HashMapiteration order is not guaranteed, so the same exports can be serialized in different order across builds → different content → different content hash.By contrast:
crates/rspack_core/src/init_fragment.rs(ESMExportInitFragment, ~line 369): sortsexport_mapwithsort_by(|a, b| a.0.cmp(&b.0))before rendering, so that path is deterministic.crates/rspack_plugin_javascript/src/runtime.rs(line 69): sorts modules bymodule_identifierbefore emitting the chunk module map, so module order in the chunk is deterministic.So the only place that emits an export object without sorting keys is the ConcatenatedModule (scope hoisting) path in
concatenated_module.rs.Expected behavior
Builds with the same source and lockfile should produce the same content hashes for the same chunks (deterministic builds). Export key order in ConcatenatedModule output should be deterministic (e.g. sorted by key).
Suggested fix
Before building the
definitionsstring fromexports_map, sort entries by key (same approach as ESMExportInitFragment), e.g.:This makes the export object key order deterministic and stabilizes content hashes for chunks that include ConcatenatedModule output (e.g. scope-hoisted bundles like
iconResources.ts + 404 modules).Additional context
[name].[contenthash:8].js.Reproduce link
No response
Reproduce Steps
Reproduce Steps