diff --git a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts index f15cdab3e6ea..eaf8c0549117 100644 --- a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts +++ b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts @@ -206,7 +206,26 @@ type IDebugAdapter = { ) => void | DAP.ResponseMap[R] | Promise | Promise; }; -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 & { [E in keyof DAP.EventMap as E extends string ? `Adapter.${E}` : never]: [DAP.EventMap[E]]; } & { "Adapter.request": [DAP.Request]; @@ -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 = 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; @@ -300,7 +309,7 @@ export abstract class BaseDebugAdapter 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; }; diff --git a/packages/bun-inspector-protocol/.gitattributes b/packages/bun-inspector-protocol/.gitattributes index f2af6eac270b..f78864971e4a 100644 --- a/packages/bun-inspector-protocol/.gitattributes +++ b/packages/bun-inspector-protocol/.gitattributes @@ -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 diff --git a/packages/bun-inspector-protocol/scripts/generate-protocol.ts b/packages/bun-inspector-protocol/scripts/generate-protocol.ts index 3157614372c5..6a074a44c728 100644 --- a/packages/bun-inspector-protocol/scripts/generate-protocol.ts +++ b/packages/bun-inspector-protocol/scripts/generate-protocol.ts @@ -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 /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; @@ -135,14 +150,6 @@ async function downloadV8(): Promise { })); } -async function getJSC(): Promise { - let bunExecutable = Bun.which("bun-debug") || process.execPath; - if (!bunExecutable) { - throw new Error("bun-debug not found"); - } - bunExecutable = realpathSync(bunExecutable); -} - async function download(url: string): Promise { const response = await fetch(url); if (!response.ok) { @@ -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: + // /webkit-[-][-][-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 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); +} diff --git a/packages/bun-inspector-protocol/src/protocol/jsc/index.d.ts b/packages/bun-inspector-protocol/src/protocol/jsc/index.d.ts index e4c69554cb6d..fa466c69f6b0 100644 --- a/packages/bun-inspector-protocol/src/protocol/jsc/index.d.ts +++ b/packages/bun-inspector-protocol/src/protocol/jsc/index.d.ts @@ -55,6 +55,165 @@ export namespace JSC { */ export type TeardownResponse = {}; } + export namespace BunFrontendDevServer { + /** + * Unique identifier for Bun.serve + */ + export type ServerId = number; + /** + * Unique identifier for a connected HMR WebSocket client. + */ + export type ConnectionId = number; + /** + * Identifier for a specific route bundle within DevServer. + */ + export type RouteBundleId = number; + /** + * A base64 encoded string representing a binary payload originally defined by DevServer's WebSocket protocol (MessageId enum in DevServer.zig). + */ + export type SerializedPayloadBase64 = string; + /** + * Fired when a new HMR WebSocket client connects. + * @event `BunFrontendDevServer.clientConnected` + */ + export type ClientConnectedEvent = { + /** + * Server ID + */ + serverId: ServerId; + /** + * Identifier for the newly connected client. + */ + connectionId: ConnectionId; + }; + /** + * Fired when an HMR WebSocket client disconnects. + * @event `BunFrontendDevServer.clientDisconnected` + */ + export type ClientDisconnectedEvent = { + /** + * Server ID + */ + serverId: ServerId; + /** + * Identifier for the disconnected client. + */ + connectionId: ConnectionId; + }; + /** + * Fired when the DevServer starts processing a new bundle. + * @event `BunFrontendDevServer.bundleStart` + */ + export type BundleStartEvent = { + /** + * Server ID + */ + serverId: ServerId; + /** + * List of file paths that triggered this bundle. + */ + triggerFiles: string[]; + }; + /** + * Fired when the DevServer successfully completes a bundle without build errors. + * @event `BunFrontendDevServer.bundleComplete` + */ + export type BundleCompleteEvent = { + /** + * Server ID + */ + serverId: ServerId; + /** + * Time taken for the bundle in milliseconds. + */ + durationMs: number; + }; + /** + * Fired when the DevServer completes a bundle with build errors. The payload is the base64 encoded binary data corresponding to the 'errors' MessageId. + * @event `BunFrontendDevServer.bundleFailed` + */ + export type BundleFailedEvent = { + /** + * Server ID + */ + serverId: ServerId; + /** + * Base64 encoded binary payload containing serialized build errors (MessageId.errors format). + */ + buildErrorsPayloadBase64: SerializedPayloadBase64; + }; + /** + * Fired when a connected client navigates to a new URL (via history API or initial load). + * @event `BunFrontendDevServer.clientNavigated` + */ + export type ClientNavigatedEvent = { + /** + * Server ID + */ + serverId: ServerId; + /** + * Identifier for the client that navigated. + */ + connectionId: ConnectionId; + /** + * The new URL the client navigated to. + */ + url: string; + /** + * The DevServer route bundle ID associated with this URL, if matched. + */ + routeBundleId?: RouteBundleId | undefined; + }; + /** + * Fired when an error reported by a client (via the /_bun/report_error endpoint) has been processed and potentially remapped by the server. The payload is the base64 encoded binary data corresponding to the remapped error format (similar to MessageId.errors but potentially containing only one error). + * @event `BunFrontendDevServer.clientErrorReported` + */ + export type ClientErrorReportedEvent = { + /** + * Server ID + */ + serverId: ServerId; + /** + * Base64 encoded binary payload containing the processed/remapped client error. + */ + clientErrorPayloadBase64: SerializedPayloadBase64; + }; + /** + * Fired when the client logs a message to the console. + * @event `BunFrontendDevServer.consoleLog` + */ + export type ConsoleLogEvent = { + serverId: ServerId; + /** + * The kind of log message. + */ + kind: number; + /** + * The log message. + */ + message: string; + }; + /** + * Enables the BunFrontendDevServer domain, sending events as they occur. + * @request `BunFrontendDevServer.enable` + */ + export type EnableRequest = {}; + /** + * Enables the BunFrontendDevServer domain, sending events as they occur. + * @response `BunFrontendDevServer.enable` + */ + export type EnableResponse = {}; + /** + * Disables the BunFrontendDevServer domain, stopping further events from being sent. + * @request `BunFrontendDevServer.disable` + */ + export type DisableRequest = {}; + /** + * Disables the BunFrontendDevServer domain, stopping further events from being sent. + * @response `BunFrontendDevServer.disable` + */ + export type DisableResponse = {}; + } export namespace Console { /** * Channels for different types of log messages. @@ -65,9 +224,9 @@ export namespace JSC { | "network" | "console-api" | "storage" - | "appcache" | "rendering" | "css" + | "accessibility" | "security" | "content-blocker" | "media" @@ -339,6 +498,7 @@ export namespace JSC { * Unique script identifier. */ export type ScriptId = string; + export type ScriptType = "program" | "module" | "webassembly"; /** * Call frame identifier. */ @@ -356,7 +516,7 @@ export namespace JSC { */ lineNumber: number; /** - * Column number in the script (0-based). + * Column number in the script (0-based) or bytecode offset for WebAssembly modules (0-based). */ columnNumber?: number | undefined; }; @@ -560,9 +720,17 @@ export namespace JSC { */ endLine: number; /** - * Length of the last line of the script. + * Length of the last line of the script or the end bytecode offset for WebAssembly modules. */ endColumn: number; + /** + * Identifier of the execution context in which this script was parsed. + */ + executionContextId: Runtime.ExecutionContextId; + /** + * Type of script. + */ + scriptType: ScriptType; /** * Determines whether this script is a user extension script. */ @@ -576,9 +744,13 @@ export namespace JSC { */ sourceMapURL?: string | undefined; /** - * True if this script was parsed as a module. + * Human-readable name of the script. */ - module?: boolean | undefined; + displayName?: string | undefined; + /** + * Identifier of the network request associated with this script (if any). + */ + requestId?: Network.RequestId | undefined; }; /** * Fired when virtual machine fails to parse the script. @@ -1191,11 +1363,11 @@ export namespace JSC { url: string; shouldBlackbox: boolean; /** - * If true, url is case sensitive. + * If true, url is case sensitive. Defaults to true. */ caseSensitive?: boolean | undefined; /** - * If true, treat url as regular expression. + * If true, treat url as regular expression. Defaults to false. */ isRegex?: boolean | undefined; /** @@ -1382,6 +1554,341 @@ export namespace JSC { result: Runtime.RemoteObject; }; } + export namespace HTTPServer { + /** + * Unique identifier for an HTTP server instance. + */ + export type ServerId = number; + /** + * Unique identifier for an HTTP request. + */ + export type RequestId = number; + /** + * Unique identifier for a server route. + */ + export type RouteId = number; + /** + * Identifier for a hot reload instance of a server. Increments each time the server associated with a ServerId is reloaded. + */ + export type HotReloadId = number; + /** + * Request / response headers as a flat array of key/value strings. [key1, value1, key2, value2, ...] + */ + export type Headers = string[]; + /** + * Request route parameters as a flat array of key/value strings. [key1, value1, key2, value2, ...] + */ + export type RequestParams = string[]; + /** + * HTTP request method represented as an integer. (Mapping to be defined, e.g., 1=GET, 2=POST, ...) + */ + export type HTTPMethod = number; + /** + * Type of the server route. + */ + export type RouteType = "default" | "api" | "html" | "static"; + /** + * Represents a defined server route. + */ + export type Route = { + /** + * Unique identifier for the route. + */ + routeId: RouteId; + /** + * The path pattern for the route (e.g., '/users/:id'). + */ + path: string; + /** + * The type of the route. + */ + type: RouteType; + /** + * Names of the parameters defined in the path pattern. + */ + paramNames?: string[] | undefined; + /** + * Filesystem path associated with 'static' or 'html' routes. + */ + filePath?: string | undefined; + /** + * The HTTP method associated with the route. + */ + method?: HTTPMethod | undefined; + /** + * url of the script the route is in. Available when the debugger is not attached. + */ + scriptUrl?: string | undefined; + /** + * Line number in the script that started the route. Available when the debugger is not attached. + */ + scriptLine: number; + }; + /** + * Metadata about an incoming server request. + */ + export type Request = { + /** + * Unique identifier for this request. + */ + requestId: RequestId; + /** + * Identifier of the server processing this request. + */ + serverId: ServerId; + /** + * Identifier of the route that matched this request. 0 means no route matched. + */ + routeId: RouteId; + /** + * Request URL (path and query string). + */ + url: string; + /** + * Full request URL including host and protocol. + */ + fullUrl: string; + /** + * HTTP method as an integer. + */ + method: HTTPMethod; + /** + * HTTP request headers. + */ + headers: Headers; + /** + * Matched route parameters. + */ + params?: RequestParams | undefined; + /** + * Indicates if the request has a body. + */ + hasBody: boolean; + /** + * Timestamp when the request was received by the server (Unix epoch milliseconds). + */ + timestamp: number; + }; + /** + * Metadata about an outgoing server response. + */ + export type Response = { + /** + * Identifier of the request this response corresponds to. + */ + requestId: RequestId; + /** + * Identifier of the server sending this response. + */ + serverId: ServerId; + /** + * HTTP status code. + */ + statusCode: number; + /** + * HTTP status text. + */ + statusText: string; + /** + * HTTP response headers. + */ + headers: Headers; + /** + * Indicates if the response has a body. + */ + hasBody: boolean; + /** + * Timestamp when the response headers were sent (Unix epoch milliseconds). + */ + timestamp: number; + }; + /** + * A chunk of a request or response body. + */ + export type BodyChunk = { + /** + * Identifier of the request this chunk belongs to. + */ + requestId: RequestId; + /** + * Identifier of the server handling the request. + */ + serverId: ServerId; + /** + * Flags indicating the type of chunk. Bit 0: 1 if this chunk belongs to the request body, 0 for response body. Bit 1: 1 if this is the final chunk of the body, Bit 2: 1 if the chunk is base64 encoded. 0 otherwise. + */ + flags: number; + /** + * The body chunk data + */ + chunk: string; + /** + * Timestamp when the chunk was received/sent (Unix epoch milliseconds). + */ + timestamp: number; + }; + /** + * Details about an unhandled exception in a request handler. + */ + export type RequestHandlerError = { + /** + * Identifier of the request where the error occurred. + */ + requestId: RequestId; + /** + * Identifier of the server processing the request. + */ + serverId: ServerId; + /** + * Error message. + */ + message: string; + /** + * Timestamp when the error occurred (Unix epoch milliseconds). + */ + timestamp: number; + /** + * url of the script the route is in. Available when the debugger is not attached. + */ + url?: string | undefined; + /** + * Line number in the script that started the route. + */ + line: number; + }; + /** + * Fired when an HTTP server starts listening. + * @event `HTTPServer.listen` + */ + export type ListenEvent = { + /** + * Unique identifier for this server instance. + */ + serverId: ServerId; + /** + * A URL you can fetch from the server. Example: 'http://localhost:3000' or 'https://localhost:3000' + */ + url: string; + /** + * Timestamp when the server started (Unix epoch milliseconds). + */ + startTime: number; + }; + /** + * Fired when an HTTP server stops listening. + * @event `HTTPServer.close` + */ + export type CloseEvent = { + /** + * Identifier of the server that stopped. + */ + serverId: ServerId; + /** + * Timestamp when the server stopped (Unix epoch milliseconds). + */ + timestamp: number; + }; + /** + * Fired when a server starts or its routes are updated (e.g., via hot reload). Provides the complete list of current routes. + * @event `HTTPServer.serverRoutesUpdated` + */ + export type ServerRoutesUpdatedEvent = { + /** + * Identifier of the server whose routes were updated. + */ + serverId: ServerId; + /** + * The number of times Bun has hot reloaded. When hot reloading server-side code, the server ID will be the same and the hotReloadId will increment. When not hot reloading, the server ID will be different and the hotReloadId will not increment. + */ + hotReloadId: HotReloadId; + /** + * The complete list of routes currently served. + */ + routes: Route[]; + }; + /** + * Fired when the server receives an HTTP request, before the handler is invoked. Body content is not included by default. + * @event `HTTPServer.requestWillBeSent` + */ + export type RequestWillBeSentEvent = { + /** + * Request metadata. + */ + request: Request; + }; + /** + * Fired when the server begins sending an HTTP response. Body content is not included by default. + * @event `HTTPServer.responseReceived` + */ + export type ResponseReceivedEvent = { + /** + * Response metadata. + */ + response: Response; + }; + /** + * Fired when a chunk of the request or response body is available, after being requested via `getRequestBody` or `getResponseBody`. + * @event `HTTPServer.bodyChunkReceived` + */ + export type BodyChunkReceivedEvent = { + /** + * The body chunk data. + */ + chunk: BodyChunk; + }; + /** + * Fired when the server has finished processing a request and sent the response. + * @event `HTTPServer.requestFinished` + */ + export type RequestFinishedEvent = { + /** + * Identifier of the finished request. + */ + requestId: RequestId; + /** + * Identifier of the server handling the request. + */ + serverId: ServerId; + /** + * Timestamp when the request finished (Unix epoch milliseconds). + */ + timestamp: number; + /** + * Total duration of the request handling in milliseconds. + */ + duration?: number | undefined; + }; + /** + * Fired when an unhandled exception occurs within a request handler. + * @event `HTTPServer.requestHandlerException` + */ + export type RequestHandlerExceptionEvent = { + /** + * Details about the error. + */ + error: RequestHandlerError; + }; + /** + * Enables the HTTPServer domain, clearing any previous data. + * @request `HTTPServer.enable` + */ + export type EnableRequest = {}; + /** + * Enables the HTTPServer domain, clearing any previous data. + * @response `HTTPServer.enable` + */ + export type EnableResponse = {}; + /** + * Disables the HTTPServer domain. + * @request `HTTPServer.disable` + */ + export type DisableRequest = {}; + /** + * Disables the HTTPServer domain. + * @response `HTTPServer.disable` + */ + export type DisableResponse = {}; + } export namespace Inspector { /** * undefined @@ -1501,6 +2008,37 @@ export namespace JSC { * @response `LifecycleReporter.stopPreventingExit` */ export type StopPreventingExitResponse = {}; + /** + * Returns the current module graph containing ESM and CJS modules. + * @request `LifecycleReporter.getModuleGraph` + */ + export type GetModuleGraphRequest = {}; + /** + * Returns the current module graph containing ESM and CJS modules. + * @response `LifecycleReporter.getModuleGraph` + */ + export type GetModuleGraphResponse = { + /** + * Array of ESM module paths. + */ + esm: string[]; + /** + * Array of CJS module paths. + */ + cjs: string[]; + /** + * Current working directory + */ + cwd: string; + /** + * The main file + */ + main: string; + /** + * argv that launched the process + */ + argv: string[]; + }; } export namespace Runtime { /** @@ -2593,6 +3131,14 @@ export namespace JSC { export type DisableResponse = {}; } export type EventMap = { + "BunFrontendDevServer.clientConnected": BunFrontendDevServer.ClientConnectedEvent; + "BunFrontendDevServer.clientDisconnected": BunFrontendDevServer.ClientDisconnectedEvent; + "BunFrontendDevServer.bundleStart": BunFrontendDevServer.BundleStartEvent; + "BunFrontendDevServer.bundleComplete": BunFrontendDevServer.BundleCompleteEvent; + "BunFrontendDevServer.bundleFailed": BunFrontendDevServer.BundleFailedEvent; + "BunFrontendDevServer.clientNavigated": BunFrontendDevServer.ClientNavigatedEvent; + "BunFrontendDevServer.clientErrorReported": BunFrontendDevServer.ClientErrorReportedEvent; + "BunFrontendDevServer.consoleLog": BunFrontendDevServer.ConsoleLogEvent; "Console.messageAdded": Console.MessageAddedEvent; "Console.messageRepeatCountUpdated": Console.MessageRepeatCountUpdatedEvent; "Console.messagesCleared": Console.MessagesClearedEvent; @@ -2608,6 +3154,14 @@ export namespace JSC { "Heap.garbageCollected": Heap.GarbageCollectedEvent; "Heap.trackingStart": Heap.TrackingStartEvent; "Heap.trackingComplete": Heap.TrackingCompleteEvent; + "HTTPServer.listen": HTTPServer.ListenEvent; + "HTTPServer.close": HTTPServer.CloseEvent; + "HTTPServer.serverRoutesUpdated": HTTPServer.ServerRoutesUpdatedEvent; + "HTTPServer.requestWillBeSent": HTTPServer.RequestWillBeSentEvent; + "HTTPServer.responseReceived": HTTPServer.ResponseReceivedEvent; + "HTTPServer.bodyChunkReceived": HTTPServer.BodyChunkReceivedEvent; + "HTTPServer.requestFinished": HTTPServer.RequestFinishedEvent; + "HTTPServer.requestHandlerException": HTTPServer.RequestHandlerExceptionEvent; "Inspector.evaluateForTestInFrontend": Inspector.EvaluateForTestInFrontendEvent; "Inspector.inspect": Inspector.InspectEvent; "LifecycleReporter.reload": LifecycleReporter.ReloadEvent; @@ -2624,6 +3178,8 @@ export namespace JSC { "Audit.setup": Audit.SetupRequest; "Audit.run": Audit.RunRequest; "Audit.teardown": Audit.TeardownRequest; + "BunFrontendDevServer.enable": BunFrontendDevServer.EnableRequest; + "BunFrontendDevServer.disable": BunFrontendDevServer.DisableRequest; "Console.enable": Console.EnableRequest; "Console.disable": Console.DisableRequest; "Console.clearMessages": Console.ClearMessagesRequest; @@ -2667,6 +3223,8 @@ export namespace JSC { "Heap.stopTracking": Heap.StopTrackingRequest; "Heap.getPreview": Heap.GetPreviewRequest; "Heap.getRemoteObject": Heap.GetRemoteObjectRequest; + "HTTPServer.enable": HTTPServer.EnableRequest; + "HTTPServer.disable": HTTPServer.DisableRequest; "Inspector.enable": Inspector.EnableRequest; "Inspector.disable": Inspector.DisableRequest; "Inspector.initialized": Inspector.InitializedRequest; @@ -2674,6 +3232,7 @@ export namespace JSC { "LifecycleReporter.disable": LifecycleReporter.DisableRequest; "LifecycleReporter.preventExit": LifecycleReporter.PreventExitRequest; "LifecycleReporter.stopPreventingExit": LifecycleReporter.StopPreventingExitRequest; + "LifecycleReporter.getModuleGraph": LifecycleReporter.GetModuleGraphRequest; "Runtime.parse": Runtime.ParseRequest; "Runtime.evaluate": Runtime.EvaluateRequest; "Runtime.awaitPromise": Runtime.AwaitPromiseRequest; @@ -2703,6 +3262,8 @@ export namespace JSC { "Audit.setup": Audit.SetupResponse; "Audit.run": Audit.RunResponse; "Audit.teardown": Audit.TeardownResponse; + "BunFrontendDevServer.enable": BunFrontendDevServer.EnableResponse; + "BunFrontendDevServer.disable": BunFrontendDevServer.DisableResponse; "Console.enable": Console.EnableResponse; "Console.disable": Console.DisableResponse; "Console.clearMessages": Console.ClearMessagesResponse; @@ -2746,6 +3307,8 @@ export namespace JSC { "Heap.stopTracking": Heap.StopTrackingResponse; "Heap.getPreview": Heap.GetPreviewResponse; "Heap.getRemoteObject": Heap.GetRemoteObjectResponse; + "HTTPServer.enable": HTTPServer.EnableResponse; + "HTTPServer.disable": HTTPServer.DisableResponse; "Inspector.enable": Inspector.EnableResponse; "Inspector.disable": Inspector.DisableResponse; "Inspector.initialized": Inspector.InitializedResponse; @@ -2753,6 +3316,7 @@ export namespace JSC { "LifecycleReporter.disable": LifecycleReporter.DisableResponse; "LifecycleReporter.preventExit": LifecycleReporter.PreventExitResponse; "LifecycleReporter.stopPreventingExit": LifecycleReporter.StopPreventingExitResponse; + "LifecycleReporter.getModuleGraph": LifecycleReporter.GetModuleGraphResponse; "Runtime.parse": Runtime.ParseResponse; "Runtime.evaluate": Runtime.EvaluateResponse; "Runtime.awaitPromise": Runtime.AwaitPromiseResponse; diff --git a/packages/bun-inspector-protocol/src/protocol/jsc/protocol.json b/packages/bun-inspector-protocol/src/protocol/jsc/protocol.json index 0b2b4d7d07df..ed5e1657d39c 100644 --- a/packages/bun-inspector-protocol/src/protocol/jsc/protocol.json +++ b/packages/bun-inspector-protocol/src/protocol/jsc/protocol.json @@ -9,8 +9,8 @@ "domain": "Audit", "description": "", "version": 4, - "debuggableTypes": ["itml", "javascript", "page", "service-worker", "web-page"], - "targetTypes": ["itml", "javascript", "page", "service-worker", "worker"], + "debuggableTypes": ["itml", "javascript", "service-worker", "web-page", "wasm-debugger"], + "targetTypes": ["itml", "javascript", "page", "service-worker", "worker", "wasm-debugger"], "commands": [ { "name": "setup", @@ -60,11 +60,197 @@ } ] }, + { + "domain": "BunFrontendDevServer", + "description": "Provides an interface for tools to interact with Bun's development server.", + "debuggableTypes": ["itml", "javascript"], + "targetTypes": ["itml", "javascript"], + "types": [ + { + "id": "ServerId", + "type": "integer", + "description": "Unique identifier for Bun.serve" + }, + { + "id": "ConnectionId", + "type": "integer", + "description": "Unique identifier for a connected HMR WebSocket client." + }, + { + "id": "RouteBundleId", + "type": "integer", + "description": "Identifier for a specific route bundle within DevServer." + }, + { + "id": "SerializedPayloadBase64", + "type": "string", + "description": "A base64 encoded string representing a binary payload originally defined by DevServer's WebSocket protocol (MessageId enum in DevServer.zig)." + } + ], + "commands": [ + { + "name": "enable", + "description": "Enables the BunFrontendDevServer domain, sending events as they occur." + }, + { + "name": "disable", + "description": "Disables the BunFrontendDevServer domain, stopping further events from being sent." + } + ], + "events": [ + { + "name": "clientConnected", + "description": "Fired when a new HMR WebSocket client connects.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Server ID" + }, + { + "name": "connectionId", + "$ref": "ConnectionId", + "description": "Identifier for the newly connected client." + } + ] + }, + { + "name": "clientDisconnected", + "description": "Fired when an HMR WebSocket client disconnects.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Server ID" + }, + { + "name": "connectionId", + "$ref": "ConnectionId", + "description": "Identifier for the disconnected client." + } + ] + }, + { + "name": "bundleStart", + "description": "Fired when the DevServer starts processing a new bundle.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Server ID" + }, + { + "name": "triggerFiles", + "type": "array", + "items": { + "type": "string" + }, + "description": "List of file paths that triggered this bundle." + } + ] + }, + { + "name": "bundleComplete", + "description": "Fired when the DevServer successfully completes a bundle without build errors.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Server ID" + }, + { + "name": "durationMs", + "type": "number", + "description": "Time taken for the bundle in milliseconds." + } + ] + }, + { + "name": "bundleFailed", + "description": "Fired when the DevServer completes a bundle with build errors. The payload is the base64 encoded binary data corresponding to the 'errors' MessageId.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Server ID" + }, + { + "name": "buildErrorsPayloadBase64", + "$ref": "SerializedPayloadBase64", + "description": "Base64 encoded binary payload containing serialized build errors (MessageId.errors format)." + } + ] + }, + { + "name": "clientNavigated", + "description": "Fired when a connected client navigates to a new URL (via history API or initial load).", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Server ID" + }, + { + "name": "connectionId", + "$ref": "ConnectionId", + "description": "Identifier for the client that navigated." + }, + { + "name": "url", + "type": "string", + "description": "The new URL the client navigated to." + }, + { + "name": "routeBundleId", + "$ref": "RouteBundleId", + "optional": true, + "description": "The DevServer route bundle ID associated with this URL, if matched." + } + ] + }, + { + "name": "clientErrorReported", + "description": "Fired when an error reported by a client (via the /_bun/report_error endpoint) has been processed and potentially remapped by the server. The payload is the base64 encoded binary data corresponding to the remapped error format (similar to MessageId.errors but potentially containing only one error).", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Server ID" + }, + { + "name": "clientErrorPayloadBase64", + "$ref": "SerializedPayloadBase64", + "description": "Base64 encoded binary payload containing the processed/remapped client error." + } + ] + }, + { + "name": "consoleLog", + "description": "Fired when the client logs a message to the console.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId" + }, + { + "name": "kind", + "type": "number", + "description": "The kind of log message." + }, + { + "name": "message", + "type": "string", + "description": "The log message." + } + ] + } + ] + }, { "domain": "Console", "description": "Console domain defines methods and events for interaction with the JavaScript console. Console collects messages created by means of the JavaScript Console API. One needs to enable this domain using enable command in order to start receiving the console messages. Browser collects messages issued while console domain is not enabled as well and reports them using messageAdded notification upon enabling.", - "debuggableTypes": ["itml", "javascript", "page", "service-worker", "web-page"], - "targetTypes": ["itml", "javascript", "page", "service-worker", "worker"], + "debuggableTypes": ["itml", "javascript", "service-worker", "web-page", "wasm-debugger"], + "targetTypes": ["itml", "javascript", "frame", "page", "service-worker", "worker", "wasm-debugger"], "types": [ { "id": "ChannelSource", @@ -75,9 +261,9 @@ "network", "console-api", "storage", - "appcache", "rendering", "css", + "accessibility", "security", "content-blocker", "media", @@ -303,6 +489,7 @@ { "name": "getLoggingChannels", "description": "List of the different message sources that are non-default logging channels.", + "targetTypes": ["frame", "page"], "returns": [ { "name": "channels", @@ -317,6 +504,7 @@ { "name": "setLoggingChannelLevel", "description": "Modify the level of a channel.", + "targetTypes": ["frame", "page"], "parameters": [ { "name": "source", @@ -397,8 +585,8 @@ { "domain": "Debugger", "description": "Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing breakpoints, stepping through execution, exploring stack traces, etc.", - "debuggableTypes": ["itml", "javascript", "page", "service-worker", "web-page"], - "targetTypes": ["itml", "javascript", "page", "service-worker", "worker"], + "debuggableTypes": ["itml", "javascript", "service-worker", "web-page", "wasm-debugger"], + "targetTypes": ["frame", "itml", "javascript", "page", "service-worker", "worker", "wasm-debugger"], "types": [ { "id": "BreakpointId", @@ -415,6 +603,11 @@ "type": "string", "description": "Unique script identifier." }, + { + "id": "ScriptType", + "type": "string", + "enum": ["program", "module", "webassembly"] + }, { "id": "CallFrameId", "type": "string", @@ -439,7 +632,7 @@ "name": "columnNumber", "type": "integer", "optional": true, - "description": "Column number in the script (0-based)." + "description": "Column number in the script (0-based) or bytecode offset for WebAssembly modules (0-based)." } ] }, @@ -1175,13 +1368,13 @@ "name": "caseSensitive", "type": "boolean", "optional": true, - "description": "If true, url is case sensitive." + "description": "If true, url is case sensitive. Defaults to true." }, { "name": "isRegex", "type": "boolean", "optional": true, - "description": "If true, treat url as regular expression." + "description": "If true, treat url as regular expression. Defaults to false." }, { "name": "sourceRanges", @@ -1242,7 +1435,17 @@ { "name": "endColumn", "type": "integer", - "description": "Length of the last line of the script." + "description": "Length of the last line of the script or the end bytecode offset for WebAssembly modules." + }, + { + "name": "executionContextId", + "$ref": "Runtime.ExecutionContextId", + "description": "Identifier of the execution context in which this script was parsed." + }, + { + "name": "scriptType", + "$ref": "ScriptType", + "description": "Type of script." }, { "name": "isContentScript", @@ -1263,10 +1466,16 @@ "description": "URL of source map associated with script (if any)." }, { - "name": "module", - "type": "boolean", + "name": "displayName", + "type": "string", "optional": true, - "description": "True if this script was parsed as a module." + "description": "Human-readable name of the script." + }, + { + "name": "requestId", + "$ref": "Network.RequestId", + "optional": true, + "description": "Identifier of the network request associated with this script (if any)." } ] }, @@ -1397,8 +1606,8 @@ { "domain": "Heap", "description": "Heap domain exposes JavaScript heap attributes and capabilities.", - "debuggableTypes": ["itml", "javascript", "page", "service-worker", "web-page"], - "targetTypes": ["itml", "javascript", "page", "service-worker", "worker"], + "debuggableTypes": ["itml", "javascript", "service-worker", "web-page", "wasm-debugger"], + "targetTypes": ["itml", "javascript", "page", "service-worker", "worker", "wasm-debugger"], "types": [ { "id": "GarbageCollection", @@ -1561,10 +1770,433 @@ } ] }, + { + "domain": "HTTPServer", + "description": "Provides events and commands for debugging server-side HTTP requests and responses within the Bun runtime.", + "debuggableTypes": ["itml", "javascript"], + "targetTypes": ["itml", "javascript"], + "types": [ + { + "id": "ServerId", + "type": "integer", + "description": "Unique identifier for an HTTP server instance." + }, + { + "id": "RequestId", + "type": "integer", + "description": "Unique identifier for an HTTP request." + }, + { + "id": "RouteId", + "type": "integer", + "description": "Unique identifier for a server route." + }, + { + "id": "HotReloadId", + "type": "integer", + "description": "Identifier for a hot reload instance of a server. Increments each time the server associated with a ServerId is reloaded." + }, + { + "id": "Headers", + "type": "array", + "items": { + "type": "string" + }, + "description": "Request / response headers as a flat array of key/value strings. [key1, value1, key2, value2, ...]" + }, + { + "id": "RequestParams", + "type": "array", + "items": { + "type": "string" + }, + "description": "Request route parameters as a flat array of key/value strings. [key1, value1, key2, value2, ...]" + }, + { + "id": "HTTPMethod", + "type": "integer", + "description": "HTTP request method represented as an integer. (Mapping to be defined, e.g., 1=GET, 2=POST, ...)" + }, + { + "id": "RouteType", + "type": "string", + "enum": ["default", "api", "html", "static"], + "description": "Type of the server route." + }, + { + "id": "Route", + "type": "object", + "description": "Represents a defined server route.", + "properties": [ + { + "name": "routeId", + "$ref": "RouteId", + "description": "Unique identifier for the route." + }, + { + "name": "path", + "type": "string", + "description": "The path pattern for the route (e.g., '/users/:id')." + }, + { + "name": "type", + "$ref": "RouteType", + "description": "The type of the route." + }, + { + "name": "paramNames", + "type": "array", + "items": { + "type": "string" + }, + "optional": true, + "description": "Names of the parameters defined in the path pattern." + }, + { + "name": "filePath", + "type": "string", + "optional": true, + "description": "Filesystem path associated with 'static' or 'html' routes." + }, + { + "name": "method", + "$ref": "HTTPMethod", + "description": "The HTTP method associated with the route.", + "optional": true + }, + { + "name": "scriptUrl", + "type": "string", + "description": "url of the script the route is in. Available when the debugger is not attached.", + "optional": true + }, + { + "name": "scriptLine", + "type": "integer", + "description": "Line number in the script that started the route. Available when the debugger is not attached." + } + ] + }, + { + "id": "Request", + "type": "object", + "description": "Metadata about an incoming server request.", + "properties": [ + { + "name": "requestId", + "$ref": "RequestId", + "description": "Unique identifier for this request." + }, + { + "name": "serverId", + "$ref": "ServerId", + "description": "Identifier of the server processing this request." + }, + { + "name": "routeId", + "$ref": "RouteId", + "description": "Identifier of the route that matched this request. 0 means no route matched." + }, + { + "name": "url", + "type": "string", + "description": "Request URL (path and query string)." + }, + { + "name": "fullUrl", + "type": "string", + "description": "Full request URL including host and protocol." + }, + { + "name": "method", + "$ref": "HTTPMethod", + "description": "HTTP method as an integer." + }, + { + "name": "headers", + "$ref": "Headers", + "description": "HTTP request headers." + }, + { + "name": "params", + "$ref": "RequestParams", + "optional": true, + "description": "Matched route parameters." + }, + { + "name": "hasBody", + "type": "boolean", + "description": "Indicates if the request has a body." + }, + { + "name": "timestamp", + "type": "number", + "description": "Timestamp when the request was received by the server (Unix epoch milliseconds)." + } + ] + }, + { + "id": "Response", + "type": "object", + "description": "Metadata about an outgoing server response.", + "properties": [ + { + "name": "requestId", + "$ref": "RequestId", + "description": "Identifier of the request this response corresponds to." + }, + { + "name": "serverId", + "$ref": "ServerId", + "description": "Identifier of the server sending this response." + }, + { + "name": "statusCode", + "type": "integer", + "description": "HTTP status code." + }, + { + "name": "statusText", + "type": "string", + "description": "HTTP status text." + }, + { + "name": "headers", + "$ref": "Headers", + "description": "HTTP response headers." + }, + { + "name": "hasBody", + "type": "boolean", + "description": "Indicates if the response has a body." + }, + { + "name": "timestamp", + "type": "number", + "description": "Timestamp when the response headers were sent (Unix epoch milliseconds)." + } + ] + }, + { + "id": "BodyChunk", + "type": "object", + "description": "A chunk of a request or response body.", + "properties": [ + { + "name": "requestId", + "$ref": "RequestId", + "description": "Identifier of the request this chunk belongs to." + }, + { + "name": "serverId", + "$ref": "ServerId", + "description": "Identifier of the server handling the request." + }, + { + "name": "flags", + "type": "integer", + "description": "Flags indicating the type of chunk. Bit 0: 1 if this chunk belongs to the request body, 0 for response body. Bit 1: 1 if this is the final chunk of the body, Bit 2: 1 if the chunk is base64 encoded. 0 otherwise." + }, + { + "name": "chunk", + "type": "string", + "description": "The body chunk data" + }, + { + "name": "timestamp", + "type": "number", + "description": "Timestamp when the chunk was received/sent (Unix epoch milliseconds)." + } + ] + }, + { + "id": "RequestHandlerError", + "type": "object", + "description": "Details about an unhandled exception in a request handler.", + "properties": [ + { + "name": "requestId", + "$ref": "RequestId", + "description": "Identifier of the request where the error occurred." + }, + { + "name": "serverId", + "$ref": "ServerId", + "description": "Identifier of the server processing the request." + }, + { + "name": "message", + "type": "string", + "description": "Error message." + }, + { + "name": "timestamp", + "type": "number", + "description": "Timestamp when the error occurred (Unix epoch milliseconds)." + }, + { + "name": "url", + "type": "string", + "description": "url of the script the route is in. Available when the debugger is not attached.", + "optional": true + }, + { + "name": "line", + "type": "integer", + "description": "Line number in the script that started the route." + } + ] + } + ], + "commands": [ + { + "name": "enable", + "description": "Enables the HTTPServer domain, clearing any previous data." + }, + { + "name": "disable", + "description": "Disables the HTTPServer domain." + } + ], + "events": [ + { + "name": "listen", + "description": "Fired when an HTTP server starts listening.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Unique identifier for this server instance." + }, + { + "name": "url", + "type": "string", + "description": "A URL you can fetch from the server. Example: 'http://localhost:3000' or 'https://localhost:3000'" + }, + { + "name": "startTime", + "type": "number", + "description": "Timestamp when the server started (Unix epoch milliseconds)." + } + ] + }, + { + "name": "close", + "description": "Fired when an HTTP server stops listening.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Identifier of the server that stopped." + }, + { + "name": "timestamp", + "type": "number", + "description": "Timestamp when the server stopped (Unix epoch milliseconds)." + } + ] + }, + { + "name": "serverRoutesUpdated", + "description": "Fired when a server starts or its routes are updated (e.g., via hot reload). Provides the complete list of current routes.", + "parameters": [ + { + "name": "serverId", + "$ref": "ServerId", + "description": "Identifier of the server whose routes were updated." + }, + { + "name": "hotReloadId", + "$ref": "HotReloadId", + "description": "The number of times Bun has hot reloaded. When hot reloading server-side code, the server ID will be the same and the hotReloadId will increment. When not hot reloading, the server ID will be different and the hotReloadId will not increment." + }, + { + "name": "routes", + "type": "array", + "items": { + "$ref": "Route" + }, + "description": "The complete list of routes currently served." + } + ] + }, + { + "name": "requestWillBeSent", + "description": "Fired when the server receives an HTTP request, before the handler is invoked. Body content is not included by default.", + "parameters": [ + { + "name": "request", + "$ref": "Request", + "description": "Request metadata." + } + ] + }, + { + "name": "responseReceived", + "description": "Fired when the server begins sending an HTTP response. Body content is not included by default.", + "parameters": [ + { + "name": "response", + "$ref": "Response", + "description": "Response metadata." + } + ] + }, + { + "name": "bodyChunkReceived", + "description": "Fired when a chunk of the request or response body is available, after being requested via `getRequestBody` or `getResponseBody`.", + "parameters": [ + { + "name": "chunk", + "$ref": "BodyChunk", + "description": "The body chunk data." + } + ] + }, + { + "name": "requestFinished", + "description": "Fired when the server has finished processing a request and sent the response.", + "parameters": [ + { + "name": "requestId", + "$ref": "RequestId", + "description": "Identifier of the finished request." + }, + { + "name": "serverId", + "$ref": "ServerId", + "description": "Identifier of the server handling the request." + }, + { + "name": "timestamp", + "type": "number", + "description": "Timestamp when the request finished (Unix epoch milliseconds)." + }, + { + "name": "duration", + "type": "number", + "optional": true, + "description": "Total duration of the request handling in milliseconds." + } + ] + }, + { + "name": "requestHandlerException", + "description": "Fired when an unhandled exception occurs within a request handler.", + "parameters": [ + { + "name": "error", + "$ref": "RequestHandlerError", + "description": "Details about the error." + } + ] + } + ] + }, { "domain": "Inspector", - "debuggableTypes": ["itml", "javascript", "page", "web-page"], - "targetTypes": ["itml", "javascript", "page"], + "debuggableTypes": ["itml", "javascript", "service-worker", "web-page", "wasm-debugger"], + "targetTypes": ["itml", "javascript", "page", "service-worker", "wasm-debugger"], "commands": [ { "name": "enable", @@ -1609,7 +2241,6 @@ "description": "LifecycleReporter domain allows reporting of lifecycle events.", "debuggableTypes": ["itml", "javascript"], "targetTypes": ["itml", "javascript"], - "types": [], "commands": [ { "name": "enable", @@ -1626,6 +2257,49 @@ { "name": "stopPreventingExit", "description": "Does not prevent the process from exiting." + }, + { + "name": "getModuleGraph", + "description": "Returns the current module graph containing ESM and CJS modules.", + "returns": [ + { + "name": "esm", + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of ESM module paths." + }, + { + "name": "cjs", + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of CJS module paths." + }, + { + "name": "cwd", + "type": "string", + "items": { + "type": "string" + }, + "description": "Current working directory" + }, + { + "name": "main", + "type": "string", + "description": "The main file" + }, + { + "name": "argv", + "type": "array", + "items": { + "type": "string" + }, + "description": "argv that launched the process" + } + ] } ], "events": [ @@ -1677,8 +2351,8 @@ { "domain": "Runtime", "description": "Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects. Evaluation results are returned as mirror object that expose object type, string representation and unique identifier that can be used for further object reference. Original objects are maintained in memory unless they are either explicitly released or are released along with the other objects in their object group.", - "debuggableTypes": ["itml", "javascript", "page", "service-worker", "web-page"], - "targetTypes": ["itml", "javascript", "page", "service-worker", "worker"], + "debuggableTypes": ["itml", "javascript", "service-worker", "web-page", "wasm-debugger"], + "targetTypes": ["frame", "itml", "javascript", "page", "service-worker", "worker", "wasm-debugger"], "types": [ { "id": "RemoteObjectId", @@ -2839,8 +3513,8 @@ { "domain": "ScriptProfiler", "description": "Profiler domain exposes JavaScript evaluation timing and profiling.", - "debuggableTypes": ["itml", "javascript", "page", "web-page"], - "targetTypes": ["itml", "javascript", "page"], + "debuggableTypes": ["itml", "javascript", "web-page", "wasm-debugger"], + "targetTypes": ["itml", "javascript", "page", "worker", "wasm-debugger"], "types": [ { "id": "EventType", diff --git a/packages/bun-inspector-protocol/src/protocol/schema.d.ts b/packages/bun-inspector-protocol/src/protocol/schema.d.ts index a92bea546881..ec5e0a672e1e 100644 --- a/packages/bun-inspector-protocol/src/protocol/schema.d.ts +++ b/packages/bun-inspector-protocol/src/protocol/schema.d.ts @@ -11,8 +11,9 @@ export type Protocol = { export type Domain = { readonly domain: string; + readonly debuggableTypes?: readonly string[]; readonly dependencies?: readonly string[]; - readonly types: readonly Property[]; + readonly types?: readonly Property[]; readonly commands?: readonly Command[]; readonly events?: readonly Event[]; }; @@ -27,7 +28,7 @@ export type Command = { export type Event = { readonly name: string; readonly description?: string; - readonly parameters: readonly Property[]; + readonly parameters?: readonly Property[]; }; export type Property = { diff --git a/src/jsc/bindings/InspectorLifecycleAgent.cpp b/src/jsc/bindings/InspectorLifecycleAgent.cpp index 8a4f4394aa71..5cd0fcfe0dd3 100644 --- a/src/jsc/bindings/InspectorLifecycleAgent.cpp +++ b/src/jsc/bindings/InspectorLifecycleAgent.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "BunProcess.h" #include "headers.h" @@ -128,7 +129,14 @@ using ModuleGraph = std::tuple> /* esm */, Ref InspectorLifecycleAgent::getModuleGraph() { auto& vm = m_globalObject.vm(); - auto scope = DECLARE_THROW_SCOPE(vm); + // The caller is the generated protocol dispatcher, which never does JSC + // exception checks, so exceptions must not escape this function: report + // them as a protocol error and clear them (keeping only termination). + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + auto fail = [&](WTF::ASCIILiteral message) { + (void)scope.tryClearException(); + return makeUnexpected(ErrorString { message }); + }; auto* global = defaultGlobalObject(&m_globalObject); auto* cjsMap = global->requireMap(); @@ -149,13 +157,13 @@ Protocol::ErrorStringOr InspectorLifecycleAgent::getModuleGraph() Ref> cjs = JSON::ArrayOf::create(); { auto iter2 = JSC::JSMapIterator::create(vm, global->mapIteratorStructure(), cjsMap, JSC::IterationKind::Keys); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to create iterator"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to create iterator"_s)); JSC::JSValue value; while (iter2->next(global, value)) { cjs->addItem(value.toWTFString(global)); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to add item to cjs array"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to add item to cjs array"_s)); } - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to iterate over cjs map"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to iterate over cjs map"_s)); } auto* process = global->processObject(); @@ -164,12 +172,12 @@ Protocol::ErrorStringOr InspectorLifecycleAgent::getModuleGraph() { auto* array = uncheckedDowncast(process->getArgv(global)); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to get argv"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to get argv"_s)); for (size_t i = 0, length = array->length(); i < length; i++) { auto value = array->getIndex(global, i); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to get value at index"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to get value at index"_s)); auto string = value.toWTFString(global); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to convert value to string"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to convert value to string"_s)); argv->addItem(string); } } @@ -178,17 +186,17 @@ Protocol::ErrorStringOr InspectorLifecycleAgent::getModuleGraph() { auto& builtinNames = Bun::builtinNames(vm); auto value = global->bunObject()->get(global, builtinNames.mainPublicName()); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to get main"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to get main"_s)); main = value.toWTFString(global); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to convert value to string"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to convert value to string"_s)); } String cwd; { auto cwdValue = JSC::JSValue::decode(Bun__Process__getCwd(&m_globalObject)); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to get cwd"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to get cwd"_s)); cwd = cwdValue.toWTFString(global); - RETURN_IF_EXCEPTION(scope, makeUnexpected(ErrorString("Failed to convert value to string"_s))); + RETURN_IF_EXCEPTION(scope, fail("Failed to convert value to string"_s)); } return ModuleGraph { WTF::move(esm), WTF::move(cjs), WTF::move(cwd), WTF::move(main), WTF::move(argv) }; diff --git a/test/cli/inspect/bun-inspector-protocol.test.ts b/test/cli/inspect/bun-inspector-protocol.test.ts new file mode 100644 index 000000000000..64d58cc7936b --- /dev/null +++ b/test/cli/inspect/bun-inspector-protocol.test.ts @@ -0,0 +1,264 @@ +// packages/bun-inspector-protocol ships a snapshot of the inspector protocol of the WebKit +// build bun links against (src/protocol/jsc/protocol.json, from which index.d.ts is +// generated). Nothing regenerates it when WebKit is bumped, so this test runs a short +// debugging session against this build of bun and validates every message it sends +// against the snapshot. If it fails after a WebKit upgrade, regenerate the snapshot: +// +// bun packages/bun-inspector-protocol/scripts/generate-protocol.ts +import { spawn } from "bun"; +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; +import { basename } from "node:path"; +import protocolJson from "../../../packages/bun-inspector-protocol/src/protocol/jsc/protocol.json"; +import type { Property, Protocol } from "../../../packages/bun-inspector-protocol/src/protocol/schema"; + +const protocol = protocolJson as Protocol; +const domains = new Map(protocol.domains.map(domain => [domain.domain, domain])); + +function declaredEventParameters(method: string): readonly Property[] | undefined { + const [domain, name] = method.split("."); + const event = domains.get(domain)?.events?.find(event => event.name === name); + return event && (event.parameters ?? []); +} + +function declaredCommandReturns(method: string): readonly Property[] | undefined { + const [domain, name] = method.split("."); + const command = domains.get(domain)?.commands?.find(command => command.name === name); + return command && (command.returns ?? []); +} + +/** Appends to `problems` every way `value` disagrees with `property`, the snapshot's declaration of it. */ +function check(value: unknown, property: Property, domain: string, where: string, problems: string[]): void { + if ("$ref" in property) { + const [refDomain, refId] = property.$ref.includes(".") ? property.$ref.split(".") : [domain, property.$ref]; + const type = domains.get(refDomain)?.types?.find(type => type.id === refId); + // A few JavaScriptCore types reference domains that only exist for web pages (e.g. Network.RequestId). + // Those domains are not part of the snapshot, and bun never sends values of those types. + if (type) check(value, type, refDomain, where, problems); + return; + } + switch (property.type) { + case "string": + if (typeof value !== "string") { + problems.push(`${where}: expected a string, got ${JSON.stringify(value)}`); + } else if (property.enum && !property.enum.includes(value)) { + problems.push(`${where}: ${JSON.stringify(value)} is not one of: ${property.enum.join(", ")}`); + } + return; + case "boolean": + if (typeof value !== "boolean") problems.push(`${where}: expected a boolean, got ${JSON.stringify(value)}`); + return; + case "number": + case "integer": + if (typeof value !== "number") problems.push(`${where}: expected a number, got ${JSON.stringify(value)}`); + return; + case "array": { + const { items } = property; + if (!Array.isArray(value)) { + problems.push(`${where}: expected an array, got ${JSON.stringify(value)}`); + } else if (items) { + value.forEach((item, i) => check(item, items, domain, `${where}[${i}]`, problems)); + } + return; + } + case "object": { + const { properties } = property; + if (typeof value !== "object" || value === null || Array.isArray(value)) { + problems.push(`${where}: expected an object, got ${JSON.stringify(value)}`); + } else if (properties) { + checkObject(value as Record, properties, domain, where, problems); + } + return; + } + } +} + +function checkObject( + object: Record, + declared: readonly Property[], + domain: string, + where: string, + problems: string[], +): void { + for (const property of declared) { + const name = property.name!; + if (!(name in object)) { + if (!property.optional) problems.push(`${where}: missing required property ${name}`); + } else { + check(object[name], property, domain, `${where}.${name}`, problems); + } + } + const declaredNames = new Set(declared.map(property => property.name)); + for (const name of Object.keys(object)) { + if (!declaredNames.has(name)) problems.push(`${where}: property ${name} is not in the snapshot`); + } +} + +// An ES module and a CommonJS module, so Debugger.scriptParsed is sent for both script types. +const fixtureFiles = ["entry.mjs", "dep.cjs"]; + +test("the protocol snapshot in packages/bun-inspector-protocol matches what bun sends", async () => { + using dir = tempDir("bun-inspector-protocol", { + "entry.mjs": ` + import "./dep.cjs"; + console.log("hello"); + reportError(new Error("reported")); + debugger; + setInterval(() => {}, 60_000); + `, + "dep.cjs": `module.exports = 1;`, + }); + + await using proc = spawn({ + cmd: [bunExe(), "--inspect-wait=127.0.0.1:0", "entry.mjs"], + env: bunEnv, + cwd: String(dir), + stdout: "ignore", + stderr: "pipe", + }); + + // stderr is drained for the lifetime of the process (reportError prints to it); the + // inspector's WebSocket URL is on its own line of the listening banner. + let stderr = ""; + const { promise: inspectorUrl, resolve: foundUrl, reject: noUrl } = Promise.withResolvers(); + const stderrDone = (async () => { + const decoder = new TextDecoder(); + for await (const chunk of proc.stderr) { + stderr += decoder.decode(chunk, { stream: true }); + // Only complete lines: a chunk boundary mid-line would yield a truncated URL. + const line = stderr + .split("\n") + .slice(0, -1) + .find(line => line.trim().startsWith("ws://")); + if (line) foundUrl(new URL(line.trim())); + } + noUrl(new Error(`No inspector URL in stderr:\n${stderr}`)); + })().catch(error => noUrl(error instanceof Error ? error : new Error(String(error)))); + + const ws = new WebSocket(await inspectorUrl); + const { promise: failed, reject: fail } = Promise.withResolvers(); + // The inspectee's stderr carries the diagnosis for a dropped socket (a crash + // report, an error it printed before dying), so wait for the pipe to drain + // (bounded: the child may still be alive holding it open) before rejecting. + async function failWith(what: string): Promise { + await Promise.race([Promise.allSettled([stderrDone, proc.exited]), Bun.sleep(1_000)]); + const exit = proc.exitCode ?? proc.signalCode ?? "still running"; + fail(new Error(`${what} (inspectee exit: ${exit})\ninspectee stderr:\n${stderr}`)); + } + ws.addEventListener("error", () => failWith("WebSocket error")); + ws.addEventListener("close", event => failWith(`WebSocket closed (${event.code})`)); + proc.exited.then(() => failWith("inspectee exited")); + failed.catch(() => {}); + + const problems: string[] = []; + const eventsSeen = new Set(); + const scriptTypes: Record = {}; + const eventWaiters = new Map void>(); + const responseWaiters = new Map void>(); + + ws.addEventListener("message", ({ data }) => { + const message = JSON.parse(String(data)); + if (typeof message.id === "number") { + responseWaiters.get(message.id)!(message); + return; + } + const { method, params = {} } = message; + eventsSeen.add(method); + const declared = declaredEventParameters(method); + if (declared) { + checkObject(params, declared, method.split(".")[0], method, problems); + } else { + problems.push(`${method}: event is not in the snapshot`); + } + if (method === "Debugger.scriptParsed" && fixtureFiles.includes(basename(String(params.url)))) { + scriptTypes[basename(String(params.url))] = params.scriptType; + } + eventWaiters.get(method)?.(params); + }); + + let nextId = 1; + function request(method: string, params: Record = {}): Promise { + const id = nextId++; + ws.send(JSON.stringify({ id, method, params })); + return Promise.race([new Promise(resolve => responseWaiters.set(id, resolve)), failed]); + } + /** Sends a command and validates its response against the snapshot. */ + async function send(method: string, params: Record = {}): Promise { + const { result, error } = await request(method, params); + const returns = declaredCommandReturns(method); + if (!returns) { + problems.push(`${method}: command is not in the snapshot`); + } else if (error) { + problems.push(`${method}: error response: ${error.message}`); + } else { + checkObject(result, returns, method.split(".")[0], `${method} response`, problems); + } + return result; + } + function waitForEvent(method: string): Promise { + return Promise.race([new Promise(resolve => eventWaiters.set(method, resolve)), failed]); + } + + try { + await Promise.race([new Promise(resolve => ws.addEventListener("open", () => resolve())), failed]); + + // Enabling every domain in the snapshot checks that bun has an agent for each of them. + const enableCommands = protocol.domains + .filter(domain => domain.commands?.some(command => command.name === "enable")) + .map(domain => `${domain.domain}.enable`); + await Promise.all([ + ...enableCommands.map(method => send(method)), + send("Debugger.setBreakpointsActive", { active: true }), + send("Debugger.setPauseOnDebuggerStatements", { enabled: true }), + ]); + + // Conversely, these domains are left out of the snapshot by generate-protocol.ts because bun has + // no agent for them. Once bun answers, remove the domain from the generator's list and regenerate. + for (const domain of ["File", "Process"]) { + const { error } = await request(`${domain}.enable`); + if (error?.message !== `'${domain}' domain was not found`) { + problems.push(`${domain}: bun implements this domain, but generate-protocol.ts excludes it from the snapshot`); + } + } + + const paused = waitForEvent("Debugger.paused"); + await send("Inspector.initialized"); + const { callFrames } = await paused; + + await send("Runtime.evaluate", { expression: "({ a: [1, 'two', null] })", generatePreview: true }); + await send("Debugger.evaluateOnCallFrame", { callFrameId: callFrames[0].callFrameId, expression: "globalThis" }); + await send("LifecycleReporter.getModuleGraph"); + + const resumed = waitForEvent("Debugger.resumed"); + await send("Debugger.resume"); + await resumed; + } finally { + ws.close(); + } + + expect([...new Set(problems)]).toEqual([]); + // JavaScriptCore's own domains plus the agents bun registers in src/jsc/bindings/BunDebugger.cpp. + expect(protocol.domains.map(domain => domain.domain)).toEqual([ + "Audit", + "BunFrontendDevServer", + "Console", + "Debugger", + "Heap", + "HTTPServer", + "Inspector", + "LifecycleReporter", + "Runtime", + "ScriptProfiler", + "TestReporter", + ]); + expect(scriptTypes).toEqual({ "entry.mjs": "module", "dep.cjs": "program" }); + expect([...eventsSeen].sort()).toEqual( + expect.arrayContaining([ + "Console.messageAdded", + "Debugger.paused", + "Debugger.resumed", + "Debugger.scriptParsed", + "LifecycleReporter.error", + ]), + ); +});