Skip to content

fix: case-insensitive filename conflict detection for chunk deduplication#8458

Merged
shulaoda merged 6 commits intomainfrom
copilot/fix-case-insensitive-filename-conflict
Feb 25, 2026
Merged

fix: case-insensitive filename conflict detection for chunk deduplication#8458
shulaoda merged 6 commits intomainfrom
copilot/fix-case-insensitive-filename-conflict

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Feb 25, 2026

On case-insensitive filesystems (macOS APFS, Windows NTFS), chunk filenames differing only by case (e.g., Edit.js vs edit.js) resolve to the same inode, causing silent file overwrites and lost modules.

Root Cause

make_unique_name tracked used names with case-sensitive keys, so Edit.js and edit.js were treated as distinct — no suffix was generated, and the second write would overwrite the first on case-insensitive filesystems.

Fix

crates/rolldown_utils/src/make_unique_name.rs — Store lowercase keys in used_name_counts for conflict detection while preserving the original case in the returned filename:

Edit.js         → Edit.js   (registered as "edit.js" in map)
edit.js         → edit2.js  ✅ (detects collision via lowercase key)
EDIT.js         → EDIT3.js  ✅
  • The map key is now candidate.to_lowercase(), but the returned value retains the original-case candidate.
  • All existing deduplication behavior (numeric suffix, double-extension, counter propagation) is preserved.

Tests

  • Added case_insensitive unit test in make_unique_name.rs
  • Added case_insensitive_chunk_filenames integration fixture: dynamic imports of Edit.js and lowercase/edit.js produce distinct outputs Edit.js and edit2.js, with main.js correctly referencing ./edit2.js
Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: Rolldown Case-Insensitive Filename Conflict Reproduction</issue_title>
<issue_description>### Reproduction link or steps

https://github.com/shengnoyi/rolldown-case-conflict-repro

What is expected?

When building with chunkFileNames: '[name].js' on macOS (case-insensitive filesystem), Rolldown should detect filename conflicts where files differ only in case (e.g., Edit.js vs edit.js) and automatically resolve them by adding numeric suffixes, similar to how Rollup handles it:

src/Edit.js         → dist/Edit.js
src/lowercase/edit.js → dist/edit2.js   ✅ (automatically renamed to avoid collision)
src/List.js         → dist/List.js
src/lowercase/list.js → dist/list2.js
src/View.js         → dist/View.js
src/lowercase/view.js → dist/view2.js

Expected output: 13 files, all modules preserved.

What is actually happening?

Rolldown does not detect case-only filename conflicts on case-insensitive filesystems. It attempts to write both Edit.js and edit.js as separate files, but on macOS (APFS case-insensitive), they reference the same inode, causing file overwrites:

src/Edit.js         → dist/Edit.js (written first)
src/lowercase/edit.js → dist/edit.js  ❌ (overwrites Edit.js on macOS)
src/List.js         → dist/List.js
src/lowercase/list.js → dist/list.js  ❌ (overwrites List.js)
src/View.js         → dist/View.js
src/lowercase/view.js → dist/view.js  ❌ (overwrites View.js)

Actual output: Only 10 files, 3 modules lost. The overwritten files contain content from the wrong source module (e.g., Edit.js contains content from src/lowercase/edit.js instead of src/Edit.js).

System Info

System:
    OS: macOS 15.5
    CPU: (10) arm64 Apple M4
    Memory: 240.08 MB / 16.00 GB
    Shell: 3.2.57 - /bin/bash
  Binaries:
    Node: 18.20.8 - /Users/xxx/.nvm/versions/node/v18.20.8/bin/node
    Yarn: 1.22.22 - /Users/xxx/.nvm/versions/node/v18.20.8/bin/yarn
    npm: 10.8.2 - /Users/xxx/.nvm/versions/node/v18.20.8/bin/npm
    pnpm: 8.15.9 - /Users/xxx/.nvm/versions/node/v18.20.8/bin/pnpm
  Browsers:
    Chrome: 145.0.7632.45
    Safari: 18.5

Any additional comments?

