Skip to content

fix(bake): invalidate affected framework client bundles - #46705

Open
RentnerKev wants to merge 2 commits into
oven-sh:mainfrom
RentnerKev:fix/bake-client-entry-cache
Open

RentnerKev wants to merge 2 commits into
oven-sh:mainfrom
RentnerKev:fix/bake-client-entry-cache

Conversation

@RentnerKev

@RentnerKev RentnerKev commented Oct 24, 2026 •

Copy link
Copy Markdown

What does this PR do?

Invalidate a custom framework route's cached client bundle when its client entry or an imported module changes. HMR receives the new code, but a fresh page request can otherwise receive the old cached script and URL. Reuse the existing pass that traces affected client importers and decode the stored opaque client file ID before checking the client graph bitset.

Also include a cached dependency's reachable client definitions when an update introduces an edge to that dependency. After one document switches A to B, a newly loaded document only contains B; switching back to cached A must send A's dependency closure again. Trace the closure after graph edges have been updated and mark these additional definitions as fill-only, preserving modules and identities already loaded by other documents. This covers eager, edited, transitive, and cyclic dependencies.

How did you verify your code works?

Based on upstream a8fcd319be58ad4d284330b86602cfb3c8e49c83. Native release-with-assertions build on Debian 13 x64 in the documented Bun development container, with Clang 23, the pinned Rust nightly, and --lto=off:

bun run build:assert --lto=off -j2 test test/bake/dev/bundle.test.ts test/bake/dev/hot.test.ts test/bake/dev/plugins.test.ts test/bake/dev/sourcemap.test.ts test/bake/serve-plugins-dev-server.test.ts test/bake/dev/esm.test.ts
  • Original cache regressions: unchanged upstream fails both; patched build passes all 56 bundle cases.
  • Four new cached-closure regressions against the prior own-draft stack: 1 pass and 3 fail before this follow-up; 4 pass after it. The edited case is a control; eager, transitive, and cyclic cases expose missing definitions after a fresh page load.
  • Final native build combining the four own drafts and their focused follow-ups: 125 pass, 0 fail, 0 skipped, 437 assertions across the six suites above. This source contains no foreign input-map or cycle patches from the separate combined validation build.
  • The companion real-Chrome ordering regression passes on that native Linux build (1 pass, 5 assertions).
  • bun run rust:check-all: all 12 configured targets pass, none skipped. These are compile checks, not native execution on twelve platforms.

The tests use public APIs. The cached-closure implementation does not require the companion script-ordering change: its optional callback argument extends the existing upstream HMR queue metadata.

The final combined native Windows x64, Linux x64, and macOS ARM64 candidates pass 7 production and 14 development Router browser cases each without skips, including fresh-document cached dependency restoration and authored source maps. All 9 imported-helper generator cases and 9 live-browser helper cases pass per platform. The complete macOS run also passes 118 tests across nine native suites (3 existing todos), all 12 selected regressions individually, and the Chrome ordering test. These are integration-stack runs; isolated-branch Windows/macOS execution and upstream CI remain outstanding. Prepared with AI assistance; ready for maintainer review.

@RentnerKev
RentnerKev marked this pull request as ready for review October 24, 2026 13:54

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: oven-sh/bun
  • Branch: main
  • File: .coderabbit.yaml
  • Commit: 811afe7
  • Layers: Repository YAML
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 22496582-f29d-480f-9dfc-043e39fa3ab9
Commits

Reviewing files that changed from the base of the PR and between 811afe7 and 32f0198.

Files selected for processing: 6
  • src/runtime/bake/dev_server/finalize.rs
  • src/runtime/bake/dev_server/incremental_graph.rs
  • src/runtime/bake/dev_server/render.rs
  • src/runtime/bake/hmr-module.ts
  • src/runtime/bake/hmr-runtime-client.ts
  • test/bake/dev/bundle.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 8 remain after this review.

Walkthrough

