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
8 changes: 3 additions & 5 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2316,10 +2316,6 @@ unsafe extern "C" {
global: &JSGlobalObject,
argv1: JSValue,
) -> JSValue;
safe fn JSC__JSInternalPromise__resolvedPromise(
global: &JSGlobalObject,
value: JSValue,
) -> *mut JSInternalPromise;
}

fn get_origin_timestamp() -> u64 {
Expand Down Expand Up @@ -2711,7 +2707,9 @@ impl VirtualMachine {
if let Some(stored) = self.pending_internal_promise {
return Ok(stored);
}
let resolved = JSC__JSInternalPromise__resolvedPromise(global_ref, ret);
let resolved =
crate::cpp::JSC__JSInternalPromise__resolvedPromise(global_ref, ret)
.map_err(|_| crate::CrateError::JSError)?;
self.pending_internal_promise = Some(resolved);
self.pending_internal_promise_is_protected = false;
return Ok(resolved);
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4069,6 +4069,9 @@ void JSC__JSInternalPromise__resolve(JSC::JSPromise* arg0, JSC::JSGlobalObject*
arg0->resolve(arg1, arg1->vm(), JSC::JSValue::decode(JSValue2));
}

// `JSPromise::resolvedPromise` runs `promiseResolve`, which reads `.constructor`
// off a promise argument and returns null when that getter throws.
[[ZIG_EXPORT(check_slow)]]
JSC::JSPromise* JSC__JSInternalPromise__resolvedPromise(JSC::JSGlobalObject* arg0,
JSC::EncodedJSValue JSValue1)
{
Expand Down
75 changes: 65 additions & 10 deletions test/js/node/module/node-module-module.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,12 @@ console.log("survived", require("./late.js"));`,
expect(require("./esm_to_cjs_interop.mjs")).toEqual(Symbol.for("meow"));
});

// BUN_JSC_validateExceptionChecks=1 makes a debug build abort on the first
// JSC call whose exception state is never checked; it is a no-op in release
// builds. The runMain override path wraps the override's return value in a
// promise natively, so these run under it.
const validateExceptions = { ...bunEnv, BUN_JSC_validateExceptionChecks: "1" };

test("Module.runMain", async () => {
await using proc = Bun.spawn({
cmd: [
Expand All @@ -576,14 +582,13 @@ console.log("survived", require("./late.js"));`,
path.join(import.meta.dir, "overwrite-module-run-main-1.cjs"),
path.join(import.meta.dir, "overwrite-module-run-main-2.cjs"),
],
env: bunEnv,
stderr: "inherit",
env: validateExceptions,
stderr: "pipe",
stdout: "pipe",
});

const stdout = await proc.stdout.text();
expect(stdout.trim()).toBe("pass");
expect(await proc.exited).toBe(0);
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ stdout: "pass", stderr: "", exitCode: 0 });
});
test("Module.runMain 2", async () => {
await using proc = Bun.spawn({
Expand All @@ -593,14 +598,64 @@ console.log("survived", require("./late.js"));`,
path.join(import.meta.dir, "overwrite-module-run-main-3.cjs"),
path.join(import.meta.dir, "overwrite-module-run-main-2.cjs"),
],
env: bunEnv,
stderr: "inherit",
env: validateExceptions,
stderr: "pipe",
stdout: "pipe",
});

const stdout = await proc.stdout.text();
expect(stdout.trim()).toBe("pass");
expect(await proc.exited).toBe(0);
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout: stdout.trim(), stderr, exitCode }).toEqual({ stdout: "pass", stderr: "", exitCode: 0 });
});

// When an override does not call the original runMain, bun adopts whatever
// it returned (Promise.resolve semantics) as the entry point promise.
test.each([
{
name: "a plain value",
runMain: `() => { console.log("override ran"); return 42; }`,
expected: { stdout: "override ran\n", stderr: "", exitCode: 0 },
},
{
name: "a pending promise that fulfills",
runMain: `async () => { await 0; console.log("override resolved"); }`,
expected: { stdout: "override resolved\n", stderr: "", exitCode: 0 },
},
{
name: "a thenable that fulfills",
runMain: `() => ({ then(resolve) { console.log("thenable adopted"); resolve(); } })`,
expected: { stdout: "thenable adopted\n", stderr: "", exitCode: 0 },
},
{
name: "a thenable that rejects",
runMain: `() => ({ then(_, reject) { reject(new Error("thenable rejected")); } })`,
expected: { stdout: "", stderr: expect.stringContaining("thenable rejected"), exitCode: 1 },
},
{
// Promise.resolve(p) reads p.constructor; the getter throwing is the one
// way adopting the return value itself throws.
name: "a promise whose constructor getter throws",
runMain: `() => {
const p = Promise.resolve();
Object.defineProperty(p, "constructor", { get() { throw new Error("constructor getter threw"); } });
return p;
}`,
expected: { stdout: "", stderr: expect.stringContaining("Error occurred loading entry point"), exitCode: 1 },
},
])("Module.runMain override returning $name", async ({ runMain, expected }) => {
using dir = tempDir("run-main-override", {
"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: validateExceptions,
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(expected);
});
test.each(["no args", "--access-early"])("children, %s", async arg => {
await using proc = Bun.spawn({
Expand Down
Loading