No response</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

@netlify
Copy link
Copy Markdown

netlify bot commented Feb 25, 2026

Deploy Preview for rolldown-rs canceled.

Name Link
🔨 Latest commit 0157cf4
🔍 Latest deploy log https://app.netlify.com/projects/rolldown-rs/deploys/699f07d953ee560008b94d72

Copilot AI changed the title [WIP] Fix Rolldown case-insensitive filename conflict issue fix: case-insensitive filename conflict detection for chunk deduplication Feb 25, 2026
Copilot AI requested a review from shulaoda February 25, 2026 06:58
@shulaoda shulaoda force-pushed the copilot/fix-case-insensitive-filename-conflict branch from d6ed301 to bd0956e Compare February 25, 2026 13:51
@shulaoda shulaoda marked this pull request as ready for review February 25, 2026 13:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical bug where chunk filenames differing only by case (e.g., Edit.js vs edit.js) would silently overwrite each other on case-insensitive filesystems (macOS APFS, Windows NTFS), resulting in lost modules.

Changes:

  • Modified make_unique_name to use lowercase keys for conflict detection while preserving original case in output filenames
  • Added unit test case_insensitive to verify the fix handles case variations correctly
  • Added integration test fixture case_insensitive_chunk_filenames with dynamic imports to validate end-to-end behavior

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/rolldown_utils/src/make_unique_name.rs Core fix: uses to_lowercase() for map keys to detect case-insensitive conflicts while returning original-case candidate
crates/rolldown/tests/snapshots/integration_rolldown__filename_with_hash.snap Updated snapshot showing Edit.js and edit2.js are correctly deduplicated
crates/rolldown/tests/rolldown/function/case_insensitive_chunk_filenames/main.js Test entry with dynamic imports of modules differing only by case
crates/rolldown/tests/rolldown/function/case_insensitive_chunk_filenames/lowercase/edit.js Lowercase variant test module
crates/rolldown/tests/rolldown/function/case_insensitive_chunk_filenames/_config.json Test configuration using [name].js pattern to trigger conflict
crates/rolldown/tests/rolldown/function/case_insensitive_chunk_filenames/Edit.js Capitalized test module

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 25, 2026

Benchmarks Rust

  • target: main(6ab3c56)
  • pr: copilot/fix-case-insensitive-filename-conflict(0157cf4)
group                                                        pr                                     target
-----                                                        --                                     ------
bundle/bundle@multi-duplicated-top-level-symbol              1.00     72.0±2.93ms        ? ?/sec    1.07     76.8±2.53ms        ? ?/sec
bundle/bundle@multi-duplicated-top-level-symbol-sourcemap    1.00     77.4±2.78ms        ? ?/sec    1.00     77.7±2.52ms        ? ?/sec
bundle/bundle@rome_ts                                        1.01    102.4±3.16ms        ? ?/sec    1.00    101.4±2.42ms        ? ?/sec
bundle/bundle@rome_ts-sourcemap                              1.02    114.4±2.58ms        ? ?/sec    1.00    112.1±2.61ms        ? ?/sec
bundle/bundle@threejs                                        1.03     37.9±3.12ms        ? ?/sec    1.00     36.8±0.84ms        ? ?/sec
bundle/bundle@threejs-sourcemap                              1.00     40.9±0.87ms        ? ?/sec    1.01     41.4±0.71ms        ? ?/sec
bundle/bundle@threejs10x                                     1.00    370.1±7.12ms        ? ?/sec    1.05    389.1±8.60ms        ? ?/sec
bundle/bundle@threejs10x-sourcemap                           1.00    430.8±7.51ms        ? ?/sec    1.01   433.4±10.15ms        ? ?/sec
scan/scan@rome_ts                                            1.00     76.1±1.71ms        ? ?/sec    1.01     77.0±2.08ms        ? ?/sec
scan/scan@threejs                                            1.02     27.6±1.55ms        ? ?/sec    1.00     27.0±0.50ms        ? ?/sec
scan/scan@threejs10x                                         1.00    277.9±6.52ms        ? ?/sec    1.00    277.6±4.61ms        ? ?/sec

