From 45cd24b942bf409ffd91c80be969567dd21b37ee Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 17:03:43 +0000 Subject: [PATCH 1/3] bake: keep a __proto__ export an own key of the dev server ssr manifest entry The HMR server runtime built the per-module ssrManifest entry with client[exportName] = ... on a plain object. For an export named __proto__ that assignment hit the Object.prototype setter and replaced the entry's prototype. The export was missing from Object.keys of the entry, and the delete loop in componentManifestDelete never removed its serverManifest id. Create the entry with Object.create(null) so every export name is an own key. --- src/runtime/bake/hmr-runtime-server.ts | 4 +- test/bake/dev/bundle.test.ts | 58 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/runtime/bake/hmr-runtime-server.ts b/src/runtime/bake/hmr-runtime-server.ts index 94d27b70203c..d33e883da533 100644 --- a/src/runtime/bake/hmr-runtime-server.ts +++ b/src/runtime/bake/hmr-runtime-server.ts @@ -166,7 +166,9 @@ server_exports = { try { const exports = await loadExports<{}>(uid); - const client = {}; + // Keyed by user export names. A null prototype keeps an export named + // "__proto__" an own key instead of hitting the Object.prototype setter. + const client = Object.create(null); for (const exportName of Object.keys(exports)) { serverManifest[uid + "#" + exportName] = { id: uid, diff --git a/test/bake/dev/bundle.test.ts b/test/bake/dev/bundle.test.ts index 6c6d3657029c..1297ce7bbb23 100644 --- a/test/bake/dev/bundle.test.ts +++ b/test/bake/dev/bundle.test.ts @@ -358,6 +358,64 @@ devTest("removing 'use client' from a component with a pending resolution failur expect(res).toBeInstanceOf(Response); }, }); +// The HMR runtime builds the per-module ssrManifest entry by assigning +// `entry[exportName]`. An export named "__proto__" must become an own key, not +// the prototype of the entry, and its serverManifest id must be removed again +// when the module stops being a client component. +devTest("client component exported as __proto__ is an own key of the ssr manifest", { + framework: minimalFramework, + files: { + "components/impl.ts": ` + export function Client() { return "client"; } + export function Other() { return "other"; } + `, + // Re-exported imports are lowered to getters, so the module namespace has + // an own "__proto__" key for the runtime to register. + "components/Client.ts": ` + "use client"; + import { Client, Other } from "./impl"; + export { Client as __proto__, Other as other }; + `, + "routes/index.ts": ` + import * as client from '../components/Client'; + import { ssrManifest, serverManifest } from 'bun:bake/server'; + export default function(req, meta) { + // JSON.stringify only serializes own keys of each manifest entry. + return new Response(JSON.stringify({ + exports: Object.keys(client), + ssr: ssrManifest, + server: Object.keys(serverManifest).sort(), + })); + } + `, + }, + async test(dev) { + expect(await dev.fetch("/").json()).toEqual({ + exports: ["__proto__", "other"], + ssr: { + "components/Client.ts": { + ["__proto__"]: { specifier: "ssr:components/Client.ts", name: "__proto__" }, + other: { specifier: "ssr:components/Client.ts", name: "other" }, + }, + }, + server: ["components/Client.ts#__proto__", "components/Client.ts#other"], + }); + + // Dropping the directive removes the module from both manifests. + await dev.write( + "components/Client.ts", + ` + import { Client, Other } from "./impl"; + export { Client as __proto__, Other as other }; + `, + ); + expect(await dev.fetch("/").json()).toEqual({ + exports: ["__proto__", "other"], + ssr: {}, + server: [], + }); + }, +}); devTest("deinit with a free-list slot in DirectoryWatchStore.dependencies", { files: { "index.html": emptyHtmlFile({ scripts: ["index.ts"] }), From 15b9ca79815b1157a4184a83f8905a36d0eaa7b5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 22:59:06 +0000 Subject: [PATCH 2/3] bake: build the ssr manifest entry with Object.fromEntries Object.fromEntries defines own keys, so the entry stays a plain object with the same shape as the production manifest entry, and an export named __proto__ is an own key instead of the prototype. --- src/runtime/bake/hmr-runtime-server.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/runtime/bake/hmr-runtime-server.ts b/src/runtime/bake/hmr-runtime-server.ts index d33e883da533..de7e4320d16b 100644 --- a/src/runtime/bake/hmr-runtime-server.ts +++ b/src/runtime/bake/hmr-runtime-server.ts @@ -165,22 +165,21 @@ server_exports = { for (const uid of componentManifestAdd) { try { const exports = await loadExports<{}>(uid); + const exportNames = Object.keys(exports); - // Keyed by user export names. A null prototype keeps an export named - // "__proto__" an own key instead of hitting the Object.prototype setter. - const client = Object.create(null); - for (const exportName of Object.keys(exports)) { + for (const exportName of exportNames) { serverManifest[uid + "#" + exportName] = { id: uid, name: exportName, chunks: [], }; - client[exportName] = { - specifier: "ssr:" + uid, - name: exportName, - }; } - ssrManifest[uid] = client; + // Object.fromEntries defines own keys, so an export named "__proto__" + // does not hit the Object.prototype setter like `client[exportName] = ...` + // would. The result is a plain object like the production manifest entry. + ssrManifest[uid] = Object.fromEntries( + exportNames.map(exportName => [exportName, { specifier: "ssr:" + uid, name: exportName }]), + ); } catch (err) { console.log(err); } From e9a659fe6d247e659936bd10c538c27abe70c0de Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 28 Aug 2026 23:01:03 +0000 Subject: [PATCH 3/3] bake: shorten the ssr manifest entry comment --- src/runtime/bake/hmr-runtime-server.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/runtime/bake/hmr-runtime-server.ts b/src/runtime/bake/hmr-runtime-server.ts index de7e4320d16b..0100ebe2c3da 100644 --- a/src/runtime/bake/hmr-runtime-server.ts +++ b/src/runtime/bake/hmr-runtime-server.ts @@ -174,9 +174,7 @@ server_exports = { chunks: [], }; } - // Object.fromEntries defines own keys, so an export named "__proto__" - // does not hit the Object.prototype setter like `client[exportName] = ...` - // would. The result is a plain object like the production manifest entry. + // Defines own keys, so an export named "__proto__" does not set the prototype. ssrManifest[uid] = Object.fromEntries( exportNames.map(exportName => [exportName, { specifier: "ssr:" + uid, name: exportName }]), );