Skip to content
Merged
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: 1 addition & 1 deletion scripts/build/deps/webkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* for local mode. Override via `--webkit-version=<hash>` to test a branch.
* From https://github.com/oven-sh/WebKit releases.
*/
export const WEBKIT_VERSION = "447082ab6897278727b44e1ba3c326ae6e1504c3";
export const WEBKIT_VERSION = "09e477744721074f73a67eba197c6103afb9eab6";

/**
* WebKit (JavaScriptCore) — the JS engine.
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/modules/NodeUtilTypesModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
#include <JavaScriptCore/StringObject.h>
#include <JavaScriptCore/RegExpObject.h>
#include <JavaScriptCore/JSSetIterator.h>
#include <JavaScriptCore/ObjectPrototype.h>
#include <JavaScriptCore/ObjectPrototypeInlines.h>
#include <cmath>
#include "JSEventTarget.h"
#include "JavaScriptCore/TopExceptionScope.h"
Expand Down
246 changes: 246 additions & 0 deletions test/cli/inspect/debugger-buntranspiledmodule.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
// `bun test --isolate` (and `bun build --compile` output) hands JSC a
// SourceProvider tagged BunTranspiledModule instead of Module so Bun's
// pre-computed module record is reused. That tag must behave like Module at
// every `sourceType()` switch in JSC's debugger/inspector; when it falls
// through, `Debugger.scriptParsed` reports a non-module `scriptType` and
// `Debugger.setBreakpoint` replies "Could not resolve breakpoint".
// See oven-sh/WebKit#405.
import { spawn } from "bun";
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "node:path";

async function runDebuggerProbe(extraArgs: readonly string[], expectedSourceType: string | null) {
using dir = tempDir("inspect-buntranspiledmodule", {
"mod.test.ts": `import { test, expect } from "bun:test";
import { isolatedModuleCacheSourceType } from "bun:internal-for-testing";
export const x = 1;
globalThis.__providerSourceType = isolatedModuleCacheSourceType(import.meta.path);
debugger;
test("t", () => { expect(x).toBe(1); });
`,
});

await using proc = spawn({
cmd: [
bunExe(),
"--inspect-wait=ws://127.0.0.1:0/buntranspiledmodule",
"test",
...extraArgs,
join(String(dir), "mod.test.ts"),
],
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});

// Scan complete stderr lines for the inspector WebSocket URL while draining
// the stream so the child never back-pressures.
let stderrBuf = "";
let stderrLineBuf = "";
const { promise: urlPromise, resolve: urlResolve, reject: urlReject } = Promise.withResolvers<URL>();
let urlFound = false;
(async () => {
const decoder = new TextDecoder();
for await (const chunk of proc.stderr as ReadableStream<Uint8Array>) {
const text = decoder.decode(chunk, { stream: true });
stderrBuf += text;
if (urlFound) continue;
stderrLineBuf += text;
const lines = stderrLineBuf.split("\n");
stderrLineBuf = lines.pop() ?? "";
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed) continue;
try {
const u = new URL(trimmed);
if (u.protocol === "ws:" || u.protocol === "wss:") {
urlFound = true;
urlResolve(u);
break;
}
} catch {}
}
}
if (!urlFound && stderrLineBuf.trim()) {
try {
const u = new URL(stderrLineBuf.trim());
if (u.protocol === "ws:" || u.protocol === "wss:") {
urlFound = true;
urlResolve(u);
}
} catch {}
}
if (!urlFound) urlReject(new Error(`Inspector URL not found: ${JSON.stringify(stderrBuf)}`));
})().catch(err => {
if (!urlFound) urlReject(err);
});
Comment thread
alii marked this conversation as resolved.
(async () => {
for await (const _ of proc.stdout as ReadableStream<Uint8Array>) {
}
})().catch(() => {});

const url = await urlPromise;
const ws = new WebSocket(url);
try {
await new Promise<void>((resolve, reject) => {
ws.addEventListener("open", () => resolve(), { once: true });
ws.addEventListener("error", e => reject(new Error("WebSocket error", { cause: e })), { once: true });
ws.addEventListener("close", e => reject(new Error("WebSocket closed", { cause: e })), { once: true });
});

type Waiter = { resolve: (value: any) => void; reject: (error: Error) => void };
let nextId = 1;
const pending = new Map<number, Waiter>();
const eventWaiters = new Map<string, Waiter>();
let closeError: Error | undefined;
let userScript: { scriptId: string; scriptType: string; url: string } | undefined;

const failAll = (err: Error) => {
if (closeError) return;
closeError = err;
for (const w of pending.values()) w.reject(err);
pending.clear();
for (const w of eventWaiters.values()) w.reject(err);
eventWaiters.clear();
};
ws.addEventListener("error", e => failAll(new Error("WebSocket error", { cause: e })));
ws.addEventListener("close", e => failAll(new Error(`WebSocket closed (${e.code})`, { cause: e })));
ws.addEventListener("message", ev => {
const msg = JSON.parse(String(ev.data));
if (typeof msg.id === "number") {
const w = pending.get(msg.id);
if (w) {
pending.delete(msg.id);
w.resolve(msg);
}
} else if (typeof msg.method === "string") {
if (msg.method === "Debugger.scriptParsed") {
const p = msg.params;
if (String(p.url).endsWith("mod.test.ts") || String(p.sourceURL).endsWith("mod.test.ts")) {
userScript = { scriptId: String(p.scriptId), scriptType: String(p.scriptType), url: String(p.url) };
}
}
const w = eventWaiters.get(msg.method);
if (w) {
eventWaiters.delete(msg.method);
w.resolve(msg.params);
}
}
});

const send = (method: string, params: Record<string, unknown> = {}) =>
new Promise<any>((resolve, reject) => {
if (closeError) return reject(closeError);
const id = nextId++;
pending.set(id, { resolve, reject });
ws.send(JSON.stringify({ id, method, params }));
});
const waitForEvent = (method: string) =>
new Promise<any>((resolve, reject) => {
if (closeError) return reject(closeError);
eventWaiters.set(method, { resolve, reject });
});

await Promise.all([
send("Inspector.enable"),
send("Debugger.enable"),
send("Debugger.setBreakpointsActive", { active: true }),
send("Debugger.setPauseOnDebuggerStatements", { enabled: true }),
]);

const pausedPromise = waitForEvent("Debugger.paused");
send("Inspector.initialized").catch(err => failAll(err instanceof Error ? err : new Error(String(err))));
const paused = await pausedPromise;
expect(paused.reason).toBe("DebuggerStatement");
Comment thread
alii marked this conversation as resolved.

if (!userScript) {
throw new Error(`No Debugger.scriptParsed for mod.test.ts; stderr=${JSON.stringify(stderrBuf)}`);
}

// Self-check the premise: the --isolate run must actually be exercising a
// BunTranspiledModule provider. Without this, a refactor that stops
// attaching module_info to the entrypoint would leave both cases as
// Module-vs-Module and the regression guard would evaporate. The fixture
// stashed the value on globalThis because evaluateOnCallFrame parses its
// expression as a Program (no `import.meta`) and module scope has no
// `require`.
const callFrameId = paused.callFrames?.[0]?.callFrameId;
expect(callFrameId).toEqual(expect.any(String));
const sourceTypeEval = await send("Debugger.evaluateOnCallFrame", {
callFrameId,
expression: `globalThis.__providerSourceType`,
returnByValue: true,
});

const setBreakpoint = await send("Debugger.setBreakpoint", {
location: { scriptId: userScript.scriptId, lineNumber: 5, columnNumber: 0 },
});
// Use the URL the inspector reported (bun realpaths the script path before
// reporting it) and a different line so the scriptId breakpoint above
// doesn't collide.
const setBreakpointByUrl = await send("Debugger.setBreakpointByUrl", {
url: userScript.url,
lineNumber: 2,
columnNumber: 0,
});
// Assert the full inspector replies (breakpoints + module flag + the
// provider-type self-check) together so a failure shows the full picture.
// Matching the whole reply object keeps a CDP `error` or `wasThrown: true`
// visible in the diff instead of collapsing to undefined/null.
expect({
sourceTypeEval,
setBreakpoint,
setBreakpointByUrl,
scriptType: userScript.scriptType,
}).toEqual({
sourceTypeEval: {
id: expect.any(Number),
result: {
result:
expectedSourceType === null
? { type: "object", subtype: "null", value: null }
: { type: "string", value: expectedSourceType },
wasThrown: false,
},
},
setBreakpoint: {
id: expect.any(Number),
result: {
breakpointId: expect.any(String),
actualLocation: {
scriptId: userScript.scriptId,
lineNumber: 5,
columnNumber: expect.any(Number),
},
},
},
setBreakpointByUrl: {
id: expect.any(Number),
result: {
breakpointId: expect.any(String),
locations: [{ scriptId: userScript.scriptId, lineNumber: 2, columnNumber: expect.any(Number) }],
},
},
scriptType: "module",
});

await send("Debugger.resume").catch(() => {});
} finally {
try {
ws.close();
} catch {}
}
}

test.concurrent("bun test --isolate: Debugger.scriptParsed reports a module and breakpoints resolve", async () => {
await runDebuggerProbe(["--isolate"], "BunTranspiledModule");
});

// Sanity: without --isolate the provider is plain Module, the isolation cache
// is empty (hence null), and this has always worked; pinning it alongside
// ensures the --isolate case is being compared against the correct baseline.
test.concurrent("bun test (no --isolate): Debugger.scriptParsed reports a module and breakpoints resolve", async () => {
await runDebuggerProbe([], null);
});
21 changes: 4 additions & 17 deletions test/js/node/inspector/inspector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,6 @@ import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
import inspector from "node:inspector";

// Child processes that send Runtime.evaluate or hit a Debugger pause go through
// JSC's InjectedScript, which has missing RELEASE_AND_RETURN at
// JSInjectedScriptHostPrototype.cpp jsInjectedScriptHostPrototypeFunctionEvaluateWithScopeExtension
// and JSJavaScriptCallFrame::scopeChain (constructArray return). Both live in
// the prebuilt WebKit, so validateExceptionChecks aborts the child before the
// test can observe anything. Strip the flag for those spawns so ASAN/LSAN still
// run against the child; drop this once the WebKit prebuilt has the two
// RELEASE_AND_RETURN wraps.
const injectedScriptChildEnv = (() => {
const { BUN_JSC_validateExceptionChecks, BUN_JSC_dumpSimulatedThrows, ...env } = bunEnv;
return env;
})();

test("inspector.url()", () => {
expect(inspector.url()).toBeUndefined();
});
Expand Down Expand Up @@ -204,7 +191,7 @@ test("inspector.open() serves the DevTools protocol and /json discovery endpoint

await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.mjs"],
env: injectedScriptChildEnv,
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
Expand Down Expand Up @@ -422,7 +409,7 @@ test("inspector.waitForDebugger() blocks until a client resumes the process", as

await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.mjs"],
env: injectedScriptChildEnv,
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
Expand Down Expand Up @@ -499,7 +486,7 @@ test("inspector.waitForDebugger() blocks again on the second call after a fronte

await using proc = Bun.spawn({
cmd: [bunExe(), "fixture.mjs"],
env: injectedScriptChildEnv,
env: bunEnv,
cwd: String(dir),
stderr: "pipe",
});
Expand Down Expand Up @@ -917,7 +904,7 @@ export { after };

await using proc = Bun.spawn({
cmd: [bunExe(), "entry.mjs"],
env: injectedScriptChildEnv,
env: bunEnv,
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
Expand Down
4 changes: 0 additions & 4 deletions test/no-validate-exceptions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,3 @@ test/napi/napi.test.ts
# unchecked at defineOwnNonIndexProperty. Moved here from expectations.txt.
test/cli/run/require-cache.test.ts

# The inspector's Runtime.evaluate / inspectee spawn hits an unchecked
# getOwnNonIndexPropertyNames -> JSObjectInlines::get scope. Moved here from
# expectations.txt so the whole file runs on ASAN without validateExceptionChecks.
test/cli/inspect/inspect.test.ts