diff options
author | 2023-08-29 23:44:39 -0700 | |
---|---|---|
committer | 2023-08-29 23:44:39 -0700 | |
commit | f2553d24543d72a777ba60213473332809866cb2 (patch) | |
tree | 61faac4292dbdf1b4d0543e33d3f5d2c792825c5 | |
parent | c028b206bce3f9b5c3cba7899c6bf34856efe43f (diff) | |
download | bun-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.ts | 1130 | ||||
-rw-r--r-- | packages/bun-debug-adapter-protocol/src/debugger/capabilities.ts | 271 | ||||
-rw-r--r-- | packages/bun-debug-adapter-protocol/src/debugger/signal.ts | 6 | ||||
-rw-r--r-- | packages/bun-debug-adapter-protocol/src/debugger/sourcemap.ts | 12 | ||||
-rw-r--r-- | packages/bun-inspector-protocol/src/inspector/websocket.ts | 22 | ||||
-rw-r--r-- | packages/bun-vscode/example/example.test.ts | 19 | ||||
-rw-r--r-- | packages/bun-vscode/example/example.ts | 8 | ||||
-rw-r--r-- | packages/bun-vscode/package.json | 55 | ||||
-rw-r--r-- | packages/bun-vscode/src/features/debug.ts | 57 | ||||
-rw-r--r-- | src/bun.js/bindings/BunDebugger.cpp | 11 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 47 | ||||
-rw-r--r-- | src/cli.zig | 9 | ||||
-rw-r--r-- | src/js/internal/debugger.ts | 580 | ||||
-rw-r--r-- | src/js/out/InternalModuleRegistryConstants.h | 6 |
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; // // |