Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/runtime/bake/hmr-runtime-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
58 changes: 58 additions & 0 deletions test/bake/dev/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }),
Expand Down
Loading