Repository navigation
Conversation
…change finalize_bundle dropped a framework route's cached styles array only in the code that writes the hot update payload. That code runs when a client is subscribed to hot updates, the rebuild came from a file change, and no file has a bundling failure. With no browser tab open, a route kept the styles of its first request. Collect the framework routes that a rebuild reached before the payload code, and drop their cached styles array there. invalidate_client_bundle drops it for the routes reached through a client component. The payload loop no longer clears caches. The payload itself does not change. A stylesheet that fails to build is not a CSS root, and its fix marks no route. A styles array computed while the trace visits a failed client file is therefore not cached.
|
Warning Review limit reached
On-demand reviews are free for the next 5 days. After that, they cost $0.25 per reviewed file. Or wait 2 minutes for your next included review. View limit detailsLimit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (3)
Comment |
|
Status: fix pushed, waiting for CI. Reproduction: the dev server test in the Notes block of the description, run on the 1.4.3 canary (09bb546). Tests: |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Beyond the inline findings, I also checked the html.cached_response = None clear that was removed from the List 2 loop in finalize_bundle: every HTML route that reached that loop on the base branch (hard-affected with a subscriber via route_bits_client, soft-affected, and client-component-affected) still goes through RouteBundle::invalidate_client_bundle, which clears cached_response, so that removal is behavior-preserving.
Extended reasoning...
Two confirmed findings are already attached inline (the SAFETY comment at DevServer.rs:2524-2536 no longer describing the borrow pattern after the if complete change, and the uncovered case where an already-cached meta.styles keeps a stylesheet URL after that stylesheet's build breaks). The one additional thing worth recording is that the refactor's dropped HTML cached_response clearing was traced: the only ways an HTML route enters route_bits (DevServer.rs:4415-4417 under the subscriber-gated payload block, the soft-affected loop at 4480-4486, and the client-components block at 4451-4470) all also call invalidate_client_bundle on the same route, which sets cached_response = None in route_bundle.rs. Human review is still warranted given the inline findings and that this is dev-server cache-invalidation logic with *mut RouteBundle reborrows.
|
Updated 8:00 PM PT - Sep 15th, 2026
✅ @robobun, your commit 4554c3764459ce255d591cab2bac6efa585d8e3b passed in 🧪 To try this PR locally: bunx bun-pr 42849That installs a local version of the PR into your bun-42849 --bun |
Problem
meta.stylesstays stale after a save changes its CSS imports. With no browser tab open, later requests get the first list.finalize_bundle(src/runtime/bake/DevServer.rs:4363) clearscached_css_file_arrayonly for routes inroute_bits. Server-side routes enterroute_bitsonly in the hot update payload block. That block needs a subscriber, a file-change rebuild, and zero bundling failures.Fix
finalize_bundledropscached_css_file_arrayfor every route the rebuild reached and every route below an affected layout.invalidate_client_bundledrops it for routes reached through a client component.test/bake/dev/css.test.ts, all fail on 1.4.3-canary.1. Self-reviewed: 5 items addressed, 1 rejected (Notes).Background
meta.styles: the stylesheet URLs its imports reach, rendered as<link>tags.cached_css_file_arrayis its cachedmeta.styles./_bun/hmrWebSocket. Each rebuild sends it one payload that starts with the routes to reload._layoutfile wraps the routes below it. A"use client"file is a client component.Notes
Relation to #39488 and #37867. The
finalize_bundleandroute_bundle.rshunks are the same text as #37867, which was closed into #39488 (commitdebf54eadethere). #39488 is still open, was last pushed on August 20 and conflicts with main. Main does not have the fix. Those hunks apply on main with no change, so a rebase of #39488 turns them into no-ops. The guard ingenerate_css_js_arrayand its test are new and are not in #39488.Relation to #40255. #40255 moves this code into
finalize_bundle_phases, one indent level deeper, and keeps the subscriber-gated clear. It also rewritescompute_arguments_for_framework_requestandgenerate_css_js_arrayas safe methods that take the route bundle index. Theroute_bundle.rshunks apply there as they are. Six of the eightDevServer.rshunks do not apply: the four infinalize_bundle, and the signature and the call site of the guard. The port below has the same edits in the shape of that branch. It applies to #40255 at408cbf5be0. I did not compile it there, because that branch needs its own codegen output.Port of the
DevServer.rshunks to #40255Relation to #42841. #42841 makes a save under the
clientEntryPointinvalidate the client bundle of every route of the router type. Its notes rely on the List 2 loop to dropcached_css_file_arrayfor those routes. This PR moves that drop intoinvalidate_client_bundle, which the new block of #42841 already calls. The two branches merge with no conflict. I built the merge:css.test.ts(18) andbundle.test.ts(26) pass, and both tests of #42847 pass.Other open PRs. #42813 makes
route_lookuphold a list of routes per file. It has no hunk near this change, and the new loop readsframework_routes_affectedas before, so the two compose. These PRs also edittest/bake/dev/css.test.ts: #37051, #40337, #42697. All three touch the harness import line, and #42697 also appends at the end of the file.Repro from the report (fails on 1.4.3-canary.1+09bb54630:
Expected: "styles=1 v2",Received: "styles=0 v2").src/runtime/bakeof that build is identical to main at 7e56b40.The harness socket subscribes only to the test synchronization topic
r, not to hot updatesh. With a socket that sendsshandn/first, the same sequence gives the new list on main.What this PR refreshes, and what it does not.
meta.stylescomes from four roots: the page, its layouts, theserverEntryPointand theclientEntryPointof the router type. Client components are reached through the first three.serverEntryPoint(it is inroute_lookupwith the recurse flag, so a save marks every route of the type) and client components.clientEntryPoint. That file has no path back to a route. Dev server: a save below a router type'sclientEntryPointreaches no route #42847 has the repro, and dev server: fail a loaded route when a build error is below its client entry point #42841 is the fix (see above). Main has the same gap, with or without this PR.meta.stylesis now right. The client bundle of the route (meta.modules[0]) stays old. Dev server: a framework route that starts to import an already bundled client component keeps its old client bundle #42846 has the repro and a possible fix in the new loop. Main has the same gap.meta.styleskeeps the URL of a stylesheet that fails to build, and the URL answers 404 #42854 has the repro and two options. Main does the same.serverEntryPoint. A save below it then marks only the routes of the last type, becauseroute_lookupholds one route per file. bake: serve routes behind symlinks, reload every route that shares a file #42813 changes that.Why the guard. A stylesheet that fails to build loses its content in the client graph. It is no longer a CSS root, so the styles trace does not list it. Its fix rebuilds only the stylesheet. A stylesheet that a server file imports has no importer in the client graph, so the fix marks no route. Main hides most of this by accident. Its clear needs
bundling_failuresto be empty, so a cached list survives the failure. That list still has the URL of the stylesheet, and the URL works again after the fix. Main still has the bug when the first request of a route falls in the failure window (the first half of the third test). With the unconditional clear of this PR, a save that reaches the route in that window would cache the incomplete list too (the second half). The guard checks the client files that the trace visited, so an unrelated failing file costs nothing. While a stylesheet of the route fails, each request of that route traces its imports again.The three tests.
"use client"component, then an import in_layoutseen from two routes./. While another route has a syntax error, the payload names no routes (unchanged) andmeta.stylesis refreshed (new). After the error is fixed, the payload names the viewed route in List 1. List 2 carries exactly the stylesheet ids that the route then serves. The subscriber is the harness socket: one socket orders the hot update before the frame that resolvesdev.write(), so a stray rebuild cannot shift the accounting.Each clause of the fix breaks a test when removed (checked with a debug build for each):
invalidate_client_bundle: test 1 fails at the client component step (/otherstays[]).framework_route_bits.copy_into(&mut route_bits): test 2 fails at the last payload check (reloadedRoutes: []).mark_all_route_childrenin the new block: test 1 fails at the layout step for the child route/other. The route/that owns the layout still refreshes.[]in place of["two.css"]), the same as on main.Why the removed
html.cached_response = Nonewas redundant. An HTML route entersroute_bitsin two ways. A hard-affected route is also put inroute_bits_client, and both branches that follow callinvalidate_client_bundlefor every bit ofroute_bits_client. A soft-affected route getsinvalidate_client_bundledirectly. That function setscached_response = None.Why no
had_adjusted_edgescondition on the new clear. A swap of two imports rebuilds the edge list in the new order. It adds or removes no edge, sohad_adjusted_edgesstays false. The order ofmeta.stylesis the order of the<link>tags, so it decides the cascade. The cost of a clear is one import trace on the next request.Self-review. Addressed: (1) the incomplete list cached in a failure window, found as a regression of the first version of this change, fixed by the guard and test 3. (2) The subscriber test used a second socket and counted hot updates, which a stray rebuild could shift. It now uses the harness socket. (3) #40255 named, with the port. (4) The coverage limits above stated and tracked. (5) The overlap with other open PRs listed. Rejected: a note that the client component branch of
finalize_bundleclearshtml_routes_soft_affectedbefore the block that reads it. That needsappand an HTML import in one server.Bun.serverejects that ('app' + HTML loader not supported.), and withdevelopment: { hmr: false }a debug assertion inServerConfig.rsforbids it.Not in this PR.
meta.stylesis in reverse import order (import "a.css"; import "b.css"gives[b, a]). The tests do not assert source order. bake: serve an HTML route's stylesheets in source order #37845 (also in bake: consolidate the open robobun dev-server, HMR runtime, production build and router fixes #39488) is the same matter for HTML routes.cached_module_listis not dropped. It depends only on the router, andscan_allruns one time at startup.Failed to load bundled module. bake: keep the dev server alive when a server-side hot update fails to load #42790 covers that. Test 3 saves a helper module for this reason.The Zig dev server had the same logic (
DevServer.zigbefore #30412), so this is not a port regression.Suites run on a debug build with the fix, all pass.
test/bake/dev/css.test.ts(18), and intest/bake/dev/:bundle(22),hot(11),html(11),esm(17),react-spa(6),ssg-pages-router(9),response-to-bake-response(5),react-response(11),plugins(3),incremental-graph-edge-deletion(1),vfile(1),request-cookies(2),server-sourcemap(5),sourcemap(2),harness(3),import-meta-inline(6). Alsotest/bake/dev-and-prod.test.ts(12),test/bake/deinitialization.test.ts(10) andtest/js/bun/http/bun-serve-html.test.ts(30). The three new tests passed five runs in a row.[human-review] gate passed · iteration 0 · 3 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