Repository navigation
Conversation
The VM's source map table is keyed by source URL. Every server patch was evaluated as "bake://server.patch.js", so each patch's DevServerSourceProvider replaced the previous patch's entry and frames from modules that a later hot update did not re-bundle were remapped through the newest patch's map (either printed raw as bake://server.patch.js:N:M or attributed to a file of the newer patch). Name the patches bake://server.patch.<n>.js with a process-wide counter so every patch keeps its own entry until its provider is destroyed.
|
Warning Review limit reached
Next review available in: 17 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 (2)
Comment |
|
Status Reproduced on the released build and on main with Fix is in this PR (#37440). CI is green across the pipeline. Self-review turned up nothing in the code; it did catch an inaccurate sentence in the description about the no-source-map variant ( |
There was a problem hiding this comment.
LGTM — small, well-reasoned fix with a targeted regression test.
What was reviewed:
- Verified
put_source_providerkeys by URL and replaces, andremove_source_providercompares provider identity, so unique per-patch URLs both stop the map collision and make the destructor's removal land. - Grepped for other consumers of
bake://server.patch.js— only the no-source-mapBakeLoadServerHmrPatchremains, which does not register a provider (as the PR notes). - Checked the atomic counter: function-local
static std::atomic<unsigned>, seq_cst++, thread-safe init; process-wide scope is the conservative choice givenvm.source_mappingsis per-VM. - New test uses the same
__bunfallbackJSON extraction asreact-response.test.ts, asserts relative line deltas, and handles Windows path separators.
Extended reasoning...
Overview
The PR changes one function in src/runtime/bake/BakeSourceProvider.cpp (BakeLoadServerHmrPatchWithSourceMap) to give each server HMR patch a unique source URL (bake://server.patch.<n>.js) via a process-wide std::atomic<unsigned> counter, instead of reusing bake://server.patch.js for every patch. It adds a new minimalFramework test case in test/bake/dev/server-sourcemap.test.ts that updates the route and lib independently and asserts all three stack frames remap correctly after each patch, plus one extra assertion in the existing react HMR test that react-stack-bottom-frame still remaps after an update. A comment on the existing "repeated reloads" test is updated to reflect that patches now coexist rather than replace.
Security risks
None. This is dev-server-only source map bookkeeping — the URL is an internal key into SavedSourceMap and never exposed to untrusted input. No parsing, auth, crypto, or filesystem changes.
Level of scrutiny
Low-to-medium. The native change is 6 lines: a static atomic counter and a makeString call replacing a string literal. I traced the mechanism through DevServerSourceProvider::create → Bun__addDevServerSourceProvider → SavedSourceMap::put_source_provider (replaces by path key) and ~DevServerSourceProvider → remove_source_provider (matches on provider identity), which confirms the PR's cause analysis and that unique keys fix both the overwrite and the previously no-op removal. The atomic is correctly scoped: function-local static (thread-safe init in C++11+), seq_cst post-increment. unsigned wraparound at ~4.3B patches is not a practical concern for a dev session.
Other factors
- Grepped
src/for any code that pattern-matches on the oldbake://server.patch.jsstring (special-casing in error display, source map lookup, etc.) — none. The only remaining occurrence is inBakeLoadServerHmrPatch(the no-source-map variant), which uses the plainSourceProviderand does not touchSavedSourceMap, so leaving it unchanged is correct as the PR states. - The new test extracts frames from the dev error page's
__bunfallbackJSON payload — the same approach already used intest/bake/dev/react-response.test.tsandtest/js/bun/http/serve.test.ts— rather than regex over ANSI-colored stdout, and asserts line numbers relative to the first round to avoid coupling to absolute positions. Path comparison splits on[\\/]for Windows. - The PR description documents that both new assertions fail without the fix and pass with it, along with the neighboring bake test files and the ASAN destructor case.
- No CODEOWNERS entry covers
src/runtime/bake/. No prior human review comments to address.
Repro
Dev server with a server-side framework (
minimalFrameworkfrom the bake test harness is enough),lib/boom.tsthrowing androutes/index.tsimporting and calling it. The first request prints the stack correctly:After a hot update that only touches
routes/index.ts(solib/boom.tsand the framework entry are not re-bundled), the same request prints:boom2is printed raw andrender, which lives inminimal.server.ts, is attributed to a file of the new patch. Withframework: "react"the same thing happens to every react-server-dom frame after the first hot update (at react-stack-bottom-frame (bake://server.patch.js:3054:29)). The dev error page embeds the same wrong frames in its JSON payload, anderror.stackgoes through the same lookup.Cause
BakeLoadServerHmrPatchWithSourceMap(src/runtime/bake/BakeSourceProvider.cpp) evaluated every server patch under the same URL,bake://server.patch.js. Each patch'sDevServerSourceProviderregisters itself in the VM'sSavedSourceMapkeyed by that URL, andput_source_providerreplaces the existing entry, so after the second patch every frame with that URL, whichever patch it actually belongs to, is remapped through the newest patch's map. Modules that a hot update does not re-bundle keep running the code of the patch that loaded them (replaceModulesonly swaps the updated module ids), so that code is live for the whole session while its map is gone after the first unrelated update. Lines past the end of the newer map print raw; lines inside it get attributed to one of the newer patch's files.Fix
Each patch gets its own URL,
bake://server.patch.<n>.js, from a counter, so each patch keeps its own table entry. The entry is removed when the patch's provider is destroyed, which is what~DevServerSourceProvideralready does (remove_source_providercompares the provider identity, so with unique keys the removal now actually hits instead of being a no-op on the replaced entry). This mirrors the client side, where every hot update chunk already gets its own/_bun/client/<id>.js.map.Why a counter and not the dev server's own bundle generation: the table is per VM and every dev server in a process shares it, so two dev servers would collide on the same generation number. A content hash (what the client uses for its ids) would reintroduce the collision for identical re-bundles, which is exactly the case where two live patches must stay distinct.
Memory: a patch's entry (and its lazily parsed mappings, once a frame from it has been looked up) now lives as long as the patch's code, rather than one bundle round. The source map JSON itself was already retained for that long by the provider; this only keeps it reachable through the table.
BakeLoadServerHmrPatch(the variant without a source map) is left as is. It is unreachable:receive_chunkpushes acurrent_chunk_source_mapsentry (a real map or a line count) for every server chunk it adds tocurrent_chunk_code, so whenever there is a patch to load there is also a map and theelsebranch infinalize_bundlenever runs. It also uses the productionBake::SourceProvider, which registers under its URL too but never has a dev server map to offer and, unlikeDevServerSourceProvider, has no destructor that unregisters it; giving that path unique URLs would only accumulate entries. Removing the dead variant and fixing that class's unregistration are separate changes.Verification
test/bake/dev/server-sourcemap.test.ts:minimalFramework, three requests): update only the route, then update only the lib, and check all three frames each time. The route frame has to move by the number of lines prepended to it, the lib frame has to stay put and then move once the lib itself is updated, and the framework frame has to keep pointing atminimal.server.ts. Lines are asserted relative to the first round because the absolute numbers are the subject of a separate dev server source map fix.react-stack-bottom-frameframe still remaps after the update.Without the fix, both cases fail (
bake://server.patch.js:42androutes/index.ts:4show up in the first one's diff); with it the file passes, including the ASAN destructor case, as dotest/bake/dev/sourcemap.test.ts,bundle.test.ts,react-response.test.tsandtest/bake/deinitialization.test.tson a debug build.[review] gate passed · iteration 0 · 2 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 0
evidence per changed file
self-review · no surviving concerns
33 concerns were raised and did not survive verification.