The dev server now tracks cached client modules and includes their paths in HMR output. The client runtime uses those paths to exclude cached modules from replacements. Tests cover client bundle refreshes and cached dependency closures.

Changes

Client HMR cached modules

Layer / File(s) Summary
Identify and emit cached module paths
src/runtime/bake/dev_server/incremental_graph.rs, src/runtime/bake/dev_server/finalize.rs, src/runtime/bake/dev_server/render.rs, test/bake/dev/bundle.test.ts
The dev server traces client imports and adds cached module paths to HMR output when a cached-module boundary exists. Framework bundles are invalidated when their client file is marked in client_bits. The initial-response options set the boundary to None. Tests verify that editing a client entry point or its imported message module updates the client bundle and script URL.
Filter cached modules during HMR
src/runtime/bake/hmr-runtime-client.ts, src/runtime/bake/hmr-module.ts, test/bake/dev/bundle.test.ts
The HMR callback passes optional cached module IDs to replaceModules. That function removes registered cached modules from the incoming update and adjusts source-map references. Tests cover eager, edited, transitive, and cyclic cached dependency closures.

Suggested reviewers: alii

Priority: Normal

Merge Risk: Minimal · up to 32f01

No merge-blocking issue was identified; the change is ready for normal merge checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title "fix(bake): invalidate affected framework client bundles" matches the main change. The diff adds logic in finalize_bundle that invalidates framework route bundles when a client file changes,…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description includes both required sections. It clearly explains the cache invalidation and cached dependency closure changes. It also provides detailed verification steps, test resul…
Full details: Title check

Explanation

The title "fix(bake): invalidate affected framework client bundles" matches the main change. The diff adds logic in finalize_bundle that invalidates framework route bundles when a client file changes, which is the primary objective. The title does not mention the second part of the PR, which adds cached client-dependency closure restoration (cached_modules_start, include_cached_imports, replaceModules cachedModules). That omission is acceptable because the title need not cover every detail.

Full details: Description check

Explanation

The pull request description includes both required sections. It clearly explains the cache invalidation and cached dependency closure changes. It also provides detailed verification steps, test results, build details, and remaining validation limits.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 24, 2026

Copy link
Copy Markdown
Collaborator

I compared this with #42841, which fixes the same stale client script. These are the results on a debug build of main (179664e) with the two commits of this PR applied:

Two checks in that test file fail on main and with this PR. Both are outside the scope here:

  • meta.styles does not follow a stylesheet import that is added under the client entry point, because the cached style list is not dropped. dev server: refresh a framework route's meta.styles when its imports change #42849 moves that drop into invalidate_client_bundle, which the new block in finalize_bundle calls.
  • With a build error under the client entry point, the next page load answers 200 with the cached script, not the build error page.

#42841 will drop its client entry point part, because this PR covers it. The tests are here if they are useful:

