Skip to content

fix: deterministically keep the shortest name for deduplicated assets#9948

Merged
shulaoda merged 4 commits into
rolldown:mainfrom
x1024:fix/deterministic-asset-dedup-filename
Jun 26, 2026
Merged

fix: deterministically keep the shortest name for deduplicated assets#9948
shulaoda merged 4 commits into
rolldown:mainfrom
x1024:fix/deterministic-asset-dedup-filename

Conversation

@x1024

@x1024 x1024 commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

When two emitted assets have identical content but different names, rolldown deduplicates them by content hash. The surviving asset's [name] was taken from whichever copy emits first, which is non-deterministic.

Fix by always choosing the alphabetically-first name.

Fixes #9940

Claude was used, the work was reviewed by me. I mostly have Rust experience from advent of code.


The problem

When several assets are emitted with identical content but different names, rolldown deduplicates them by content hash into one output asset. The surviving asset took its [name] from whichever copy was emitted first, and because assets are emitted concurrently during module processing that order is not stable. The chosen name flows into assetFileNames and changes the [hash] of every chunk that references the asset, so byte-identical input could produce different file names from one build to the next. Fixes #9940.

Deterministic survivor name

The survivor is now chosen deterministically: the shortest name wins, with ties broken lexicographically. This is the rule Rollup uses in finalizeAssetsWithSameSource, and the same order rolldown already applies when it sorts an asset's names, so the chosen filename stays consistent with names[0]. An earlier version of this PR kept the alphabetically-first name, which ignores length and disagrees with Rollup; it has been changed to shortest-then-lexicographic.

Fixing a metadata-loss race

The first time a hash is seen, the asset is now inserted into self.files while the source_hash_to_reference_id shard lock is still held, before the reference id becomes visible. Previously the lock was dropped before that insert, so a concurrent duplicate could look up the reference id, find nothing in self.files yet, and silently lose its name and originalFileName.

Behavior change (Rollup parity)

This restores Rollup's behavior. For example, two identical CSS assets named style and style2 now deduplicate to style-[hash].css, the shorter name, exactly as Rollup does. Vite's css-codesplit-consistent test was flipped to expect style2 when vite started building with rolldown, precisely because rolldown diverged here; with this fix it can go back to its original Rollup assertion.

Tests

Tests live in crates/rolldown/tests/rolldown/function/asset_dedup_filename and cover the shortest name winning regardless of emission order, the shortest name beating the lexicographically-first one, every duplicate's name and originalFileName being collected and sorted onto the survivor, explicit fileName assets being left untouched, and concurrent emission never dropping a name. Each test fails on the current code and passes with the fix.

When two emitted assets have identical content but different names,
rolldown deduplicates them by content hash. The surviving asset's
[name] was taken from whichever copy emits first, which is
non-deterministic.

Fix by always choosing the alphabetically-first name.

Fixes rolldown#9940
@codspeed-hq

codspeed-hq Bot commented Jun 23, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 7 untouched benchmarks
⏩ 10 skipped benchmarks1


Comparing x1024:fix/deterministic-asset-dedup-filename (cdf5d10) with main (08b9793)2

Open in CodSpeed

Footnotes

  1. 10 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (dc7bc73) during the generation of this report, so 08b9793 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@shulaoda shulaoda self-assigned this Jun 23, 2026
@x1024

x1024 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

The test is failing because vite is testing for consistent code-splitting (how? This must be non-deterministic), but asserting that only the file with the alphabetically-last name gets kept. The vite tests needs to be updated to assert the other way around and we'll be OK.

@shulaoda shulaoda force-pushed the fix/deterministic-asset-dedup-filename branch from 8985e1a to a265a5f Compare June 26, 2026 01:53
@pkg-pr-new

pkg-pr-new Bot commented Jun 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

@rolldown/browser

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/browser@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/browser@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/browser@9948.tgz -D

@rolldown/debug

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/debug@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/debug@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/debug@9948.tgz -D

rolldown

pnpm add https://pkg.pr.new/rolldown/rolldown@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown@9948.tgz -D

@rolldown/binding-android-arm64

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-android-arm64@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-android-arm64@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-android-arm64@9948.tgz -D

@rolldown/binding-darwin-arm64

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-darwin-arm64@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-darwin-arm64@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-darwin-arm64@9948.tgz -D

