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
39 changes: 24 additions & 15 deletions packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,26 @@ type IDebugAdapter = {
) => void | DAP.ResponseMap[R] | Promise<DAP.ResponseMap[R]> | Promise<void>;
};

export type DebugAdapterEventMap = InspectorEventMap & {
/**
* Inspector event domains that the adapter re-emits. Restricting this keeps a debug target from
* invoking the adapter's own `Adapter.*` and `Process.*` handlers by sending events with those names,
* and `DebugAdapterEventMap` only contains the inspector events that can actually be re-emitted.
*/
const inspectorEventDomains = [
"Audit",
"Console",
"Debugger",
"Heap",
"Inspector",
"LifecycleReporter",
"Runtime",
"ScriptProfiler",
"TestReporter",
] as const;

type InspectorEvent = keyof InspectorEventMap & `${(typeof inspectorEventDomains)[number]}.${string}`;

export type DebugAdapterEventMap = Pick<InspectorEventMap, InspectorEvent> & {
[E in keyof DAP.EventMap as E extends string ? `Adapter.${E}` : never]: [DAP.EventMap[E]];
} & {
"Adapter.request": [DAP.Request];
Expand All @@ -225,24 +244,14 @@ export type DebugAdapterEventMap = InspectorEventMap & {
const isDebug = process.env.NODE_ENV === "development";
const debugSilentEvents = new Set(["Adapter.event", "Inspector.event"]);

const inspectorEventDomains = new Set([
"Audit",
"Console",
"Debugger",
"Heap",
"Inspector",
"LifecycleReporter",
"Runtime",
"ScriptProfiler",
"TestReporter",
]);
const inspectorEventDomainSet: ReadonlySet<string> = new Set(inspectorEventDomains);

function isInspectorEvent(event: unknown): boolean {
function isInspectorEvent(event: unknown): event is InspectorEvent {
if (typeof event !== "string") {
return false;
}
const dot = event.indexOf(".");
return dot !== -1 && inspectorEventDomains.has(event.slice(0, dot));
return dot !== -1 && inspectorEventDomainSet.has(event.slice(0, dot));
}

let threadId = 1;
Expand Down Expand Up @@ -300,7 +309,7 @@ export abstract class BaseDebugAdapter<T extends Inspector = Inspector>
let sent = false;
sent ||= emit(event, ...args);
if (isInspectorEvent(event)) {
sent ||= this.emit(event as keyof JSC.EventMap, ...(args as any));
sent ||= this.emit(event, ...(args as any));
}
return sent;
};
Expand Down
4 changes: 2 additions & 2 deletions packages/bun-inspector-protocol/.gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
protocol/*/protocol.json linguist-generated=true
protocol/*/index.d.ts linguist-generated=true
src/protocol/*/protocol.json linguist-generated=true
src/protocol/*/index.d.ts linguist-generated=true
112 changes: 81 additions & 31 deletions packages/bun-inspector-protocol/scripts/generate-protocol.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
// Regenerates src/protocol/jsc/{protocol.json,index.d.ts} from the inspector
// protocol of the WebKit build Bun links against.
//
// bun scripts/generate-protocol.ts [path/to/CombinedDomains.json] [--v8]
//
// CombinedDomains.json is what JavaScriptCore's build produces from
// Source/JavaScriptCore/inspector/protocol/*.json. The bun-webkit prebuilt
// tarball ships it at its top level, so after `bun bd` it can be found in the
// build cache for the WEBKIT_VERSION pinned in scripts/build/deps/webkit.ts;
// that is what is used when no path is given. A local WebKit build writes it
// to <build>/JavaScriptCore/DerivedSources/CombinedDomains.json.
//
// Pass --v8 to also refresh src/protocol/v8 from the Chrome DevTools protocol
// repository (requires network access).
import { spawnSync } from "node:child_process";
import { readFileSync, realpathSync, writeFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import path from "node:path";
import type { Property, Protocol } from "../src/protocol/schema";
import type { Domain, Property, Protocol } from "../src/protocol/schema";

function formatProtocol(protocol: Protocol, extraTs?: string): string {
const { name, domains } = protocol;
Expand Down Expand Up @@ -135,14 +150,6 @@ async function downloadV8(): Promise<Protocol> {
}));
}

async function getJSC(): Promise<Protocol> {
let bunExecutable = Bun.which("bun-debug") || process.execPath;
if (!bunExecutable) {
throw new Error("bun-debug not found");
}
bunExecutable = realpathSync(bunExecutable);
}

async function download<V>(url: string): Promise<V> {
const response = await fetch(url);
if (!response.ok) {
Expand All @@ -163,38 +170,81 @@ function toComment(description?: string): string {
return lines.join("\n");
}

const cwd = new URL("../src/protocol/", import.meta.url);
const runner = "Bun" in globalThis ? "bunx" : "npx";
const repoRoot = path.resolve(import.meta.dir, "..", "..", "..");

/** The CombinedDomains.json of the prebuilt WebKit pinned by scripts/build/deps/webkit.ts, if it has been downloaded. */
function findPinnedCombinedDomains(): string | undefined {
const webkitTs = readFileSync(path.join(repoRoot, "scripts", "build", "deps", "webkit.ts"), "utf-8");
const version = /^export const WEBKIT_VERSION = "([^"]+)";/m.exec(webkitTs)?.[1];
if (!version) {
throw new Error("Could not find WEBKIT_VERSION in scripts/build/deps/webkit.ts");
}
// Mirrors prebuiltDestDir() in scripts/build/deps/webkit.ts:
// <cache>/webkit-<version>[-<os>][-<arch>][-debug|-lto][-asan]/
const dirVersion = version.startsWith("autobuild-") ? version.slice("autobuild-".length) : version.slice(0, 16);
const bunInstall = process.env.BUN_INSTALL
? path.resolve(repoRoot, process.env.BUN_INSTALL)
: path.join(homedir(), ".bun");
const cacheDir = path.join(bunInstall, "build-cache");
if (!existsSync(cacheDir)) {
return undefined;
}
const glob = new Bun.Glob(`webkit-${dirVersion}*/CombinedDomains.json`);
const [match] = [...glob.scanSync({ cwd: cacheDir })].sort();
return match && path.join(cacheDir, match);
}

/**
* Domains that Bun's WebKit fork declares as debuggable from JavaScript, but that bun registers no
* agent for (see src/jsc/bindings/BunDebugger.cpp): bun answers every command in them with
* "'<domain>' domain was not found". test/cli/inspect/bun-inspector-protocol.test.ts checks that
* this is still true, so remove a domain from here once bun implements it.
*/
const domainsWithoutAgent = new Set(["File", "Process"]);

const args = process.argv.slice(2);
const includeV8 = args.includes("--v8");
const combinedDomainsPath = args.find(arg => !arg.startsWith("--")) ?? findPinnedCombinedDomains();
if (!combinedDomainsPath) {
console.error(
"Could not find CombinedDomains.json for the pinned WebKit version. " +
"Run `bun bd` first or pass the path to a WebKit build's CombinedDomains.json.",
);
process.exit(1);
}
console.log(`Reading ${combinedDomainsPath}`);
const combinedDomains: { domains: Domain[] } = await Bun.file(combinedDomainsPath).json();

const protocolDir = path.resolve(import.meta.dir, "..", "src", "protocol");
const written: string[] = [];
const write = (name: string, data: string) => {
const filePath = path.resolve(__dirname, "..", "src", "protocol", name);
const filePath = path.join(protocolDir, name);
writeFileSync(filePath, data);
spawnSync(runner, ["prettier", "--write", filePath], { cwd, stdio: "ignore" });
written.push(filePath);
};
const base = readFileSync(new URL("protocol.d.ts", cwd), "utf-8");
const base = readFileSync(path.join(protocolDir, "protocol.d.ts"), "utf-8");
const baseNoComments = base.replace(/\/\/.*/g, "");

const jscJsonFile = path.resolve(__dirname, process.argv.at(-1) ?? "");
let jscJSONFile;
try {
jscJSONFile = await Bun.file(jscJsonFile).json();
} catch (error) {
console.warn("Failed to read CombinedDomains.json from WebKit build. Is this a WebKit build from Bun?");
console.error(error);
process.exit(1);
}

const jsc = {
const jsc: Protocol = {
name: "JSC",
version: {
major: 1,
minor: 4,
},
domains: jscJSONFile.domains
.filter(a => a.debuggableTypes?.includes?.("javascript"))
domains: combinedDomains.domains
.filter(domain => domain.debuggableTypes?.includes("javascript") && !domainsWithoutAgent.has(domain.domain))
.sort((a, b) => a.domain.localeCompare(b.domain)),
};
write("jsc/protocol.json", JSON.stringify(jsc, null, 2));
write("jsc/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(jsc, baseNoComments));
const v8 = await downloadV8();
write("v8/protocol.json", JSON.stringify(v8));
write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));

if (includeV8) {
const v8 = await downloadV8();
write("v8/protocol.json", JSON.stringify(v8));
write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));
}

const { status } = spawnSync("bunx", ["prettier", "--write", ...written], { cwd: repoRoot, stdio: "inherit" });
if (status !== 0) {
process.exit(status ?? 1);
}
Loading