aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ashcon Partovi <ashcon@partovi.net> 2023-08-29 23:44:39 -0700
committerGravatar GitHub <noreply@github.com> 2023-08-29 23:44:39 -0700
commitf2553d24543d72a777ba60213473332809866cb2 (patch)
tree61faac4292dbdf1b4d0543e33d3f5d2c792825c5
parentc028b206bce3f9b5c3cba7899c6bf34856efe43f (diff)
downloadbun-f2553d24543d72a777ba60213473332809866cb2.tar.gz
bun-f2553d24543d72a777ba60213473332809866cb2.tar.zst
bun-f2553d24543d72a777ba60213473332809866cb2.zip
More support for DAP (#4380)
* Fix reconnect with --watch * Support setVariable * Support setExpression * Support watch variables * Conditional and hit breakpoints * Support exceptionInfo * Support goto and gotoTargets * Support completions * Support both a URL and UNIX inspector at the same time * Fix url * WIP, add timeouts to figure out issue * Fix messages being dropped from debugger.ts * Progress * Fix breakpoints and ref-event-loop * More fixes * Fix exit * Make hovers better * Fix --hot
-rw-r--r--packages/bun-debug-adapter-protocol/src/debugger/adapter.ts1130
-rw-r--r--packages/bun-debug-adapter-protocol/src/debugger/capabilities.ts271
-rw-r--r--packages/bun-debug-adapter-protocol/src/debugger/signal.ts6
-rw-r--r--packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts12
-rw-r--r--packages/bun-inspector-protocol/src/inspector/websocket.ts22
-rw-r--r--packages/bun-vscode/example/example.test.ts19
-rw-r--r--packages/bun-vscode/example/example.ts8
-rw-r--r--packages/bun-vscode/package.json55
-rw-r--r--packages/bun-vscode/src/features/debug.ts57
-rw-r--r--src/bun.js/bindings/BunDebugger.cpp11
-rw-r--r--src/bun.js/javascript.zig47
-rw-r--r--src/cli.zig9
-rw-r--r--src/js/internal/debugger.ts580
-rw-r--r--src/js/out/InternalModuleRegistryConstants.h6
14 files changed, 1373 insertions, 860 deletions
diff --git a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
index 1996863e6..f3a60c793 100644
--- a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
+++ b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts
@@ -4,13 +4,91 @@ import type { InspectorEventMap } from "../../../bun-inspector-protocol/src/insp
// @ts-ignore
import { WebSocketInspector, remoteObjectToString } from "../../../bun-inspector-protocol/index";
import type { ChildProcess } from "node:child_process";
-import { spawn, spawnSync } from "node:child_process";
-import capabilities from "./capabilities";
+import { spawn } from "node:child_process";
import { Location, SourceMap } from "./sourcemap";
-import { compare, parse } from "semver";
import { EventEmitter } from "node:events";
import { UnixSignal, randomUnixPath } from "./signal";
+const capabilities: DAP.Capabilities = {
+ supportsConfigurationDoneRequest: true,
+ supportsFunctionBreakpoints: true,
+ supportsConditionalBreakpoints: true,
+ supportsHitConditionalBreakpoints: true,
+ supportsEvaluateForHovers: true,
+ exceptionBreakpointFilters: [
+ {
+ filter: "all",
+ label: "All Exceptions",
+ default: false,
+ supportsCondition: true,
+ description: "Breaks on all throw errors, even if they're caught later.",
+ conditionDescription: `error.name == "CustomError"`,
+ },
+ {
+ filter: "uncaught",
+ label: "Uncaught Exceptions",
+ default: false,
+ supportsCondition: true,
+ description: "Breaks only on errors or promise rejections that are not handled.",
+ conditionDescription: `error.name == "CustomError"`,
+ },
+ {
+ filter: "debugger",
+ label: "Debugger Statements",
+ default: true,
+ supportsCondition: false,
+ description: "Breaks on `debugger` statements.",
+ },
+ {
+ filter: "assert",
+ label: "Assertion Failures",
+ default: false,
+ supportsCondition: false,
+ description: "Breaks on failed assertions.",
+ },
+ {
+ filter: "microtask",
+ label: "Microtasks",
+ default: false,
+ supportsCondition: false,
+ description: "Breaks on microtasks.",
+ },
+ ],
+ supportsStepBack: false,
+ supportsSetVariable: true,
+ supportsRestartFrame: false,
+ supportsGotoTargetsRequest: true,
+ supportsStepInTargetsRequest: false,
+ supportsCompletionsRequest: true,
+ completionTriggerCharacters: [".", "[", '"', "'"],
+ supportsModulesRequest: false,
+ additionalModuleColumns: [],
+ supportedChecksumAlgorithms: [],
+ supportsRestartRequest: false, // TODO
+ supportsExceptionOptions: false, // TODO
+ supportsValueFormattingOptions: false,
+ supportsExceptionInfoRequest: true,
+ supportTerminateDebuggee: true,
+ supportSuspendDebuggee: false,
+ supportsDelayedStackTraceLoading: true,
+ supportsLoadedSourcesRequest: true,
+ supportsLogPoints: true,
+ supportsTerminateThreadsRequest: false,
+ supportsSetExpression: true,
+ supportsTerminateRequest: true,
+ supportsDataBreakpoints: false, // TODO
+ supportsReadMemoryRequest: false,
+ supportsWriteMemoryRequest: false,
+ supportsDisassembleRequest: false,
+ supportsCancelRequest: false,
+ supportsBreakpointLocationsRequest: true,
+ supportsClipboardContext: false,
+ supportsSteppingGranularity: false,
+ supportsInstructionBreakpoints: false,
+ supportsExceptionFilterOptions: false,
+ supportsSingleThreadExecutionRequests: false,
+};
+
type InitializeRequest = DAP.InitializeRequest & {
supportsConfigurationDoneRequest?: boolean;
};
@@ -25,16 +103,17 @@ type LaunchRequest = DAP.LaunchRequest & {
strictEnv?: boolean;
stopOnEntry?: boolean;
noDebug?: boolean;
- watch?: boolean | "hot";
+ watchMode?: boolean | "hot";
};
type AttachRequest = DAP.AttachRequest & {
url?: string;
- hostname?: string;
- port?: number;
- restart?: boolean;
+ noDebug?: boolean;
+ stopOnEntry?: boolean;
};
+type DebuggerOptions = (LaunchRequest & { type: "launch" }) | (AttachRequest & { type: "attach" });
+
type Source = DAP.Source & {
scriptId: string;
sourceMap: SourceMap;
@@ -54,7 +133,11 @@ type Source = DAP.Source & {
type Breakpoint = DAP.Breakpoint & {
id: number;
breakpointId: string;
- generatedLocation: JSC.Debugger.Location;
+ generatedLocation?: JSC.Debugger.Location;
+ source?: Source;
+};
+
+type Target = (DAP.GotoTarget | DAP.StepInTarget) & {
source: Source;
};
@@ -103,8 +186,10 @@ export type DebugAdapterEventMap = InspectorEventMap & {
"Process.stderr": [string];
};
+const isDebug = process.env.NODE_ENV === "development";
+const debugSilentEvents = new Set(["Adapter.event", "Inspector.event"]);
+
let threadId = 1;
-let isDebug = process.env.NODE_ENV === "development";
export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements IDebugAdapter {
#threadId: number;
@@ -115,13 +200,15 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
#sources: Map<string | number, Source>;
#stackFrames: StackFrame[];
#stopped?: DAP.StoppedEvent["reason"];
+ #exception?: Variable;
#breakpointId: number;
- #breakpoints: Breakpoint[];
+ #breakpoints: Map<string, Breakpoint>;
#functionBreakpoints: Map<string, FunctionBreakpoint>;
+ #targets: Map<number, Target>;
#variableId: number;
#variables: Map<number, Variable>;
#initialized?: InitializeRequest;
- #launched?: LaunchRequest;
+ #options?: DebuggerOptions;
constructor(url?: string | URL) {
super();
@@ -138,10 +225,11 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
this.#pendingSources = new Map();
this.#sources = new Map();
this.#stackFrames = [];
- this.#stopped = undefined;
+ this.#stopped = "start";
this.#breakpointId = 1;
- this.#breakpoints = [];
+ this.#breakpoints = new Map();
this.#functionBreakpoints = new Map();
+ this.#targets = new Map();
this.#variableId = 1;
this.#variables = new Map();
}
@@ -159,7 +247,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
* @returns if the inspector was able to connect
*/
start(url?: string): Promise<boolean> {
- return this.#inspector.start(url);
+ return this.#attach({ url });
}
/**
@@ -187,7 +275,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
* @returns if the event was sent to a listener
*/
emit<E extends keyof DebugAdapterEventMap>(event: E, ...args: DebugAdapterEventMap[E] | []): boolean {
- if (isDebug && event !== "Adapter.event" && event !== "Inspector.event") {
+ if (isDebug && !debugSilentEvents.has(event)) {
console.log(this.#threadId, event, ...args);
}
@@ -224,12 +312,11 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
});
}
- #reverseSend<R extends keyof DAP.RequestMap>(name: R, request: DAP.RequestMap[R]): void {
- this.emit("Adapter.request", {
- type: "request",
- seq: 0,
- command: name,
- arguments: request,
+ #emitAfterResponse<E extends keyof DAP.EventMap>(event: E, body?: DAP.EventMap[E]): void {
+ this.once("Adapter.response", () => {
+ process.nextTick(() => {
+ this.#emit(event, body);
+ });
});
}
@@ -240,11 +327,29 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
return;
}
+ let timerId: number | undefined;
let result: unknown;
try {
- // @ts-ignore
- result = await this[command](args);
+ result = await Promise.race([
+ // @ts-ignore
+ this[command](args),
+ new Promise((_, reject) => {
+ timerId = +setTimeout(() => reject(new Error(`Timed out: ${command}`)), 15_000);
+ }),
+ ]);
} catch (cause) {
+ if (cause === Cancel) {
+ this.emit("Adapter.response", {
+ type: "response",
+ command,
+ success: false,
+ message: "cancelled",
+ request_seq: request.seq,
+ seq: 0,
+ });
+ return;
+ }
+
const error = unknownToError(cause);
this.emit("Adapter.error", error);
@@ -258,6 +363,10 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
seq: 0,
});
return;
+ } finally {
+ if (timerId) {
+ clearTimeout(timerId);
+ }
}
this.emit("Adapter.response", {
@@ -276,21 +385,25 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
}
initialize(request: InitializeRequest): DAP.InitializeResponse {
- const { clientID, supportsConfigurationDoneRequest } = (this.#initialized = request);
+ this.#initialized = request;
this.send("Inspector.enable");
this.send("Runtime.enable");
this.send("Console.enable");
- this.send("Debugger.enable");
+ this.send("Debugger.enable").catch(error => {
+ const { message } = unknownToError(error);
+ if (message !== "Debugger domain already enabled") {
+ throw error;
+ }
+ });
this.send("Debugger.setAsyncStackTraceDepth", { depth: 200 });
- this.send("Debugger.setPauseOnDebuggerStatements", { enabled: true });
- this.send("Debugger.setBlackboxBreakpointEvaluations", { blackboxBreakpointEvaluations: true });
this.send("Debugger.setBreakpointsActive", { active: true });
+ this.send("Debugger.pause");
+ this.send("Inspector.initialized");
- // If the client will not send a `configurationDone` request, then we need to
- // tell the debugger that everything is ready.
+ const { clientID, supportsConfigurationDoneRequest } = request;
if (!supportsConfigurationDoneRequest && clientID !== "vscode") {
- this.send("Inspector.initialized");
+ this.configurationDone();
}
// Tell the client what capabilities this adapter supports.
@@ -300,21 +413,21 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
configurationDone(): void {
// If the client requested that `noDebug` mode be enabled,
// then we need to disable all breakpoints and pause on statements.
- if (this.#launched?.noDebug) {
+ if (this.#options?.noDebug) {
this.send("Debugger.setBreakpointsActive", { active: false });
- this.send("Debugger.setPauseOnExceptions", { state: "none" });
- this.send("Debugger.setPauseOnDebuggerStatements", { enabled: false });
- this.send("Debugger.setPauseOnMicrotasks", { enabled: false });
- this.send("Debugger.setPauseForInternalScripts", { shouldPause: false });
- this.send("Debugger.setPauseOnAssertions", { enabled: false });
+ this.setExceptionBreakpoints({ filters: [] });
}
- // Tell the debugger that everything is ready.
- this.send("Inspector.initialized");
+ if (this.#options?.stopOnEntry) {
+ this.send("Debugger.pause");
+ } else {
+ // TODO: Check that the current location is not on a breakpoint before resuming.
+ this.send("Debugger.resume");
+ }
}
async launch(request: DAP.LaunchRequest): Promise<void> {
- this.#launched = request;
+ this.#options = { ...request, type: "launch" };
try {
await this.#launch(request);
@@ -339,9 +452,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
cwd,
env = {},
strictEnv = false,
- stopOnEntry = false,
- noDebug = false,
- watch = false,
+ watchMode = false,
} = request;
if (!program) {
@@ -358,8 +469,8 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
processArgs.unshift("test");
}
- if (watch && !runtimeArgs.includes("--watch") && !runtimeArgs.includes("--hot")) {
- processArgs.unshift(watch === "hot" ? "--hot" : "--watch");
+ if (watchMode && !runtimeArgs.includes("--watch") && !runtimeArgs.includes("--hot")) {
+ processArgs.unshift(watchMode === "hot" ? "--hot" : "--watch");
}
const processEnv = strictEnv
@@ -374,10 +485,24 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
const url = `ws+unix://${randomUnixPath()}`;
const signal = new UnixSignal();
- const i = stopOnEntry ? "2" : "1";
- processEnv["BUN_INSPECT"] = `${i}${url}`;
+ signal.on("Signal.received", () => {
+ this.#attach({ url });
+ });
+
+ this.once("Adapter.terminated", () => {
+ signal.close();
+ });
+
+ // Break on entry is always set so the debugger has a chance
+ // to set breakpoints before the program starts. If `stopOnEntry`
+ // was not set, then the debugger will auto-continue after the first pause.
+ processEnv["BUN_INSPECT"] = `${url}?break=1`;
processEnv["BUN_INSPECT_NOTIFY"] = signal.url;
+
+ // This is probably not correct, but it's the best we can do for now.
processEnv["FORCE_COLOR"] = "1";
+ processEnv["BUN_QUIET_DEBUG_LOGS"] = "1";
+ processEnv["BUN_DEBUG_QUIET_LOGS"] = "1";
const started = await this.#spawn({
command: runtime,
@@ -390,28 +515,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
if (!started) {
throw new Error("Program could not be started.");
}
-
- await new Promise<void>(resolve => {
- signal.on("Signal.received", () => {
- resolve();
- });
- setTimeout(resolve, 5000);
- });
-
- const attached = await this.#attach(url);
-
- if (attached) {
- return;
- }
-
- const { stdout: version } = spawnSync(runtime, ["--version"], { stdio: "pipe", encoding: "utf-8" });
-
- const minVersion = "0.8.2";
- if (parse(version, true) && compare(minVersion, version, true)) {
- throw new Error(`This extension requires Bun v${minVersion} or later. Please upgrade by running: bun upgrade`);
- }
-
- throw new Error("Program started, but the debugger could not be attached.");
}
async #spawn(options: {
@@ -458,6 +561,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
this.#emit("exited", {
exitCode: code ?? -1,
});
+ this.#emit("terminated");
}
});
@@ -477,10 +581,10 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
}
async attach(request: AttachRequest): Promise<void> {
- const { url } = request;
+ this.#options = { ...request, type: "attach" };
try {
- await this.#attach(url);
+ await this.#attach(request);
} catch (error) {
// Some clients, like VSCode, will show a system-level popup when a `launch` request fails.
// Instead, we want to show the error as a sidebar notification.
@@ -493,19 +597,27 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
}
}
- async #attach(url?: string | URL): Promise<boolean> {
- for (let i = 0; i < 5; i++) {
+ async #attach(request: AttachRequest): Promise<boolean> {
+ const { url } = request;
+
+ for (let i = 0; i < 3; i++) {
const ok = await this.#inspector.start(url);
if (ok) {
return true;
}
await new Promise(resolve => setTimeout(resolve, 100 * i));
}
+
return false;
}
terminate(): void {
- this.#process?.kill();
+ if (!this.#process?.kill()) {
+ this.#evaluate({
+ expression: "process.exit(0)",
+ });
+ }
+
this.#emit("terminated");
}
@@ -521,7 +633,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
async source(request: DAP.SourceRequest): Promise<DAP.SourceResponse> {
const { source } = request;
-
const { scriptId } = await this.#getSource(sourceToId(source));
const { scriptSource } = await this.send("Debugger.getScriptSource", { scriptId });
@@ -570,14 +681,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
const { line, endLine, column, endColumn, source: source0 } = request;
const source = await this.#getSource(sourceToId(source0));
- const [start, end] = await Promise.all([
- this.#generatedLocation(source, line, column),
- this.#generatedLocation(source, endLine ?? line + 1, endColumn),
- ]);
-
const { locations } = await this.send("Debugger.getBreakpointLocations", {
- start,
- end,
+ start: this.#generatedLocation(source, line, column),
+ end: this.#generatedLocation(source, endLine ?? line + 1, endColumn),
});
return {
@@ -636,7 +742,9 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
return {
line: this.#lineFrom0BasedLine(oline),
- column: this.#columnFrom0BasedColumn(ocolumn),
+ // For now, remove the column from locations because
+ // it can be inaccurate and causes weird rendering issues in VSCode.
+ column: this.#columnFrom0BasedColumn(0), // ocolumn
};
}
@@ -655,13 +763,52 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
}
async setBreakpoints(request: DAP.SetBreakpointsRequest): Promise<DAP.SetBreakpointsResponse> {
- const { source: source0, breakpoints: requests } = request;
- const sourceId = sourceToId(source0);
- const source = await this.#getSource(sourceId);
+ const { source, breakpoints } = request;
+ if (!breakpoints?.length) {
+ return {
+ breakpoints: [],
+ };
+ }
+
+ const { path } = source;
+ const verifiedSource = this.#getSourceIfPresent(sourceToId(source));
+
+ let results: Breakpoint[];
+ if (verifiedSource) {
+ results = await this.#setBreakpointsById(verifiedSource, breakpoints);
+ } else if (path) {
+ results = await this.#setBreakpointsByUrl(path, breakpoints);
+ } else {
+ results = [];
+ }
+
+ return {
+ breakpoints: results,
+ };
+ }
+
+ async #setBreakpointsByUrl(url: string, requests: DAP.SourceBreakpoint[]): Promise<Breakpoint[]> {
+ const source = await this.#getSourceByUrl(url);
+
+ // If there is no source, this is likely a breakpoint from an unrelated
+ // file that can be ignored. Return an unverified breakpoint for each request.
+ if (!source) {
+ return requests.map(() => {
+ const breakpointId = this.#breakpointId++;
+ return this.#addBreakpoint({
+ id: breakpointId,
+ breakpointId: `${breakpointId}`,
+ verified: false,
+ });
+ });
+ }
+
+ const sourceId = sourceToId(source);
const oldBreakpoints = this.#getBreakpoints(sourceId);
+
const breakpoints = await Promise.all(
- requests!.map(async ({ line, column, ...options }) => {
+ requests.map(async ({ line, column, ...options }) => {
const location = this.#generatedLocation(source, line, column);
for (const breakpoint of oldBreakpoints) {
@@ -671,25 +818,102 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
}
}
+ let result;
try {
- const { breakpointId, actualLocation } = await this.send("Debugger.setBreakpoint", {
- location,
+ result = await this.send("Debugger.setBreakpointByUrl", {
+ url,
options: breakpointOptions(options),
+ ...location,
});
+ } catch (error) {
+ // If there was an error setting the breakpoint,
+ // mark it as unverified and add a message.
+ const { message } = unknownToError(error);
+ const breakpointId = this.#breakpointId++;
+ return this.#addBreakpoint({
+ id: breakpointId,
+ breakpointId: `${breakpointId}`,
+ line,
+ column,
+ source,
+ verified: false,
+ message,
+ generatedLocation: location,
+ });
+ }
- const originalLocation = this.#originalLocation(source, actualLocation);
+ const { breakpointId, locations } = result;
+ if (!locations.length) {
return this.#addBreakpoint({
id: this.#breakpointId++,
breakpointId,
+ line,
+ column,
source,
- verified: true,
+ verified: false,
generatedLocation: location,
- ...originalLocation,
+ });
+ }
+
+ const originalLocation = this.#originalLocation(source, locations[0]);
+ return this.#addBreakpoint({
+ id: this.#breakpointId++,
+ breakpointId,
+ source,
+ verified: true,
+ generatedLocation: location,
+ ...originalLocation,
+ });
+ }),
+ );
+
+ await Promise.all(
+ oldBreakpoints.map(async ({ breakpointId }) => {
+ const isRemoved = !breakpoints.filter(({ breakpointId: id }) => breakpointId === id).length;
+ if (isRemoved) {
+ await this.send("Debugger.removeBreakpoint", {
+ breakpointId,
+ });
+ this.#removeBreakpoint(breakpointId);
+ }
+ }),
+ );
+
+ const duplicateBreakpoints = breakpoints.filter(
+ ({ message }) => message === "Breakpoint for given location already exists",
+ );
+ for (const { breakpointId } of duplicateBreakpoints) {
+ this.#removeBreakpoint(breakpointId);
+ }
+
+ return breakpoints;
+ }
+
+ async #setBreakpointsById(source: Source, requests: DAP.SourceBreakpoint[]): Promise<Breakpoint[]> {
+ const { sourceId } = source;
+ const oldBreakpoints = this.#getBreakpoints(sourceId);
+
+ const breakpoints = await Promise.all(
+ requests!.map(async ({ line, column, ...options }) => {
+ const location = this.#generatedLocation(source, line, column);
+
+ for (const breakpoint of oldBreakpoints) {
+ const { generatedLocation } = breakpoint;
+ if (locationIsSame(generatedLocation, location)) {
+ return breakpoint;
+ }
+ }
+
+ let result;
+ try {
+ result = await this.send("Debugger.setBreakpoint", {
+ location,
+ options: breakpointOptions(options),
});
} catch (error) {
- const { message } = unknownToError(error);
// If there was an error setting the breakpoint,
// mark it as unverified and add a message.
+ const { message } = unknownToError(error);
const breakpointId = this.#breakpointId++;
return this.#addBreakpoint({
id: breakpointId,
@@ -702,6 +926,18 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
generatedLocation: location,
});
}
+
+ const { breakpointId, actualLocation } = result;
+ const originalLocation = this.#originalLocation(source, actualLocation);
+
+ return this.#addBreakpoint({
+ id: this.#breakpointId++,
+ breakpointId,
+ source,
+ verified: true,
+ generatedLocation: location,
+ ...originalLocation,
+ });
}),
);
@@ -717,17 +953,26 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
}),
);
- return {
- breakpoints,
- };
+ const duplicateBreakpoints = breakpoints.filter(
+ ({ message }) => message === "Breakpoint for given location already exists",
+ );
+ for (const { breakpointId } of duplicateBreakpoints) {
+ this.#removeBreakpoint(breakpointId);
+ }
+
+ return breakpoints;
}
- #getBreakpoints(sourceId: string | number): Breakpoint[] {
+ #getBreakpoints(sourceId?: string | number): Breakpoint[] {
const breakpoints: Breakpoint[] = [];
+ if (!sourceId) {
+ return breakpoints;
+ }
+
for (const breakpoint of this.#breakpoints.values()) {
const { source } = breakpoint;
- if (sourceId === sourceToId(source)) {
+ if (source && sourceId === sourceToId(source)) {
breakpoints.push(breakpoint);
}
}
@@ -736,28 +981,19 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
}
#addBreakpoint(breakpoint: Breakpoint): Breakpoint {
- this.#breakpoints.push(breakpoint);
-
- // For now, remove the column from breakpoints because
- // it can be inaccurate and causes weird rendering issues in VSCode.
- breakpoint.column = this.#lineFrom0BasedLine(0);
-
- this.#emit("breakpoint", {
- reason: "changed",
- breakpoint,
- });
-
+ const { breakpointId } = breakpoint;
+ this.#breakpoints.set(breakpointId, breakpoint);
return breakpoint;
}
#removeBreakpoint(breakpointId: string): void {
- const breakpoint = this.#breakpoints.find(({ breakpointId: id }) => id === breakpointId);
- if (!breakpoint) {
+ const breakpoint = this.#breakpoints.get(breakpointId);
+
+ if (!breakpoint || !this.#breakpoints.delete(breakpointId)) {
return;
}
- this.#breakpoints = this.#breakpoints.filter(({ breakpointId: id }) => id !== breakpointId);
- this.#emit("breakpoint", {
+ this.#emitAfterResponse("breakpoint", {
reason: "removed",
breakpoint,
});
@@ -831,12 +1067,6 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
#addFunctionBreakpoint(breakpoint: FunctionBreakpoint): FunctionBreakpoint {
const { name } = breakpoint;
this.#functionBreakpoints.set(name, breakpoint);
-
- this.#emit("breakpoint", {
- reason: "changed",
- breakpoint,
- });
-
return breakpoint;
}
@@ -847,22 +1077,86 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
return;
}
- this.#emit("breakpoint", {
+ this.#emitAfterResponse("breakpoint", {
reason: "removed",
breakpoint,
});
}
- async setExceptionBreakpoints(request: DAP.SetExceptionBreakpointsRequest): Promise<void> {
- const { filters, filterOptions } = request;
+ async setExceptionBreakpoints(
+ request: DAP.SetExceptionBreakpointsRequest,
+ ): Promise<DAP.SetExceptionBreakpointsResponse> {
+ const { filters } = request;
- const filterIds = [...filters];
- if (filterOptions) {
- filterIds.push(...filterOptions.map(({ filterId }) => filterId));
+ let state: "all" | "uncaught" | "none";
+ if (filters.includes("all")) {
+ state = "all";
+ } else if (filters.includes("uncaught")) {
+ state = "uncaught";
+ } else {
+ state = "none";
}
- await this.send("Debugger.setPauseOnExceptions", {
- state: exceptionFiltersToPauseOnExceptionsState(filterIds),
+ await Promise.all([
+ this.send("Debugger.setPauseOnExceptions", { state }),
+ this.send("Debugger.setPauseOnAssertions", {
+ enabled: filters.includes("assert"),
+ }),
+ this.send("Debugger.setPauseOnDebuggerStatements", {
+ enabled: filters.includes("debugger"),
+ }),
+ this.send("Debugger.setPauseOnMicrotasks", {
+ enabled: filters.includes("microtask"),
+ }),
+ ]);
+
+ return {
+ breakpoints: [],
+ };
+ }
+
+ async gotoTargets(request: DAP.GotoTargetsRequest): Promise<DAP.GotoTargetsResponse> {
+ const { source: source0 } = request;
+ const source = await this.#getSource(sourceToId(source0));
+
+ const { breakpoints } = await this.breakpointLocations(request);
+ const targets = breakpoints.map(({ line, column }) =>
+ this.#addTarget({
+ id: this.#targets.size,
+ label: `${line}:${column}`,
+ source,
+ line,
+ column,
+ }),
+ );
+
+ return {
+ targets,
+ };
+ }
+
+ #addTarget<T extends DAP.GotoTarget | DAP.StepInTarget>(target: T & { source: Source }): T {
+ const { id } = target;
+ this.#targets.set(id, target);
+ return target;
+ }
+
+ #getTarget(targetId: number): Target | undefined {
+ return this.#targets.get(targetId);
+ }
+
+ async goto(request: DAP.GotoRequest): Promise<void> {
+ const { targetId } = request;
+ const target = this.#getTarget(targetId);
+ if (!target) {
+ throw new Error("No target found.");
+ }
+
+ const { source, line, column } = target;
+ const location = this.#generatedLocation(source, line, column);
+
+ await this.send("Debugger.continueToLocation", {
+ location,
});
}
@@ -871,14 +1165,18 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
const callFrameId = this.#getCallFrameId(frameId);
const objectGroup = callFrameId ? "debugger" : context;
- const { result, wasThrown } = await this.#evaluate(expression, objectGroup, callFrameId);
- const { className } = result;
+ const { result, wasThrown } = await this.#evaluate({
+ expression,
+ objectGroup,
+ callFrameId,
+ });
- if (context === "hover" && wasThrown && (className === "SyntaxError" || className === "ReferenceError")) {
- return {
- result: "",
- variablesReference: 0,
- };
+ if (wasThrown) {
+ if (context === "hover" && isSyntaxError(result)) {
+ throw Cancel;
+ }
+
+ throw new Error(remoteObjectToString(result));
}
const { name, value, ...variable } = this.#addObject(result, { objectGroup });
@@ -888,11 +1186,12 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
};
}
- async #evaluate(
- expression: string,
- objectGroup?: string,
- callFrameId?: string,
- ): Promise<JSC.Runtime.EvaluateResponse> {
+ async #evaluate(options: {
+ expression: string;
+ objectGroup?: string;
+ callFrameId?: string;
+ }): Promise<JSC.Runtime.EvaluateResponse> {
+ const { expression, objectGroup, callFrameId } = options;
const method = callFrameId ? "Debugger.evaluateOnCallFrame" : "Runtime.evaluate";
return this.send(method, {
@@ -906,14 +1205,40 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
});
}
- restart(): void {
- this.initialize(this.#initialized!);
- this.configurationDone();
+ async completions(request: DAP.CompletionsRequest): Promise<DAP.CompletionsResponse> {
+ const { text, column, frameId } = request;
+ const callFrameId = this.#getCallFrameId(frameId);
- this.#emit("output", {
- category: "debug console",
- output: "Debugger reloaded.\n",
+ const { expression, hint } = completionToExpression(text);
+ const { result, wasThrown } = await this.#evaluate({
+ expression: expression || "this",
+ callFrameId,
+ objectGroup: "repl",
+ });
+
+ if (wasThrown) {
+ if (isSyntaxError(result)) {
+ return {
+ targets: [],
+ };
+ }
+ throw new Error(remoteObjectToString(result));
+ }
+
+ const variable = this.#addObject(result, {
+ objectGroup: "repl",
+ evaluateName: expression,
});
+
+ const properties = await this.#getProperties(variable);
+ const targets = properties
+ .filter(({ name }) => isIdentifier(name) && (!hint || name.includes(hint)))
+ .sort(variablesSortBy)
+ .map(variableToCompletionItem);
+
+ return {
+ targets,
+ };
}
["Inspector.connected"](): void {
@@ -939,8 +1264,11 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
});
}
- this.#emit("terminated");
this.#reset();
+
+ if (this.#process?.exitCode !== null) {
+ this.#emit("terminated");
+ }
}
async ["Debugger.scriptParsed"](event: JSC.Debugger.ScriptParsedEvent): Promise<void> {
@@ -986,7 +1314,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
["Debugger.scriptFailedToParse"](event: JSC.Debugger.ScriptFailedToParseEvent): void {
const { url, errorMessage, errorLine } = event;
- // If no url is present, the script is from a `evaluate` request.
+ // If no url is present, the script is from an `evaluate` request.
if (!url) {
return;
}
@@ -1004,14 +1332,24 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
["Debugger.paused"](event: JSC.Debugger.PausedEvent): void {
const { reason, callFrames, asyncStackTrace, data } = event;
- // if (reason === "PauseOnNextStatement") {
- // for (const { functionName } of callFrames) {
- // if (functionName === "module code") {
- // this.send("Debugger.resume");
- // return;
- // }
- // }
- // }
+ if (reason === "PauseOnNextStatement") {
+ if (this.#stopped === "start" && !this.#options?.stopOnEntry) {
+ this.#stopped = undefined;
+ return;
+ }
+ }
+
+ if (reason === "DebuggerStatement") {
+ // FIXME: This is a hacky fix for the `Debugger.paused` event being fired
+ // when the debugger is started in hot mode.
+ for (const { functionName } of callFrames) {
+ // @ts-ignore
+ if (functionName === "module code" && this.#options?.watchMode === "hot") {
+ this.send("Debugger.resume");
+ return;
+ }
+ }
+ }
this.#stackFrames.length = 0;
this.#stopped ||= stoppedReason(reason);
@@ -1030,6 +1368,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
if (data) {
if (reason === "exception") {
const remoteObject = data as JSC.Runtime.RemoteObject;
+ this.#exception = this.#addObject(remoteObject, { objectGroup: "debugger" });
}
if (reason === "FunctionCall") {
@@ -1059,16 +1398,16 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
});
}
- ["Debugger.resumed"](event: JSC.Debugger.ResumedEvent): void {
+ ["Debugger.resumed"](): void {
this.#stackFrames.length = 0;
this.#stopped = undefined;
- console.log("VARIABLES BEFORE", this.#variables.size);
+ this.#exception = undefined;
for (const { variablesReference, objectGroup } of this.#variables.values()) {
if (objectGroup === "debugger") {
this.#variables.delete(variablesReference);
}
}
- console.log("VARIABLES AFTER", this.#variables.size);
+
this.#emit("continued", {
threadId: this.#threadId,
});
@@ -1240,6 +1579,62 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
});
}
+ async #getSourceByUrl(url: string): Promise<Source | undefined> {
+ const source = this.#getSourceIfPresent(url);
+ if (source) {
+ return source;
+ }
+
+ // If the source is not present, the debugger did not send the `Debugger.scriptParsed` event.
+ // Since there is no request to retrieve a script by its url, the hacky solution is to set
+ // a no-op breakpoint by url, then immediately remove it.
+ let result;
+ try {
+ result = await this.send("Debugger.setBreakpointByUrl", {
+ url,
+ lineNumber: 0,
+ options: {
+ autoContinue: true,
+ },
+ });
+ } catch {
+ // If there was an error setting the breakpoint,
+ // the source probably does not exist.
+ return undefined;
+ }
+
+ const { breakpointId, locations } = result;
+ await this.send("Debugger.removeBreakpoint", {
+ breakpointId,
+ });
+
+ if (!locations.length) {
+ return undefined;
+ }
+
+ // It is possible that the source was loaded between the time it took
+ // to set and remove the breakpoint, so check again.
+ const [{ scriptId }] = locations;
+ const recentSource = this.#getSourceIfPresent(scriptId) || this.#getSourceIfPresent(url);
+ if (recentSource) {
+ return recentSource;
+ }
+
+ // Otherwise, retrieve the source and source map url and add the source.
+ const { scriptSource } = await this.send("Debugger.getScriptSource", {
+ scriptId,
+ });
+
+ return this.#addSource({
+ scriptId,
+ sourceId: url,
+ name: sourceName(url),
+ path: url,
+ sourceMap: SourceMap(scriptSource),
+ presentationHint: sourcePresentationHint(url),
+ });
+ }
+
async stackTrace(request: DAP.StackTraceRequest): Promise<DAP.StackTraceResponse> {
const { length } = this.#stackFrames;
const { startFrame = 0, levels } = request;
@@ -1279,7 +1674,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
}
#addStackFrame(callFrame: JSC.Debugger.CallFrame): StackFrame {
- const { callFrameId, functionName, location, scopeChain } = callFrame;
+ const { callFrameId, functionName, location, scopeChain, this: thisObject } = callFrame;
const { scriptId } = location;
const source = this.#getSourceIfPresent(scriptId);
@@ -1398,7 +1793,7 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
async variables(request: DAP.VariablesRequest): Promise<DAP.VariablesResponse> {
const { variablesReference, start, count } = request;
- const variable = this.#variables.get(variablesReference);
+ const variable = this.#getVariable(variablesReference);
let variables: Variable[];
if (!variable) {
@@ -1414,68 +1809,88 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
};
}
+ async setVariable(request: DAP.SetVariableRequest): Promise<DAP.SetVariableResponse> {
+ const { variablesReference, name, value } = request;
+
+ const variable = this.#getVariable(variablesReference);
+ if (!variable) {
+ throw new Error("Variable not found.");
+ }
+
+ const { objectId, objectGroup } = variable;
+ if (!objectId) {
+ throw new Error("Variable cannot be modified.");
+ }
+
+ const { result, wasThrown } = await this.send("Runtime.callFunctionOn", {
+ objectId,
+ functionDeclaration: `function (name) { this[name] = ${value}; return this[name]; }`,
+ arguments: [{ value: name }],
+ doNotPauseOnExceptionsAndMuteConsole: true,
+ });
+
+ if (wasThrown) {
+ throw new Error(remoteObjectToString(result));
+ }
+
+ return this.#addObject(result, { name, objectGroup });
+ }
+
+ async setExpression(request: DAP.SetExpressionRequest): Promise<DAP.SetExpressionResponse> {
+ const { expression, value, frameId } = request;
+ const callFrameId = this.#getCallFrameId(frameId);
+ const objectGroup = callFrameId ? "debugger" : "repl";
+
+ const { result, wasThrown } = await this.#evaluate({
+ expression: `${expression} = (${value});`,
+ objectGroup: "repl",
+ callFrameId,
+ });
+
+ if (wasThrown) {
+ throw new Error(remoteObjectToString(result));
+ }
+
+ return this.#addObject(result, { objectGroup });
+ }
+
+ #getVariable(variablesReference?: number): Variable | undefined {
+ if (!variablesReference) {
+ return undefined;
+ }
+ return this.#variables.get(variablesReference);
+ }
+
#addObject(
remoteObject: JSC.Runtime.RemoteObject,
- propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor> & { objectGroup?: string },
+ propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor> & { objectGroup?: string; evaluateName?: string },
): Variable {
const { objectId, type, subtype, size } = remoteObject;
+ const { objectGroup, evaluateName } = propertyDescriptor ?? {};
const variablesReference = objectId ? this.#variableId++ : 0;
const variable: Variable = {
objectId,
- objectGroup: propertyDescriptor?.objectGroup,
- name: propertyDescriptorToName(propertyDescriptor),
+ objectGroup,
+ variablesReference,
type: subtype || type,
value: remoteObjectToString(remoteObject),
- variablesReference,
- indexedVariables: isIndexed(subtype) ? size : undefined,
- namedVariables: isNamedIndexed(subtype) ? size : undefined,
+ name: propertyDescriptorToName(propertyDescriptor),
+ evaluateName: propertyDescriptorToEvaluateName(propertyDescriptor, evaluateName),
+ indexedVariables: isArrayLike(subtype) ? size : undefined,
+ namedVariables: isMap(subtype) ? size : undefined,
presentationHint: remoteObjectToVariablePresentationHint(remoteObject, propertyDescriptor),
};
if (variablesReference) {
this.#variables.set(variablesReference, variable);
- console.log("addObject", variablesReference, variable);
}
return variable;
}
- #addProperty(
- propertyDescriptor:
- | JSC.Runtime.PropertyDescriptor
- | (JSC.Runtime.InternalPropertyDescriptor & { objectGroup?: string }),
- ): Variable[] {
- const { value, get, set, symbol } = propertyDescriptor as JSC.Runtime.PropertyDescriptor;
- const variables: Variable[] = [];
-
- if (value) {
- variables.push(this.#addObject(value, propertyDescriptor));
- }
-
- if (get) {
- const { type } = get;
- if (type !== "undefined") {
- variables.push(this.#addObject(get, propertyDescriptor));
- }
- }
-
- if (set) {
- const { type } = set;
- if (type !== "undefined") {
- variables.push(this.#addObject(set, propertyDescriptor));
- }
- }
-
- if (symbol) {
- variables.push(this.#addObject(symbol, propertyDescriptor));
- }
-
- return variables;
- }
-
async #getProperties(variable: Variable, offset?: number, count?: number): Promise<Variable[]> {
- const { objectId, objectGroup, type, indexedVariables, namedVariables } = variable;
+ const { objectId, objectGroup, type, evaluateName, indexedVariables, namedVariables } = variable;
const variables: Variable[] = [];
if (!objectId || type === "symbol") {
@@ -1488,12 +1903,14 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
});
for (const property of properties) {
- variables.push(...this.#addProperty({ ...property, objectGroup }));
+ variables.push(...this.#addProperty(property, { objectGroup, evaluateName, parentType: type }));
}
if (internalProperties) {
for (const property of internalProperties) {
- variables.push(...this.#addProperty({ ...property, objectGroup, configurable: false }));
+ variables.push(
+ ...this.#addProperty(property, { objectGroup, evaluateName, parentType: type, isSynthetic: true }),
+ );
}
}
@@ -1512,29 +1929,135 @@ export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements
const { value, description } = key;
name = String(value ?? description);
}
- variables.push(this.#addObject(value, { name, objectGroup }));
+ variables.push(
+ ...this.#addProperty(
+ { name, value },
+ {
+ objectGroup,
+ evaluateName,
+ parentType: type,
+ isSynthetic: true,
+ },
+ ),
+ );
}
}
return variables;
}
+ #addProperty(
+ propertyDescriptor: JSC.Runtime.PropertyDescriptor | JSC.Runtime.InternalPropertyDescriptor,
+ options?: {
+ objectGroup?: string;
+ evaluateName?: string;
+ isSynthetic?: boolean;
+ parentType?: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"];
+ },
+ ): Variable[] {
+ const { value, get, set, symbol } = propertyDescriptor as JSC.Runtime.PropertyDescriptor;
+ const descriptor = { ...propertyDescriptor, ...options };
+ const variables: Variable[] = [];
+
+ if (value) {
+ variables.push(this.#addObject(value, descriptor));
+ }
+
+ if (get) {
+ const { type } = get;
+ if (type !== "undefined") {
+ variables.push(this.#addObject(get, descriptor));
+ }
+ }
+
+ if (set) {
+ const { type } = set;
+ if (type !== "undefined") {
+ variables.push(this.#addObject(set, descriptor));
+ }
+ }
+
+ if (symbol) {
+ variables.push(this.#addObject(symbol, descriptor));
+ }
+
+ return variables;
+ }
+
+ async exceptionInfo(): Promise<DAP.ExceptionInfoResponse> {
+ const exception = this.#exception;
+ if (!exception) {
+ throw new Error("No exception found.");
+ }
+
+ const { code, ...details } = await this.#getExceptionDetails(exception);
+ return {
+ exceptionId: code || "",
+ breakMode: "always",
+ details,
+ };
+ }
+
+ async #getExceptionDetails(variable: Variable): Promise<DAP.ExceptionDetails & { code?: string }> {
+ const properties = await this.#getProperties(variable);
+
+ let fullTypeName: string | undefined;
+ let message: string | undefined;
+ let code: string | undefined;
+ let stackTrace: string | undefined;
+ let innerException: DAP.ExceptionDetails[] | undefined;
+
+ for (const property of properties) {
+ const { name, value, type } = property;
+ if (name === "name") {
+ fullTypeName = value;
+ } else if (name === "message") {
+ message = type === "string" ? JSON.parse(value) : value;
+ } else if (name === "stack") {
+ stackTrace = type === "string" ? JSON.parse(value) : value;
+ } else if (name === "code") {
+ code = type === "string" ? JSON.parse(value) : value;
+ } else if (name === "cause") {
+ const cause = await this.#getExceptionDetails(property);
+ innerException = [cause];
+ } else if (name === "errors") {
+ const errors = await this.#getProperties(property);
+ innerException = await Promise.all(errors.map(error => this.#getExceptionDetails(error)));
+ }
+ }
+
+ if (!stackTrace) {
+ const { value } = variable;
+ stackTrace ||= value;
+ }
+
+ return {
+ fullTypeName,
+ message,
+ code,
+ stackTrace: stripAnsi(stackTrace),
+ innerException,
+ };
+ }
+
close(): void {
this.#process?.kill();
+ // this.#signal?.close();
this.#inspector.close();
+ this.#reset();
}
#reset(): void {
this.#pendingSources.clear();
this.#sources.clear();
this.#stackFrames.length = 0;
- this.#stopped = undefined;
- this.#breakpointId = 1;
- this.#breakpoints.length = 0;
+ this.#stopped = "start";
+ this.#exception = undefined;
+ this.#breakpoints.clear();
this.#functionBreakpoints.clear();
+ this.#targets.clear();
this.#variables.clear();
- this.#launched = undefined;
- this.#initialized = undefined;
+ this.#options = undefined;
}
}
@@ -1602,31 +2125,86 @@ function scopePresentationHint(type: JSC.Debugger.Scope["type"]): DAP.Scope["pre
}
}
-function isIndexed(subtype: JSC.Runtime.RemoteObject["subtype"]): boolean {
- return subtype === "array" || subtype === "set" || subtype === "weakset";
+function isSet(subtype: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]): boolean {
+ return subtype === "set" || subtype === "weakset";
}
-function isNamedIndexed(subtype: JSC.Runtime.RemoteObject["subtype"]): boolean {
+function isArrayLike(subtype: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]): boolean {
+ return subtype === "array" || isSet(subtype);
+}
+
+function isMap(subtype: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]): boolean {
return subtype === "map" || subtype === "weakmap";
}
-function exceptionFiltersToPauseOnExceptionsState(
- filters?: string[],
-): JSC.Debugger.SetPauseOnExceptionsRequest["state"] {
- if (filters?.includes("all")) {
- return "all";
+function breakpointOptions(breakpoint: Partial<DAP.SourceBreakpoint>): JSC.Debugger.BreakpointOptions {
+ const { condition, hitCondition, logMessage } = breakpoint;
+ return {
+ condition,
+ ignoreCount: hitConditionToIgnoreCount(hitCondition),
+ autoContinue: !!logMessage,
+ actions: [
+ {
+ type: "evaluate",
+ data: logMessageToExpression(logMessage),
+ emulateUserGesture: true,
+ },
+ ],
+ };
+}
+
+function hitConditionToIgnoreCount(hitCondition?: string): number | undefined {
+ if (!hitCondition) {
+ return undefined;
+ }
+
+ if (hitCondition.includes("<")) {
+ throw new Error("Hit condition with '<' is not supported, use '>' or '>=' instead.");
}
- if (filters?.includes("uncaught")) {
- return "uncaught";
+
+ const count = parseInt(hitCondition.replace(/[^\d+]/g, ""));
+ if (isNaN(count)) {
+ throw new Error("Hit condition is not a number.");
}
- return "none";
+
+ if (hitCondition.includes(">") && !hitCondition.includes("=")) {
+ return Math.max(0, count);
+ }
+ return Math.max(0, count - 1);
}
-function breakpointOptions(breakpoint?: Partial<DAP.SourceBreakpoint>): JSC.Debugger.BreakpointOptions {
- const { condition } = breakpoint ?? {};
- // TODO: hitCondition, logMessage
+function logMessageToExpression(logMessage?: string): string | undefined {
+ if (!logMessage) {
+ return undefined;
+ }
+ // Convert expressions from "hello {name}!" to "`hello ${name}!`"
+ return `console.log(\`${logMessage.replace(/\$?{/g, "${")}\`);`;
+}
+
+function completionToExpression(completion: string): { expression: string; hint?: string } {
+ const lastDot = completion.lastIndexOf(".");
+ const last = (s0: string, s1: string) => {
+ const i0 = completion.lastIndexOf(s0);
+ const i1 = completion.lastIndexOf(s1);
+ return i1 > i0 ? i1 + 1 : i0;
+ };
+
+ const lastIdentifier = Math.max(lastDot, last("[", "]"), last("(", ")"), last("{", "}"));
+
+ let expression: string;
+ let remainder: string;
+ if (lastIdentifier > 0) {
+ expression = completion.slice(0, lastIdentifier);
+ remainder = completion.slice(lastIdentifier);
+ } else {
+ expression = "";
+ remainder = completion;
+ }
+
+ const [hint] = completion.slice(lastIdentifier).match(/[#$a-z_][0-9a-z_$]*/gi) ?? [];
return {
- condition,
+ expression,
+ hint,
};
}
@@ -1674,10 +2252,13 @@ function sanitizeExpression(expression: string): string {
function remoteObjectToVariablePresentationHint(
remoteObject: JSC.Runtime.RemoteObject,
- propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor>,
+ propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor> & {
+ isSynthetic?: boolean;
+ parentType?: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"];
+ },
): DAP.VariablePresentationHint {
const { type, subtype } = remoteObject;
- const { name, configurable, writable, isPrivate, symbol, get, set, wasThrown } = propertyDescriptor ?? {};
+ const { name, enumerable, writable, isPrivate, isSynthetic, symbol, get, set, wasThrown } = propertyDescriptor ?? {};
const hasGetter = get?.type === "function";
const hasSetter = set?.type === "function";
const hasSymbol = symbol?.type === "symbol";
@@ -1693,13 +2274,16 @@ function remoteObjectToVariablePresentationHint(
if (subtype === "class") {
kind = "class";
}
- if (isPrivate || configurable === false || hasSymbol || name === "__proto__") {
+ if (isSynthetic || isPrivate || hasSymbol) {
+ visibility = "protected";
+ }
+ if (enumerable === false || name === "__proto__") {
visibility = "internal";
}
if (type === "string") {
attributes.push("rawString");
}
- if (writable === false || (hasGetter && !hasSetter)) {
+ if (isSynthetic || writable === false || (hasGetter && !hasSetter)) {
attributes.push("readOnly");
}
if (wasThrown || hasGetter) {
@@ -1726,6 +2310,51 @@ function propertyDescriptorToName(propertyDescriptor?: Partial<JSC.Runtime.Prope
return name ?? "";
}
+function propertyDescriptorToEvaluateName(
+ propertyDescriptor?: Partial<JSC.Runtime.PropertyDescriptor> & {
+ isSynthetic?: boolean;
+ parentType?: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"];
+ },
+ evaluateName?: string,
+): string | undefined {
+ if (!propertyDescriptor) {
+ return evaluateName;
+ }
+ const { name: property, isSynthetic, parentType: type } = propertyDescriptor;
+ if (!property) {
+ return evaluateName;
+ }
+ if (!evaluateName) {
+ return property;
+ }
+ if (isSynthetic) {
+ if (isMap(type)) {
+ if (isNumeric(property)) {
+ return `${evaluateName}.get(${property})`;
+ }
+ return `${evaluateName}.get(${JSON.stringify(property)})`;
+ }
+ if (isSet(type)) {
+ return `[...${evaluateName}.values()][${property}]`;
+ }
+ }
+ if (isNumeric(property)) {
+ return `${evaluateName}[${property}]`;
+ }
+ if (isIdentifier(property)) {
+ return `${evaluateName}.${property}`;
+ }
+ return `${evaluateName}[${JSON.stringify(property)}]`;
+}
+
+function isNumeric(string: string): boolean {
+ return /^\d+$/.test(string);
+}
+
+function isIdentifier(string: string): boolean {
+ return /^[#$a-z_][0-9a-z_$]*$/i.test(string);
+}
+
function unknownToError(input: unknown): Error {
if (input instanceof Error) {
return input;
@@ -1741,6 +2370,36 @@ function isTestJavaScript(path: string): boolean {
return /\.(test|spec)\.(c|m)?(j|t)sx?$/.test(path);
}
+function isSyntaxError(remoteObject: JSC.Runtime.RemoteObject): boolean {
+ const { className } = remoteObject;
+
+ switch (className) {
+ case "SyntaxError":
+ case "ReferenceError":
+ return true;
+ }
+
+ return false;
+}
+
+function variableToCompletionItem(variable: Variable): DAP.CompletionItem {
+ const { name, type } = variable;
+ return {
+ label: name,
+ type: variableTypeToCompletionItemType(type),
+ };
+}
+
+function variableTypeToCompletionItemType(type: Variable["type"]): DAP.CompletionItem["type"] {
+ switch (type) {
+ case "class":
+ return "class";
+ case "function":
+ return "function";
+ }
+ return "property";
+}
+
function variablesSortBy(a: DAP.Variable, b: DAP.Variable): number {
const visibility = (variable: DAP.Variable): number => {
const { presentationHint } = variable;
@@ -1766,6 +2425,13 @@ function variablesSortBy(a: DAP.Variable, b: DAP.Variable): number {
if (isFinite(index)) {
return index;
}
+ switch (name[0]) {
+ case "_":
+ case "$":
+ return 1;
+ case "#":
+ return 2;
+ }
return 0;
};
const av = visibility(a);
@@ -1797,6 +2463,12 @@ function numberIsValid(number?: number): number is number {
return typeof number === "number" && isFinite(number) && number >= 0;
}
-function locationIsSame(a: JSC.Debugger.Location, b: JSC.Debugger.Location): boolean {
- return a.scriptId === b.scriptId && a.lineNumber === b.lineNumber && a.columnNumber === b.columnNumber;
+function locationIsSame(a?: JSC.Debugger.Location, b?: JSC.Debugger.Location): boolean {
+ return a?.scriptId === b?.scriptId && a?.lineNumber === b?.lineNumber && a?.columnNumber === b?.columnNumber;
}
+
+function stripAnsi(string: string): string {
+ return string.replace(/\u001b\[\d+m/g, "");
+}
+
+const Cancel = Symbol("Cancel");
diff --git a/packages/bun-debug-adapter-protocol/src/debugger/capabilities.ts b/packages/bun-debug-adapter-protocol/src/debugger/capabilities.ts
deleted file mode 100644
index 7de639712..000000000
--- a/packages/bun-debug-adapter-protocol/src/debugger/capabilities.ts
+++ /dev/null
@@ -1,271 +0,0 @@
-import type { DAP } from "../protocol";
-
-const capabilities: DAP.Capabilities = {
- /**
- * The debug adapter supports the `configurationDone` request.
- * @see configurationDone
- */
- supportsConfigurationDoneRequest: true,
-
- /**
- * The debug adapter supports function breakpoints using the `setFunctionBreakpoints` request.
- * @see setFunctionBreakpoints
- */
- supportsFunctionBreakpoints: true,
-
- /**
- * The debug adapter supports conditional breakpoints.
- * @see setBreakpoints
- * @see setInstructionBreakpoints
- * @see setFunctionBreakpoints
- * @see setExceptionBreakpoints
- * @see setDataBreakpoints
- */
- supportsConditionalBreakpoints: true,
-
- /**
- * The debug adapter supports breakpoints that break execution after a specified number of hits.
- * @see setBreakpoints
- * @see setInstructionBreakpoints
- * @see setFunctionBreakpoints
- * @see setExceptionBreakpoints
- * @see setDataBreakpoints
- */
- supportsHitConditionalBreakpoints: true,
-
- /**
- * The debug adapter supports a (side effect free) `evaluate` request for data hovers.
- * @see evaluate
- */
- supportsEvaluateForHovers: true,
-
- /**
- * Available exception filter options for the `setExceptionBreakpoints` request.
- * @see setExceptionBreakpoints
- */
- exceptionBreakpointFilters: [
- {
- filter: "all",
- label: "Caught Exceptions",
- default: false,
- supportsCondition: true,
- description: "Breaks on all throw errors, even if they're caught later.",
- conditionDescription: `error.name == "CustomError"`,
- },
- {
- filter: "uncaught",
- label: "Uncaught Exceptions",
- default: false,
- supportsCondition: true,
- description: "Breaks only on errors or promise rejections that are not handled.",
- conditionDescription: `error.name == "CustomError"`,
- },
- ],
-
- /**
- * The debug adapter supports stepping back via the `stepBack` and `reverseContinue` requests.
- * @see stepBack
- * @see reverseContinue
- */
- supportsStepBack: false,
-
- /**
- * The debug adapter supports setting a variable to a value.
- * @see setVariable
- */
- supportsSetVariable: false,
-
- /**
- * The debug adapter supports restarting a frame.
- * @see restartFrame
- */
- supportsRestartFrame: false,
-
- /**
- * The debug adapter supports the `gotoTargets` request.
- * @see gotoTargets
- */
- supportsGotoTargetsRequest: false,
-
- /**
- * The debug adapter supports the `stepInTargets` request.
- * @see stepInTargets
- */
- supportsStepInTargetsRequest: false,
-
- /**
- * The debug adapter supports the `completions` request.
- * @see completions
- */
- supportsCompletionsRequest: false,
-
- /**
- * The set of characters that should trigger completion in a REPL.
- * If not specified, the UI should assume the `.` character.
- * @see completions
- */
- completionTriggerCharacters: [".", "[", '"', "'"],
-
- /**
- * The debug adapter supports the `modules` request.
- * @see modules
- */
- supportsModulesRequest: false,
-
- /**
- * The set of additional module information exposed by the debug adapter.
- * @see modules
- */
- additionalModuleColumns: [],
-
- /**
- * Checksum algorithms supported by the debug adapter.
- */
- supportedChecksumAlgorithms: [],
-
- /**
- * The debug adapter supports the `restart` request.
- * In this case a client should not implement `restart` by terminating
- * and relaunching the adapter but by calling the `restart` request.
- * @see restart
- */
- supportsRestartRequest: false,
-
- /**
- * The debug adapter supports `exceptionOptions` on the `setExceptionBreakpoints` request.
- * @see setExceptionBreakpoints
- */
- supportsExceptionOptions: false,
-
- /**
- * The debug adapter supports a `format` attribute on the `stackTrace`, `variables`, and `evaluate` requests.
- * @see stackTrace
- * @see variables
- * @see evaluate
- */
- supportsValueFormattingOptions: false,
-
- /**
- * The debug adapter supports the `exceptionInfo` request.
- * @see exceptionInfo
- */
- supportsExceptionInfoRequest: true,
-
- /**
- * The debug adapter supports the `terminateDebuggee` attribute on the `disconnect` request.
- * @see disconnect
- */
- supportTerminateDebuggee: true,
-
- /**
- * The debug adapter supports the `suspendDebuggee` attribute on the `disconnect` request.
- * @see disconnect
- */
- supportSuspendDebuggee: false,
-
- /**
- * The debug adapter supports the delayed loading of parts of the stack,
- * which requires that both the `startFrame` and `levels` arguments and
- * the `totalFrames` result of the `stackTrace` request are supported.
- * @see stackTrace
- */
- supportsDelayedStackTraceLoading: true,
-
- /**
- * The debug adapter supports the `loadedSources` request.
- * @see loadedSources
- */
- supportsLoadedSourcesRequest: true,
-
- /**
- * The debug adapter supports log points by interpreting the `logMessage` attribute of the `SourceBreakpoint`.
- * @see setBreakpoints
- */
- supportsLogPoints: true,
-
- /**
- * The debug adapter supports the `terminateThreads` request.
- * @see terminateThreads
- */
- supportsTerminateThreadsRequest: false,
-
- /**
- * The debug adapter supports the `setExpression` request.
- * @see setExpression
- */
- supportsSetExpression: false,
-
- /**
- * The debug adapter supports the `terminate` request.
- * @see terminate
- */
- supportsTerminateRequest: true,
-
- /**
- * The debug adapter supports data breakpoints.
- * @see setDataBreakpoints
- */
- supportsDataBreakpoints: false,
-
- /**
- * The debug adapter supports the `readMemory` request.
- * @see readMemory
- */
- supportsReadMemoryRequest: false,
-
- /**
- * The debug adapter supports the `writeMemory` request.
- * @see writeMemory
- */
- supportsWriteMemoryRequest: false,
-
- /**
- * The debug adapter supports the `disassemble` request.
- * @see disassemble
- */
- supportsDisassembleRequest: false,
-
- /**
- * The debug adapter supports the `cancel` request.
- * @see cancel
- */
- supportsCancelRequest: false,
-
- /**
- * The debug adapter supports the `breakpointLocations` request.
- * @see breakpointLocations
- */
- supportsBreakpointLocationsRequest: true,
-
- /**
- * The debug adapter supports the `clipboard` context value in the `evaluate` request.
- * @see evaluate
- */
- supportsClipboardContext: false,
-
- /**
- * The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests.
- * @see stepIn
- */
- supportsSteppingGranularity: false,
-
- /**
- * The debug adapter supports adding breakpoints based on instruction references.
- * @see setInstructionBreakpoints
- */
- supportsInstructionBreakpoints: false,
-
- /**
- * The debug adapter supports `filterOptions` as an argument on the `setExceptionBreakpoints` request.
- * @see setExceptionBreakpoints
- */
- supportsExceptionFilterOptions: true,
-
- /**
- * The debug adapter supports the `singleThread` property on the execution requests
- * (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`).
- */
- supportsSingleThreadExecutionRequests: false,
-};
-
-export default capabilities;
diff --git a/packages/bun-debug-adapter-protocol/src/debugger/signal.ts b/packages/bun-debug-adapter-protocol/src/debugger/signal.ts
index 3c635fb4a..d7c52d448 100644
--- a/packages/bun-debug-adapter-protocol/src/debugger/signal.ts
+++ b/packages/bun-debug-adapter-protocol/src/debugger/signal.ts
@@ -21,7 +21,7 @@ export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
#server: Server;
#ready: Promise<void>;
- constructor(path?: string) {
+ constructor(path?: string | URL) {
super();
this.#path = path ? parseUnixPath(path) : randomUnixPath();
this.#server = createServer();
@@ -74,8 +74,8 @@ export function randomUnixPath(): string {
return join(tmpdir(), `${Math.random().toString(36).slice(2)}.sock`);
}
-function parseUnixPath(path: string): string {
- if (path.startsWith("/")) {
+function parseUnixPath(path: string | URL): string {
+ if (typeof path === "string" && path.startsWith("/")) {
return path;
}
try {
diff --git a/packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts b/packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts
index 3b3f05c6b..cae0eb526 100644
--- a/packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts
+++ b/packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts
@@ -80,7 +80,7 @@ class ActualSourceMap implements SourceMap {
const { line: gline, column: gcolumn } = lineRange;
return {
- line: lineToLine(gline),
+ line: lineTo0BasedLine(gline),
column: columnToColumn(gcolumn),
verified: true,
};
@@ -144,9 +144,17 @@ class NoopSourceMap implements SourceMap {
const defaultSourceMap = new NoopSourceMap();
export function SourceMap(url?: string): SourceMap {
- if (!url || !url.startsWith("data:")) {
+ if (!url) {
return defaultSourceMap;
}
+ if (!url.startsWith("data:")) {
+ const match = url.match(/\/\/[#@]\s*sourceMappingURL=(.*)$/m);
+ if (!match) {
+ return defaultSourceMap;
+ }
+ const [_, sourceMapUrl] = match;
+ url = sourceMapUrl;
+ }
try {
const [_, base64] = url.split(",", 2);
const decoded = Buffer.from(base64, "base64url").toString("utf8");
diff --git a/packages/bun-inspector-protocol/src/inspector/websocket.ts b/packages/bun-inspector-protocol/src/inspector/websocket.ts
index 0c203a11b..f88217bd4 100644
--- a/packages/bun-inspector-protocol/src/inspector/websocket.ts
+++ b/packages/bun-inspector-protocol/src/inspector/websocket.ts
@@ -52,10 +52,10 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
// @ts-expect-error: Support both Bun and Node.js version of `headers`.
webSocket = new WebSocket(url, {
headers: {
- "Ref-Event-Loop": "1",
+ "Ref-Event-Loop": "0",
},
finishRequest: (request: import("http").ClientRequest) => {
- request.setHeader("Ref-Event-Loop", "1");
+ request.setHeader("Ref-Event-Loop", "0");
request.end();
},
});
@@ -67,18 +67,23 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
webSocket.addEventListener("open", () => {
this.emit("Inspector.connected");
- for (const request of this.#pendingRequests) {
+ for (let i = 0; i < this.#pendingRequests.length; i++) {
+ const request = this.#pendingRequests[i];
+
if (this.#send(request)) {
this.emit("Inspector.request", request);
+ } else {
+ this.#pendingRequests = this.#pendingRequests.slice(i);
+ break;
}
}
-
- this.#pendingRequests.length = 0;
});
webSocket.addEventListener("message", ({ data }) => {
if (typeof data === "string") {
this.#accept(data);
+ } else {
+ this.emit("Inspector.error", new Error(`WebSocket received unexpected binary message: ${data.toString()}`));
}
});
@@ -125,8 +130,12 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
};
return new Promise((resolve, reject) => {
+ let timerId: number | undefined;
const done = (result: any) => {
this.#pendingResponses.delete(id);
+ if (timerId) {
+ clearTimeout(timerId);
+ }
if (result instanceof Error) {
reject(result);
} else {
@@ -136,6 +145,7 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
this.#pendingResponses.set(id, done);
if (this.#send(request)) {
+ timerId = +setTimeout(() => done(new Error(`Timed out: ${method}`)), 10_000);
this.emit("Inspector.request", request);
} else {
this.emit("Inspector.pendingRequest", request);
@@ -183,7 +193,6 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
return;
}
- this.#pendingResponses.delete(id);
if ("error" in data) {
const { error } = data;
const { message } = error;
@@ -218,6 +227,7 @@ export class WebSocketInspector extends EventEmitter<InspectorEventMap> implemen
resolve(error ?? new Error("WebSocket closed"));
}
this.#pendingResponses.clear();
+
if (error) {
this.emit("Inspector.error", error);
}
diff --git a/packages/bun-vscode/example/example.test.ts b/packages/bun-vscode/example/example.test.ts
index a9da929eb..8e855745c 100644
--- a/packages/bun-vscode/example/example.test.ts
+++ b/packages/bun-vscode/example/example.test.ts
@@ -5,7 +5,24 @@ describe("example", () => {
expect(1).toBe(1);
expect(1).not.toBe(2);
expect(() => {
- throw new Error("error");
+ throw new TypeError("Oops! I did it again.");
+ }).toThrow();
+ expect(() => {
+ throw new Error("Parent error.", {
+ cause: new TypeError("Child error."),
+ });
+ }).toThrow();
+ expect(() => {
+ throw new AggregateError([new TypeError("Child error 1."), new TypeError("Child error 2.")], "Parent error.");
+ }).toThrow();
+ expect(() => {
+ throw "This is a string error";
+ }).toThrow();
+ expect(() => {
+ throw {
+ message: "This is an object error",
+ code: -1021,
+ };
}).toThrow();
});
});
diff --git a/packages/bun-vscode/example/example.ts b/packages/bun-vscode/example/example.ts
index d46c60415..3e2d87cd2 100644
--- a/packages/bun-vscode/example/example.ts
+++ b/packages/bun-vscode/example/example.ts
@@ -1,10 +1,14 @@
export default {
async fetch(request: Request): Promise<Response> {
a(request);
+ const object = {
+ a: "1",
+ b: "2",
+ c: new Map([[1, 2]]),
+ };
const coolThing: CoolThing = new SuperCoolThing();
coolThing.doCoolThing();
- debugger;
- return new Response("BAI BAI");
+ return new Response("Hello World");
},
};
diff --git a/packages/bun-vscode/package.json b/packages/bun-vscode/package.json
index a37e1ec91..da51a3297 100644
--- a/packages/bun-vscode/package.json
+++ b/packages/bun-vscode/package.json
@@ -131,35 +131,53 @@
"description": "The path to Bun.",
"default": "bun"
},
+ "runtimeArgs": {
+ "type": "array",
+ "description": "The command-line arguments passed to Bun.",
+ "items": {
+ "type": "string"
+ },
+ "default": []
+ },
"program": {
"type": "string",
- "description": "The file to debug.",
+ "description": "The path to a JavaScript or TypeScript file.",
"default": "${file}"
},
- "cwd": {
- "type": "string",
- "description": "The working directory.",
- "default": "${workspaceFolder}"
- },
"args": {
"type": "array",
- "description": "The arguments passed to Bun.",
+ "description": "The command-line arguments passed to the program.",
"items": {
"type": "string"
},
"default": []
},
+ "cwd": {
+ "type": "string",
+ "description": "The working directory.",
+ "default": "${workspaceFolder}"
+ },
"env": {
"type": "object",
"description": "The environment variables passed to Bun.",
"default": {}
},
- "inheritEnv": {
+ "strictEnv": {
"type": "boolean",
- "description": "If environment variables should be inherited from the parent process.",
- "default": true
+ "description": "If environment variables should not be inherited from the parent process.",
+ "default": false
+ },
+ "stopOnEntry": {
+ "type": "boolean",
+ "description": "If a breakpoint should be set at the first line.",
+ "default": false
},
- "watch": {
+ "noDebug": {
+ "type": "boolean",
+ "description": "If the debugger should be disabled.",
+ "default": false
+ },
+ "watchMode": {
"type": ["boolean", "string"],
"description": "If the process should be restarted when files change.",
"enum": [
@@ -168,11 +186,6 @@
"hot"
],
"default": false
- },
- "debug": {
- "type": "boolean",
- "description": "If the process should be started in debug mode.",
- "default": true
}
}
},
@@ -181,6 +194,16 @@
"url": {
"type": "string",
"description": "The URL of the Bun process to attach to."
+ },
+ "noDebug": {
+ "type": "boolean",
+ "description": "If the debugger should be disabled.",
+ "default": false
+ },
+ "stopOnEntry": {
+ "type": "boolean",
+ "description": "If a breakpoint should when the program is attached.",
+ "default": false
}
}
}
diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts
index e6322b73b..984031e87 100644
--- a/packages/bun-vscode/src/features/debug.ts
+++ b/packages/bun-vscode/src/features/debug.ts
@@ -9,7 +9,8 @@ const debugConfiguration: vscode.DebugConfiguration = {
request: "launch",
name: "Debug Bun",
program: "${file}",
- watch: false,
+ stopOnEntry: false,
+ watchMode: false,
};
const runConfiguration: vscode.DebugConfiguration = {
@@ -17,8 +18,8 @@ const runConfiguration: vscode.DebugConfiguration = {
request: "launch",
name: "Run Bun",
program: "${file}",
- debug: false,
- watch: false,
+ noDebug: true,
+ watchMode: false,
};
const attachConfiguration: vscode.DebugConfiguration = {
@@ -48,15 +49,25 @@ export default function (context: vscode.ExtensionContext, factory?: vscode.Debu
vscode.window.registerTerminalProfileProvider("bun", new TerminalProfileProvider()),
);
- const { terminalProfile } = new TerminalDebugSession();
- const { options } = terminalProfile;
- const terminal = vscode.window.createTerminal(options);
- terminal.show();
- context.subscriptions.push(terminal);
+ const document = getActiveDocument();
+ if (isJavaScript(document?.languageId)) {
+ vscode.workspace.findFiles("bun.lockb", "node_modules", 1).then(files => {
+ const { terminalProfile } = new TerminalDebugSession();
+ const { options } = terminalProfile;
+ const terminal = vscode.window.createTerminal(options);
+
+ const focus = files.length > 0;
+ if (focus) {
+ terminal.show();
+ }
+
+ context.subscriptions.push(terminal);
+ });
+ }
}
function RunFileCommand(resource?: vscode.Uri): void {
- const path = getCurrentPath(resource);
+ const path = getActivePath(resource);
if (path) {
vscode.debug.startDebugging(undefined, {
...runConfiguration,
@@ -67,7 +78,7 @@ function RunFileCommand(resource?: vscode.Uri): void {
}
function DebugFileCommand(resource?: vscode.Uri): void {
- const path = getCurrentPath(resource);
+ const path = getActivePath(resource);
if (path) {
vscode.debug.startDebugging(undefined, {
...debugConfiguration,
@@ -178,18 +189,36 @@ class TerminalDebugSession extends FileDebugSession {
return new vscode.TerminalProfile({
name: "Bun Terminal",
env: {
- "BUN_INSPECT": `1${this.adapter.url}`,
+ "BUN_INSPECT": `${this.adapter.url}?wait=1`,
"BUN_INSPECT_NOTIFY": `${this.signal.url}`,
},
isTransient: true,
iconPath: new vscode.ThemeIcon("debug-console"),
});
}
+
+ dispose(): void {
+ super.dispose();
+ this.signal.close();
+ }
+}
+
+function getActiveDocument(): vscode.TextDocument | undefined {
+ return vscode.window.activeTextEditor?.document;
}
-function getCurrentPath(target?: vscode.Uri): string | undefined {
- if (!target && vscode.window.activeTextEditor) {
- target = vscode.window.activeTextEditor.document.uri;
+function getActivePath(target?: vscode.Uri): string | undefined {
+ if (!target) {
+ target = getActiveDocument()?.uri;
}
return target?.fsPath;
}
+
+function isJavaScript(languageId?: string): boolean {
+ return (
+ languageId === "javascript" ||
+ languageId === "javascriptreact" ||
+ languageId === "typescript" ||
+ languageId === "typescriptreact"
+ );
+}
diff --git a/src/bun.js/bindings/BunDebugger.cpp b/src/bun.js/bindings/BunDebugger.cpp
index 440a5125b..f4a5f535a 100644
--- a/src/bun.js/bindings/BunDebugger.cpp
+++ b/src/bun.js/bindings/BunDebugger.cpp
@@ -109,7 +109,7 @@ public:
globalObject->setInspectable(true);
auto& inspector = globalObject->inspectorDebuggable();
inspector.setInspectable(true);
- globalObject->inspectorController().connectFrontend(*connection, true, waitingForConnection);
+ globalObject->inspectorController().connectFrontend(*connection, true, false); // waitingForConnection
Inspector::JSGlobalObjectDebugger* debugger = reinterpret_cast<Inspector::JSGlobalObjectDebugger*>(globalObject->debugger());
if (debugger) {
@@ -482,7 +482,7 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionCreateConnection, (JSGlobalObject * globalObj
return JSValue::encode(JSBunInspectorConnection::create(vm, JSBunInspectorConnection::createStructure(vm, globalObject, globalObject->objectPrototype()), connection));
}
-extern "C" BunString Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGlobalObject, ScriptExecutionContextIdentifier scriptId, BunString* portOrPathString)
+extern "C" void Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGlobalObject, ScriptExecutionContextIdentifier scriptId, BunString* portOrPathString)
{
if (!debuggerScriptExecutionContext)
debuggerScriptExecutionContext = debuggerGlobalObject->scriptExecutionContext();
@@ -498,12 +498,7 @@ extern "C" BunString Bun__startJSDebuggerThread(Zig::GlobalObject* debuggerGloba
arguments.append(JSFunction::create(vm, debuggerGlobalObject, 1, String("send"_s), jsFunctionSend, ImplementationVisibility::Public));
arguments.append(JSFunction::create(vm, debuggerGlobalObject, 0, String("disconnect"_s), jsFunctionDisconnect, ImplementationVisibility::Public));
- JSValue serverURLValue = JSC::call(debuggerGlobalObject, debuggerDefaultFn, arguments, "Bun__initJSDebuggerThread - debuggerDefaultFn"_s);
-
- if (serverURLValue.isUndefinedOrNull())
- return BunStringEmpty;
-
- return Bun::toStringRef(debuggerGlobalObject, serverURLValue);
+ JSC::call(debuggerGlobalObject, debuggerDefaultFn, arguments, "Bun__initJSDebuggerThread - debuggerDefaultFn"_s);
}
enum class AsyncCallTypeUint8 : uint8_t {
diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig
index 90a3bbc66..32cf04346 100644
--- a/src/bun.js/javascript.zig
+++ b/src/bun.js/javascript.zig
@@ -502,6 +502,7 @@ pub const VirtualMachine = struct {
worker: ?*JSC.WebWorker = null,
debugger: ?Debugger = null,
+ has_started_debugger: bool = false,
pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSC.JSGlobalObject, JSC.JSValue) void;
@@ -794,7 +795,8 @@ pub const VirtualMachine = struct {
pub var has_created_debugger: bool = false;
pub const Debugger = struct {
- path_or_port: []const u8 = "",
+ path_or_port: ?[]const u8 = null,
+ unix: []const u8 = "",
script_execution_context_id: u32 = 0,
next_debugger_id: u64 = 1,
poll_ref: JSC.PollRef = .{},
@@ -805,8 +807,7 @@ pub const VirtualMachine = struct {
extern "C" fn Bun__createJSDebugger(*JSC.JSGlobalObject) u32;
extern "C" fn Bun__ensureDebugger(u32, bool) void;
- extern "C" fn Bun__startJSDebuggerThread(*JSC.JSGlobalObject, u32, *bun.String) bun.String;
- var has_started_debugger_thread: bool = false;
+ extern "C" fn Bun__startJSDebuggerThread(*JSC.JSGlobalObject, u32, *bun.String) void;
var futex_atomic: std.atomic.Atomic(u32) = undefined;
pub fn create(this: *VirtualMachine, globalObject: *JSGlobalObject) !void {
@@ -816,8 +817,8 @@ pub const VirtualMachine = struct {
has_created_debugger = true;
var debugger = &this.debugger.?;
debugger.script_execution_context_id = Bun__createJSDebugger(globalObject);
- if (!has_started_debugger_thread) {
- has_started_debugger_thread = true;
+ if (!this.has_started_debugger) {
+ this.has_started_debugger = true;
futex_atomic = std.atomic.Atomic(u32).init(0);
var thread = try std.Thread.spawn(.{}, startJSDebuggerThread, .{this});
thread.detach();
@@ -865,8 +866,6 @@ pub const VirtualMachine = struct {
vm.global.vm().holdAPILock(other_vm, @ptrCast(&start));
}
- pub export var Bun__debugger_server_url: bun.String = undefined;
-
pub export fn Debugger__didConnect() void {
var this = VirtualMachine.get();
std.debug.assert(this.debugger.?.wait_for_connection);
@@ -878,9 +877,17 @@ pub const VirtualMachine = struct {
JSC.markBinding(@src());
var this = VirtualMachine.get();
- var str = bun.String.create(other_vm.debugger.?.path_or_port);
- Bun__debugger_server_url = Bun__startJSDebuggerThread(this.global, other_vm.debugger.?.script_execution_context_id, &str);
- Bun__debugger_server_url.toThreadSafe();
+ var debugger = other_vm.debugger.?;
+
+ if (debugger.unix.len > 0) {
+ var url = bun.String.create(debugger.unix);
+ Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url);
+ }
+
+ if (debugger.path_or_port) |path_or_port| {
+ var url = bun.String.create(path_or_port);
+ Bun__startJSDebuggerThread(this.global, debugger.script_execution_context_id, &url);
+ }
this.global.handleRejectedPromises();
@@ -1189,13 +1196,27 @@ pub const VirtualMachine = struct {
}
fn configureDebugger(this: *VirtualMachine, debugger: bun.CLI.Command.Debugger) void {
+ var unix = bun.getenvZ("BUN_INSPECT") orelse "";
+ var set_breakpoint_on_first_line = unix.len > 0 and strings.endsWith(unix, "?break=1");
+ var wait_for_connection = set_breakpoint_on_first_line or (unix.len > 0 and strings.endsWith(unix, "?wait=1"));
+
switch (debugger) {
- .unspecified => {},
+ .unspecified => {
+ if (unix.len > 0) {
+ this.debugger = Debugger{
+ .path_or_port = null,
+ .unix = unix,
+ .wait_for_connection = wait_for_connection,
+ .set_breakpoint_on_first_line = set_breakpoint_on_first_line,
+ };
+ }
+ },
.enable => {
this.debugger = Debugger{
.path_or_port = debugger.enable.path_or_port,
- .wait_for_connection = debugger.enable.wait_for_connection,
- .set_breakpoint_on_first_line = debugger.enable.set_breakpoint_on_first_line,
+ .unix = unix,
+ .wait_for_connection = wait_for_connection or debugger.enable.wait_for_connection,
+ .set_breakpoint_on_first_line = set_breakpoint_on_first_line or debugger.enable.set_breakpoint_on_first_line,
};
},
}
diff --git a/src/cli.zig b/src/cli.zig
index 68ae7e4c2..c68297baa 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -543,15 +543,6 @@ pub const Arguments = struct {
.wait_for_connection = true,
.set_breakpoint_on_first_line = true,
} };
- } else if (bun.getenvZ("BUN_INSPECT")) |inspect_value| {
- ctx.runtime_options.debugger = if (inspect_value.len == 0 or inspect_value[0] == '0')
- Command.Debugger{ .unspecified = {} }
- else
- Command.Debugger{ .enable = .{
- .path_or_port = inspect_value[1..],
- .wait_for_connection = inspect_value[0] == '1' or inspect_value[0] == '2',
- .set_breakpoint_on_first_line = inspect_value[0] == '2',
- } };
}
}
diff --git a/src/js/internal/debugger.ts b/src/js/internal/debugger.ts
index 2e76b2c7c..cd6f8f516 100644
--- a/src/js/internal/debugger.ts
+++ b/src/js/internal/debugger.ts
@@ -1,309 +1,322 @@
-import type * as BunType from "bun";
-
-// We want to avoid dealing with creating a prototype for the inspector class
-let sendFn_, disconnectFn_;
-const colors = Bun.enableANSIColors && process.env.NO_COLOR !== "1";
-
-var debuggerCounter = 1;
-class DebuggerWithMessageQueue {
- debugger?: Debugger = undefined;
- messageQueue: string[] = [];
- count: number = debuggerCounter++;
+import type { Server as WebSocketServer, WebSocketHandler, ServerWebSocket, SocketHandler, Socket } from "bun";
+
+export default function (
+ executionContextId: string,
+ url: string,
+ createBackend: (
+ executionContextId: string,
+ refEventLoop: boolean,
+ receive: (...messages: string[]) => void,
+ ) => unknown,
+ send: (message: string) => void,
+ close: () => void,
+): void {
+ let debug: Debugger | undefined;
+ try {
+ debug = new Debugger(executionContextId, url, createBackend, send, close);
+ } catch (error) {
+ exit("Failed to start inspector:\n", error);
+ }
- send(msg: string) {
- sendFn_.call(this.debugger, msg);
+ const { protocol, href, host, pathname } = debug.url;
+ if (!protocol.includes("unix")) {
+ console.log(dim("--------------------- Bun Inspector ---------------------"), reset());
+ console.log(`Listening:\n ${dim(href)}`);
+ if (protocol.includes("ws")) {
+ console.log(`Inspect in browser:\n ${link(`https://debug.bun.sh/#${host}${pathname}`)}`);
+ }
+ console.log(dim("--------------------- Bun Inspector ---------------------"), reset());
}
- disconnect() {
- disconnectFn_.call(this.debugger);
- this.messageQueue.length = 0;
+ const unix = process.env["BUN_INSPECT_NOTIFY"];
+ if (unix) {
+ const { protocol, pathname } = parseUrl(unix);
+ if (protocol === "unix:") {
+ notify(pathname);
+ }
}
}
-let defaultPort = 6499;
+class Debugger {
+ #url: URL;
+ #createBackend: (refEventLoop: boolean, receive: (...messages: string[]) => void) => Writer;
+
+ constructor(
+ executionContextId: string,
+ url: string,
+ createBackend: (
+ executionContextId: string,
+ refEventLoop: boolean,
+ receive: (...messages: string[]) => void,
+ ) => unknown,
+ send: (message: string) => void,
+ close: () => void,
+ ) {
+ this.#url = parseUrl(url);
+ this.#createBackend = (refEventLoop, receive) => {
+ const backend = createBackend(executionContextId, refEventLoop, receive);
+ return {
+ write: message => {
+ send.call(backend, message);
+ return true;
+ },
+ close: () => close.call(backend),
+ };
+ };
+ this.#listen();
+ }
-let generatedPath: string = "";
-function generatePath() {
- if (!generatedPath) {
- generatedPath = "/" + Math.random().toString(36).slice(2);
+ get url(): URL {
+ return this.#url;
}
- return generatedPath;
-}
+ #listen(): void {
+ const { protocol, hostname, port, pathname } = this.#url;
+
+ if (protocol === "ws:" || protocol === "ws+tcp:") {
+ const server = Bun.serve({
+ hostname,
+ port,
+ fetch: this.#fetch.bind(this),
+ websocket: this.#websocket,
+ });
+ this.#url.hostname = server.hostname;
+ this.#url.port = `${server.port}`;
+ return;
+ }
-function terminalLink(url) {
- if (colors) {
- // bold + hyperlink + reset
- return "\x1b[1m\x1b]8;;" + url + "\x1b\\" + url + "\x1b]8;;\x1b\\" + "\x1b[22m";
- }
+ if (protocol === "ws+unix:") {
+ Bun.serve({
+ unix: pathname,
+ fetch: this.#fetch.bind(this),
+ websocket: this.#websocket,
+ });
+ return;
+ }
- return url;
-}
+ throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`);
+ }
-function dim(text) {
- if (colors) {
- return "\x1b[2m" + text + "\x1b[22m";
+ get #websocket(): WebSocketHandler<Connection> {
+ return {
+ idleTimeout: 0,
+ closeOnBackpressureLimit: false,
+ open: ws => this.#open(ws, webSocketWriter(ws)),
+ message: (ws, message) => {
+ if (typeof message === "string") {
+ this.#message(ws, message);
+ } else {
+ this.#error(ws, new Error(`Unexpected binary message: ${message.toString()}`));
+ }
+ },
+ drain: ws => this.#drain(ws),
+ close: ws => this.#close(ws),
+ };
}
- return text;
-}
+ #fetch(request: Request, server: WebSocketServer): Response | undefined {
+ const { method, url, headers } = request;
+ const { pathname } = new URL(url);
-class WebSocketListener {
- server: BunType.Server;
- url: string = "";
- createInspectorConnection;
- scriptExecutionContextId: number = 0;
- activeConnections: Set<BunType.ServerWebSocket<DebuggerWithMessageQueue>> = new Set();
-
- constructor(scriptExecutionContextId: number = 0, url: string, createInspectorConnection) {
- this.scriptExecutionContextId = scriptExecutionContextId;
- this.createInspectorConnection = createInspectorConnection;
- this.server = this.start(url);
- }
+ if (method !== "GET") {
+ return new Response(null, {
+ status: 405, // Method Not Allowed
+ });
+ }
- start(url: string): BunType.Server {
- let defaultHostname = "localhost";
- let usingDefaultPort = false;
- let isUnix = false;
-
- if (url.startsWith("ws+unix://")) {
- isUnix = true;
- url = url.slice(10);
- } else if (/^[0-9]*$/.test(url)) {
- url = "ws://" + defaultHostname + ":" + url + generatePath();
- } else if (!url || url.startsWith("/")) {
- url = "ws://" + defaultHostname + ":" + defaultPort + generatePath();
- usingDefaultPort = true;
- } else if (url.includes(":") && !url.includes("://")) {
- try {
- const insertSlash = !url.includes("/");
- url = new URL("ws://" + url).href;
- if (insertSlash) {
- url += generatePath().slice(1);
- }
- } catch (e) {
- console.error("[Inspector]", "Failed to parse url", '"' + url + '"');
- process.exit(1);
- }
+ switch (pathname) {
+ case "/json/version":
+ return Response.json(versionInfo());
+ case "/json":
+ case "/json/list":
+ // TODO?
}
- if (!isUnix) {
- try {
- var { hostname, port, pathname } = new URL(url);
- this.url = pathname.toLowerCase();
- } catch (e) {
- console.error("[Inspector]", "Failed to parse url", '"' + url + '"');
- process.exit(1);
- }
+ if (!this.#url.protocol.includes("unix") && this.#url.pathname !== pathname) {
+ return new Response(null, {
+ status: 404, // Not Found
+ });
}
- const serveOptions: BunType.WebSocketServeOptions<DebuggerWithMessageQueue> = {
- ...(isUnix ? { unix: url } : { hostname }),
- development: false,
-
- // @ts-ignore
- reusePort: false,
-
- websocket: {
- idleTimeout: 0,
- open: socket => {
- var connection = new DebuggerWithMessageQueue();
- // @ts-expect-error
- const shouldRefEventLoop = !!socket.data?.shouldRefEventLoop;
-
- socket.data = connection;
- this.activeConnections.add(socket);
- connection.debugger = this.createInspectorConnection(
- this.scriptExecutionContextId,
- shouldRefEventLoop,
- (...msgs: string[]) => {
- if (socket.readyState > 1) {
- connection.disconnect();
- return;
- }
-
- if (connection.messageQueue.length > 0) {
- connection.messageQueue.push(...msgs);
- return;
- }
-
- for (let i = 0; i < msgs.length; i++) {
- if (!socket.sendText(msgs[i])) {
- if (socket.readyState < 2) {
- connection.messageQueue.push(...msgs.slice(i));
- }
- return;
- }
- }
- },
- );
-
- if (!isUnix) {
- console.log(
- "[Inspector]",
- "Connection #" + connection.count + " opened",
- "(" +
- new Intl.DateTimeFormat(undefined, {
- "timeStyle": "long",
- "dateStyle": "short",
- }).format(new Date()) +
- ")",
- );
- }
- },
- drain: socket => {
- const queue = socket.data.messageQueue;
- for (let i = 0; i < queue.length; i++) {
- if (!socket.sendText(queue[i])) {
- socket.data.messageQueue = queue.slice(i);
- return;
- }
- }
- queue.length = 0;
- },
- message: (socket, message) => {
- if (typeof message !== "string") {
- console.warn("[Inspector]", "Received non-string message");
- return;
- }
- socket.data.send(message as string);
- },
- close: socket => {
- socket.data.disconnect();
- if (!isUnix) {
- console.log(
- "[Inspector]",
- "Connection #" + socket.data.count + " closed",
- "(" +
- new Intl.DateTimeFormat(undefined, {
- "timeStyle": "long",
- "dateStyle": "short",
- }).format(new Date()) +
- ")",
- );
- }
- this.activeConnections.delete(socket);
+ const data: Connection = {
+ refEventLoop: headers.get("Ref-Event-Loop") === "0",
+ };
+
+ if (!server.upgrade(request, { data })) {
+ return new Response(null, {
+ status: 426, // Upgrade Required
+ headers: {
+ "Upgrade": "websocket",
},
- },
- fetch: (req, server) => {
- let { pathname } = new URL(req.url);
- pathname = pathname.toLowerCase();
-
- if (pathname === "/json/version") {
- return Response.json({
- "Browser": navigator.userAgent,
- "WebKit-Version": process.versions.webkit,
- "Bun-Version": Bun.version,
- "Bun-Revision": Bun.revision,
- });
- }
+ });
+ }
+ }
- if (!this.url || pathname === this.url) {
- const refHeader = req.headers.get("Ref-Event-Loop");
- if (
- server.upgrade(req, {
- data: {
- shouldRefEventLoop: !!refHeader && refHeader !== "0",
- },
- })
- ) {
- return new Response();
- }
+ get #socket(): SocketHandler<Connection> {
+ return {
+ open: socket => this.#open(socket, socketWriter(socket)),
+ data: (socket, message) => this.#message(socket, message.toString()),
+ drain: socket => this.#drain(socket),
+ close: socket => this.#close(socket),
+ error: (socket, error) => this.#error(socket, error),
+ connectError: (_, error) => exit("Failed to start inspector:\n", error),
+ };
+ }
- return new Response("WebSocket expected", {
- status: 400,
- });
- }
+ #open(connection: ConnectionOwner, writer: Writer): void {
+ const { data } = connection;
+ const { refEventLoop } = data;
- return new Response("Not found", {
- status: 404,
- });
- },
- };
+ const client = bufferedWriter(writer);
+ const backend = this.#createBackend(refEventLoop, (...messages: string[]) => {
+ for (const message of messages) {
+ client.write(message);
+ }
+ });
- if (port === "") {
- port = defaultPort + "";
- }
+ data.client = client;
+ data.backend = backend;
+ }
- let portNumber = Number(port);
- var server, lastError;
-
- if (usingDefaultPort) {
- for (let tries = 0; tries < 10 && !server; tries++) {
- try {
- lastError = undefined;
- server = Bun.serve<DebuggerWithMessageQueue>({
- ...serveOptions,
- port: portNumber++,
- });
- if (isUnix) {
- notify();
- }
- } catch (e) {
- lastError = e;
- }
+ #message(connection: ConnectionOwner, message: string): void {
+ const { data } = connection;
+ const { backend } = data;
+ backend?.write(message);
+ }
+
+ #drain(connection: ConnectionOwner): void {
+ const { data } = connection;
+ const { client } = data;
+ client?.drain?.();
+ }
+
+ #close(connection: ConnectionOwner): void {
+ const { data } = connection;
+ const { backend } = data;
+ backend?.close();
+ }
+
+ #error(connection: ConnectionOwner, error: Error): void {
+ const { data } = connection;
+ const { backend } = data;
+ console.error(error);
+ backend?.close();
+ }
+}
+
+function versionInfo(): unknown {
+ return {
+ "Protocol-Version": "1.3",
+ "Browser": "Bun",
+ // @ts-ignore: Missing types for `navigator`
+ "User-Agent": navigator.userAgent,
+ "WebKit-Version": process.versions.webkit,
+ "Bun-Version": Bun.version,
+ "Bun-Revision": Bun.revision,
+ };
+}
+
+function webSocketWriter(ws: ServerWebSocket<unknown>): Writer {
+ return {
+ write: message => !!ws.sendText(message),
+ close: () => ws.close(),
+ };
+}
+
+function socketWriter(socket: Socket<unknown>): Writer {
+ return {
+ write: message => !!socket.write(message),
+ close: () => socket.end(),
+ };
+}
+
+function bufferedWriter(writer: Writer): Writer {
+ let draining = false;
+ let pendingMessages: string[] = [];
+
+ return {
+ write: message => {
+ if (draining || !writer.write(message)) {
+ pendingMessages.push(message);
}
- } else {
+ return true;
+ },
+ drain: () => {
+ draining = true;
try {
- server = Bun.serve<DebuggerWithMessageQueue>({
- ...serveOptions,
- port: portNumber,
- });
- if (isUnix) {
- notify();
+ for (let i = 0; i < pendingMessages.length; i++) {
+ if (!writer.write(pendingMessages[i])) {
+ pendingMessages = pendingMessages.slice(i);
+ return;
+ }
}
- } catch (e) {
- lastError = e;
+ } finally {
+ draining = false;
}
- }
+ },
+ close: () => {
+ writer.close();
+ pendingMessages.length = 0;
+ },
+ };
+}
- if (!server) {
- console.error("[Inspector]", "Failed to start server");
- if (lastError) console.error(lastError);
- process.exit(1);
- }
+const defaultHostname = "localhost";
+const defaultPort = 6499;
- let textToWrite = "";
- function writeToConsole(text) {
- textToWrite += text;
- }
- function flushToConsole() {
- console.write(textToWrite);
+function parseUrl(url: string): URL {
+ try {
+ if (!url) {
+ return new URL(randomId(), `ws://${defaultHostname}:${defaultPort}/`);
+ } else if (url.startsWith("/")) {
+ return new URL(url, `ws://${defaultHostname}:${defaultPort}/`);
+ } else if (/^[a-z+]+:\/\//i.test(url)) {
+ return new URL(url);
+ } else if (/^\d+$/.test(url)) {
+ return new URL(randomId(), `ws://${defaultHostname}:${url}/`);
+ } else if (!url.includes("/") && url.includes(":")) {
+ return new URL(randomId(), `ws://${url}/`);
+ } else if (!url.includes(":")) {
+ const [hostname, pathname] = url.split("/", 2);
+ return new URL(`ws://${hostname}:${defaultPort}/${pathname}`);
+ } else {
+ return new URL(randomId(), `ws://${url}`);
}
+ } catch {
+ throw new TypeError(`Invalid hostname or URL: '${url}'`);
+ }
+}
- if (!this.url) {
- return server;
- }
+function randomId() {
+ return Math.random().toString(36).slice(2);
+}
- // yellow foreground
- writeToConsole(dim(`------------------ Bun Inspector ------------------` + "\n"));
- if (colors) {
- // reset background
- writeToConsole("\x1b[49m");
- }
+const { enableANSIColors } = Bun;
- writeToConsole(
- "Listening at:\n " +
- `ws://${hostname}:${server.port}${this.url}` +
- "\n\n" +
- "Inspect in browser:\n " +
- terminalLink(new URL(`https://debug.bun.sh#${server.hostname}:${server.port}${this.url}`).href) +
- "\n",
- );
- writeToConsole(dim(`------------------ Bun Inspector ------------------` + "\n"));
- flushToConsole();
-
- return server;
+function dim(string: string): string {
+ if (enableANSIColors) {
+ return `\x1b[2m${string}\x1b[22m`;
}
+ return string;
}
-function notify(): void {
- const unix = process.env["BUN_INSPECT_NOTIFY"];
- if (!unix || !unix.startsWith("unix://")) {
- return;
+function link(url: string): string {
+ if (enableANSIColors) {
+ return `\x1b[1m\x1b]8;;${url}\x1b\\${url}\x1b]8;;\x1b\\\x1b[22m`;
+ }
+ return url;
+}
+
+function reset(): string {
+ if (enableANSIColors) {
+ return "\x1b[49m";
}
+ return "";
+}
+
+function notify(unix: string): void {
Bun.connect({
- unix: unix.slice(7),
+ unix,
socket: {
open: socket => {
socket.end("1");
@@ -311,26 +324,27 @@ function notify(): void {
data: () => {}, // required or it errors
},
}).finally(() => {
- // Do nothing
+ // Best-effort
});
}
-interface Debugger {
- send(msg: string): void;
- disconnect(): void;
+function exit(...args: unknown[]): never {
+ console.error(...args);
+ process.exit(1);
}
-var listener: WebSocketListener;
-
-export default function start(debuggerId, hostOrPort, createInspectorConnection, sendFn, disconnectFn) {
- try {
- sendFn_ = sendFn;
- disconnectFn_ = disconnectFn;
- globalThis.listener = listener ||= new WebSocketListener(debuggerId, hostOrPort, createInspectorConnection);
- } catch (e) {
- console.error("Bun Inspector threw an exception\n", e);
- process.exit(1);
- }
-
- return `http://${listener.server.hostname}:${listener.server.port}${listener.url}`;
-}
+type ConnectionOwner = {
+ data: Connection;
+};
+
+type Connection = {
+ refEventLoop: boolean;
+ client?: Writer;
+ backend?: Writer;
+};
+
+type Writer = {
+ write: (message: string) => boolean;
+ drain?: () => void;
+ close: () => void;
+};
diff --git a/src/js/out/InternalModuleRegistryConstants.h b/src/js/out/InternalModuleRegistryConstants.h
index 31d51538f..66a8f169d 100644
--- a/src/js/out/InternalModuleRegistryConstants.h
+++ b/src/js/out/InternalModuleRegistryConstants.h
@@ -14,7 +14,7 @@ static constexpr ASCIILiteral BunSqliteCode = "(function (){\"use strict\";// sr
//
//
-static constexpr ASCIILiteral InternalDebuggerCode = "(function (){\"use strict\";// src/js/out/tmp/internal/debugger.ts\nvar generatePath = function() {\n if (!generatedPath)\n generatedPath = \"/\" + Math.random().toString(36).slice(2);\n return generatedPath;\n}, terminalLink = function(url) {\n if (colors)\n return \"\\x1B[1m\\x1B]8;;\" + url + \"\\x1B\\\\\" + url + \"\\x1B]8;;\\x1B\\\\\" + \"\\x1B[22m\";\n return url;\n}, dim = function(text) {\n if (colors)\n return \"\\x1B[2m\" + text + \"\\x1B[22m\";\n return text;\n}, notify = function() {\n const unix = process.env.BUN_INSPECT_NOTIFY;\n if (!unix || !unix.startsWith(\"unix://\"))\n return;\n Bun.connect({\n unix: unix.slice(7),\n socket: {\n open: (socket) => {\n socket.end(\"1\");\n },\n data: () => {\n }\n }\n }).finally(() => {\n });\n}, $, sendFn_, disconnectFn_, colors = Bun.enableANSIColors && process.env.NO_COLOR !== \"1\", debuggerCounter = 1;\n\nclass DebuggerWithMessageQueue {\n debugger = void 0;\n messageQueue = [];\n count = debuggerCounter++;\n send(msg) {\n sendFn_.call(this.debugger, msg);\n }\n disconnect() {\n disconnectFn_.call(this.debugger), this.messageQueue.length = 0;\n }\n}\nvar defaultPort = 6499, generatedPath = \"\";\n\nclass WebSocketListener {\n server;\n url = \"\";\n createInspectorConnection;\n scriptExecutionContextId = 0;\n activeConnections = new Set;\n constructor(scriptExecutionContextId = 0, url, createInspectorConnection) {\n this.scriptExecutionContextId = scriptExecutionContextId, this.createInspectorConnection = createInspectorConnection, this.server = this.start(url);\n }\n start(url) {\n let defaultHostname = \"localhost\", usingDefaultPort = !1, isUnix = !1;\n if (url.startsWith(\"ws+unix://\"))\n isUnix = !0, url = url.slice(10);\n else if (/^[0-9]*$/.test(url))\n url = \"ws://\" + defaultHostname + \":\" + url + generatePath();\n else if (!url || url.startsWith(\"/\"))\n url = \"ws://\" + defaultHostname + \":\" + defaultPort + generatePath(), usingDefaultPort = !0;\n else if (url.includes(\":\") && !url.includes(\"://\"))\n try {\n const insertSlash = !url.includes(\"/\");\n if (url = new URL(\"ws://\" + url).href, insertSlash)\n url += generatePath().slice(1);\n } catch (e) {\n console.error(\"[Inspector]\", \"Failed to parse url\", '\"' + url + '\"'), process.exit(1);\n }\n if (!isUnix)\n try {\n var { hostname, port, pathname } = new URL(url);\n this.url = pathname.toLowerCase();\n } catch (e) {\n console.error(\"[Inspector]\", \"Failed to parse url\", '\"' + url + '\"'), process.exit(1);\n }\n const serveOptions = {\n ...isUnix \? { unix: url } : { hostname },\n development: !1,\n reusePort: !1,\n websocket: {\n idleTimeout: 0,\n open: (socket) => {\n var connection = new DebuggerWithMessageQueue;\n const shouldRefEventLoop = !!socket.data\?.shouldRefEventLoop;\n if (socket.data = connection, this.activeConnections.add(socket), connection.debugger = this.createInspectorConnection(this.scriptExecutionContextId, shouldRefEventLoop, (...msgs) => {\n if (socket.readyState > 1) {\n connection.disconnect();\n return;\n }\n if (connection.messageQueue.length > 0) {\n connection.messageQueue.push(...msgs);\n return;\n }\n for (let i = 0;i < msgs.length; i++)\n if (!socket.sendText(msgs[i])) {\n if (socket.readyState < 2)\n connection.messageQueue.push(...msgs.slice(i));\n return;\n }\n }), !isUnix)\n console.log(\"[Inspector]\", \"Connection #\" + connection.count + \" opened\", \"(\" + new Intl.DateTimeFormat(void 0, {\n timeStyle: \"long\",\n dateStyle: \"short\"\n }).format(new Date) + \")\");\n },\n drain: (socket) => {\n const queue = socket.data.messageQueue;\n for (let i = 0;i < queue.length; i++)\n if (!socket.sendText(queue[i])) {\n socket.data.messageQueue = queue.slice(i);\n return;\n }\n queue.length = 0;\n },\n message: (socket, message) => {\n if (typeof message !== \"string\") {\n console.warn(\"[Inspector]\", \"Received non-string message\");\n return;\n }\n socket.data.send(message);\n },\n close: (socket) => {\n if (socket.data.disconnect(), !isUnix)\n console.log(\"[Inspector]\", \"Connection #\" + socket.data.count + \" closed\", \"(\" + new Intl.DateTimeFormat(void 0, {\n timeStyle: \"long\",\n dateStyle: \"short\"\n }).format(new Date) + \")\");\n this.activeConnections.delete(socket);\n }\n },\n fetch: (req, server2) => {\n let { pathname: pathname2 } = new URL(req.url);\n if (pathname2 = pathname2.toLowerCase(), pathname2 === \"/json/version\")\n return Response.json({\n Browser: navigator.userAgent,\n \"WebKit-Version\": process.versions.webkit,\n \"Bun-Version\": Bun.version,\n \"Bun-Revision\": Bun.revision\n });\n if (!this.url || pathname2 === this.url) {\n const refHeader = req.headers.get(\"Ref-Event-Loop\");\n if (server2.upgrade(req, {\n data: {\n shouldRefEventLoop: !!refHeader && refHeader !== \"0\"\n }\n }))\n return new Response;\n return new Response(\"WebSocket expected\", {\n status: 400\n });\n }\n return new Response(\"Not found\", {\n status: 404\n });\n }\n };\n if (port === \"\")\n port = defaultPort + \"\";\n let portNumber = Number(port);\n var server, lastError;\n if (usingDefaultPort)\n for (let tries = 0;tries < 10 && !server; tries++)\n try {\n if (lastError = void 0, server = Bun.serve({\n ...serveOptions,\n port: portNumber++\n }), isUnix)\n notify();\n } catch (e) {\n lastError = e;\n }\n else\n try {\n if (server = Bun.serve({\n ...serveOptions,\n port: portNumber\n }), isUnix)\n notify();\n } catch (e) {\n lastError = e;\n }\n if (!server) {\n if (console.error(\"[Inspector]\", \"Failed to start server\"), lastError)\n console.error(lastError);\n process.exit(1);\n }\n let textToWrite = \"\";\n function writeToConsole(text) {\n textToWrite += text;\n }\n function flushToConsole() {\n console.write(textToWrite);\n }\n if (!this.url)\n return server;\n if (writeToConsole(dim(\"------------------ Bun Inspector ------------------\\n\")), colors)\n writeToConsole(\"\\x1B[49m\");\n return writeToConsole(\"Listening at:\\n \" + `ws://${hostname}:${server.port}${this.url}` + \"\\n\\nInspect in browser:\\n \" + terminalLink(new URL(`https://debug.bun.sh#${server.hostname}:${server.port}${this.url}`).href) + \"\\n\"), writeToConsole(dim(\"------------------ Bun Inspector ------------------\\n\")), flushToConsole(), server;\n }\n}\nvar listener;\n$ = function start(debuggerId, hostOrPort, createInspectorConnection, sendFn, disconnectFn) {\n try {\n sendFn_ = sendFn, disconnectFn_ = disconnectFn, globalThis.listener = listener ||= new WebSocketListener(debuggerId, hostOrPort, createInspectorConnection);\n } catch (e) {\n console.error(\"Bun Inspector threw an exception\\n\", e), process.exit(1);\n }\n return `http://${listener.server.hostname}:${listener.server.port}${listener.url}`;\n};\nreturn $})\n"_s;
+static constexpr ASCIILiteral InternalDebuggerCode = "(function (){\"use strict\";// src/js/out/tmp/internal/debugger.ts\nvar versionInfo = function() {\n return {\n \"Protocol-Version\": \"1.3\",\n Browser: \"Bun\",\n \"User-Agent\": navigator.userAgent,\n \"WebKit-Version\": process.versions.webkit,\n \"Bun-Version\": Bun.version,\n \"Bun-Revision\": Bun.revision\n };\n}, webSocketWriter = function(ws) {\n return {\n write: (message) => !!ws.sendText(message),\n close: () => ws.close()\n };\n}, socketWriter = function(socket) {\n return {\n write: (message) => !!socket.write(message),\n close: () => socket.end()\n };\n}, bufferedWriter = function(writer) {\n let draining = !1, pendingMessages = [];\n return {\n write: (message) => {\n if (draining || !writer.write(message))\n pendingMessages.push(message);\n return !0;\n },\n drain: () => {\n draining = !0;\n try {\n for (let i = 0;i < pendingMessages.length; i++)\n if (!writer.write(pendingMessages[i])) {\n pendingMessages = pendingMessages.slice(i);\n return;\n }\n } finally {\n draining = !1;\n }\n },\n close: () => {\n writer.close(), pendingMessages.length = 0;\n }\n };\n}, parseUrl = function(url) {\n try {\n if (!url)\n return new URL(randomId(), `ws://${defaultHostname}:${defaultPort}/`);\n else if (url.startsWith(\"/\"))\n return new URL(url, `ws://${defaultHostname}:${defaultPort}/`);\n else if (/^[a-z+]+:\\/\\//i.test(url))\n return new URL(url);\n else if (/^\\d+$/.test(url))\n return new URL(randomId(), `ws://${defaultHostname}:${url}/`);\n else if (!url.includes(\"/\") && url.includes(\":\"))\n return new URL(randomId(), `ws://${url}/`);\n else if (!url.includes(\":\")) {\n const [hostname, pathname] = url.split(\"/\", 2);\n return new URL(`ws://${hostname}:${defaultPort}/${pathname}`);\n } else\n return new URL(randomId(), `ws://${url}`);\n } catch {\n @throwTypeError(`Invalid hostname or URL: '${url}'`);\n }\n}, randomId = function() {\n return Math.random().toString(36).slice(2);\n}, dim = function(string) {\n if (enableANSIColors)\n return `\\x1B[2m${string}\\x1B[22m`;\n return string;\n}, link = function(url) {\n if (enableANSIColors)\n return `\\x1B[1m\\x1B]8;;${url}\\x1B\\\\${url}\\x1B]8;;\\x1B\\\\\\x1B[22m`;\n return url;\n}, reset = function() {\n if (enableANSIColors)\n return \"\\x1B[49m\";\n return \"\";\n}, notify = function(unix) {\n Bun.connect({\n unix,\n socket: {\n open: (socket) => {\n socket.end(\"1\");\n },\n data: () => {\n }\n }\n }).finally(() => {\n });\n}, exit = function(...args) {\n console.error(...args), process.exit(1);\n}, $;\n$ = function(executionContextId, url, createBackend, send, close) {\n let debug;\n try {\n debug = new Debugger(executionContextId, url, createBackend, send, close);\n } catch (error) {\n exit(\"Failed to start inspector:\\n\", error);\n }\n const { protocol, href, host, pathname } = debug.url;\n if (!protocol.includes(\"unix\")) {\n if (console.log(dim(\"--------------------- Bun Inspector ---------------------\"), reset()), console.log(`Listening:\\n ${dim(href)}`), protocol.includes(\"ws\"))\n console.log(`Inspect in browser:\\n ${link(`https://debug.bun.sh/#${host}${pathname}`)}`);\n console.log(dim(\"--------------------- Bun Inspector ---------------------\"), reset());\n }\n const unix = process.env.BUN_INSPECT_NOTIFY;\n if (unix) {\n const { protocol: protocol2, pathname: pathname2 } = parseUrl(unix);\n if (protocol2 === \"unix:\")\n notify(pathname2);\n }\n};\n\nclass Debugger {\n #url;\n #createBackend;\n constructor(executionContextId, url, createBackend, send, close) {\n this.#url = parseUrl(url), this.#createBackend = (refEventLoop, receive) => {\n const backend = createBackend(executionContextId, refEventLoop, receive);\n return {\n write: (message) => {\n return send.call(backend, message), !0;\n },\n close: () => close.call(backend)\n };\n }, this.#listen();\n }\n get url() {\n return this.#url;\n }\n #listen() {\n const { protocol, hostname, port, pathname } = this.#url;\n if (protocol === \"ws:\" || protocol === \"ws+tcp:\") {\n const server = Bun.serve({\n hostname,\n port,\n fetch: this.#fetch.bind(this),\n websocket: this.#websocket\n });\n this.#url.hostname = server.hostname, this.#url.port = `${server.port}`;\n return;\n }\n if (protocol === \"ws+unix:\") {\n Bun.serve({\n unix: pathname,\n fetch: this.#fetch.bind(this),\n websocket: this.#websocket\n });\n return;\n }\n @throwTypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`);\n }\n get #websocket() {\n return {\n idleTimeout: 0,\n closeOnBackpressureLimit: !1,\n open: (ws) => this.#open(ws, webSocketWriter(ws)),\n message: (ws, message) => {\n if (typeof message === \"string\")\n this.#message(ws, message);\n else\n this.#error(ws, new Error(`Unexpected binary message: ${message.toString()}`));\n },\n drain: (ws) => this.#drain(ws),\n close: (ws) => this.#close(ws)\n };\n }\n #fetch(request, server) {\n const { method, url, headers } = request, { pathname } = new URL(url);\n if (method !== \"GET\")\n return new Response(null, {\n status: 405\n });\n switch (pathname) {\n case \"/json/version\":\n return Response.json(versionInfo());\n case \"/json\":\n case \"/json/list\":\n }\n if (!this.#url.protocol.includes(\"unix\") && this.#url.pathname !== pathname)\n return new Response(null, {\n status: 404\n });\n const data = {\n refEventLoop: headers.get(\"Ref-Event-Loop\") === \"0\"\n };\n if (!server.upgrade(request, { data }))\n return new Response(null, {\n status: 426,\n headers: {\n Upgrade: \"websocket\"\n }\n });\n }\n get #socket() {\n return {\n open: (socket) => this.#open(socket, socketWriter(socket)),\n data: (socket, message) => this.#message(socket, message.toString()),\n drain: (socket) => this.#drain(socket),\n close: (socket) => this.#close(socket),\n error: (socket, error) => this.#error(socket, error),\n connectError: (_, error) => exit(\"Failed to start inspector:\\n\", error)\n };\n }\n #open(connection, writer) {\n const { data } = connection, { refEventLoop } = data, client = bufferedWriter(writer), backend = this.#createBackend(refEventLoop, (...messages) => {\n for (let message of messages)\n client.write(message);\n });\n data.client = client, data.backend = backend;\n }\n #message(connection, message) {\n const { data } = connection, { backend } = data;\n backend\?.write(message);\n }\n #drain(connection) {\n const { data } = connection, { client } = data;\n client\?.drain\?.();\n }\n #close(connection) {\n const { data } = connection, { backend } = data;\n backend\?.close();\n }\n #error(connection, error) {\n const { data } = connection, { backend } = data;\n console.error(error), backend\?.close();\n }\n}\nvar defaultHostname = \"localhost\", defaultPort = 6499, { enableANSIColors } = Bun;\nreturn $})\n"_s;
//
//
@@ -247,7 +247,7 @@ static constexpr ASCIILiteral BunSqliteCode = "(function (){\"use strict\";// sr
//
//
-static constexpr ASCIILiteral InternalDebuggerCode = "(function (){\"use strict\";// src/js/out/tmp/internal/debugger.ts\nvar generatePath = function() {\n if (!generatedPath)\n generatedPath = \"/\" + Math.random().toString(36).slice(2);\n return generatedPath;\n}, terminalLink = function(url) {\n if (colors)\n return \"\\x1B[1m\\x1B]8;;\" + url + \"\\x1B\\\\\" + url + \"\\x1B]8;;\\x1B\\\\\" + \"\\x1B[22m\";\n return url;\n}, dim = function(text) {\n if (colors)\n return \"\\x1B[2m\" + text + \"\\x1B[22m\";\n return text;\n}, notify = function() {\n const unix = process.env.BUN_INSPECT_NOTIFY;\n if (!unix || !unix.startsWith(\"unix://\"))\n return;\n Bun.connect({\n unix: unix.slice(7),\n socket: {\n open: (socket) => {\n socket.end(\"1\");\n },\n data: () => {\n }\n }\n }).finally(() => {\n });\n}, $, sendFn_, disconnectFn_, colors = Bun.enableANSIColors && process.env.NO_COLOR !== \"1\", debuggerCounter = 1;\n\nclass DebuggerWithMessageQueue {\n debugger = void 0;\n messageQueue = [];\n count = debuggerCounter++;\n send(msg) {\n sendFn_.call(this.debugger, msg);\n }\n disconnect() {\n disconnectFn_.call(this.debugger), this.messageQueue.length = 0;\n }\n}\nvar defaultPort = 6499, generatedPath = \"\";\n\nclass WebSocketListener {\n server;\n url = \"\";\n createInspectorConnection;\n scriptExecutionContextId = 0;\n activeConnections = new Set;\n constructor(scriptExecutionContextId = 0, url, createInspectorConnection) {\n this.scriptExecutionContextId = scriptExecutionContextId, this.createInspectorConnection = createInspectorConnection, this.server = this.start(url);\n }\n start(url) {\n let defaultHostname = \"localhost\", usingDefaultPort = !1, isUnix = !1;\n if (url.startsWith(\"ws+unix://\"))\n isUnix = !0, url = url.slice(10);\n else if (/^[0-9]*$/.test(url))\n url = \"ws://\" + defaultHostname + \":\" + url + generatePath();\n else if (!url || url.startsWith(\"/\"))\n url = \"ws://\" + defaultHostname + \":\" + defaultPort + generatePath(), usingDefaultPort = !0;\n else if (url.includes(\":\") && !url.includes(\"://\"))\n try {\n const insertSlash = !url.includes(\"/\");\n if (url = new URL(\"ws://\" + url).href, insertSlash)\n url += generatePath().slice(1);\n } catch (e) {\n console.error(\"[Inspector]\", \"Failed to parse url\", '\"' + url + '\"'), process.exit(1);\n }\n if (!isUnix)\n try {\n var { hostname, port, pathname } = new URL(url);\n this.url = pathname.toLowerCase();\n } catch (e) {\n console.error(\"[Inspector]\", \"Failed to parse url\", '\"' + url + '\"'), process.exit(1);\n }\n const serveOptions = {\n ...isUnix \? { unix: url } : { hostname },\n development: !1,\n reusePort: !1,\n websocket: {\n idleTimeout: 0,\n open: (socket) => {\n var connection = new DebuggerWithMessageQueue;\n const shouldRefEventLoop = !!socket.data\?.shouldRefEventLoop;\n if (socket.data = connection, this.activeConnections.add(socket), connection.debugger = this.createInspectorConnection(this.scriptExecutionContextId, shouldRefEventLoop, (...msgs) => {\n if (socket.readyState > 1) {\n connection.disconnect();\n return;\n }\n if (connection.messageQueue.length > 0) {\n connection.messageQueue.push(...msgs);\n return;\n }\n for (let i = 0;i < msgs.length; i++)\n if (!socket.sendText(msgs[i])) {\n if (socket.readyState < 2)\n connection.messageQueue.push(...msgs.slice(i));\n return;\n }\n }), !isUnix)\n console.log(\"[Inspector]\", \"Connection #\" + connection.count + \" opened\", \"(\" + new Intl.DateTimeFormat(void 0, {\n timeStyle: \"long\",\n dateStyle: \"short\"\n }).format(new Date) + \")\");\n },\n drain: (socket) => {\n const queue = socket.data.messageQueue;\n for (let i = 0;i < queue.length; i++)\n if (!socket.sendText(queue[i])) {\n socket.data.messageQueue = queue.slice(i);\n return;\n }\n queue.length = 0;\n },\n message: (socket, message) => {\n if (typeof message !== \"string\") {\n console.warn(\"[Inspector]\", \"Received non-string message\");\n return;\n }\n socket.data.send(message);\n },\n close: (socket) => {\n if (socket.data.disconnect(), !isUnix)\n console.log(\"[Inspector]\", \"Connection #\" + socket.data.count + \" closed\", \"(\" + new Intl.DateTimeFormat(void 0, {\n timeStyle: \"long\",\n dateStyle: \"short\"\n }).format(new Date) + \")\");\n this.activeConnections.delete(socket);\n }\n },\n fetch: (req, server2) => {\n let { pathname: pathname2 } = new URL(req.url);\n if (pathname2 = pathname2.toLowerCase(), pathname2 === \"/json/version\")\n return Response.json({\n Browser: navigator.userAgent,\n \"WebKit-Version\": process.versions.webkit,\n \"Bun-Version\": Bun.version,\n \"Bun-Revision\": Bun.revision\n });\n if (!this.url || pathname2 === this.url) {\n const refHeader = req.headers.get(\"Ref-Event-Loop\");\n if (server2.upgrade(req, {\n data: {\n shouldRefEventLoop: !!refHeader && refHeader !== \"0\"\n }\n }))\n return new Response;\n return new Response(\"WebSocket expected\", {\n status: 400\n });\n }\n return new Response(\"Not found\", {\n status: 404\n });\n }\n };\n if (port === \"\")\n port = defaultPort + \"\";\n let portNumber = Number(port);\n var server, lastError;\n if (usingDefaultPort)\n for (let tries = 0;tries < 10 && !server; tries++)\n try {\n if (lastError = void 0, server = Bun.serve({\n ...serveOptions,\n port: portNumber++\n }), isUnix)\n notify();\n } catch (e) {\n lastError = e;\n }\n else\n try {\n if (server = Bun.serve({\n ...serveOptions,\n port: portNumber\n }), isUnix)\n notify();\n } catch (e) {\n lastError = e;\n }\n if (!server) {\n if (console.error(\"[Inspector]\", \"Failed to start server\"), lastError)\n console.error(lastError);\n process.exit(1);\n }\n let textToWrite = \"\";\n function writeToConsole(text) {\n textToWrite += text;\n }\n function flushToConsole() {\n console.write(textToWrite);\n }\n if (!this.url)\n return server;\n if (writeToConsole(dim(\"------------------ Bun Inspector ------------------\\n\")), colors)\n writeToConsole(\"\\x1B[49m\");\n return writeToConsole(\"Listening at:\\n \" + `ws://${hostname}:${server.port}${this.url}` + \"\\n\\nInspect in browser:\\n \" + terminalLink(new URL(`https://debug.bun.sh#${server.hostname}:${server.port}${this.url}`).href) + \"\\n\"), writeToConsole(dim(\"------------------ Bun Inspector ------------------\\n\")), flushToConsole(), server;\n }\n}\nvar listener;\n$ = function start(debuggerId, hostOrPort, createInspectorConnection, sendFn, disconnectFn) {\n try {\n sendFn_ = sendFn, disconnectFn_ = disconnectFn, globalThis.listener = listener ||= new WebSocketListener(debuggerId, hostOrPort, createInspectorConnection);\n } catch (e) {\n console.error(\"Bun Inspector threw an exception\\n\", e), process.exit(1);\n }\n return `http://${listener.server.hostname}:${listener.server.port}${listener.url}`;\n};\nreturn $})\n"_s;
+static constexpr ASCIILiteral InternalDebuggerCode = "(function (){\"use strict\";// src/js/out/tmp/internal/debugger.ts\nvar versionInfo = function() {\n return {\n \"Protocol-Version\": \"1.3\",\n Browser: \"Bun\",\n \"User-Agent\": navigator.userAgent,\n \"WebKit-Version\": process.versions.webkit,\n \"Bun-Version\": Bun.version,\n \"Bun-Revision\": Bun.revision\n };\n}, webSocketWriter = function(ws) {\n return {\n write: (message) => !!ws.sendText(message),\n close: () => ws.close()\n };\n}, socketWriter = function(socket) {\n return {\n write: (message) => !!socket.write(message),\n close: () => socket.end()\n };\n}, bufferedWriter = function(writer) {\n let draining = !1, pendingMessages = [];\n return {\n write: (message) => {\n if (draining || !writer.write(message))\n pendingMessages.push(message);\n return !0;\n },\n drain: () => {\n draining = !0;\n try {\n for (let i = 0;i < pendingMessages.length; i++)\n if (!writer.write(pendingMessages[i])) {\n pendingMessages = pendingMessages.slice(i);\n return;\n }\n } finally {\n draining = !1;\n }\n },\n close: () => {\n writer.close(), pendingMessages.length = 0;\n }\n };\n}, parseUrl = function(url) {\n try {\n if (!url)\n return new URL(randomId(), `ws://${defaultHostname}:${defaultPort}/`);\n else if (url.startsWith(\"/\"))\n return new URL(url, `ws://${defaultHostname}:${defaultPort}/`);\n else if (/^[a-z+]+:\\/\\//i.test(url))\n return new URL(url);\n else if (/^\\d+$/.test(url))\n return new URL(randomId(), `ws://${defaultHostname}:${url}/`);\n else if (!url.includes(\"/\") && url.includes(\":\"))\n return new URL(randomId(), `ws://${url}/`);\n else if (!url.includes(\":\")) {\n const [hostname, pathname] = url.split(\"/\", 2);\n return new URL(`ws://${hostname}:${defaultPort}/${pathname}`);\n } else\n return new URL(randomId(), `ws://${url}`);\n } catch {\n @throwTypeError(`Invalid hostname or URL: '${url}'`);\n }\n}, randomId = function() {\n return Math.random().toString(36).slice(2);\n}, dim = function(string) {\n if (enableANSIColors)\n return `\\x1B[2m${string}\\x1B[22m`;\n return string;\n}, link = function(url) {\n if (enableANSIColors)\n return `\\x1B[1m\\x1B]8;;${url}\\x1B\\\\${url}\\x1B]8;;\\x1B\\\\\\x1B[22m`;\n return url;\n}, reset = function() {\n if (enableANSIColors)\n return \"\\x1B[49m\";\n return \"\";\n}, notify = function(unix) {\n Bun.connect({\n unix,\n socket: {\n open: (socket) => {\n socket.end(\"1\");\n },\n data: () => {\n }\n }\n }).finally(() => {\n });\n}, exit = function(...args) {\n console.error(...args), process.exit(1);\n}, $;\n$ = function(executionContextId, url, createBackend, send, close) {\n let debug;\n try {\n debug = new Debugger(executionContextId, url, createBackend, send, close);\n } catch (error) {\n exit(\"Failed to start inspector:\\n\", error);\n }\n const { protocol, href, host, pathname } = debug.url;\n if (!protocol.includes(\"unix\")) {\n if (console.log(dim(\"--------------------- Bun Inspector ---------------------\"), reset()), console.log(`Listening:\\n ${dim(href)}`), protocol.includes(\"ws\"))\n console.log(`Inspect in browser:\\n ${link(`https://debug.bun.sh/#${host}${pathname}`)}`);\n console.log(dim(\"--------------------- Bun Inspector ---------------------\"), reset());\n }\n const unix = process.env.BUN_INSPECT_NOTIFY;\n if (unix) {\n const { protocol: protocol2, pathname: pathname2 } = parseUrl(unix);\n if (protocol2 === \"unix:\")\n notify(pathname2);\n }\n};\n\nclass Debugger {\n #url;\n #createBackend;\n constructor(executionContextId, url, createBackend, send, close) {\n this.#url = parseUrl(url), this.#createBackend = (refEventLoop, receive) => {\n const backend = createBackend(executionContextId, refEventLoop, receive);\n return {\n write: (message) => {\n return send.call(backend, message), !0;\n },\n close: () => close.call(backend)\n };\n }, this.#listen();\n }\n get url() {\n return this.#url;\n }\n #listen() {\n const { protocol, hostname, port, pathname } = this.#url;\n if (protocol === \"ws:\" || protocol === \"ws+tcp:\") {\n const server = Bun.serve({\n hostname,\n port,\n fetch: this.#fetch.bind(this),\n websocket: this.#websocket\n });\n this.#url.hostname = server.hostname, this.#url.port = `${server.port}`;\n return;\n }\n if (protocol === \"ws+unix:\") {\n Bun.serve({\n unix: pathname,\n fetch: this.#fetch.bind(this),\n websocket: this.#websocket\n });\n return;\n }\n @throwTypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`);\n }\n get #websocket() {\n return {\n idleTimeout: 0,\n closeOnBackpressureLimit: !1,\n open: (ws) => this.#open(ws, webSocketWriter(ws)),\n message: (ws, message) => {\n if (typeof message === \"string\")\n this.#message(ws, message);\n else\n this.#error(ws, new Error(`Unexpected binary message: ${message.toString()}`));\n },\n drain: (ws) => this.#drain(ws),\n close: (ws) => this.#close(ws)\n };\n }\n #fetch(request, server) {\n const { method, url, headers } = request, { pathname } = new URL(url);\n if (method !== \"GET\")\n return new Response(null, {\n status: 405\n });\n switch (pathname) {\n case \"/json/version\":\n return Response.json(versionInfo());\n case \"/json\":\n case \"/json/list\":\n }\n if (!this.#url.protocol.includes(\"unix\") && this.#url.pathname !== pathname)\n return new Response(null, {\n status: 404\n });\n const data = {\n refEventLoop: headers.get(\"Ref-Event-Loop\") === \"0\"\n };\n if (!server.upgrade(request, { data }))\n return new Response(null, {\n status: 426,\n headers: {\n Upgrade: \"websocket\"\n }\n });\n }\n get #socket() {\n return {\n open: (socket) => this.#open(socket, socketWriter(socket)),\n data: (socket, message) => this.#message(socket, message.toString()),\n drain: (socket) => this.#drain(socket),\n close: (socket) => this.#close(socket),\n error: (socket, error) => this.#error(socket, error),\n connectError: (_, error) => exit(\"Failed to start inspector:\\n\", error)\n };\n }\n #open(connection, writer) {\n const { data } = connection, { refEventLoop } = data, client = bufferedWriter(writer), backend = this.#createBackend(refEventLoop, (...messages) => {\n for (let message of messages)\n client.write(message);\n });\n data.client = client, data.backend = backend;\n }\n #message(connection, message) {\n const { data } = connection, { backend } = data;\n backend\?.write(message);\n }\n #drain(connection) {\n const { data } = connection, { client } = data;\n client\?.drain\?.();\n }\n #close(connection) {\n const { data } = connection, { backend } = data;\n backend\?.close();\n }\n #error(connection, error) {\n const { data } = connection, { backend } = data;\n console.error(error), backend\?.close();\n }\n}\nvar defaultHostname = \"localhost\", defaultPort = 6499, { enableANSIColors } = Bun;\nreturn $})\n"_s;
//
//
@@ -481,7 +481,7 @@ static constexpr ASCIILiteral BunSqliteCode = "(function (){\"use strict\";// sr
//
//
-static constexpr ASCIILiteral InternalDebuggerCode = "(function (){\"use strict\";// src/js/out/tmp/internal/debugger.ts\nvar generatePath = function() {\n if (!generatedPath)\n generatedPath = \"/\" + Math.random().toString(36).slice(2);\n return generatedPath;\n}, terminalLink = function(url) {\n if (colors)\n return \"\\x1B[1m\\x1B]8;;\" + url + \"\\x1B\\\\\" + url + \"\\x1B]8;;\\x1B\\\\\" + \"\\x1B[22m\";\n return url;\n}, dim = function(text) {\n if (colors)\n return \"\\x1B[2m\" + text + \"\\x1B[22m\";\n return text;\n}, notify = function() {\n const unix = process.env.BUN_INSPECT_NOTIFY;\n if (!unix || !unix.startsWith(\"unix://\"))\n return;\n Bun.connect({\n unix: unix.slice(7),\n socket: {\n open: (socket) => {\n socket.end(\"1\");\n },\n data: () => {\n }\n }\n }).finally(() => {\n });\n}, $, sendFn_, disconnectFn_, colors = Bun.enableANSIColors && process.env.NO_COLOR !== \"1\", debuggerCounter = 1;\n\nclass DebuggerWithMessageQueue {\n debugger = void 0;\n messageQueue = [];\n count = debuggerCounter++;\n send(msg) {\n sendFn_.call(this.debugger, msg);\n }\n disconnect() {\n disconnectFn_.call(this.debugger), this.messageQueue.length = 0;\n }\n}\nvar defaultPort = 6499, generatedPath = \"\";\n\nclass WebSocketListener {\n server;\n url = \"\";\n createInspectorConnection;\n scriptExecutionContextId = 0;\n activeConnections = new Set;\n constructor(scriptExecutionContextId = 0, url, createInspectorConnection) {\n this.scriptExecutionContextId = scriptExecutionContextId, this.createInspectorConnection = createInspectorConnection, this.server = this.start(url);\n }\n start(url) {\n let defaultHostname = \"localhost\", usingDefaultPort = !1, isUnix = !1;\n if (url.startsWith(\"ws+unix://\"))\n isUnix = !0, url = url.slice(10);\n else if (/^[0-9]*$/.test(url))\n url = \"ws://\" + defaultHostname + \":\" + url + generatePath();\n else if (!url || url.startsWith(\"/\"))\n url = \"ws://\" + defaultHostname + \":\" + defaultPort + generatePath(), usingDefaultPort = !0;\n else if (url.includes(\":\") && !url.includes(\"://\"))\n try {\n const insertSlash = !url.includes(\"/\");\n if (url = new URL(\"ws://\" + url).href, insertSlash)\n url += generatePath().slice(1);\n } catch (e) {\n console.error(\"[Inspector]\", \"Failed to parse url\", '\"' + url + '\"'), process.exit(1);\n }\n if (!isUnix)\n try {\n var { hostname, port, pathname } = new URL(url);\n this.url = pathname.toLowerCase();\n } catch (e) {\n console.error(\"[Inspector]\", \"Failed to parse url\", '\"' + url + '\"'), process.exit(1);\n }\n const serveOptions = {\n ...isUnix \? { unix: url } : { hostname },\n development: !1,\n reusePort: !1,\n websocket: {\n idleTimeout: 0,\n open: (socket) => {\n var connection = new DebuggerWithMessageQueue;\n const shouldRefEventLoop = !!socket.data\?.shouldRefEventLoop;\n if (socket.data = connection, this.activeConnections.add(socket), connection.debugger = this.createInspectorConnection(this.scriptExecutionContextId, shouldRefEventLoop, (...msgs) => {\n if (socket.readyState > 1) {\n connection.disconnect();\n return;\n }\n if (connection.messageQueue.length > 0) {\n connection.messageQueue.push(...msgs);\n return;\n }\n for (let i = 0;i < msgs.length; i++)\n if (!socket.sendText(msgs[i])) {\n if (socket.readyState < 2)\n connection.messageQueue.push(...msgs.slice(i));\n return;\n }\n }), !isUnix)\n console.log(\"[Inspector]\", \"Connection #\" + connection.count + \" opened\", \"(\" + new Intl.DateTimeFormat(void 0, {\n timeStyle: \"long\",\n dateStyle: \"short\"\n }).format(new Date) + \")\");\n },\n drain: (socket) => {\n const queue = socket.data.messageQueue;\n for (let i = 0;i < queue.length; i++)\n if (!socket.sendText(queue[i])) {\n socket.data.messageQueue = queue.slice(i);\n return;\n }\n queue.length = 0;\n },\n message: (socket, message) => {\n if (typeof message !== \"string\") {\n console.warn(\"[Inspector]\", \"Received non-string message\");\n return;\n }\n socket.data.send(message);\n },\n close: (socket) => {\n if (socket.data.disconnect(), !isUnix)\n console.log(\"[Inspector]\", \"Connection #\" + socket.data.count + \" closed\", \"(\" + new Intl.DateTimeFormat(void 0, {\n timeStyle: \"long\",\n dateStyle: \"short\"\n }).format(new Date) + \")\");\n this.activeConnections.delete(socket);\n }\n },\n fetch: (req, server2) => {\n let { pathname: pathname2 } = new URL(req.url);\n if (pathname2 = pathname2.toLowerCase(), pathname2 === \"/json/version\")\n return Response.json({\n Browser: navigator.userAgent,\n \"WebKit-Version\": process.versions.webkit,\n \"Bun-Version\": Bun.version,\n \"Bun-Revision\": Bun.revision\n });\n if (!this.url || pathname2 === this.url) {\n const refHeader = req.headers.get(\"Ref-Event-Loop\");\n if (server2.upgrade(req, {\n data: {\n shouldRefEventLoop: !!refHeader && refHeader !== \"0\"\n }\n }))\n return new Response;\n return new Response(\"WebSocket expected\", {\n status: 400\n });\n }\n return new Response(\"Not found\", {\n status: 404\n });\n }\n };\n if (port === \"\")\n port = defaultPort + \"\";\n let portNumber = Number(port);\n var server, lastError;\n if (usingDefaultPort)\n for (let tries = 0;tries < 10 && !server; tries++)\n try {\n if (lastError = void 0, server = Bun.serve({\n ...serveOptions,\n port: portNumber++\n }), isUnix)\n notify();\n } catch (e) {\n lastError = e;\n }\n else\n try {\n if (server = Bun.serve({\n ...serveOptions,\n port: portNumber\n }), isUnix)\n notify();\n } catch (e) {\n lastError = e;\n }\n if (!server) {\n if (console.error(\"[Inspector]\", \"Failed to start server\"), lastError)\n console.error(lastError);\n process.exit(1);\n }\n let textToWrite = \"\";\n function writeToConsole(text) {\n textToWrite += text;\n }\n function flushToConsole() {\n console.write(textToWrite);\n }\n if (!this.url)\n return server;\n if (writeToConsole(dim(\"------------------ Bun Inspector ------------------\\n\")), colors)\n writeToConsole(\"\\x1B[49m\");\n return writeToConsole(\"Listening at:\\n \" + `ws://${hostname}:${server.port}${this.url}` + \"\\n\\nInspect in browser:\\n \" + terminalLink(new URL(`https://debug.bun.sh#${server.hostname}:${server.port}${this.url}`).href) + \"\\n\"), writeToConsole(dim(\"------------------ Bun Inspector ------------------\\n\")), flushToConsole(), server;\n }\n}\nvar listener;\n$ = function start(debuggerId, hostOrPort, createInspectorConnection, sendFn, disconnectFn) {\n try {\n sendFn_ = sendFn, disconnectFn_ = disconnectFn, globalThis.listener = listener ||= new WebSocketListener(debuggerId, hostOrPort, createInspectorConnection);\n } catch (e) {\n console.error(\"Bun Inspector threw an exception\\n\", e), process.exit(1);\n }\n return `http://${listener.server.hostname}:${listener.server.port}${listener.url}`;\n};\nreturn $})\n"_s;
+static constexpr ASCIILiteral InternalDebuggerCode = "(function (){\"use strict\";// src/js/out/tmp/internal/debugger.ts\nvar versionInfo = function() {\n return {\n \"Protocol-Version\": \"1.3\",\n Browser: \"Bun\",\n \"User-Agent\": navigator.userAgent,\n \"WebKit-Version\": process.versions.webkit,\n \"Bun-Version\": Bun.version,\n \"Bun-Revision\": Bun.revision\n };\n}, webSocketWriter = function(ws) {\n return {\n write: (message) => !!ws.sendText(message),\n close: () => ws.close()\n };\n}, socketWriter = function(socket) {\n return {\n write: (message) => !!socket.write(message),\n close: () => socket.end()\n };\n}, bufferedWriter = function(writer) {\n let draining = !1, pendingMessages = [];\n return {\n write: (message) => {\n if (draining || !writer.write(message))\n pendingMessages.push(message);\n return !0;\n },\n drain: () => {\n draining = !0;\n try {\n for (let i = 0;i < pendingMessages.length; i++)\n if (!writer.write(pendingMessages[i])) {\n pendingMessages = pendingMessages.slice(i);\n return;\n }\n } finally {\n draining = !1;\n }\n },\n close: () => {\n writer.close(), pendingMessages.length = 0;\n }\n };\n}, parseUrl = function(url) {\n try {\n if (!url)\n return new URL(randomId(), `ws://${defaultHostname}:${defaultPort}/`);\n else if (url.startsWith(\"/\"))\n return new URL(url, `ws://${defaultHostname}:${defaultPort}/`);\n else if (/^[a-z+]+:\\/\\//i.test(url))\n return new URL(url);\n else if (/^\\d+$/.test(url))\n return new URL(randomId(), `ws://${defaultHostname}:${url}/`);\n else if (!url.includes(\"/\") && url.includes(\":\"))\n return new URL(randomId(), `ws://${url}/`);\n else if (!url.includes(\":\")) {\n const [hostname, pathname] = url.split(\"/\", 2);\n return new URL(`ws://${hostname}:${defaultPort}/${pathname}`);\n } else\n return new URL(randomId(), `ws://${url}`);\n } catch {\n @throwTypeError(`Invalid hostname or URL: '${url}'`);\n }\n}, randomId = function() {\n return Math.random().toString(36).slice(2);\n}, dim = function(string) {\n if (enableANSIColors)\n return `\\x1B[2m${string}\\x1B[22m`;\n return string;\n}, link = function(url) {\n if (enableANSIColors)\n return `\\x1B[1m\\x1B]8;;${url}\\x1B\\\\${url}\\x1B]8;;\\x1B\\\\\\x1B[22m`;\n return url;\n}, reset = function() {\n if (enableANSIColors)\n return \"\\x1B[49m\";\n return \"\";\n}, notify = function(unix) {\n Bun.connect({\n unix,\n socket: {\n open: (socket) => {\n socket.end(\"1\");\n },\n data: () => {\n }\n }\n }).finally(() => {\n });\n}, exit = function(...args) {\n console.error(...args), process.exit(1);\n}, $;\n$ = function(executionContextId, url, createBackend, send, close) {\n let debug;\n try {\n debug = new Debugger(executionContextId, url, createBackend, send, close);\n } catch (error) {\n exit(\"Failed to start inspector:\\n\", error);\n }\n const { protocol, href, host, pathname } = debug.url;\n if (!protocol.includes(\"unix\")) {\n if (console.log(dim(\"--------------------- Bun Inspector ---------------------\"), reset()), console.log(`Listening:\\n ${dim(href)}`), protocol.includes(\"ws\"))\n console.log(`Inspect in browser:\\n ${link(`https://debug.bun.sh/#${host}${pathname}`)}`);\n console.log(dim(\"--------------------- Bun Inspector ---------------------\"), reset());\n }\n const unix = process.env.BUN_INSPECT_NOTIFY;\n if (unix) {\n const { protocol: protocol2, pathname: pathname2 } = parseUrl(unix);\n if (protocol2 === \"unix:\")\n notify(pathname2);\n }\n};\n\nclass Debugger {\n #url;\n #createBackend;\n constructor(executionContextId, url, createBackend, send, close) {\n this.#url = parseUrl(url), this.#createBackend = (refEventLoop, receive) => {\n const backend = createBackend(executionContextId, refEventLoop, receive);\n return {\n write: (message) => {\n return send.call(backend, message), !0;\n },\n close: () => close.call(backend)\n };\n }, this.#listen();\n }\n get url() {\n return this.#url;\n }\n #listen() {\n const { protocol, hostname, port, pathname } = this.#url;\n if (protocol === \"ws:\" || protocol === \"ws+tcp:\") {\n const server = Bun.serve({\n hostname,\n port,\n fetch: this.#fetch.bind(this),\n websocket: this.#websocket\n });\n this.#url.hostname = server.hostname, this.#url.port = `${server.port}`;\n return;\n }\n if (protocol === \"ws+unix:\") {\n Bun.serve({\n unix: pathname,\n fetch: this.#fetch.bind(this),\n websocket: this.#websocket\n });\n return;\n }\n @throwTypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'unix:')`);\n }\n get #websocket() {\n return {\n idleTimeout: 0,\n closeOnBackpressureLimit: !1,\n open: (ws) => this.#open(ws, webSocketWriter(ws)),\n message: (ws, message) => {\n if (typeof message === \"string\")\n this.#message(ws, message);\n else\n this.#error(ws, new Error(`Unexpected binary message: ${message.toString()}`));\n },\n drain: (ws) => this.#drain(ws),\n close: (ws) => this.#close(ws)\n };\n }\n #fetch(request, server) {\n const { method, url, headers } = request, { pathname } = new URL(url);\n if (method !== \"GET\")\n return new Response(null, {\n status: 405\n });\n switch (pathname) {\n case \"/json/version\":\n return Response.json(versionInfo());\n case \"/json\":\n case \"/json/list\":\n }\n if (!this.#url.protocol.includes(\"unix\") && this.#url.pathname !== pathname)\n return new Response(null, {\n status: 404\n });\n const data = {\n refEventLoop: headers.get(\"Ref-Event-Loop\") === \"0\"\n };\n if (!server.upgrade(request, { data }))\n return new Response(null, {\n status: 426,\n headers: {\n Upgrade: \"websocket\"\n }\n });\n }\n get #socket() {\n return {\n open: (socket) => this.#open(socket, socketWriter(socket)),\n data: (socket, message) => this.#message(socket, message.toString()),\n drain: (socket) => this.#drain(socket),\n close: (socket) => this.#close(socket),\n error: (socket, error) => this.#error(socket, error),\n connectError: (_, error) => exit(\"Failed to start inspector:\\n\", error)\n };\n }\n #open(connection, writer) {\n const { data } = connection, { refEventLoop } = data, client = bufferedWriter(writer), backend = this.#createBackend(refEventLoop, (...messages) => {\n for (let message of messages)\n client.write(message);\n });\n data.client = client, data.backend = backend;\n }\n #message(connection, message) {\n const { data } = connection, { backend } = data;\n backend\?.write(message);\n }\n #drain(connection) {\n const { data } = connection, { client } = data;\n client\?.drain\?.();\n }\n #close(connection) {\n const { data } = connection, { backend } = data;\n backend\?.close();\n }\n #error(connection, error) {\n const { data } = connection, { backend } = data;\n console.error(error), backend\?.close();\n }\n}\nvar defaultHostname = \"localhost\", defaultPort = 6499, { enableANSIColors } = Bun;\nreturn $})\n"_s;
//
//