@shulaoda shulaoda marked this pull request as draft February 25, 2026 14:28
@shulaoda shulaoda marked this pull request as ready for review February 25, 2026 14:55
Copilot AI review requested due to automatic review settings February 25, 2026 14:55
@shulaoda shulaoda merged commit a23b437 into main Feb 25, 2026
36 checks passed
@shulaoda shulaoda deleted the copilot/fix-case-insensitive-filename-conflict branch February 25, 2026 14:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

match used_name_counts.entry(candidate.clone()) {
// Lowercase key for case-insensitive filesystems (macOS APFS, Windows NTFS).
// When already lowercase, reuse the `candidate` Arc directly to avoid allocation.
let lowercase_candidate = match candidate.as_str().cow_to_ascii_lowercase() {
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

cow_to_ascii_lowercase() only normalizes ASCII A-Z, so filenames that differ by non-ASCII case (e.g. "Ä.js" vs "ä.js") can still collide on case-insensitive filesystems. This also diverges from other filename-conflict logic in the repo (e.g. to_lowercase() in rolldown_common/src/file_emitter.rs). Consider using full Unicode lowercasing (e.g. to_lowercase() / a cow_to_lowercase() helper) for the map key to make conflict detection truly case-insensitive.

Suggested change
let lowercase_candidate = match candidate.as_str().cow_to_ascii_lowercase() {
let lowercase_candidate = match candidate.as_str().cow_to_lowercase() {

Copilot uses AI. Check for mistakes.
],
"chunkFilenames": "[name].js"
},
"snapshot": false,
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

This fixture disables snapshots and output execution, and there is no _test.mjs in the folder, so the regular fixture test run doesn't assert the expected behavior (e.g. that the output contains both Edit.js and edit2.js, and that main references ./edit2.js). Consider adding a small _test.mjs assertion or enabling snapshots for this fixture so the regression is directly validated.

Suggested change
"snapshot": false,
"snapshot": true,

Copilot uses AI. Check for mistakes.
This was referenced Feb 26, 2026
shulaoda added a commit that referenced this pull request Feb 26, 2026
## [1.0.0-rc.6] - 2026-02-26

### 💥 BREAKING CHANGES

- css: remove `css_entry_filenames` , `css_chunk_filenames` and related code (#8402) by @hyf0
- css: drop builtin CSS bundling to explore alternative solutions (#8399) by @hyf0

### 🚀 Features

- rust/data-url: use hash as id for data url modules to prevent long string overhead (#8420) by @hyf0
- validate bundle stays within output dir (#8441) by @sapphi-red
- rust: support `PluginOrder::PinPost` (#8417) by @hyf0
- support `ModuleType:Copy` (#8407) by @hyf0
- expose `ESTree` types from `rolldown/utils` (#8400) by @sapphi-red

### 🐛 Bug Fixes

- incorrect sourcemap when postBanner/postFooter is used with shebang (#8459) by @Copilot
- resolver: disable node_path option to align ESM resolver behavior (#8472) by @sapphi-red
- parse `.js` within `"type": "commonjs"` as ESM for now (#8470) by @sapphi-red
- case-insensitive filename conflict detection for chunk deduplication (#8458) by @Copilot
- prevent inlining CJS exports that are mutated by importers (#8456) by @IWANABETHATGUY
- parse `.cjs` / `.cts` / `.js` within `"type": "commonjs"` as CommonJS (#8455) by @sapphi-red
- plugin/copy-module: correct hooks' priority (#8423) by @hyf0
- plugin/chunk-import-map: ensure `render_chunk_meta` run after users plugin (#8422) by @hyf0
- rust: correct hooks order of `DataUriPlugin` (#8418) by @hyf0
- `jsx.preserve` should also considering tsconfig json preserve (#8324) by @IWANABETHATGUY
- `deferred_scan_data.rs "Should have resolved id: NotFound"` error (#8379) by @sapphi-red
- cli: require value for `--dir`/`-d` and `--file`/`-o` (#8378) by @Copilot
- dev: avoid mutex deadlock caused by inconsistent lock order (#8370) by @sapphi-red

### 🚜 Refactor

- watch: rename TaskStart/TaskEnd to BundleStart/BundleEnd (#8463) by @hyf0
- rust: rename `rolldown_plugin_data_uri` to `rolldown_plugin_data_url` (#8421) by @hyf0
- bindingify-build-hook: extract helper for PluginContextImpl (#8438) by @ShroXd
- give source loading a proper name (#8436) by @IWANABETHATGUY
- ban holding DashMap refs across awaits (#8362) by @sapphi-red

### 📚 Documentation

- add glob pattern usage example to input option (#8469) by @IWANABETHATGUY
- remove `https://rolldown.rs` from links in reference docs (#8454) by @sapphi-red
- mention execution order issue in `output.codeSplitting` docs (#8452) by @sapphi-red
- clarify `output.comments` behavior a bit (#8451) by @sapphi-red
- replace npmjs package links with npmx.dev (#8439) by @Boshen
- reference: add `Exported from` for values / types exported from subpath exports (#8394) by @sapphi-red
- add JSDocs for APIs exposed from subpath exports (#8393) by @sapphi-red
- reference: generate reference pages for APIs exposed from subpath exports (#8392) by @sapphi-red
- avoid pipe character in codeSplitting example to fix broken rendering (#8391) by @IWANABETHATGUY

### ⚡ Performance

- avoid redundant PathBuf allocations in resolve paths (#8435) by @Brooooooklyn
- bump to `sugar_path@2` (#8432) by @hyf0
- use flag-based convergence detection in include_statements (#8412) by @Brooooooklyn

### 🧪 Testing

- execute `_test.mjs` even if `executeOutput` is false (#8398) by @sapphi-red
- add retry to tree-shake/module-side-effects-proxy4 as it is flaky (#8397) by @sapphi-red
- avoid `expect.assertions()` as it is not concurrent test friendly (#8383) by @sapphi-red
- disable `mockReset` option (#8382) by @sapphi-red
- fix flaky failure caused by concurrent resolveId calls (#8381) by @sapphi-red

### ⚙️ Miscellaneous Tasks

- deps: update dependency rollup to v4.59.0 [security] (#8471) by @renovate[bot]
- ai/design: add design doc about watch mode (#8453) by @hyf0
- deps: update oxc resolver to v11.19.0 (#8461) by @renovate[bot]
- ai: introduce progressive spec-driven development pattern (#8446) by @hyf0
- deprecate output.legalComments (#8450) by @sapphi-red
- deps: update dependency oxlint-tsgolint to v0.15.0 (#8448) by @renovate[bot]
- ai: make CLAUDE.md a symlink of AGENTS.md (#8445) by @hyf0
- deps: update rollup submodule for tests to v4.59.0 (#8433) by @sapphi-red
- deps: update test262 submodule for tests (#8434) by @sapphi-red
- deps: update oxc to v0.115.0 (#8430) by @renovate[bot]
- deps: update oxc apps (#8429) by @renovate[bot]
- deps: update npm packages (#8426) by @renovate[bot]
- deps: update rust crate owo-colors to v4.3.0 (#8428) by @renovate[bot]
- deps: update github-actions (#8424) by @renovate[bot]
- deps: update rust crates (#8425) by @renovate[bot]
- deps: update oxc resolver to v11.18.0 (#8406) by @renovate[bot]
- deps: update dependency oxlint-tsgolint to v0.14.2 (#8405) by @renovate[bot]
- ban `expect.assertions` in all fixture tests (#8395) by @sapphi-red
- deps: update oxc apps (#8389) by @renovate[bot]
- ban `expect.assertions` in fixture tests (#8387) by @sapphi-red
- enable lint for `_config.ts` files (#8386) by @sapphi-red
- deps: update dependency oxlint-tsgolint to v0.14.1 (#8385) by @renovate[bot]

Co-authored-by: shulaoda <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Rolldown Case-Insensitive Filename Conflict Reproduction

4 participants