Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 8 additions & 1 deletion scripts/glob-sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,14 @@ const patterns = {
bakeRuntime: {
// dev_server/mod.rs is read by bake-codegen.ts to derive the
// MessageId/IncomingMessageId const-enums in generated.ts.
paths: ["src/runtime/bake/*.ts", "src/runtime/bake/*/*.{ts,css}", "src/runtime/bake/dev_server/mod.rs"],
// hmr-module.ts bundles every export of runtime.bun.js as `bun:wrap`.
paths: [
"src/runtime/bake/*.ts",
"src/runtime/bake/*/*.{ts,css}",
"src/runtime/bake/dev_server/mod.rs",
"src/runtime.js",
"src/runtime.bun.js",
Comment on lines +68 to +69

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.

🟣 pre-existing, not blocking: Pre-existing sibling of this fix: a developer who edits src/runtime.js still gets a stale bun run helper module after bun bd, because only the bake bundles now track it. The runtime.out.js step in scripts/build/codegen.ts:504-525 lists only src/runtime.bun.js as its input, and the esbuild rule (scripts/build/codegen.ts:144) has no depfile, so an edit to runtime.js (which runtime.bun.js re-exports) never re-runs it. Fix: make every consumer of runtime.js rebuild when it changes, i.e. add resolve(cfg.cwd, "src", "runtime.js") to the inputs of emitRuntimeJs alongside the glob-sources change here, so the bun:wrap module served by bun run (src/runtime/jsc_hooks.rs:3599) is regenerated too.
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

A developer changes a helper in src/runtime.js and runs an incremental build. scripts/glob-sources.ts:68-69 now adds src/runtime.js and src/runtime.bun.js to bakeRuntime, so bake.client.js and bake.server.js are regenerated. But emitRuntimeJs at scripts/build/codegen.ts:500-529 declares inputs: [src] where src is only src/runtime.bun.js, and the "esbuild" rule at scripts/build/codegen.ts:144-147 declares no depfile, so ninja does not know runtime.out.js depends on src/runtime.js. runtime.out.js is what src/ast/runtime.rs:21 embeds and what src/runtime/jsc_hooks.rs:3595-3602 serves as the bun:wrap module for bun run. The result is a binary whose dev-server helpers are current but whose bun run helpers are the previous version; tests for bun run behaviour pass or fail against stale code until a clean build. The base branch has the same gap for runtime.out.js and additionally had it for the bake bundles; this PR closes only the bake half of the class.

Verification: src/runtime.bun.js:1 is export * from "./runtime";, so runtime.out.js is a bundle of runtime.js content. scripts/build/codegen.ts:500-509 declares inputs: [src] only, and the esbuild rule at scripts/build/codegen.ts:144-147 sets no depfile/deps. So ninja has no edge from src/runtime.js to runtime.out.js. The base branch already fails the same way for runtime.out.js.

],
exclude: ["src/runtime/bake/generated.ts"],
},
/** legacy bindgen input */
Expand Down
11 changes: 0 additions & 11 deletions src/runtime/bake/bake.private.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,3 @@ declare module "react-dom/server.node" {
options: RenderToPipeableStreamOptions,
): PipeableStream<Uint8Array>;
}

declare module "bun:wrap" {
export const __name: unique symbol;
export const __legacyDecorateClassTS: unique symbol;
export const __legacyDecorateParamTS: unique symbol;
export const __legacyMetadataTS: unique symbol;
export const __using: unique symbol;
export const __callDispose: unique symbol;
export const __MEMO_CACHE_SENTINEL: unique symbol;
export const __EARLY_RETURN_SENTINEL: unique symbol;
}
25 changes: 4 additions & 21 deletions src/runtime/bake/hmr-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,7 @@
// Some build failures from the bundler surface as runtime errors here, such as
// `require` on a module with transitive top-level await, or a missing export.
// This was done to make incremental updates as isolated as possible.
import {
__callDispose,
__EARLY_RETURN_SENTINEL,
__legacyDecorateClassTS,
__legacyDecorateParamTS,
__legacyMetadataTS,
__MEMO_CACHE_SENTINEL,
__name,
__using,
} from "../../runtime.bun";
import * as runtimeHelpers from "../../runtime.bun";

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.

