Describe the bug
With build.chunkImportMap: true, a chunk's emitted file name is computed before Vite substitutes the dynamic-import preload marker, so a graph change that only alters a chunk's preload dependency list changes the file's bytes while leaving its content-hashed name unchanged.
That breaks the assumption behind Cache-Control: immutable: a returning browser reuses its cached copy of a name whose contents have since changed. It equally breaks CDN caches and content-addressed object stores keyed on the file name.
The emitted difference is confined to the injected array:
the only difference is inside the injected __vite__mapDeps array:
- "assets/dep-a-zYMrD63L.js"
+ "assets/dep-b-ClVQIekE.js"
…while the emitted file keeps the same index-<hash>.js name across both builds. The reproduction prints both sha256 sums alongside it.
Related, but not a duplicate
#22946 (chunkImportMap css import uses contenthash, fixed by #22947) reported the same symptom in the reporter's words — "the content of the referencing js file is changing, but the content-hash stays the same" — for the case where a dependency's own content hash appeared in the array. That fix made the entries stable chunk-ids, and it works: in this reproduction the __vite__mapDeps entries are import-map keys, not hashed file names.
This is the residual case that stable ids cannot fix: the membership of the array changes, because the importer genuinely gains or loses a dependency. No naming scheme makes that array identical, so the importer's file name has to move — and it does not. This reproduction contains no CSS at all, so it never goes through the #22947 path.
Analysis
vite:build-import-analysis (packages/vite/src/node/plugins/importAnalysisBuild.ts) substitutes preloadMarker inside generateBundle, which runs after Rollup has finalised chunk file names, so the substituted content is not part of the hashed input.
Without chunkImportMap this is invisible: an importer references its dependencies by their real hashed names, so a dependency rotation also changes the importer's hashed input and the name moves anyway. chunkImportMap rewrites static imports to stable placeholder specifiers, which removes that masking effect — the option exposes the late substitution rather than causing it. That is why the control build in the reproduction is clean.
A fix presumably needs the substituted dependency list to participate in the chunk hash. augmentChunkHash runs at renderChunk, when the final dependency file names are not yet known, so this may need handling inside the hashing pass rather than by a plugin.
I checked main as well: importAnalysisBuild.ts still performs the substitution inside generateBundle and has no augmentChunkHash, and the main build served by pkg.pr.new reproduces identically.
Reproduction
https://github.com/antur84/vite-chunkimportmap-stale-hash-repro
Steps to reproduce
npm install
npm run repro
The script builds twice per mode. Between the two builds only src/deep.js changes — it gains one extra import; every other source file is byte-identical across builds.
Expected: a file whose bytes differ should not keep the same content-hashed name — the behaviour the control mode already shows.
Actual:
=== build.chunkImportMap: true (the bug) ===
SAME NAME, DIFFERENT BYTES: index-<hash>.js
build 1 sha256 c08d5a72...
build 2 sha256 ec9f44de...
the only difference is inside the injected __vite__mapDeps array:
- "assets/dep-a-zYMrD63L.js"
+ "assets/dep-b-ClVQIekE.js"
SAME NAME, DIFFERENT BYTES: lazy-<hash>.js
(same difference)
=== build.chunkImportMap: false (control) ===
no collisions
SUMMARY
chunkImportMap: true -> 2 file(s) reused a content-hashed name for different bytes
chunkImportMap: false -> 0 file(s)
Two notes for anyone trimming the reproduction further:
src/other.js is load-bearing. Without a second importer the dependencies are inlined, no preload array is emitted, and there is nothing to rotate.
- Emitted hashes differ between checkouts at different absolute paths; within one checkout the build is deterministic (verified by repeated runs), so a different hash than the one above is expected and harmless.
Reproduced on vite@8.2.1, 8.2.0, and the main build from https://pkg.pr.new/vitejs/vite/vite@main.
System Info
System:
OS: macOS 26.5.2
CPU: (18) arm64 Apple M5 Max
Memory: 1.17 GB / 64.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.14.1
npm: 11.11.0
pnpm: 10.33.4
bun: 1.3.13
Browsers:
Chrome: 151.0.7922.77
Firefox: 153.0.3
Safari: 26.5.2
npmPackages:
vite: 8.2.1 => 8.2.1
Used Package Manager
npm
Logs
No response
Validations
Describe the bug
With
build.chunkImportMap: true, a chunk's emitted file name is computed before Vite substitutes the dynamic-import preload marker, so a graph change that only alters a chunk's preload dependency list changes the file's bytes while leaving its content-hashed name unchanged.That breaks the assumption behind
Cache-Control: immutable: a returning browser reuses its cached copy of a name whose contents have since changed. It equally breaks CDN caches and content-addressed object stores keyed on the file name.The emitted difference is confined to the injected array:
…while the emitted file keeps the same
index-<hash>.jsname across both builds. The reproduction prints both sha256 sums alongside it.Related, but not a duplicate
#22946 (chunkImportMap css import uses contenthash, fixed by #22947) reported the same symptom in the reporter's words — "the content of the referencing js file is changing, but the content-hash stays the same" — for the case where a dependency's own content hash appeared in the array. That fix made the entries stable chunk-ids, and it works: in this reproduction the
__vite__mapDepsentries are import-map keys, not hashed file names.This is the residual case that stable ids cannot fix: the membership of the array changes, because the importer genuinely gains or loses a dependency. No naming scheme makes that array identical, so the importer's file name has to move — and it does not. This reproduction contains no CSS at all, so it never goes through the #22947 path.
Analysis
vite:build-import-analysis(packages/vite/src/node/plugins/importAnalysisBuild.ts) substitutespreloadMarkerinsidegenerateBundle, which runs after Rollup has finalised chunk file names, so the substituted content is not part of the hashed input.Without
chunkImportMapthis is invisible: an importer references its dependencies by their real hashed names, so a dependency rotation also changes the importer's hashed input and the name moves anyway.chunkImportMaprewrites static imports to stable placeholder specifiers, which removes that masking effect — the option exposes the late substitution rather than causing it. That is why the control build in the reproduction is clean.A fix presumably needs the substituted dependency list to participate in the chunk hash.
augmentChunkHashruns atrenderChunk, when the final dependency file names are not yet known, so this may need handling inside the hashing pass rather than by a plugin.I checked
mainas well:importAnalysisBuild.tsstill performs the substitution insidegenerateBundleand has noaugmentChunkHash, and themainbuild served bypkg.pr.newreproduces identically.Reproduction
https://github.com/antur84/vite-chunkimportmap-stale-hash-repro
Steps to reproduce
The script builds twice per mode. Between the two builds only
src/deep.jschanges — it gains one extra import; every other source file is byte-identical across builds.Expected: a file whose bytes differ should not keep the same content-hashed name — the behaviour the control mode already shows.
Actual:
Two notes for anyone trimming the reproduction further:
src/other.jsis load-bearing. Without a second importer the dependencies are inlined, no preload array is emitted, and there is nothing to rotate.Reproduced on
vite@8.2.1,8.2.0, and themainbuild fromhttps://pkg.pr.new/vitejs/vite/vite@main.System Info
System: OS: macOS 26.5.2 CPU: (18) arm64 Apple M5 Max Memory: 1.17 GB / 64.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 24.14.1 npm: 11.11.0 pnpm: 10.33.4 bun: 1.3.13 Browsers: Chrome: 151.0.7922.77 Firefox: 153.0.3 Safari: 26.5.2 npmPackages: vite: 8.2.1 => 8.2.1Used Package Manager
npm
Logs
No response
Validations