@rolldown/binding-darwin-x64

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-darwin-x64@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-darwin-x64@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-darwin-x64@9948.tgz -D

@rolldown/binding-freebsd-x64

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-freebsd-x64@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-freebsd-x64@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-freebsd-x64@9948.tgz -D

@rolldown/binding-linux-arm-gnueabihf

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm-gnueabihf@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm-gnueabihf@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm-gnueabihf@9948.tgz -D

@rolldown/binding-linux-arm64-gnu

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm64-gnu@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm64-gnu@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm64-gnu@9948.tgz -D

@rolldown/binding-linux-arm64-musl

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm64-musl@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm64-musl@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-arm64-musl@9948.tgz -D

@rolldown/binding-linux-ppc64-gnu

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-ppc64-gnu@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-ppc64-gnu@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-ppc64-gnu@9948.tgz -D

@rolldown/binding-linux-s390x-gnu

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-s390x-gnu@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-s390x-gnu@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-s390x-gnu@9948.tgz -D

@rolldown/binding-linux-x64-gnu

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-x64-gnu@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-x64-gnu@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-x64-gnu@9948.tgz -D

@rolldown/binding-linux-x64-musl

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-x64-musl@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-x64-musl@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-linux-x64-musl@9948.tgz -D

@rolldown/binding-openharmony-arm64

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-openharmony-arm64@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-openharmony-arm64@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-openharmony-arm64@9948.tgz -D

@rolldown/binding-wasm32-wasi

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-wasm32-wasi@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-wasm32-wasi@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-wasm32-wasi@9948.tgz -D

@rolldown/binding-win32-arm64-msvc

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-win32-arm64-msvc@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-win32-arm64-msvc@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-win32-arm64-msvc@9948.tgz -D

@rolldown/binding-win32-x64-msvc

pnpm add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-win32-x64-msvc@9948 -D
npm i https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-win32-x64-msvc@9948 -D
yarn add https://pkg.pr.new/rolldown/rolldown/@rolldown/binding-win32-x64-msvc@9948.tgz -D

commit: cdf5d10

@shulaoda shulaoda left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! LGTM!

@shulaoda shulaoda changed the title fix: deterministic filename for deduplicated assets fix: deterministically keep the shortest name for deduplicated assets Jun 26, 2026
@shulaoda shulaoda merged commit 2875a55 into rolldown:main Jun 26, 2026
76 of 77 checks passed
@rolldown-guard rolldown-guard Bot mentioned this pull request Jul 1, 2026
shulaoda added a commit that referenced this pull request Jul 1, 2026
## [1.1.4] - 2026-07-01

### 🚀 Features

