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
2 changes: 2 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2712,6 +2712,8 @@ impl VirtualMachine {
return Ok(stored);
}
let resolved = JSC__JSInternalPromise__resolvedPromise(global_ref, ret);
// Only the caller reports this promise, like the module loader's own.
crate::JSPromise::opaque_mut(resolved).set_handled();
self.pending_internal_promise = Some(resolved);
self.pending_internal_promise_is_protected = false;
return Ok(resolved);
Expand Down
90 changes: 90 additions & 0 deletions test/js/node/module/node-module-module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,96 @@ console.log("survived", require("./late.js"));`,
expect(stdout.trim()).toBe("pass");
expect(await proc.exited).toBe(0);
});

// When an override does not call the original runMain, bun adopts its return
// value as the entry point promise. A rejection is then the entry point
// failing, and it must be reported exactly once no matter when the promise
// rejects: before it was returned, or later while bun is waiting on it.
const rejectingRunMainOverrides = [
["an already rejected promise", `async () => { throw new Error("run-main-boom"); }`],
["a promise that rejects later", `async () => { await 0; throw new Error("run-main-boom"); }`],
["a thenable that rejects", `() => ({ then(_, reject) { reject(new Error("run-main-boom")); } })`],
];
test.each(rejectingRunMainOverrides)(
"Module.runMain override returning %s prints the error once",
async (_, runMain) => {
using dir = tempDir("run-main-rejection", {
"preload.cjs": `require("module").runMain = ${runMain};`,
"main.cjs": `console.log("main ran");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--require", "./preload.cjs", "./main.cjs"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, errorsPrinted: stderr.split("error: run-main-boom").length - 1, exitCode }).toEqual({
stdout: "",
errorsPrinted: 1,
exitCode: 1,
});
},
);
const listenersThenOverride = runMain => `
process.on("unhandledRejection", err => console.log("unhandledRejection:", err.message));
process.on("uncaughtException", (err, origin) => console.log("uncaughtException:", err.message, origin));
require("module").runMain = ${runMain};
`;
test.each(rejectingRunMainOverrides)(
"Module.runMain override returning %s reports the error to process once, as the entry point failing",
async (_, runMain) => {
using dir = tempDir("run-main-rejection-listeners", {
"preload.cjs": listenersThenOverride(runMain),
"main.cjs": `console.log("main ran");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--require", "./preload.cjs", "./main.cjs"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({
stdout: "uncaughtException: run-main-boom unhandledRejection\n",
stderr: "",
exitCode: 0,
});
},
);
// A worker's preload can override runMain too; the worker checks its own entry
// point promise the same way the main thread does.
test("Module.runMain override in a worker preload reports a late rejection to the worker's process once", async () => {
const [, lateRejection] = rejectingRunMainOverrides[1];
using dir = tempDir("run-main-rejection-worker", {
"preload.cjs": listenersThenOverride(lateRejection),
"body.cjs": `console.log("worker body ran");`,
"main.cjs": `
const { Worker } = require("worker_threads");
const path = require("path");
const worker = new Worker(path.join(__dirname, "body.cjs"), { preload: [path.join(__dirname, "preload.cjs")] });
worker.on("exit", code => { process.exitCode = code; });
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "./main.cjs"],
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
stdout: "pipe",
});

const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({
stdout: "uncaughtException: run-main-boom unhandledRejection\n",
stderr: "",
exitCode: 0,
});
});
test.each(["no args", "--access-early"])("children, %s", async arg => {
await using proc = Bun.spawn({
cmd: [bunExe(), path.join(import.meta.dir, "children-fixture/a.cjs"), arg],
Expand Down
Loading