94 lines
3.8 KiB
TypeScript
94 lines
3.8 KiB
TypeScript
import { describe, expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, tempDir } from "harness";
|
|
import { join } from "path";
|
|
|
|
// Regression: the C++ synchronous-exception fallback in JSBundlerPlugin__matchOnLoad /
|
|
// JSBundlerPlugin__matchOnResolve passed the wrong `which` value to JSBundlerPlugin__addError,
|
|
// so Zig would reinterpret a *Load as a *Resolve (and vice versa). matchOnLoad additionally
|
|
// passed plugin.config instead of the JSBundlerPlugin* as the plugin argument, so
|
|
// plugin.globalObject() in Zig dereferenced the wrong pointer.
|
|
//
|
|
// The builtin calls the public `.then` on its pending async-IIFE promise, so we can force a
|
|
// synchronous throw there to reach that fallback deterministically. Before the fix:
|
|
// onLoad crashes (null deref in JSGlobalObject under ASAN, SIGSEGV on release) and
|
|
// onResolve hangs (wrong completion handler leaves the resolve counter un-decremented).
|
|
describe.each(["onLoad", "onResolve"] as const)("Bun.build plugin %s builtin throws synchronously", hook => {
|
|
test.concurrent("addError receives the correct context type", async () => {
|
|
const fixture = /* js */ `
|
|
const originalThen = Promise.prototype.then;
|
|
let armed = false;
|
|
Promise.prototype.then = function (...args) {
|
|
if (armed) {
|
|
armed = false;
|
|
Promise.prototype.then = originalThen;
|
|
throw new Error("synchronous throw from ${hook} builtin");
|
|
}
|
|
return originalThen.apply(this, args);
|
|
};
|
|
|
|
const result = await Bun.build({
|
|
entrypoints: [Bun.fileURLToPath(new URL("./entry.ts", import.meta.url))],
|
|
throw: false,
|
|
plugins: [
|
|
{
|
|
name: "sync-throw",
|
|
setup(build) {
|
|
build.${hook}(
|
|
{ filter: ${hook === "onLoad" ? "/entry\\.ts$/" : "/^sync-throw:thing$/"} },
|
|
() => {
|
|
armed = true;
|
|
// Pending promise: the outer async IIFE in the builtin suspends on it,
|
|
// so the builtin falls through to the public .then() call which throws
|
|
// synchronously and surfaces to the C++ TOP_EXCEPTION_SCOPE fallback.
|
|
return new Promise(() => {});
|
|
},
|
|
);
|
|
${hook === "onResolve" ? 'build.onLoad({ filter: /.*/, namespace: "sync-throw" }, () => ({ contents: "", loader: "js" }));' : ""}
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
Promise.prototype.then = originalThen;
|
|
console.log(JSON.stringify({
|
|
success: result.success,
|
|
logs: result.logs.map(l => String(l.message ?? l)),
|
|
}));
|
|
`;
|
|
|
|
using dir = tempDir(`plugin-sync-exception-${hook}`, {
|
|
"entry.ts": hook === "onLoad" ? `export const x = 1;` : `import "sync-throw:thing"; export const x = 1;`,
|
|
"build.ts": fixture,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", join(String(dir), "build.ts")],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
// If the build hangs (the onResolve bug), kill it so the assertion below reports
|
|
// a useful diff instead of the test runner timing out.
|
|
timeout: 10_000,
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// Receiver includes stdout/stderr so the diff on failure shows the crash output
|
|
// (UBSan/SIGSEGV under the old code) instead of a bare "expected 0, got 1".
|
|
expect({
|
|
stdout: stdout.trim(),
|
|
stderr: stderr.trim(),
|
|
exitCode,
|
|
signalCode: proc.signalCode ?? null,
|
|
}).toMatchObject({
|
|
exitCode: 0,
|
|
signalCode: null,
|
|
});
|
|
|
|
const parsed = JSON.parse(stdout.trim());
|
|
expect(parsed.success).toBe(false);
|
|
expect(parsed.logs.join("\n")).toContain(`synchronous throw from ${hook} builtin`);
|
|
});
|
|
});
|