diff options
Diffstat (limited to 'packages/bun-debug-adapter-protocol/debugger')
11 files changed, 0 insertions, 2697 deletions
diff --git a/packages/bun-debug-adapter-protocol/debugger/__snapshots__/preview.test.ts.snap b/packages/bun-debug-adapter-protocol/debugger/__snapshots__/preview.test.ts.snap deleted file mode 100644 index 0acc17575..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/__snapshots__/preview.test.ts.snap +++ /dev/null @@ -1,143 +0,0 @@ -// Bun Snapshot v1, https://goo.gl/fbAQLP - -exports[`remoteObjectToString 1`] = `"undefined"`; - -exports[`remoteObjectToString 2`] = `"null"`; - -exports[`remoteObjectToString 3`] = `"true"`; - -exports[`remoteObjectToString 4`] = `"false"`; - -exports[`remoteObjectToString 5`] = `"0"`; - -exports[`remoteObjectToString 6`] = `"1"`; - -exports[`remoteObjectToString 7`] = `"3.141592653589793"`; - -exports[`remoteObjectToString 8`] = `"-2.718281828459045"`; - -exports[`remoteObjectToString 9`] = `"NaN"`; - -exports[`remoteObjectToString 10`] = `"Infinity"`; - -exports[`remoteObjectToString 11`] = `"-Infinity"`; - -exports[`remoteObjectToString 12`] = `"0n"`; - -exports[`remoteObjectToString 13`] = `"1n"`; - -exports[`remoteObjectToString 14`] = `"10000000000000n"`; - -exports[`remoteObjectToString 15`] = `"-10000000000000n"`; - -exports[`remoteObjectToString 16`] = `""""`; - -exports[`remoteObjectToString 17`] = `"" ""`; - -exports[`remoteObjectToString 18`] = `""Hello""`; - -exports[`remoteObjectToString 19`] = `""Hello World""`; - -exports[`remoteObjectToString 20`] = `"Array(0)"`; - -exports[`remoteObjectToString 21`] = `"Array(3) [1, 2, 3]"`; - -exports[`remoteObjectToString 22`] = `"Array(4) ["a", 1, null, undefined]"`; - -exports[`remoteObjectToString 23`] = `"Array(2) [1, Array]"`; - -exports[`remoteObjectToString 24`] = `"Array(1) [Array]"`; - -exports[`remoteObjectToString 25`] = `"{}"`; - -exports[`remoteObjectToString 26`] = `"{a: 1}"`; - -exports[`remoteObjectToString 27`] = `"{a: 1, b: 2, c: 3}"`; - -exports[`remoteObjectToString 28`] = `"{a: Object}"`; - -exports[`remoteObjectToString 29`] = ` -"ƒ() { -}" -`; - -exports[`remoteObjectToString 30`] = ` -"ƒ namedFunction() { -}" -`; - -exports[`remoteObjectToString 31`] = ` -"class { -}" -`; - -exports[`remoteObjectToString 32`] = ` -"class namedClass { -}" -`; - -exports[`remoteObjectToString 33`] = ` -"class namedClass { - a() { - } - b = 1; - c = [ - null, - undefined, - "a", - { - a: 1, - b: 2, - c: 3 - } - ]; -}" -`; - -exports[`remoteObjectToString 34`] = `"Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)"`; - -exports[`remoteObjectToString 35`] = `"Invalid Date"`; - -exports[`remoteObjectToString 36`] = `"/(?:)/"`; - -exports[`remoteObjectToString 37`] = `"/abc/"`; - -exports[`remoteObjectToString 38`] = `"/abc/g"`; - -exports[`remoteObjectToString 39`] = `"/abc/"`; - -exports[`remoteObjectToString 40`] = `"Set(0)"`; - -exports[`remoteObjectToString 41`] = `"Set(3) [1, 2, 3]"`; - -exports[`remoteObjectToString 42`] = `"WeakSet(0)"`; - -exports[`remoteObjectToString 43`] = `"WeakSet(3) [{a: 1}, {b: 2}, {c: 3}]"`; - -exports[`remoteObjectToString 44`] = `"Map(0)"`; - -exports[`remoteObjectToString 45`] = `"Map(3) {"a" => 1, "b" => 2, "c" => 3}"`; - -exports[`remoteObjectToString 46`] = `"WeakMap(0)"`; - -exports[`remoteObjectToString 47`] = `"WeakMap(3) {{a: 1} => 1, {b: 2} => 2, {c: 3} => 3}"`; - -exports[`remoteObjectToString 48`] = `"Symbol()"`; - -exports[`remoteObjectToString 49`] = `"Symbol(namedSymbol)"`; - -exports[`remoteObjectToString 50`] = `"Error"`; - -exports[`remoteObjectToString 51`] = `"TypeError: This is a TypeError"`; - -exports[`remoteObjectToString 52`] = `"Headers {append: ƒ, delete: ƒ, get: ƒ, getAll: ƒ, has: ƒ, …}"`; - -exports[`remoteObjectToString 53`] = `"Headers {a: "1", append: ƒ, b: "2", delete: ƒ, get: ƒ, …}"`; - -exports[`remoteObjectToString 54`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, cache: "default", …}"`; - -exports[`remoteObjectToString 55`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, cache: "default", …}"`; - -exports[`remoteObjectToString 56`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, clone: ƒ, …}"`; - -exports[`remoteObjectToString 57`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, clone: ƒ, …}"`; diff --git a/packages/bun-debug-adapter-protocol/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/debugger/adapter.ts deleted file mode 100644 index 9dc55fe38..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/adapter.ts +++ /dev/null @@ -1,1692 +0,0 @@ -import type { DAP } from ".."; -// @ts-ignore: FIXME - there is something wrong with the types -import type { JSC, InspectorListener } from "../../bun-inspector-protocol"; -import { WebSocketInspector } from "../../bun-inspector-protocol"; -import type { ChildProcess } from "node:child_process"; -import { spawn, spawnSync } from "node:child_process"; -import capabilities from "./capabilities"; -import { Location, SourceMap } from "./sourcemap"; -import { remoteObjectToString } from "./preview"; -import { compare, parse } from "semver"; - -type InitializeRequest = DAP.InitializeRequest & { - supportsConfigurationDoneRequest?: boolean; -}; - -type LaunchRequest = DAP.LaunchRequest & { - runtime?: string; - program?: string; - cwd?: string; - args?: string[]; - env?: Record<string, string>; - inheritEnv?: boolean; - watch?: boolean | "hot"; -}; - -type AttachRequest = DAP.AttachRequest & { - url?: string; -}; - -type Source = DAP.Source & { - scriptId: string; - sourceMap: SourceMap; -} & ( - | { - sourceId: string; - path: string; - sourceReference?: undefined; - } - | { - sourceId: number; - path?: undefined; - sourceReference: number; - } - ); - -type Breakpoint = DAP.Breakpoint & { - id: number; - breakpointId: string; - source: Source; -}; - -type FunctionBreakpoint = DAP.Breakpoint & { - id: number; - name: string; -}; - -type StackFrame = DAP.StackFrame & { - scriptId: string; - callFrameId: string; - source?: Source; - scopes?: Scope[]; -}; - -type Scope = DAP.Scope & { - source?: Source; -}; - -type Variable = DAP.Variable & { - objectId?: string; - type: JSC.Runtime.RemoteObject["type"] | JSC.Runtime.RemoteObject["subtype"]; -}; - -type IDebugAdapter = { - [E in keyof DAP.EventMap]?: (event: DAP.EventMap[E]) => void; -} & { - [R in keyof DAP.RequestMap]?: ( - request: DAP.RequestMap[R], - ) => void | DAP.ResponseMap[R] | Promise<void | DAP.ResponseMap[R]>; -}; - -export type DebugAdapterOptions = { - sendToAdapter(message: DAP.Request | DAP.Response | DAP.Event): Promise<void>; -}; - -// This adapter only support single-threaded debugging, -// which means that there is only one thread at a time. -const threadId = 1; - -export class DebugAdapter implements IDebugAdapter, InspectorListener { - #sendToAdapter: DebugAdapterOptions["sendToAdapter"]; - #inspector: WebSocketInspector; - #sourceId: number; - #pendingSources: Map<string, ((source: Source) => void)[]>; - #sources: Map<string | number, Source>; - #stackFrames: StackFrame[]; - #stopped?: DAP.StoppedEvent["reason"]; - #breakpointId: number; - #breakpoints: Breakpoint[]; - #functionBreakpoints: Map<string, FunctionBreakpoint>; - #variables: (Variable | Variable[])[]; - #process?: ChildProcess; - #initialized?: InitializeRequest; - #launched?: LaunchRequest; - #connected?: boolean; - #terminated?: boolean; - #url?: URL; - - constructor({ sendToAdapter }: DebugAdapterOptions) { - this.#inspector = new WebSocketInspector({ listener: this }); - this.#sendToAdapter = sendToAdapter; - this.#sourceId = 1; - this.#pendingSources = new Map(); - this.#sources = new Map(); - this.#stackFrames = []; - this.#stopped = undefined; - this.#breakpointId = 1; - this.#breakpoints = []; - this.#functionBreakpoints = new Map(); - this.#variables = [{ name: "", value: "", type: undefined, variablesReference: 0 }]; - } - - async accept(message: DAP.Request | DAP.Response | DAP.Event): Promise<void> { - const { type } = message; - - switch (type) { - case "request": - return this.#acceptRequest(message); - } - - throw new Error(`Not supported: ${type}`); - } - - async #acceptRequest(request: DAP.Request): Promise<void> { - const { seq, command, arguments: args } = request; - - let response; - try { - if (!(command! in this)) { - throw new Error(`Not supported: ${command}`); - } - response = await this[command as keyof this](args); - } catch (error) { - console.error(error); - const { message } = unknownToError(error); - return this.#sendToAdapter({ - type: "response", - success: false, - message, - request_seq: seq, - seq: 0, - command, - }); - } - - return this.#sendToAdapter({ - type: "response", - success: true, - request_seq: seq, - seq: 0, - command, - body: response, - }); - } - - async #send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>( - method: M, - params?: JSC.RequestMap[M], - ): Promise<JSC.ResponseMap[M]> { - return this.#inspector.send(method, params); - } - - async #emit<E extends keyof DAP.EventMap>(name: E, body?: DAP.EventMap[E]): Promise<void> { - await this.#sendToAdapter({ - type: "event", - seq: 0, - event: name, - body, - }); - } - - initialize(request: InitializeRequest): DAP.InitializeResponse { - const { clientID, supportsConfigurationDoneRequest } = (this.#initialized = request); - - this.#send("Inspector.enable"); - this.#send("Runtime.enable"); - this.#send("Console.enable"); - this.#send("Debugger.enable"); - this.#send("Debugger.setAsyncStackTraceDepth", { depth: 200 }); - this.#send("Debugger.setPauseOnDebuggerStatements", { enabled: true }); - this.#send("Debugger.setBlackboxBreakpointEvaluations", { blackboxBreakpointEvaluations: true }); - this.#send("Debugger.setBreakpointsActive", { active: true }); - - // If the client will not send a `configurationDone` request, then we need to - // tell the debugger that everything is ready. - if (!supportsConfigurationDoneRequest && clientID !== "vscode") { - this.#send("Inspector.initialized"); - } - - // Tell the client what capabilities this adapter supports. - return capabilities; - } - - 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) { - 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 }); - } - - // Tell the debugger that everything is ready. - this.#send("Inspector.initialized"); - } - - async launch(request: DAP.LaunchRequest): Promise<void> { - this.#launched = request; - - try { - await this.#launch(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. - const { message } = unknownToError(error); - this.#emit("output", { - category: "stderr", - output: `Failed to start debugger.\n${message}`, - }); - this.#emit("terminated"); - } - } - - async #launch(request: LaunchRequest): Promise<void> { - if (this.#process?.exitCode === null) { - throw new Error("Another program is already running. Did you terminate the last session?"); - } - - const { program, runtime = "bun", args = [], cwd, env = {}, inheritEnv = true, watch = true } = request; - if (!program) { - throw new Error("No program specified. Did you set the 'program' property in your launch.json?"); - } - - if (!isJavaScript(program)) { - throw new Error("Program must be a JavaScript or TypeScript file."); - } - - const argz = ["--inspect-wait=0", ...args]; - if (watch) { - argz.push(watch === "hot" ? "--hot" : "--watch"); - } - console.log(argz); - - const subprocess = spawn(runtime, [...argz, program], { - stdio: ["ignore", "pipe", "pipe", "pipe"], - cwd, - env: inheritEnv ? { ...process.env, ...env } : env, - }); - - subprocess.on("spawn", () => { - this.#process = subprocess; - this.#emit("process", { - name: program, - systemProcessId: subprocess.pid, - isLocalProcess: true, - startMethod: "launch", - }); - }); - - subprocess.on("exit", code => { - this.#emit("exited", { - exitCode: code ?? -1, - }); - this.#process = undefined; - }); - - const stdout: string[] = []; - subprocess.stdout!.on("data", data => { - if (!this.#url) { - const text = data.toString(); - stdout.push(text); - const url = (this.#url = parseUrlMaybe(text)); - this.#inspector.start(url); - } else if (stdout.length) { - stdout.length = 0; - } - }); - - const stderr: string[] = []; - subprocess.stderr!.on("data", data => { - if (!this.#url) { - stderr.push(data.toString()); - } else if (stderr.length) { - stderr.length = 0; - } - }); - - const start = new Promise<undefined>(resolve => { - subprocess.on("spawn", () => resolve(undefined)); - }); - - const exitOrError = new Promise<number | string | Error>(resolve => { - subprocess.on("exit", (code, signal) => resolve(code ?? signal ?? -1)); - subprocess.on("error", resolve); - }); - - const reason = await Promise.race([start, exitOrError]); - - if (reason instanceof Error) { - const { message } = reason; - throw new Error(`Program could not be started.\n${message}`); - } - - if (reason !== undefined) { - throw new Error(`Program exited with code ${reason} before the debugger could attached.`); - } - - for (let retries = 0; !this.#url && retries < 10; retries++) { - await new Promise(resolve => setTimeout(resolve, 100 * retries)); - } - - if (this.#url) { - return; - } - - if (subprocess.exitCode === null && !subprocess.kill() && !subprocess.kill("SIGKILL")) { - this.#emit("output", { - category: "debug console", - output: `Failed to kill process ${subprocess.pid}\n`, - }); - } - - const { stdout: version } = spawnSync(runtime, ["--version"], { stdio: "pipe", encoding: "utf-8" }); - - if (parse(version, true) && compare("0.8.0", version, true)) { - throw new Error( - `Bun v${version.trim()} does not have debugger support. Please upgrade to v0.8 or later by running: \`bun upgrade\``, - ); - } - - for (const message of stderr) { - this.#emit("output", { - category: "stderr", - output: message, - source: { - path: program, - }, - }); - } - - for (const message of stdout) { - this.#emit("output", { - category: "stdout", - output: message, - source: { - path: program, - }, - }); - } - - throw new Error("Program started, but the debugger could not be attached."); - } - - attach(request: AttachRequest): void { - const { url } = request; - this.#inspector.start(parseUrl(url)); - } - - terminate(): void { - this.#terminated = true; - this.#process?.kill(); - } - - disconnect(request: DAP.DisconnectRequest): void { - const { terminateDebuggee } = request; - - if (terminateDebuggee) { - this.terminate(); - } - - this.close(); - } - - 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 }); - - return { - content: scriptSource, - }; - } - - async threads(request: DAP.ThreadsRequest): Promise<DAP.ThreadsResponse> { - return { - threads: [ - { - id: threadId, - name: "Main Thread", - }, - ], - }; - } - - async pause(): Promise<void> { - await this.#send("Debugger.pause"); - this.#stopped = "pause"; - } - - async continue(): Promise<void> { - await this.#send("Debugger.resume"); - this.#stopped = undefined; - } - - async next(): Promise<void> { - await this.#send("Debugger.stepNext"); - this.#stopped = "step"; - } - - async stepIn(): Promise<void> { - await this.#send("Debugger.stepInto"); - this.#stopped = "step"; - } - - async stepOut(): Promise<void> { - await this.#send("Debugger.stepOut"); - this.#stopped = "step"; - } - - async breakpointLocations(request: DAP.BreakpointLocationsRequest): Promise<DAP.BreakpointLocationsResponse> { - 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, - }); - - return { - breakpoints: locations.map(location => this.#originalLocation(source, location)), - }; - } - - #generatedLocation(source: Source, line?: number, column?: number): JSC.Debugger.Location { - const { sourceMap, scriptId, path } = source; - const { line: gline, column: gcolumn } = sourceMap.generatedLocation({ - line: this.#lineTo0BasedLine(line), - column: this.#columnTo0BasedColumn(column), - url: path, - }); - - return { - scriptId, - lineNumber: gline, - columnNumber: gcolumn, - }; - } - - #lineTo0BasedLine(line?: number): number { - if (!numberIsValid(line)) { - return 0; - } - if (this.#initialized?.linesStartAt1) { - return line - 1; - } - return line; - } - - #columnTo0BasedColumn(column?: number): number { - if (!numberIsValid(column)) { - return 0; - } - if (this.#initialized?.columnsStartAt1) { - return column - 1; - } - return column; - } - - #originalLocation( - source: Source, - line?: number | JSC.Debugger.Location, - column?: number, - ): { line: number; column: number } { - if (typeof line === "object") { - const { lineNumber, columnNumber } = line; - line = lineNumber; - column = columnNumber; - } - - const { sourceMap } = source; - const { line: oline, column: ocolumn } = sourceMap.originalLocation({ line, column }); - - return { - line: this.#lineFrom0BasedLine(oline), - column: this.#columnFrom0BasedColumn(ocolumn), - }; - } - - #lineFrom0BasedLine(line?: number): number { - if (this.#initialized?.linesStartAt1) { - return numberIsValid(line) ? line + 1 : 1; - } - return numberIsValid(line) ? line : 0; - } - - #columnFrom0BasedColumn(column?: number): number { - if (this.#initialized?.columnsStartAt1) { - return numberIsValid(column) ? column + 1 : 1; - } - return numberIsValid(column) ? column : 0; - } - - 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 oldBreakpoints = this.#getBreakpoints(sourceId); - - const breakpoints = await Promise.all( - requests!.map(async ({ line, column, ...options }) => { - const breakpoint = this.#getBreakpoint(sourceId, line, column); - if (breakpoint) { - return breakpoint; - } - - const location = this.#generatedLocation(source, line, column); - try { - const { breakpointId, actualLocation } = await this.#send("Debugger.setBreakpoint", { - location, - options: breakpointOptions(options), - }); - - const originalLocation = this.#originalLocation(source, actualLocation); - return this.#addBreakpoint({ - id: this.#breakpointId++, - breakpointId, - source, - verified: true, - ...originalLocation, - }); - } catch (error) { - const { message } = unknownToError(error); - // If there was an error setting the breakpoint, - // mark it as unverified and add a message. - const breakpointId = this.#breakpointId++; - return this.#addBreakpoint({ - id: breakpointId, - breakpointId: `${breakpointId}`, - line, - column, - source, - verified: false, - message, - }); - } - }), - ); - - 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); - } - }), - ); - - return { - breakpoints, - }; - } - - #getBreakpoints(sourceId: string | number): Breakpoint[] { - const breakpoints: Breakpoint[] = []; - - for (const breakpoint of this.#breakpoints.values()) { - const { source } = breakpoint; - if (sourceId === sourceToId(source)) { - breakpoints.push(breakpoint); - } - } - - return breakpoints; - } - - #getBreakpoint(sourceId: string | number, line?: number, column?: number): Breakpoint | undefined { - for (const breakpoint of this.#getBreakpoints(sourceId)) { - if (isSameLocation(breakpoint, { line, column })) { - return breakpoint; - } - } - return undefined; - } - - #addBreakpoint(breakpoint: Breakpoint): Breakpoint { - this.#breakpoints.push(breakpoint); - - this.#emit("breakpoint", { - reason: "changed", - breakpoint, - }); - - return breakpoint; - } - - #removeBreakpoint(breakpointId: string): void { - const breakpoint = this.#breakpoints.find(({ breakpointId: id }) => id === breakpointId); - if (!breakpoint) { - return; - } - - this.#breakpoints = this.#breakpoints.filter(({ breakpointId: id }) => id !== breakpointId); - this.#emit("breakpoint", { - reason: "removed", - breakpoint, - }); - } - - async setFunctionBreakpoints( - request: DAP.SetFunctionBreakpointsRequest, - ): Promise<DAP.SetFunctionBreakpointsResponse> { - const { breakpoints: requests } = request; - - const oldBreakpoints = this.#getFunctionBreakpoints(); - - const breakpoints = await Promise.all( - requests.map(async ({ name, ...options }) => { - const breakpoint = this.#getFunctionBreakpoint(name); - if (breakpoint) { - return breakpoint; - } - - try { - await this.#send("Debugger.addSymbolicBreakpoint", { - symbol: name, - caseSensitive: true, - isRegex: false, - options: breakpointOptions(options), - }); - } catch (error) { - const { message } = unknownToError(error); - return this.#addFunctionBreakpoint({ - id: this.#breakpointId++, - name, - verified: false, - message, - }); - } - - return this.#addFunctionBreakpoint({ - id: this.#breakpointId++, - name, - verified: true, - }); - }), - ); - - await Promise.all( - oldBreakpoints.map(async ({ name }) => { - const isRemoved = !breakpoints.filter(({ name: n }) => name === n).length; - if (isRemoved) { - await this.#send("Debugger.removeSymbolicBreakpoint", { - symbol: name, - caseSensitive: true, - isRegex: false, - }); - this.#removeFunctionBreakpoint(name); - } - }), - ); - - return { - breakpoints, - }; - } - - #getFunctionBreakpoints(): FunctionBreakpoint[] { - return [...this.#functionBreakpoints.values()]; - } - - #getFunctionBreakpoint(name: string): FunctionBreakpoint | undefined { - return this.#functionBreakpoints.get(name); - } - - #addFunctionBreakpoint(breakpoint: FunctionBreakpoint): FunctionBreakpoint { - const { name } = breakpoint; - this.#functionBreakpoints.set(name, breakpoint); - this.#emit("breakpoint", { - reason: "changed", - breakpoint, - }); - return breakpoint; - } - - #removeFunctionBreakpoint(name: string): void { - const breakpoint = this.#functionBreakpoints.get(name); - if (!breakpoint || !this.#functionBreakpoints.delete(name)) { - return; - } - this.#emit("breakpoint", { - reason: "removed", - breakpoint, - }); - } - - async setExceptionBreakpoints(request: DAP.SetExceptionBreakpointsRequest): Promise<void> { - const { filters, filterOptions } = request; - - const filterIds = [...filters]; - if (filterOptions) { - filterIds.push(...filterOptions.map(({ filterId }) => filterId)); - } - - await this.#send("Debugger.setPauseOnExceptions", { - state: exceptionFiltersToPauseOnExceptionsState(filterIds), - }); - } - - async evaluate(request: DAP.EvaluateRequest): Promise<DAP.EvaluateResponse> { - const { expression, frameId, context } = request; - const callFrameId = this.#getCallFrameId(frameId); - - const { result, wasThrown } = await this.#evaluate(expression, callFrameId); - const { className } = result; - - if (context === "hover" && wasThrown && (className === "SyntaxError" || className === "ReferenceError")) { - return { - result: "", - variablesReference: 0, - }; - } - - const { name, value, ...variable } = this.#addVariable(result); - return { - ...variable, - result: value, - }; - } - - async #evaluate(expression: string, callFrameId?: string): Promise<JSC.Runtime.EvaluateResponse> { - const method = callFrameId ? "Debugger.evaluateOnCallFrame" : "Runtime.evaluate"; - - return this.#send(method, { - callFrameId, - expression: sanitizeExpression(expression), - generatePreview: true, - emulateUserGesture: true, - doNotPauseOnExceptionsAndMuteConsole: true, - includeCommandLineAPI: true, - }); - } - - restart(): void { - this.initialize(this.#initialized!); - this.configurationDone(); - - this.#emit("output", { - category: "debug console", - output: "Debugger reloaded.\n", - }); - } - - ["Inspector.connected"](): void { - if (this.#connected) { - this.restart(); - return; - } - - this.#connected = true; - - this.#emit("output", { - category: "debug console", - output: "Debugger attached.\n", - }); - - this.#emit("initialized"); - } - - ["Inspector.disconnected"](error?: Error): void { - if (this.#connected && this.#process?.exitCode === null) { - this.#url = undefined; - return; - } - - this.#emit("output", { - category: "debug console", - output: "Debugger detached.\n", - }); - - if (error && !this.#terminated) { - const { message } = error; - this.#emit("output", { - category: "stderr", - output: `${message}\n`, - }); - } - - this.#emit("terminated"); - this.#reset(); - } - - async ["Debugger.scriptParsed"](event: JSC.Debugger.ScriptParsedEvent): Promise<void> { - const { url, scriptId, sourceMapURL } = event; - - // If no url is present, the script is from a `evaluate` request. - if (!url) { - return; - } - - // Sources can be retrieved in two ways: - // 1. If it has a `path`, the client retrieves the source from the file system. - // 2. If it has a `sourceReference`, the client sends a `source` request. - // Moreover, the code is usually shown in a read-only editor. - const isUserCode = url.startsWith("/"); - const sourceMap = SourceMap(sourceMapURL); - const name = sourceName(url); - const presentationHint = sourcePresentationHint(url); - - if (isUserCode) { - this.#addSource({ - sourceId: url, - scriptId, - name, - path: url, - presentationHint, - sourceMap, - }); - return; - } - - const sourceReference = this.#sourceId++; - this.#addSource({ - sourceId: sourceReference, - scriptId, - name, - sourceReference, - presentationHint, - sourceMap, - }); - } - - ["Debugger.scriptFailedToParse"](event: JSC.Debugger.ScriptFailedToParseEvent): void { - const { url, errorMessage, errorLine } = event; - - this.#emit("output", { - category: "stderr", - output: errorMessage, - line: this.#lineFrom0BasedLine(errorLine), - source: { - path: url || undefined, - }, - }); - } - - ["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; - } - } - } - - this.#stackFrames.length = 0; - this.#stopped ||= stoppedReason(reason); - for (const callFrame of callFrames) { - this.#addStackFrame(callFrame); - } - if (asyncStackTrace) { - this.#addAsyncStackTrace(asyncStackTrace); - } - - let hitBreakpointIds: number[] | undefined; - // Depending on the reason, the `data` property is set to the reason - // why the execution was paused. For example, if the reason is "breakpoint", - // the `data` property is set to the breakpoint ID. - if (data) { - if (reason === "exception") { - const remoteObject = data as JSC.Runtime.RemoteObject; - } - - if (reason === "FunctionCall") { - const { name } = data as { name: string }; - const breakpoint = this.#getFunctionBreakpoint(name); - if (breakpoint) { - const { id } = breakpoint; - hitBreakpointIds = [id]; - } - } - - if (reason === "Breakpoint") { - const { breakpointId: hitBreakpointId } = data as { breakpointId: string }; - for (const { id, breakpointId } of this.#breakpoints.values()) { - if (breakpointId === hitBreakpointId) { - hitBreakpointIds = [id]; - break; - } - } - } - } - - this.#emit("stopped", { - threadId, - reason: this.#stopped, - hitBreakpointIds, - }); - } - - ["Debugger.resumed"](event: JSC.Debugger.ResumedEvent): void { - this.#stackFrames.length = 0; - this.#stopped = undefined; - this.#emit("continued", { - threadId, - }); - } - - ["Console.messageAdded"](event: JSC.Console.MessageAddedEvent): void { - const { message } = event; - const { type, level, text, parameters, line, column, stackTrace } = message; - - let output: string; - let variablesReference: number | undefined; - - if (parameters?.length) { - output = ""; - - const variables = parameters.map((parameter, i) => { - const variable = this.#addVariable(parameter, { name: `${i}` }); - - const { value } = variable; - output += value + " "; - - return variable; - }); - - if (variables.length === 1) { - const [{ variablesReference: reference }] = variables; - variablesReference = reference; - } else { - variablesReference = this.#setVariable(variables); - } - } else { - output = text; - } - - if (!output.endsWith("\n")) { - output += "\n"; - } - - const color = consoleLevelToAnsiColor(level); - if (color) { - output = `${color}${output}`; - } - - if (variablesReference) { - variablesReference = this.#setVariable([ - { - name: "", - value: "", - type: undefined, - variablesReference, - }, - ]); - } - - let source: Source | undefined; - if (stackTrace) { - const { callFrames } = stackTrace; - if (callFrames.length) { - const { scriptId } = callFrames.at(-1)!; - source = this.#getSourceIfPresent(scriptId); - } - } - - let location: Location | {} = {}; - if (source) { - location = this.#originalLocation(source, line, column); - } - - this.#emit("output", { - category: "debug console", - group: consoleMessageGroup(type), - output, - variablesReference, - source, - ...location, - }); - } - - #addSource(source: Source): Source { - const { sourceId, scriptId, path, sourceReference } = source; - - const oldSource = this.#getSourceIfPresent(sourceId); - if (oldSource) { - const { scriptId, path: oldPath } = oldSource; - // For now, the script ID will always change. - // Could that not be the case in the future? - this.#sources.delete(scriptId); - - // If the path changed or the source has a source reference, - // the old source should be marked as removed. - if (path !== oldPath || sourceReference) { - this.#emit("loadedSource", { - reason: "removed", - source: oldSource, - }); - } - } - - this.#sources.set(sourceId, source); - this.#sources.set(scriptId, source); - - this.#emit("loadedSource", { - // If the reason is "changed", the source will be retrieved using - // the `source` command, which is why it cannot be set when `path` is present. - reason: oldSource && !path ? "changed" : "new", - source, - }); - - if (!path) { - return source; - } - - // If there are any pending requests for this source by its path, - // resolve them now that the source has been loaded. - const resolves = this.#pendingSources.get(path); - if (resolves) { - this.#pendingSources.delete(path); - for (const resolve of resolves) { - resolve(source); - } - } - - return source; - } - - loadedSources(): DAP.LoadedSourcesResponse { - const sources = new Map(); - - // Since there are duplicate keys for each source, - // (e.g. scriptId, path, sourceReference, etc.) it needs to be deduped. - for (const source of this.#sources.values()) { - const { sourceId } = source; - sources.set(sourceId, source); - } - - return { - sources: [...sources.values()], - }; - } - - #getSourceIfPresent(sourceId: string | number): Source | undefined { - return this.#sources.get(sourceId); - } - - async #getSource(sourceId: string | number): Promise<Source> { - const source = this.#getSourceIfPresent(sourceId); - - if (source) { - return source; - } - - // If the source does not have a path or is a builtin module, - // it cannot be retrieved from the file system. - if (typeof sourceId === "number" || !sourceId.startsWith("/")) { - throw new Error(`Source not found: ${sourceId}`); - } - - // If the source is not present, it may not have been loaded yet. - // In that case, wait for it to be loaded. - let resolves = this.#pendingSources.get(sourceId); - if (!resolves) { - this.#pendingSources.set(sourceId, (resolves = [])); - } - - return new Promise(resolve => { - resolves!.push(resolve); - }); - } - - async stackTrace(request: DAP.StackTraceRequest): Promise<DAP.StackTraceResponse> { - const { length } = this.#stackFrames; - const { startFrame = 0, levels } = request; - const endFrame = levels ? startFrame + levels : length; - - return { - totalFrames: length, - stackFrames: this.#stackFrames.slice(startFrame, endFrame), - }; - } - - async scopes(request: DAP.ScopesRequest): Promise<DAP.ScopesResponse> { - const { frameId } = request; - - for (const stackFrame of this.#stackFrames) { - const { id, scopes } = stackFrame; - if (id !== frameId || !scopes) { - continue; - } - return { - scopes, - }; - } - - return { - scopes: [], - }; - } - - #getCallFrameId(frameId?: number): string | undefined { - for (const { id, callFrameId } of this.#stackFrames) { - if (id === frameId) { - return callFrameId; - } - } - return undefined; - } - - #addStackFrame(callFrame: JSC.Debugger.CallFrame): StackFrame { - const { callFrameId, functionName, location, scopeChain } = callFrame; - const { scriptId } = location; - const source = this.#getSourceIfPresent(scriptId); - - let originalLocation: Location; - if (source) { - originalLocation = this.#originalLocation(source, location); - } else { - const { lineNumber, columnNumber } = location; - originalLocation = { - line: this.#lineFrom0BasedLine(lineNumber), - column: this.#columnFrom0BasedColumn(columnNumber), - }; - } - - const { line, column } = originalLocation; - const scopes: Scope[] = []; - const stackFrame: StackFrame = { - callFrameId, - scriptId, - id: this.#stackFrames.length, - name: functionName || "<anonymous>", - line, - column, - presentationHint: stackFramePresentationHint(source?.path), - source, - scopes, - }; - this.#stackFrames.push(stackFrame); - - for (const scope of scopeChain) { - const { name, type, location, object, empty } = scope; - if (empty) { - continue; - } - - const { variablesReference } = this.#addVariable(object); - const presentationHint = scopePresentationHint(type); - const title = presentationHint ? titleize(presentationHint) : "Unknown"; - const displayName = name ? `${title}: ${name}` : title; - - let originalLocation: Location | undefined; - if (location) { - const { scriptId } = location; - const source = this.#getSourceIfPresent(scriptId); - - if (source) { - originalLocation = this.#originalLocation(source, location); - } else { - const { lineNumber, columnNumber } = location; - originalLocation = { - line: this.#lineFrom0BasedLine(lineNumber), - column: this.#columnFrom0BasedColumn(columnNumber), - }; - } - } - - const { line, column } = originalLocation ?? {}; - scopes.push({ - name: displayName, - presentationHint, - expensive: presentationHint === "globals", - variablesReference, - line, - column, - source, - }); - } - - return stackFrame; - } - - #addAsyncStackTrace(stackTrace: JSC.Console.StackTrace): void { - const { callFrames, parentStackTrace } = stackTrace; - - for (const callFrame of callFrames) { - this.#addAsyncStackFrame(callFrame); - } - - if (parentStackTrace) { - this.#addAsyncStackTrace(parentStackTrace); - } - } - - #addAsyncStackFrame(callFrame: JSC.Console.CallFrame): StackFrame { - const { scriptId, functionName } = callFrame; - const callFrameId = callFrameToId(callFrame); - const source = this.#getSourceIfPresent(scriptId); - - let originalLocation: Location; - if (source) { - originalLocation = this.#originalLocation(source, callFrame); - } else { - const { lineNumber, columnNumber } = callFrame; - originalLocation = { - line: this.#lineFrom0BasedLine(lineNumber), - column: this.#columnFrom0BasedColumn(columnNumber), - }; - } - - const { line, column } = originalLocation; - const stackFrame: StackFrame = { - callFrameId, - scriptId, - id: this.#stackFrames.length, - name: functionName || "<anonymous>", - line, - column, - source, - presentationHint: stackFramePresentationHint(source?.path), - canRestart: false, - }; - this.#stackFrames.push(stackFrame); - - return stackFrame; - } - - async variables(request: DAP.VariablesRequest): Promise<DAP.VariablesResponse> { - const { variablesReference, start, count } = request; - const variable = this.#variables[variablesReference]; - - let variables: Variable[]; - if (!variable) { - variables = []; - } else if (Array.isArray(variable)) { - variables = variable; - } else { - variables = await this.#getVariables(variable, start, count); - } - - return { - variables: variables.sort(variablesSortBy), - }; - } - - #setVariable(variable: Variable | Variable[]): number { - const variablesReference = this.#variables.length; - - this.#variables.push(variable); - - return variablesReference; - } - - #addVariable(remoteObject: JSC.Runtime.RemoteObject, propertyDescriptor?: JSC.Runtime.PropertyDescriptor): Variable { - const { objectId, type, subtype, size } = remoteObject; - const variablesReference = objectId ? this.#variables.length : 0; - - const variable: Variable = { - objectId, - name: propertyDescriptorToName(propertyDescriptor), - type: subtype || type, - value: remoteObjectToString(remoteObject), - variablesReference, - indexedVariables: isIndexed(subtype) ? size : undefined, - namedVariables: isNamedIndexed(subtype) ? size : undefined, - presentationHint: remoteObjectToVariablePresentationHint(remoteObject, propertyDescriptor), - }; - this.#setVariable(variable); - - return variable; - } - - async #getVariables(variable: Variable, offset?: number, count?: number): Promise<Variable[]> { - const { objectId, type, indexedVariables, namedVariables } = variable; - - if (!objectId || type === "symbol") { - return []; - } - - const { properties, internalProperties } = await this.#send("Runtime.getDisplayableProperties", { - objectId, - generatePreview: true, - }); - - const variables: Variable[] = []; - for (const property of properties) { - variables.push(...this.#getVariable(property)); - } - - if (internalProperties) { - for (const property of internalProperties) { - variables.push(...this.#getVariable({ ...property, configurable: false })); - } - } - - const hasEntries = type !== "array" && (indexedVariables || namedVariables); - if (hasEntries) { - const { entries } = await this.#send("Runtime.getCollectionEntries", { - objectId, - fetchStart: offset, - fetchCount: count, - }); - - let i = 0; - for (const { key, value } of entries) { - let name = String(i++); - if (key) { - const { value, description } = key; - name = String(value ?? description); - } - variables.push(this.#addVariable(value, { name })); - } - } - - return variables; - } - - #getVariable( - propertyDescriptor: JSC.Runtime.PropertyDescriptor | JSC.Runtime.InternalPropertyDescriptor, - ): Variable[] { - const { value, get, set, symbol } = propertyDescriptor as JSC.Runtime.PropertyDescriptor; - const variables: Variable[] = []; - - if (value) { - variables.push(this.#addVariable(value, propertyDescriptor)); - } - - if (get) { - const { type } = get; - if (type !== "undefined") { - variables.push(this.#addVariable(get, propertyDescriptor)); - } - } - - if (set) { - const { type } = set; - if (type !== "undefined") { - variables.push(this.#addVariable(set, propertyDescriptor)); - } - } - - if (symbol) { - variables.push(this.#addVariable(symbol, propertyDescriptor)); - } - - return variables; - } - - close(): void { - this.#terminated = true; - this.#process?.kill(); - 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.#functionBreakpoints.clear(); - this.#variables.length = 1; - this.#launched = undefined; - this.#initialized = undefined; - this.#connected = undefined; - this.#terminated = undefined; - this.#url = undefined; - } -} - -function stoppedReason(reason: JSC.Debugger.PausedEvent["reason"]): DAP.StoppedEvent["reason"] { - switch (reason) { - case "Breakpoint": - return "breakpoint"; - case "FunctionCall": - return "function breakpoint"; - case "PauseOnNextStatement": - case "DebuggerStatement": - return "pause"; - case "exception": - case "assert": - return "exception"; - default: - return "breakpoint"; - } -} - -function titleize(name: string): string { - return name.charAt(0).toUpperCase() + name.slice(1); -} - -function sourcePresentationHint(url?: string): DAP.Source["presentationHint"] { - if (!url || !url.startsWith("/")) { - return "deemphasize"; - } - if (url.includes("/node_modules/")) { - return "normal"; - } - return "emphasize"; -} - -function sourceName(url?: string): string { - if (!url) { - return "unknown.js"; - } - if (isJavaScript(url)) { - return url.split("/").pop() || url; - } - return `${url}.js`; -} - -function stackFramePresentationHint(path?: string): DAP.StackFrame["presentationHint"] { - if (!path || path.includes("/node_modules/")) { - return "subtle"; - } - return "normal"; -} - -function scopePresentationHint(type: JSC.Debugger.Scope["type"]): DAP.Scope["presentationHint"] { - switch (type) { - case "closure": - case "functionName": - case "with": - case "catch": - case "nestedLexical": - return "locals"; - case "global": - case "globalLexicalEnvironment": - return "globals"; - default: - return undefined; - } -} - -function isIndexed(subtype: JSC.Runtime.RemoteObject["subtype"]): boolean { - return subtype === "array" || subtype === "set" || subtype === "weakset"; -} - -function isNamedIndexed(subtype: JSC.Runtime.RemoteObject["subtype"]): boolean { - return subtype === "map" || subtype === "weakmap"; -} - -function exceptionFiltersToPauseOnExceptionsState( - filters?: string[], -): JSC.Debugger.SetPauseOnExceptionsRequest["state"] { - if (filters?.includes("all")) { - return "all"; - } - if (filters?.includes("uncaught")) { - return "uncaught"; - } - return "none"; -} - -function breakpointOptions(breakpoint?: Partial<DAP.SourceBreakpoint>): JSC.Debugger.BreakpointOptions { - const { condition } = breakpoint ?? {}; - // TODO: hitCondition, logMessage - return { - condition, - }; -} - -function consoleMessageGroup(type: JSC.Console.ConsoleMessage["type"]): DAP.OutputEvent["group"] { - switch (type) { - case "startGroup": - return "start"; - case "startGroupCollapsed": - return "startCollapsed"; - case "endGroup": - return "end"; - } - return undefined; -} - -function sourceToPath(source?: DAP.Source): string { - const { path } = source ?? {}; - if (!path) { - throw new Error("No source found."); - } - return path; -} - -function sourceToId(source?: DAP.Source): string | number { - const { path, sourceReference } = source ?? {}; - if (path) { - return path; - } - if (sourceReference) { - return sourceReference; - } - throw new Error("No source found."); -} - -function callFrameToId(callFrame: JSC.Console.CallFrame): string { - const { url, lineNumber, columnNumber } = callFrame; - return `${url}:${lineNumber}:${columnNumber}`; -} - -function sanitizeExpression(expression: string): string { - expression = expression.trim(); - if (expression.startsWith("{")) { - expression = `(${expression})`; - } - if (expression.startsWith("return ")) { - expression = expression.slice(7); - } - if (expression.startsWith("await ")) { - expression = expression.slice(6); - } - return expression; -} - -function remoteObjectToVariablePresentationHint( - remoteObject: JSC.Runtime.RemoteObject, - propertyDescriptor?: JSC.Runtime.PropertyDescriptor, -): DAP.VariablePresentationHint { - const { type, subtype } = remoteObject; - const { name, configurable, writable, isPrivate, symbol, get, set, wasThrown } = propertyDescriptor ?? {}; - const hasGetter = get?.type === "function"; - const hasSetter = set?.type === "function"; - const hasSymbol = symbol?.type === "symbol"; - let kind: string | undefined; - let visibility: string | undefined; - let lazy: boolean | undefined; - let attributes: string[] = []; - if (type === "function") { - kind = "method"; - } - if (subtype === "class") { - kind = "class"; - } - if (isPrivate || configurable === false || hasSymbol || name === "__proto__") { - visibility = "internal"; - } - if (type === "string") { - attributes.push("rawString"); - } - if (writable === false || (hasGetter && !hasSetter)) { - attributes.push("readOnly"); - } - if (wasThrown || hasGetter) { - lazy = true; - attributes.push("hasSideEffects"); - } - return { - kind, - visibility, - lazy, - attributes, - }; -} - -function propertyDescriptorToName(propertyDescriptor?: JSC.Runtime.PropertyDescriptor): string { - if (!propertyDescriptor) { - return ""; - } - const { name } = propertyDescriptor; - if (name === "__proto__") { - return "[[Prototype]]"; - } - return name; -} - -function unknownToError(input: unknown): Error { - if (input instanceof Error) { - return input; - } - return new Error(String(input)); -} - -function isJavaScript(path: string): boolean { - return /\.(c|m)?(j|t)sx?$/.test(path); -} - -function parseUrl(hostname?: string, port?: number): URL { - hostname ||= "localhost"; - port ||= 6499; - let url: URL; - try { - if (hostname.includes("://")) { - url = new URL(hostname); - } else if (hostname.includes(":") && !hostname.startsWith("[")) { - url = new URL(`ws://[${hostname}]:${port}/`); - } else { - url = new URL(`ws://${hostname}:${port}/`); - } - } catch { - throw new Error(`Invalid URL or hostname/port: ${hostname}`); - } - // HACK: Bun sometimes has issues connecting through "127.0.0.1" - if (url.hostname === "localhost" || url.hostname === "127.0.0.1") { - url.hostname = "[::1]"; - } - return url; -} - -function parseUrlMaybe(string: string): URL | undefined { - const match = /(wss?:\/\/.*)/im.exec(string); - if (!match) { - return undefined; - } - const [_, href] = match; - try { - return parseUrl(href); - } catch { - return undefined; - } -} - -function variablesSortBy(a: DAP.Variable, b: DAP.Variable): number { - const visibility = (variable: DAP.Variable): number => { - const { presentationHint } = variable; - switch (presentationHint?.visibility) { - case "protected": - return 1; - case "private": - return 2; - case "internal": - return 3; - } - return 0; - }; - const index = (variable: DAP.Variable): number => { - const { name } = variable; - switch (name) { - case "[[Prototype]]": - case "prototype": - case "__proto__": - return Number.MAX_VALUE; - } - const index = parseInt(name); - if (isFinite(index)) { - return index; - } - return 0; - }; - const av = visibility(a); - const bv = visibility(b); - if (av > bv) return 1; - if (av < bv) return -1; - const ai = index(a); - const bi = index(b); - if (ai > bi) return 1; - if (ai < bi) return -1; - return 0; -} - -function isSameLocation(a: { line?: number; column?: number }, b: { line?: number; column?: number }): boolean { - return (a.line === b.line || (!a.line && !b.line)) && (a.column === b.column || (!a.column && !b.column)); -} - -function consoleLevelToAnsiColor(level: JSC.Console.ConsoleMessage["level"]): string | undefined { - switch (level) { - case "warning": - return "\u001b[33m"; - case "error": - return "\u001b[31m"; - case "debug": - return "\u001b[36m"; - } - return undefined; -} - -function numberIsValid(number?: number): number is number { - return typeof number === "number" && isFinite(number) && number >= 0; -} diff --git a/packages/bun-debug-adapter-protocol/debugger/capabilities.ts b/packages/bun-debug-adapter-protocol/debugger/capabilities.ts deleted file mode 100644 index 3ba968e86..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/capabilities.ts +++ /dev/null @@ -1,271 +0,0 @@ -import type { DAP } from ".."; - -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/debugger/fixtures/preview.js b/packages/bun-debug-adapter-protocol/debugger/fixtures/preview.js deleted file mode 100644 index 15062240b..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/fixtures/preview.js +++ /dev/null @@ -1,99 +0,0 @@ -console.log( - undefined, - null, - true, - false, - 0, - 1, - Math.PI, - -Math.E, - NaN, - Infinity, - -Infinity, - BigInt(0), - BigInt(1), - BigInt("10000000000000"), - BigInt("-10000000000000"), - "", - " ", - "Hello", - "Hello World", - [], - [1, 2, 3], - ["a", 1, null, undefined], - [1, [2, [3, [4, [5, [6, [7, [8, [9, [10]]]]]]]]]], - [[[[[]]]]], - {}, - { a: 1 }, - { a: 1, b: 2, c: 3 }, - { a: { b: { c: { d: { e: { f: { g: { h: { i: { j: 10 } } } } } } } } } }, - function () {}, - function namedFunction() {}, - class {}, - class namedClass {}, - class namedClass { - a() {} - b = 1; - c = [ - null, - undefined, - "a", - { - a: 1, - b: 2, - c: 3, - }, - ]; - }, - new Date(0), - new Date(NaN), - new RegExp(), - new RegExp("abc"), - new RegExp("abc", "g"), - /abc/, - new Set(), - new Set([1, 2, 3]), - new WeakSet(), - new WeakSet([{ a: 1 }, { b: 2 }, { c: 3 }]), - new Map(), - new Map([ - ["a", 1], - ["b", 2], - ["c", 3], - ]), - new WeakMap(), - new WeakMap([ - [{ a: 1 }, 1], - [{ b: 2 }, 2], - [{ c: 3 }, 3], - ]), - Symbol(), - Symbol("namedSymbol"), - new Error(), - new TypeError("This is a TypeError"), - //"a".repeat(10000), - //["a"].fill("a", 0, 10000), - new Headers(), - new Headers({ - a: "1", - b: "2", - }), - new Request("https://example.com/"), - new Request("https://example.com/", { - method: "POST", - headers: { - a: "1", - b: "2", - }, - body: '{"example":true}', - }), - new Response(), - new Response('{"example":true}', { - status: 200, - statusText: "OK", - headers: { - a: "1", - b: "2", - }, - }), -); diff --git a/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js deleted file mode 100644 index 6c16a1202..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; -export default { - fetch(request) { - const animal = getAnimal(request.url); - const voice = animal.talk(); - return new Response(voice); - }, -}; -function getAnimal(query) { - switch (query.split("/").pop()) { - case "dog": - return new Dog(); - case "cat": - return new Cat(); - } - return new Bird(); -} -class Dog { - name = "dog"; - talk() { - return "woof"; - } -} -class Cat { - name = "cat"; - talk() { - return "meow"; - } -} -class Bird { - name = "bird"; - talk() { - return "chirp"; - } -} -//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsicGFja2FnZXMvYnVuLWRlYnVnLWFkYXB0ZXItcHJvdG9jb2wvZGVidWdnZXIvZml4dHVyZXMvd2l0aC1zb3VyY2VtYXAudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImV4cG9ydCBkZWZhdWx0IHtcbiAgZmV0Y2gocmVxdWVzdDogUmVxdWVzdCk6IFJlc3BvbnNlIHtcbiAgICBjb25zdCBhbmltYWwgPSBnZXRBbmltYWwocmVxdWVzdC51cmwpO1xuICAgIGNvbnN0IHZvaWNlID0gYW5pbWFsLnRhbGsoKTtcbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKHZvaWNlKTtcbiAgfSxcbn07XG5cbmZ1bmN0aW9uIGdldEFuaW1hbChxdWVyeTogc3RyaW5nKTogQW5pbWFsIHtcbiAgc3dpdGNoIChxdWVyeS5zcGxpdChcIi9cIikucG9wKCkpIHtcbiAgICBjYXNlIFwiZG9nXCI6XG4gICAgICByZXR1cm4gbmV3IERvZygpO1xuICAgIGNhc2UgXCJjYXRcIjpcbiAgICAgIHJldHVybiBuZXcgQ2F0KCk7XG4gIH1cbiAgcmV0dXJuIG5ldyBCaXJkKCk7XG59XG5cbmludGVyZmFjZSBBbmltYWwge1xuICByZWFkb25seSBuYW1lOiBzdHJpbmc7XG4gIHRhbGsoKTogc3RyaW5nO1xufVxuXG5jbGFzcyBEb2cgaW1wbGVtZW50cyBBbmltYWwge1xuICBuYW1lID0gXCJkb2dcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwid29vZlwiO1xuICB9XG59XG5cbmNsYXNzIENhdCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImNhdFwiO1xuXG4gIHRhbGsoKTogc3RyaW5nIHtcbiAgICByZXR1cm4gXCJtZW93XCI7XG4gIH1cbn1cblxuY2xhc3MgQmlyZCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImJpcmRcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwiY2hpcnBcIjtcbiAgfVxufVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFBLGVBQWU7QUFBQSxFQUNiLE1BQU0sU0FBNEI7QUFDaEMsVUFBTSxTQUFTLFVBQVUsUUFBUSxHQUFHO0FBQ3BDLFVBQU0sUUFBUSxPQUFPLEtBQUs7QUFDMUIsV0FBTyxJQUFJLFNBQVMsS0FBSztBQUFBLEVBQzNCO0FBQ0Y7QUFFQSxTQUFTLFVBQVUsT0FBdUI7QUFDeEMsVUFBUSxNQUFNLE1BQU0sR0FBRyxFQUFFLElBQUksR0FBRztBQUFBLElBQzlCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLElBQ2pCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLEVBQ25CO0FBQ0EsU0FBTyxJQUFJLEtBQUs7QUFDbEI7QUFPQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLEtBQXVCO0FBQUEsRUFDM0IsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7IiwKICAibmFtZXMiOiBbXQp9Cg== diff --git a/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.ts b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.ts deleted file mode 100644 index f245ebf76..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.ts +++ /dev/null @@ -1,46 +0,0 @@ -export default { - fetch(request: Request): Response { - const animal = getAnimal(request.url); - const voice = animal.talk(); - return new Response(voice); - }, -}; - -function getAnimal(query: string): Animal { - switch (query.split("/").pop()) { - case "dog": - return new Dog(); - case "cat": - return new Cat(); - } - return new Bird(); -} - -interface Animal { - readonly name: string; - talk(): string; -} - -class Dog implements Animal { - name = "dog"; - - talk(): string { - return "woof"; - } -} - -class Cat implements Animal { - name = "cat"; - - talk(): string { - return "meow"; - } -} - -class Bird implements Animal { - name = "bird"; - - talk(): string { - return "chirp"; - } -} diff --git a/packages/bun-debug-adapter-protocol/debugger/fixtures/without-sourcemap.js b/packages/bun-debug-adapter-protocol/debugger/fixtures/without-sourcemap.js deleted file mode 100644 index 6a5d9a948..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/fixtures/without-sourcemap.js +++ /dev/null @@ -1,20 +0,0 @@ -export default { - fetch(request) { - return new Response(a()); - }, -}; - -function a() { - return b(); -} - -function b() { - return c(); -} - -function c() { - function d() { - return "hello"; - } - return d(); -} diff --git a/packages/bun-debug-adapter-protocol/debugger/preview.test.ts b/packages/bun-debug-adapter-protocol/debugger/preview.test.ts deleted file mode 100644 index 666913719..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/preview.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { beforeAll, afterAll, test, expect } from "bun:test"; -import type { JSC } from "../../bun-inspector-protocol"; -import { WebSocketInspector } from "../../bun-inspector-protocol"; -import type { PipedSubprocess } from "bun"; -import { spawn } from "bun"; -import { remoteObjectToString } from "./preview"; - -let subprocess: PipedSubprocess | undefined; -let objects: JSC.Runtime.RemoteObject[] = []; - -beforeAll(async () => { - subprocess = spawn({ - cwd: import.meta.dir, - cmd: [process.argv0, "--inspect-wait=0", "fixtures/preview.js"], - stdout: "pipe", - stderr: "pipe", - stdin: "pipe", - }); - const decoder = new TextDecoder(); - let url: URL; - for await (const chunk of subprocess!.stdout) { - const text = decoder.decode(chunk); - if (text.includes("ws://")) { - url = new URL(/(ws:\/\/.*)/.exec(text)![0]); - break; - } - } - objects = await new Promise((resolve, reject) => { - const inspector = new WebSocketInspector({ - url, - listener: { - ["Inspector.connected"]: () => { - inspector.send("Inspector.enable"); - inspector.send("Runtime.enable"); - inspector.send("Console.enable"); - inspector.send("Debugger.enable"); - inspector.send("Debugger.resume"); - inspector.send("Inspector.initialized"); - }, - ["Inspector.disconnected"]: error => { - reject(error); - }, - ["Console.messageAdded"]: ({ message }) => { - const { parameters } = message; - resolve(parameters!); - inspector.close(); - }, - }, - }); - inspector.start(); - }); -}); - -afterAll(() => { - subprocess?.kill(); -}); - -test("remoteObjectToString", () => { - for (const object of objects) { - expect(remoteObjectToString(object)).toMatchSnapshot(); - } -}); diff --git a/packages/bun-debug-adapter-protocol/debugger/preview.ts b/packages/bun-debug-adapter-protocol/debugger/preview.ts deleted file mode 100644 index 6012623d2..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/preview.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { JSC } from "../../bun-inspector-protocol"; - -export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject): string { - const { type, subtype, value, description, className, preview } = remoteObject; - switch (type) { - case "undefined": - return "undefined"; - case "boolean": - case "number": - return description ?? JSON.stringify(value); - case "string": - return JSON.stringify(value ?? description); - case "symbol": - case "bigint": - return description!; - case "function": - return description!.replace("function", "ƒ") || "ƒ"; - } - switch (subtype) { - case "null": - return "null"; - case "regexp": - case "date": - case "error": - return description!; - } - if (preview) { - return objectPreviewToString(preview); - } - if (className) { - return className; - } - return description || "Object"; -} - -export function objectPreviewToString(objectPreview: JSC.Runtime.ObjectPreview): string { - const { type, subtype, entries, properties, overflow, description, size } = objectPreview; - if (type !== "object") { - return remoteObjectToString(objectPreview); - } - let items: string[]; - if (entries) { - items = entries.map(entryPreviewToString).sort(); - } else if (properties) { - if (isIndexed(subtype)) { - items = properties.map(indexedPropertyPreviewToString).sort(); - } else { - items = properties.map(namedPropertyPreviewToString).sort(); - } - } else { - items = ["…"]; - } - if (overflow) { - items.push("…"); - } - let label: string; - if (description === "Object") { - label = ""; - } else if (size === undefined) { - label = description!; - } else { - label = `${description}(${size})`; - } - if (!items.length) { - return label || "{}"; - } - if (label) { - label += " "; - } - if (isIndexed(subtype)) { - return `${label}[${items.join(", ")}]`; - } - return `${label}{${items.join(", ")}}`; -} - -function propertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string { - const { type, value, ...preview } = propertyPreview; - if (type === "accessor") { - return "ƒ"; - } - return remoteObjectToString({ ...preview, type, description: value }); -} - -function entryPreviewToString(entryPreview: JSC.Runtime.EntryPreview): string { - const { key, value } = entryPreview; - if (key) { - return `${objectPreviewToString(key)} => ${objectPreviewToString(value)}`; - } - return objectPreviewToString(value); -} - -function namedPropertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string { - const { name, valuePreview } = propertyPreview; - if (valuePreview) { - return `${name}: ${objectPreviewToString(valuePreview)}`; - } - return `${name}: ${propertyPreviewToString(propertyPreview)}`; -} - -function indexedPropertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string { - const { valuePreview } = propertyPreview; - if (valuePreview) { - return objectPreviewToString(valuePreview); - } - return propertyPreviewToString(propertyPreview); -} - -function isIndexed(type?: JSC.Runtime.RemoteObject["subtype"]): boolean { - return type === "array" || type === "set" || type === "weakset"; -} diff --git a/packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts b/packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts deleted file mode 100644 index 44d9ca362..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { test, expect } from "bun:test"; -import { readFileSync } from "node:fs"; -import { SourceMap } from "./sourcemap"; - -test("works without source map", () => { - const sourceMap = getSourceMap("without-sourcemap.js"); - expect(sourceMap.generatedLocation({ line: 7 })).toEqual({ line: 7, column: 0, verified: true }); - expect(sourceMap.generatedLocation({ line: 7, column: 2 })).toEqual({ line: 7, column: 2, verified: true }); - expect(sourceMap.originalLocation({ line: 11 })).toEqual({ line: 11, column: 0, verified: true }); - expect(sourceMap.originalLocation({ line: 11, column: 2 })).toEqual({ line: 11, column: 2, verified: true }); -}); - -test("works with source map", () => { - const sourceMap = getSourceMap("with-sourcemap.js"); - // FIXME: Columns don't appear to be accurate for `generatedLocation` - expect(sourceMap.generatedLocation({ line: 3 })).toMatchObject({ line: 4, verified: true }); - expect(sourceMap.generatedLocation({ line: 27 })).toMatchObject({ line: 20, verified: true }); - expect(sourceMap.originalLocation({ line: 32 })).toEqual({ line: 43, column: 4, verified: true }); - expect(sourceMap.originalLocation({ line: 13 })).toEqual({ line: 13, column: 6, verified: true }); -}); - -function getSourceMap(filename: string): SourceMap { - const { pathname } = new URL(`./fixtures/${filename}`, import.meta.url); - const source = readFileSync(pathname, "utf-8"); - const match = source.match(/\/\/# sourceMappingURL=(.*)$/m); - if (match) { - const [, url] = match; - return SourceMap(url); - } - return SourceMap(); -} diff --git a/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts b/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts deleted file mode 100644 index adb6dc57d..000000000 --- a/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { LineRange, MappedPosition } from "source-map-js"; -import { SourceMapConsumer } from "source-map-js"; - -export type LocationRequest = { - line?: number; - column?: number; - url?: string; -}; - -export type Location = { - line: number; // 0-based - column: number; // 0-based -} & ( - | { - verified: true; - } - | { - verified?: false; - message?: string; - } -); - -export interface SourceMap { - generatedLocation(request: LocationRequest): Location; - originalLocation(request: LocationRequest): Location; -} - -class ActualSourceMap implements SourceMap { - #sourceMap: SourceMapConsumer; - #sources: string[]; - - constructor(sourceMap: SourceMapConsumer) { - this.#sourceMap = sourceMap; - this.#sources = (sourceMap as any)._absoluteSources; - } - - #getSource(url?: string): string { - const sources = this.#sources; - if (!sources.length) { - return ""; - } - if (sources.length === 1 || !url) { - return sources[0]; - } - for (const source of sources) { - if (url.endsWith(source)) { - return source; - } - } - return ""; - } - - generatedLocation(request: LocationRequest): Location { - const { line, column, url } = request; - let lineRange: LineRange; - try { - const source = this.#getSource(url); - lineRange = this.#sourceMap.generatedPositionFor({ - line: lineTo1BasedLine(line), - column: columnToColumn(column), - source, - }); - } catch (error) { - return { - line: lineToLine(line), - column: columnToColumn(column), - verified: false, - message: unknownToError(error), - }; - } - if (!locationIsValid(lineRange)) { - return { - line: lineToLine(line), - column: columnToColumn(column), - verified: false, - }; - } - const { line: gline, column: gcolumn } = lineRange; - return { - line: lineToLine(gline), - column: columnToColumn(gcolumn), - verified: true, - }; - } - - originalLocation(request: LocationRequest): Location { - const { line, column } = request; - let mappedPosition: MappedPosition; - try { - mappedPosition = this.#sourceMap.originalPositionFor({ - line: lineTo1BasedLine(line), - column: columnToColumn(column), - }); - } catch (error) { - return { - line: lineToLine(line), - column: columnToColumn(column), - verified: false, - message: unknownToError(error), - }; - } - if (!locationIsValid(mappedPosition)) { - return { - line: lineToLine(line), - column: columnToColumn(column), - verified: false, - }; - } - const { line: oline, column: ocolumn } = mappedPosition; - return { - line: lineTo0BasedLine(oline), - column: columnToColumn(ocolumn), - verified: true, - }; - } -} - -class NoopSourceMap implements SourceMap { - generatedLocation(request: LocationRequest): Location { - const { line, column } = request; - return { - line: lineToLine(line), - column: columnToColumn(column), - verified: true, - }; - } - - originalLocation(request: LocationRequest): Location { - const { line, column } = request; - return { - line: lineToLine(line), - column: columnToColumn(column), - verified: true, - }; - } -} - -const defaultSourceMap = new NoopSourceMap(); - -export function SourceMap(url?: string): SourceMap { - if (!url || !url.startsWith("data:")) { - return defaultSourceMap; - } - try { - const [_, base64] = url.split(",", 2); - const decoded = Buffer.from(base64, "base64url").toString("utf8"); - const schema = JSON.parse(decoded); - const sourceMap = new SourceMapConsumer(schema); - return new ActualSourceMap(sourceMap); - } catch (error) { - console.warn("Failed to parse source map URL", url); - } - return defaultSourceMap; -} - -function lineTo1BasedLine(line?: number): number { - return numberIsValid(line) ? line + 1 : 1; -} - -function lineTo0BasedLine(line?: number): number { - return numberIsValid(line) ? line - 1 : 0; -} - -function lineToLine(line?: number): number { - return numberIsValid(line) ? line : 0; -} - -function columnToColumn(column?: number): number { - return numberIsValid(column) ? column : 0; -} - -function locationIsValid(location: Location): location is Location { - const { line, column } = location; - return numberIsValid(line) && numberIsValid(column); -} - -function numberIsValid(number?: number): number is number { - return typeof number === "number" && isFinite(number) && number >= 0; -} - -function unknownToError(error: unknown): string { - if (error instanceof Error) { - const { message } = error; - return message; - } - return String(error); -} |