🟣 pre-existing, not blocking: Browser users of the dev server on engines without SuppressedError get a ReferenceError from __callDispose when a using disposer throws, instead of the synthetic SuppressedError the production build gives. The namespace import at hmr-module.ts:9 pulls __callDispose from src/runtime.bun.js:19-22, which calls new SuppressedError(...) unconditionally; the browser variant at src/bundler/ParseTask.rs:533-534 has a fallback that the client HMR runtime never receives. Fix: build the client bun:wrap from the browser runtime variant (or add the typeof SuppressedError === 'function' fallback to runtime.bun.js's __callDispose) so client and bun build --target browser behave the same.
A small fix can ride a push you are already making; otherwise a short reply is enough.

Why this was flagged

If a dev-server client runs in a browser that lacks SuppressedError, a module using using/await using whose disposer throws reaches __callDispose at src/runtime.bun.js:19-23, which does new SuppressedError(e, error, ...) and throws a ReferenceError instead. The production bundler for --target browser uses RUNTIME_USING_OTHER at src/bundler/ParseTask.rs:514-549, whose __callDispose at ParseTask.rs:533-534 checks typeof SuppressedError === 'function' and falls back to a synthesized Error. The client HMR runtime is bundled by src/codegen/bake-codegen.ts:52-66 with target: "browser" but imports the Bun variant via src/runtime/bake/hmr-module.ts:9, so the dev server and the production build disagree. The base branch already imported __using and __callDispose from ../../runtime.bun, so this is pre-existing. Remedy: select the per-target __using/__callDispose when registering bun:wrap on the client, or add the fallback to runtime.bun.js.

Verification: Pre-existing and acknowledged in the PR description. A dev-server client page runs in a browser without a global SuppressedError, and a using/await using disposer throws. src/runtime.bun.js:19-23 defines __callDispose with new SuppressedError(e, error, ...) and no typeof SuppressedError === 'function' guard, unlike src/bundler/ParseTask.rs:533-535. The base already fails by the identical route, so merging makes nothing worse.

// This import is different based on client vs server side.
// On the server, remapping is done automatically.
import { type SourceMapURL, derefMapping } from "#stack-trace";
Expand Down Expand Up @@ -906,17 +897,9 @@ declare global {
}

// bun:bake/server, bun:bake/client, and bun:wrap are
// provided by this file instead of the bundler
registerSynthetic("bun:wrap", {
__name,
__legacyDecorateClassTS,
__legacyDecorateParamTS,
__legacyMetadataTS,
__using,
__callDispose,
__MEMO_CACHE_SENTINEL,
__EARLY_RETURN_SENTINEL,
});
// provided by this file instead of the bundler.
// The transpiler can emit any name in `Imports::ALL` (src/ast/runtime.rs) through `bun:wrap`.
registerSynthetic("bun:wrap", { ...runtimeHelpers });

if (side === "server") {
registerSynthetic("bun:bake/server", {
Expand Down
87 changes: 87 additions & 0 deletions test/bake/dev/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -940,3 +940,90 @@ devTest("a render() that does not return a Response is reported as that", {
}).toEqual({ status: 500, saysWhatIsWrong: true, referenceError: false });
},
});

// TC39 decorator lowering calls runtime helpers through the HMR runtime's synthetic `bun:wrap` module.
const tc39DecoratorFiles = {
"tsconfig.json": JSON.stringify({
compilerOptions: { experimentalDecorators: false, emitDecoratorMetadata: false },
}),
"decorators.ts": `
function classDecorator(value, context) {
context.metadata.label = "tc39";
context.addInitializer(function () {
this.ready = this.seed + 1;
});
}
function fieldDecorator(value, context) {
return initialValue => initialValue * 2;
}
function methodDecorator(value, context) {
return function () {
return value.call(this) + 4;
};
}
function accessorDecorator(value, context) {
return { init: initialValue => initialValue + 3 };
}

@classDecorator
export class Example {
static seed = 10;
static ready;
hasHidden() {
return #hidden in this;
}
@fieldDecorator value = 2;
@accessorDecorator accessor score = 5;
@fieldDecorator #hidden = 3;
@methodDecorator #secret() {
this.#hidden = this.#hidden + 1;
return this.#hidden;
}
@methodDecorator result() {
return this.value + this.score + this.#secret();
}
}

export function report() {
const instance = new Example();
const metadata = Example[Symbol.metadata ?? Symbol.for("Symbol.metadata")];
return [instance.value, instance.score, instance.result(), Example.ready, metadata.label, instance.hasHidden()].join(":");
}
`,
};

devTest("TC39 decorators in the client HMR runtime", {
files: {
...tc39DecoratorFiles,
"index.html": emptyHtmlFile({ scripts: ["index.ts"] }),
"index.ts": `
import { report } from "./decorators";
console.log(report());
import.meta.hot.accept();
`,
},
async test(dev) {
await using c = await dev.client("/");
await c.expectMessage("4:8:27:11:tc39:true");
await dev.patch("decorators.ts", { find: "value = 2", replace: "value = 3" });
await c.expectMessage("6:8:29:11:tc39:true");
},
});

devTest("TC39 decorators in the server HMR runtime", {
framework: minimalFramework,
files: {
...tc39DecoratorFiles,
"routes/index.ts": `
import { report } from "../decorators";
export default function () {
return new Response(report());
}
`,
},
async test(dev) {
await dev.fetch("/").equals("4:8:27:11:tc39:true");
await dev.patch("decorators.ts", { find: "value = 2", replace: "value = 3" });
await dev.fetch("/").equals("6:8:29:11:tc39:true");
},
});
Loading