Skip to content
Closed
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
14 changes: 5 additions & 9 deletions src/runtime/bake/hmr-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,12 @@ export class HMRModule {
try {
const mod = loadModuleSync(id, true, this);
return mod.esm ? (mod.cjs ??= toCommonJS(mod.exports)) : mod.cjs.exports;
} catch (e: any) {
} catch (e) {
// A new Error rather than a rewrite of `e`: enclosing require() calls and recorded failures rethrow it as-is.
if (e instanceof AsyncImportError) {
e.message = `Cannot require "${id}" because "${e.asyncId}" uses top-level await, but 'require' is a synchronous operation.`;
throw new Error(
`Cannot require "${id}" because "${e.asyncId}" uses top-level await, but 'require' is a synchronous operation.`,
);
Comment thread
claude[bot] marked this conversation as resolved.
Resolved
Comment thread
robobun marked this conversation as resolved.
Resolved
}
throw e;
}
Expand Down Expand Up @@ -855,7 +858,6 @@ class AsyncImportError extends Error {
constructor(asyncId: string) {
super(`Cannot load async module "${asyncId}" synchronously because it uses top-level await.`);
this.asyncId = asyncId;
Object.defineProperty(this, "name", { value: "Error" });
}
}

Expand Down Expand Up @@ -938,12 +940,6 @@ function isReactRefreshBoundary(esmExports): boolean {

function implicitAcceptFunction() {}

declare global {
interface Error {
asyncId?: string;
}
}

// bun:bake/server, bun:bake/client, and bun:wrap are
// provided by this file instead of the bundler
registerSynthetic("bun:wrap", {
Expand Down
76 changes: 76 additions & 0 deletions test/bake/dev/esm.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,82 @@ devTest("cannot require a module with top level await", {
});
},
});
devTest("the error for requiring a module with top level await names the require call that failed", {
// `require()` used to produce this message by rewriting, in place, the error
// the loader threw. Every enclosing `require()` the error then propagated
// through renamed it again, including the error a failed module records and
// rethrows on every later load of it.
framework: minimalFramework,
files: {
"tla.ts": `
await 1;
export const value = "tla";
`,
"imports-tla.ts": `
import "./tla";
export const value = "unreachable";
`,
"outer.ts": `
require("./inner");
export const value = "unreachable";
`,
"inner.ts": `
require("./tla");
export const value = "unreachable";
`,
"requires-tla.ts": `
require("./tla");
export const value = "unreachable";
`,
"imports-requires-tla.ts": `
import "./requires-tla";
export const value = "unreachable";
`,
"routes/index.ts": `
async function attempt(load) {
try {
await load();
return "loaded";
} catch (e) {
return e.message;
}
}
export default async function () {
return Response.json({
requireTla: await attempt(() => require("../tla")),
requireImportsTla: await attempt(() => require("../imports-tla")),
requireOuter: await attempt(() => require("../outer")),
importRequiresTla: await attempt(() => import("../requires-tla")),
requireRequiresTla: await attempt(() => require("../requires-tla")),
requireImportsRequiresTla: await attempt(() => require("../imports-requires-tla")),
importRequiresTlaAgain: await attempt(() => import("../requires-tla")),
});
}
`,
},
async test(dev) {
const cannotRequire = (id: string, asyncId: string) =>
`Cannot require "${id}" because "${asyncId}" uses top-level await, but 'require' is a synchronous operation.`;
expect(await dev.fetch("/").json()).toEqual({
// The required module, or one of its static imports, has top-level await.
requireTla: cannotRequire("tla.ts", "tla.ts"),
requireImportsTla: cannotRequire("imports-tla.ts", "tla.ts"),
// outer.ts and inner.ts can be required. The call that failed is the
// require("./tla") in inner.ts, and its error propagates out of the two
// enclosing require() calls unchanged, like any other error thrown while
// evaluating a required module. (This used to name outer.ts.)
requireOuter: cannotRequire("tla.ts", "tla.ts"),
// The import() records the error on requires-tla.ts. The two require()
// calls rethrow that recorded error (directly, then through a static
// importer); they used to rename it to requires-tla.ts and then to
// imports-requires-tla.ts, which is what the second import() reported.
importRequiresTla: cannotRequire("tla.ts", "tla.ts"),
requireRequiresTla: cannotRequire("tla.ts", "tla.ts"),
requireImportsRequiresTla: cannotRequire("tla.ts", "tla.ts"),
importRequiresTlaAgain: cannotRequire("tla.ts", "tla.ts"),
});
},
});
devTest("function that is assigned to should become a live binding", {
files: {
"index.html": emptyHtmlFile({
Expand Down