- disable `experimental.lazyBarrel` by default (#10071) by @shulaoda

### 🐛 Bug Fixes

- dev: disable lazy barrel in dev mode (#10060) by @shulaoda
- generate: keep full JSON interface under preserveModules namespa… (#10056) by @IWANABETHATGUY
- check finalize_other_specifiers in its own Debug attribute (#10032) by @shulaoda
- serialize the KeepAssign unused minify option as "keep_assign" (#10031) by @shulaoda
- keep fragments after the newline fragment in MagicString::last_line (#10023) by @shulaoda
- generate: undeclared JSON named exports under preserveModules (#10020) (#10027) by @IWANABETHATGUY
- deconflict: rename CJS-wrapped locals that shadow chunk-root bindings (#9921) by @IWANABETHATGUY
- rolldown: keep entry facade when a shared chunk holds another entry's module (#9997) by @hyf0
- treeshake: also bail JSON default split when the object escapes (#9996) by @IWANABETHATGUY
- don't classify await in a strict-mode function as top-level await (#9987) by @shulaoda
- avoid spurious leading newline in addon hooks (banner/footer/intro/outro) (#9989) by @shulaoda
- handle JSON default mutation bailouts (#9972) by @TheAlexLichter
- plugin: make lazy hook metadata enumerable (#9991) by @TheAlexLichter
- dev: make init errors in lazy-compiled modules catchable (#9981) by @h-a-n-a
- treeshake: keep computed-key side effects on namespace member access (#9986) by @shulaoda
- binding: validate replace plugin delimiters length instead of panicking (#9984) by @shulaoda
- reconstruct nested rest patterns in into_expression (#9980) by @IWANABETHATGUY
- reconstruct rest patterns as spread in into_expression (#9976) by @shulaoda
- preserve export keyword on multi-declarator exports under keepNames (#9974) by @shulaoda
- deterministically keep the shortest name for deduplicated assets (#9948) by @x1024
- treeshake: apply @__NO_SIDE_EFFECTS__ to cross-chunk namespace calls (#9960) by @IWANABETHATGUY

### 🚜 Refactor

- drop redundant program scope enter/leave in finalizer (#10049) by @shulaoda
- deconflict: extract collect_chunk_scope_captured_names (#10006) by @IWANABETHATGUY
- unify pre-scan multi-declarator split into one decision site (#9982) by @IWANABETHATGUY
- common: return bool from SymbolRef::is_not_reassigned (#9962) by @IWANABETHATGUY

### 📚 Documentation

- rolldown: remove outdated comment for removing parenthesized expression (#10062) by @Dunqing
- use GitHub-flavored alert for Etiquette note in contribution guide (#10012) by @IWANABETHATGUY
- replace: explain the delimiters left and right boundaries (#9985) by @shulaoda
- ast-mutation: remove stale Address Use section after pre-scan refactor (#9983) by @IWANABETHATGUY
- remove fathom (#9968) by @mdong1909
- contribution-guide: code-format main branch references (#9966) by @IWANABETHATGUY
- contribution-guide: fix stale REPL note and tidy wording (#9957) by @hyf0
- contribution-guide: clarify when to discuss before opening a PR (#9955) by @hyf0

### ⚡ Performance

- disable preserve_parens across all parse paths (#10057) by @Dunqing
- common: inline declared_symbols with SmallVec (#9920) by @IWANABETHATGUY
- common: pack TaggedSymbolRef into 8 bytes (#9919) by @IWANABETHATGUY
- sourcemap: skip newline scan on the no-sourcemap join fast path (#9936) by @Boshen

### 🧪 Testing

- dev: error in lazy module should be catchable (#9975) by @sapphi-red
- dev: reject unknown lazy compile modules (#9969) by @sapphi-red

### ⚙️ Miscellaneous Tasks

- deps: update actions/cache action to v6 (#10001) by @renovate[bot]
- trigger vite ecosystem-ci from PR comments (#10058) by @shulaoda
- deps: update napi to v3.10.0 (#10063) by @renovate[bot]
- remove unused From impl for RolldownLabelSpan (#10055) by @shulaoda
- remove dead Diagnostic::with_kind method (#10054) by @shulaoda
- remove unused StatementExt methods (#10053) by @shulaoda
- remove unused ExpressionExt methods (#10052) by @shulaoda
- remove commented-out re_export_all_names field (#10051) by @shulaoda
- deps: update pnpm to v11.9.0 (#10047) by @renovate[bot]
- remove the unused BindingGenerateHmrPatchReturn napi type (#10034) by @shulaoda
- remove the dead inline_entry_chunk_wrapping scaffolding (#10037) by @shulaoda
- deps: bump oxc_resolver to 11.22.0 (#10045) by @Boshen
- remove never-constructed MatchImportKind::_Ignore variant (#10041) by @shulaoda
- remove the unused ScheduledBuild napi struct (#10033) by @shulaoda
- remove dead compute_hmr_update_single method (#10040) by @shulaoda
- drop the redundant visited.insert in manual code splitting (#10038) by @shulaoda
- remove the dead output_assets vector in render_chunk_to_assets (#10036) by @shulaoda
- remove the unused From<String>/Display impls for BindingLogLevel (#10035) by @shulaoda
- deps: upgrade oxc to 0.138.0 and migrate to per-type AST construction (#10018) by @shulaoda
- deps: update rust crates (#9911) by @renovate[bot]
- deps: update test262 submodule for tests (#10016) by @rolldown-guard[bot]
- deps: update github actions (#9999) by @renovate[bot]
- deps: update npm packages (#10000) by @renovate[bot]

### ◀️ Revert

- "fix(plugin): make lazy hook metadata enumerable (#9991)" (#10005) by @shulaoda

### ❤️ New Contributors

* @x1024 made their first contribution in [#9948](#9948)

Co-authored-by: shulaoda <165626830+shulaoda@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Non-deterministic output: deduplicated assets take their filename from emission order

2 participants