// No route imports the client entry point of a router type. The script of
// every route of that router type starts at it.
const clientEntryPointFramework: Bake.Framework = {
fileSystemRouterTypes: [
{
root: "routes",
style: "nextjs-pages",
layouts: true,
serverEntryPoint: "./server.ts",
clientEntryPoint: "./client.ts",
},
{
root: "other-routes",
style: "nextjs-pages",
serverEntryPoint: "./other-server.ts",
clientEntryPoint: "./other-client.ts",
},
],
serverComponents: {
separateSSRGraph: false,
serverRuntimeImportSource: "./server.ts",
serverRegisterClientReferenceExport: "registerClientReference",
},
};
const clientEntryPointFiles = {
"server.ts": `
export function render(req, meta) {
const styles = meta.styles.map(href => '<link rel="stylesheet" href="' + href + '">').join("");
const scripts = meta.modules.map(src => '<script type="module" src="' + src + '"></script>').join("");
return new Response("<!DOCTYPE html><html><head>" + styles + "</head><body>" + scripts + "</body></html>", {
headers: { "content-type": "text/html" },
});
}
export function registerClientReference(value, file, uid) {
return { value, file, uid };
}
`,
"other-server.ts": `
export * from "./server";
`,
"client.ts": `
import "./client-dep";
console.log("client v1");
`,
"client-dep.ts": `
console.log("dep v1");
import.meta.hot.accept();
`,
"client.css": `
body {
color: red;
}
`,
"other-client.ts": `
console.log("other v1");
`,
"nav.ts": `
export const nav = "nav v1";
`,
"routes/_layout.ts": `
export { nav as default } from "../nav";
`,
"routes/index.ts": `export default "index";`,
"routes/second.ts": `export default "second";`,
"routes/third.ts": `export default "third";`,
"other-routes/other.ts": `export default "other";`,
};
async function loadClientEntryPointPages(dev: Dev) {
const pages: Record<string, { script: string; styles: number; versions: string[] }> = {};
for (const url of ["/", "/second", "/other"]) {
const html = await dev.fetch(url).text();
const script = html.match(/<script type="module" src="([^"]+)">/)![1];
const code = await dev.fetch(script).text();
pages[url] = {
script,
styles: html.match(/<link rel="stylesheet"/g)?.length ?? 0,
versions: [...new Set(code.match(/\b(client|dep|other) v\d+/g))].sort(),
};
}
return pages;
}
devTest("saving a file under the framework client entry point updates the next page load", {
framework: clientEntryPointFramework,
files: clientEntryPointFiles,
async test(dev) {
const first = await loadClientEntryPointPages(dev);
expect(first).toEqual({
"/": { script: expect.any(String), styles: 0, versions: ["client v1", "dep v1"] },
"/second": { script: expect.any(String), styles: 0, versions: ["client v1", "dep v1"] },
"/other": { script: expect.any(String), styles: 0, versions: ["other v1"] },
});
{
await using c = await dev.client("/");
await c.expectMessage("dep v1", "client v1");
// `client-dep.ts` accepts the update, so the connected page does not reload.
await dev.write(
"client-dep.ts",
`
console.log("dep v2");
import.meta.hot.accept();
`,
);
await c.expectMessage("dep v2");
const second = await loadClientEntryPointPages(dev);
expect(second).toEqual({
"/": { script: expect.any(String), styles: 0, versions: ["client v1", "dep v2"] },
"/second": { script: expect.any(String), styles: 0, versions: ["client v1", "dep v2"] },
"/other": first["/other"],
});
expect(second["/"].script).not.toBe(first["/"].script);
expect(second["/second"].script).not.toBe(first["/second"].script);
// Nothing accepts an update of `client.ts`, so the connected page reloads.
await c.expectReload(async () => {
await dev.write(
"client.ts",
`
import "./client-dep";
console.log("client v2");
`,
);
});
await c.expectMessage("dep v2", "client v2");
}
await dev.write(
"client.ts",
`
import "./client-dep";
import "./client.css";
console.log("client v3");
`,
);
expect(await loadClientEntryPointPages(dev)).toEqual({
"/": { script: expect.any(String), styles: 1, versions: ["client v3", "dep v2"] },
"/second": { script: expect.any(String), styles: 1, versions: ["client v3", "dep v2"] },
"/other": first["/other"],
});
},
});
devTest("a build error under the framework client entry point shows on the next page load", {
framework: clientEntryPointFramework,
files: clientEntryPointFiles,
async test(dev) {
expect((await dev.fetch("/")).status).toBe(200);
await dev.write("client-dep.ts", `console.log("dep v2" +);`, { errors: null });
expect((await dev.fetch("/")).status).toBe(500);
await dev.write("client-dep.ts", `console.log("dep v2");`);
expect((await loadClientEntryPointPages(dev))["/"]).toEqual({
script: expect.any(String),
styles: 0,
versions: ["client v1", "dep v2"],
});
},
});

This branch has not been deployed

No deployments
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.

2 participants