Conversation
…ule identity Bun.build plugins that return the same onResolve path under two different namespaces collapsed into a single module: whichever namespace resolved first won for both imports, and the second namespace's onLoad never ran. The same collapse also meant an onResolve result whose path matched a real file on disk but carried a custom namespace was served the disk file. PathToSourceIndexMap deduped on path.text alone. Module identity in the esbuild plugin contract is (namespace, path), so the namespace now enters the key. File-namespace entries (the overwhelming majority: every resolver, entry-point, file-map and dev-server path) keep the bare path.text key, so the common path is unchanged; non-file namespaces store namespace + NUL + text. NUL is disallowed in filesystem paths on every supported platform, so a file-namespace key cannot collide with a composite one. ResolveQueue is left path-keyed: its only two population sites (the file-map and disk-resolver arms of resolve_import_records) are file namespace by construction, and plugin onResolve results are routed through on_resolve instead of that queue.
|
Warning Review limit reached
Next review available in: 20 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThe bundler now preserves module namespaces with paths during lookup, registration, deduplication, import patching, and removal. Plugin tests verify distinct namespaces, custom namespace precedence, and identical namespace/path deduplication. ChangesNamespace-aware module identity
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Found 2 issues this PR may fix:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Beyond the inline nit, I also checked: (1) no PathToSourceIndexMap call sites exist outside the six touched files (grep confirms Graph.rs/lib.rs only declare, never call); (2) the composite-key NUL separator can't collide with a bare file-namespace key — filesystem paths reject NUL on all supported platforms, and plugin namespaces containing NUL would only self-collide, not cross into file entries; (3) bun_paths::fs::Path is Copy, so the dedup_fallback value-type change in barrel_imports.rs is a straight substitution.
Extended reasoning...
The PR changes the bundler's module-identity dedup key from path.text to (namespace, path.text) across ~15 call sites in 5 source files, plus adds three targeted plugin tests. The mechanical threading looked consistent at every site — hardcoded b"" is used only where the path is file-namespace by construction (secondary_path, bun:wrap runtime, dev-server abs_path invalidation, tsconfig enqueue), and path.namespace is used everywhere a full Path is in hand. I'm not approving because this reworks core bundler module identity across enough call sites that a maintainer should confirm each namespace choice, but nothing looked wrong.
There was a problem hiding this comment.
No issues found after re-review of ceaa0ef (the is_file_namespace nit is addressed). Deferring to a human since this changes the bundler's module-identity key across ~15 call sites in bundle_v2.rs / LinkerContext.rs / barrel_imports.rs / incremental_graph.rs, and the tests were not run locally ("no test proof" — deferred to CI).
What was checked:
- Every updated
get/put/get_or_put/removesite passes a namespace consistent with how the entry was inserted; theb""sites (secondary_path, tsconfig,bun:wrap, dev-serveron_file_deleted) are file-namespace by construction. - No direct
.mapfield access bypasses the new namespace-aware wrappers. process_resolve_queueusesvalue.path.namespacewith the resolve-queue key; per the PR description that queue is file-namespace only, so no key/namespace mismatch.composite_key's NUL separator can't collide with a file-namespace bare-text key on any supported platform.
Extended reasoning...
Overview
Re-keys PathToSourceIndexMap from bare path.text to (namespace, text), so plugin onResolve results with the same path but distinct namespaces become distinct modules (matching esbuild). File-namespace entries (empty or "file") keep the bare-text key; other namespaces store namespace ++ NUL ++ text. All get/put/get_or_put/remove signatures gain a namespace: &[u8] param, forcing every call site (~15 across bundle_v2.rs, LinkerContext.rs, barrel_imports.rs, incremental_graph.rs) to be updated. Also lifts is_file_namespace into bun_paths::fs and delegates Path::is_file() to it (addressing my earlier nit). Three new itBundled tests cover distinct-namespace-same-path, namespace-shadows-disk-file, and same-namespace-dedup control.
Security risks
None. No untrusted-input parsing, auth, or crypto. The NUL-separator key encoding is an internal map key, not exposed to user code, and NUL is disallowed in filesystem paths on every supported platform, so a file-namespace bare key cannot collide with a composite one.
Level of scrutiny
High. Module identity is the load-bearing invariant of the bundler dedup graph — a mismatched namespace at any lookup site would either silently duplicate a module (dedup miss) or collapse two distinct modules (the bug this fixes, but at a different site). The signature change means the compiler catches every miss, but whether each call site passes the correct namespace requires per-site reasoning about where the path came from. I traced each one and they look right, but this is exactly the kind of change where a maintainer familiar with the resolve-queue / secondary-path / server-components / dev-server flows should confirm.
Other factors
- Tests not run locally: the PR footer says "no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI". CI green would raise confidence significantly.
- barrel_imports.rs: the
dedup_fallbackmap now storesPath<'static>(Copy) instead of&'static [u8], threading namespace through toget_path. Mechanically correct;persist_barrel_exportstill keys the dev-server persistence on.textalone, which is unchanged behavior and only affects the dev-server barrel cache (separate from the source-index map). - Perf:
composite_keyallocates a temporaryVec<u8>per non-file-namespace lookup. Non-file namespaces are plugin-only and rare; the file-namespace fast path is unchanged. - Previous bot feedback (comment-cop, my
is_file_namespacenit) has been addressed and resolved.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/bundler/barrel_imports.rs (1)
462-476: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsider extracting the duplicated dedup-fallback/target-resolution logic.
Lines 462-476 and Lines 546-560 compute the same thing: pick
resolved_pathfromdedup_fallbackwhen the recordIS_UNUSED, otherwise useir.path, then resolvetarget/ir_targetfromir.source_indexormap.get().get_path(&resolved_path). Extract this into a small helper (e.g.fn resolve_ir_target(ir, dedup_fallback, path_to_source_index_map) -> Option<u32>) to avoid keeping the two copies in sync on future changes.Also applies to: 546-560
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/bundler/barrel_imports.rs` around lines 462 - 476, Extract the duplicated resolved-path and target lookup from the two affected call sites into a shared helper such as resolve_ir_target, accepting the import record, dedup_fallback, and path_to_source_index_map and returning Option<u32>. Preserve the existing precedence: use ir.source_index when valid, otherwise resolve the deduplicated path through the map, returning None when no target can be found. Replace both inline blocks with calls to the helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/bundler/PathToSourceIndexMap.rs`:
- Around line 41-104: Replace the NUL-delimited encoding in
PathToSourceIndexMap::composite_key with an unambiguous delimiter-free encoding
using a fixed-width namespace length prefix followed by namespace and text
bytes. Keep the file-namespace bare-text behavior unchanged, and ensure get,
put, get_or_put, and remove continue routing non-file namespaces through the
updated composite_key.
---
Outside diff comments:
In `@src/bundler/barrel_imports.rs`:
- Around line 462-476: Extract the duplicated resolved-path and target lookup
from the two affected call sites into a shared helper such as resolve_ir_target,
accepting the import record, dedup_fallback, and path_to_source_index_map and
returning Option<u32>. Preserve the existing precedence: use ir.source_index
when valid, otherwise resolve the deduplicated path through the map, returning
None when no target can be found. Replace both inline blocks with calls to the
helper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 56430538-0743-4121-a9fb-64892681a051
📒 Files selected for processing (7)
src/bundler/LinkerContext.rssrc/bundler/PathToSourceIndexMap.rssrc/bundler/barrel_imports.rssrc/bundler/bundle_v2.rssrc/paths/lib.rssrc/runtime/bake/dev_server/incremental_graph.rstest/bundler/bundler_plugin.test.ts
There was a problem hiding this comment.
Beyond the inline nit, I spot-checked the hardcoded b"" namespace call sites (secondary_path at bundle_v2.rs:2092, enqueue_file_from_dev_server_incremental_graph_invalidation at :2484/:2510, bun:wrap at :3142, incremental_graph.remove at :1564) — all are file-namespace by construction (resolver secondary paths, fs-watcher abs paths, fixed runtime sentinel), so no dedup key drift there.
Extended reasoning...
The one finding this run is a theoretical encoding-collision nit (file-namespace bare key vs composite key sharing one keyspace); it will post inline. Separately I verified that every call site the PR changed to pass a literal b"" namespace is provably file-namespace: secondary_path is populated from resolve_result.path_pair.secondary.text (disk resolver output), the dev-server invalidation entry point carries an explicit // TODO: plugins with non-file namespaces and is fed only fs-watcher absolute paths, bun:wrap is the fixed runtime sentinel (behavior unchanged), and incremental_graph::on_file_deleted receives filesystem-watcher paths. Recording so a human reviewer need not re-trace those.
|
CI status: the bundler change is green everywhere it runs. The only hard failure on builds 86262 and 86322 is Local verification: Ready for review. |
What
Bun.buildplugins that returned the samepathfromonResolveunder two different namespaces collapsed into one module. Whichever namespace resolved first won for both imports, and the second namespace'sonLoadnever ran. The same collapse meant anonResolveresult whosepathequalled a real file on disk but carried a custom namespace was served the disk file instead of the namespacedonLoad.Repro
esbuild (0.28.1) and Bun's runtime
Bun.pluginface both treat these as distinct modules.Cause
PathToSourceIndexMapkeyed onpath.textalone. In theon_resolveResolveValue::Successarm (bundle_v2.rs), the plugin's returned namespace was written onto thePathbut never entered the dedup key, so the second namespace hitfound_existingand reused the first namespace's source index.Fix
Key
PathToSourceIndexMapon(namespace, path), matching esbuild's plugin contract. File-namespace entries (every resolver / entry-point / file-map / dev-server path, which is virtually all of them) keep the barepath.textkey, so the common case is unchanged. Non-file namespaces storenamespace ++ NUL ++ text; NUL is disallowed in filesystem paths on every supported platform, so a file-namespace key cannot collide with a composite one.ResolveQueuestays path-keyed: its two population sites (the file-map and disk-resolver arms ofresolve_import_records) are file namespace by construction, and pluginonResolveresults bypass that queue viaenqueue_on_resolve_plugin_if_needed.Tests
Added to
test/bundler/bundler_plugin.test.ts:plugin/NamespaceSamePathDistinctModules: two namespaces, same path, bothonLoads run, distinct outputsplugin/NamespaceShadowsDiskFile: custom namespace with path equal to a real disk file goes through the namespacedonLoadplugin/NamespaceSamePathSameNamespaceDedup: control; same(namespace, path)via two specifiers stays one module (onLoadfires once)The first two fail on 1.4.0-canary and pass with this change; the dedup control passes on both.
no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bundler_plugin.test.ts