aboutsummaryrefslogtreecommitdiff
path: root/packages/bun-debug-adapter-protocol/debugger
diff options
context:
space:
mode:
Diffstat (limited to 'packages/bun-debug-adapter-protocol/debugger')
-rw-r--r--packages/bun-debug-adapter-protocol/debugger/adapter.ts120
-rw-r--r--packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js36
-rw-r--r--packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.ts46
-rw-r--r--packages/bun-debug-adapter-protocol/debugger/fixtures/without-sourcemap.js20
-rw-r--r--packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts31
-rw-r--r--packages/bun-debug-adapter-protocol/debugger/sourcemap.ts120
6 files changed, 299 insertions, 74 deletions
diff --git a/packages/bun-debug-adapter-protocol/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/debugger/adapter.ts
index 6af7d9ea6..ed40c86e4 100644
--- a/packages/bun-debug-adapter-protocol/debugger/adapter.ts
+++ b/packages/bun-debug-adapter-protocol/debugger/adapter.ts
@@ -5,7 +5,7 @@ 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 { SourceMap } from "./sourcemap";
+import { Location, SourceMap } from "./sourcemap";
import { compare, parse } from "semver";
type InitializeRequest = DAP.InitializeRequest & {
@@ -509,31 +509,37 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
#generatedLocation(source: Source, line?: number, column?: number): JSC.Debugger.Location {
const { sourceMap, scriptId, path } = source;
- const { line: line0, column: column0 } = sourceMap.generatedLocation(
- this.#lineTo0BasedLine(line),
- this.#columnTo0BasedColumn(column),
- path,
- );
+ const { line: gline, column: gcolumn } = sourceMap.generatedLocation({
+ line: this.#lineTo0BasedLine(line),
+ column: this.#columnTo0BasedColumn(column),
+ url: path,
+ });
return {
scriptId,
- lineNumber: line0,
- columnNumber: column0,
+ lineNumber: gline,
+ columnNumber: gcolumn,
};
}
#lineTo0BasedLine(line?: number): number {
+ if (!numberIsValid(line)) {
+ return 0;
+ }
if (this.#initialized?.linesStartAt1) {
- return line ? line - 1 : 0;
+ return line - 1;
}
- return line ?? 0;
+ return line;
}
#columnTo0BasedColumn(column?: number): number {
+ if (!numberIsValid(column)) {
+ return 0;
+ }
if (this.#initialized?.columnsStartAt1) {
- return column ? column - 1 : 0;
+ return column - 1;
}
- return column ?? 0;
+ return column;
}
#originalLocation(
@@ -548,26 +554,26 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
}
const { sourceMap } = source;
- const { line: line0, column: column0 } = sourceMap.originalLocation(line, column);
+ const { line: oline, column: ocolumn } = sourceMap.originalLocation({ line, column });
return {
- line: this.#lineFrom0BasedLine(line0),
- column: this.#columnFrom0BasedColumn(column0),
+ line: this.#lineFrom0BasedLine(oline),
+ column: this.#columnFrom0BasedColumn(ocolumn),
};
}
#lineFrom0BasedLine(line?: number): number {
if (this.#initialized?.linesStartAt1) {
- return line ? line + 1 : 1;
+ return numberIsValid(line) ? line + 1 : 1;
}
- return line ?? 0;
+ return numberIsValid(line) ? line : 0;
}
#columnFrom0BasedColumn(column?: number): number {
if (this.#initialized?.columnsStartAt1) {
- return column ? column + 1 : 1;
+ return numberIsValid(column) ? column + 1 : 1;
}
- return column ?? 0;
+ return numberIsValid(column) ? column : 0;
}
async setBreakpoints(request: DAP.SetBreakpointsRequest): Promise<DAP.SetBreakpointsResponse> {
@@ -944,6 +950,15 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
["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) {
@@ -1143,21 +1158,26 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const { scriptId } = location;
const source = this.#getSourceIfPresent(scriptId);
- let { lineNumber, columnNumber } = location;
+ let originalLocation: Location;
if (source) {
- const { line, column } = this.#originalLocation(source, location);
- lineNumber = line;
- columnNumber = column;
+ 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: lineNumber,
- column: columnNumber || 0,
+ line,
+ column,
presentationHint: stackFramePresentationHint(source?.path),
source,
scopes,
@@ -1166,30 +1186,39 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
for (const scope of scopeChain) {
const { name, type, location, object, empty } = scope;
- if (empty || !location) {
+ if (empty) {
continue;
}
- const { scriptId } = location;
- const source = this.#getSourceIfPresent(scriptId);
+
const { variablesReference } = this.#addVariable(object);
const presentationHint = scopePresentationHint(type);
const title = presentationHint ? titleize(presentationHint) : "Unknown";
const displayName = name ? `${title}: ${name}` : title;
- let { lineNumber, columnNumber } = location;
- if (source) {
- const { line, column } = this.#originalLocation(source, location);
- lineNumber = line;
- columnNumber = column;
+ 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: lineNumber,
- column: columnNumber,
+ line,
+ column,
source,
});
}
@@ -1214,20 +1243,25 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
const callFrameId = callFrameToId(callFrame);
const source = this.#getSourceIfPresent(scriptId);
- let { lineNumber, columnNumber } = callFrame;
+ let originalLocation: Location;
if (source) {
- const { line, column } = this.#originalLocation(source, callFrame);
- lineNumber = line;
- columnNumber = column;
+ 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: lineNumber,
- column: columnNumber,
+ line,
+ column,
source,
presentationHint: stackFramePresentationHint(source?.path),
canRestart: false,
@@ -1731,3 +1765,7 @@ function consoleLevelToAnsiColor(level: JSC.Console.ConsoleMessage["level"]): st
}
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/fixtures/with-sourcemap.js b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js
new file mode 100644
index 000000000..6c16a1202
--- /dev/null
+++ b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.js
@@ -0,0 +1,36 @@
+"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
new file mode 100644
index 000000000..f245ebf76
--- /dev/null
+++ b/packages/bun-debug-adapter-protocol/debugger/fixtures/with-sourcemap.ts
@@ -0,0 +1,46 @@
+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
new file mode 100644
index 000000000..6a5d9a948
--- /dev/null
+++ b/packages/bun-debug-adapter-protocol/debugger/fixtures/without-sourcemap.js
@@ -0,0 +1,20 @@
+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/sourcemap.test.ts b/packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts
new file mode 100644
index 000000000..44d9ca362
--- /dev/null
+++ b/packages/bun-debug-adapter-protocol/debugger/sourcemap.test.ts
@@ -0,0 +1,31 @@
+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
index eeceb520f..adb6dc57d 100644
--- a/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts
+++ b/packages/bun-debug-adapter-protocol/debugger/sourcemap.ts
@@ -1,13 +1,28 @@
+import type { LineRange, MappedPosition } from "source-map-js";
import { SourceMapConsumer } from "source-map-js";
-export type Location = {
- line: number;
- column: number;
+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(line?: number, column?: number, url?: string): Location;
- originalLocation(line?: number, column?: number): Location;
+ generatedLocation(request: LocationRequest): Location;
+ originalLocation(request: LocationRequest): Location;
}
class ActualSourceMap implements SourceMap {
@@ -21,11 +36,11 @@ class ActualSourceMap implements SourceMap {
#getSource(url?: string): string {
const sources = this.#sources;
- if (sources.length === 1) {
- return sources[0];
+ if (!sources.length) {
+ return "";
}
- if (!url) {
- return sources[0] ?? "";
+ if (sources.length === 1 || !url) {
+ return sources[0];
}
for (const source of sources) {
if (url.endsWith(source)) {
@@ -35,61 +50,87 @@ class ActualSourceMap implements SourceMap {
return "";
}
- generatedLocation(line?: number, column?: number, url?: string): Location {
+ generatedLocation(request: LocationRequest): Location {
+ const { line, column, url } = request;
+ let lineRange: LineRange;
try {
const source = this.#getSource(url);
- const { line: gline, column: gcolumn } = this.#sourceMap.generatedPositionFor({
+ lineRange = this.#sourceMap.generatedPositionFor({
line: lineTo1BasedLine(line),
column: columnToColumn(column),
source,
});
- console.log(`[sourcemap] -->`, { source, url, line, column }, { gline, gcolumn });
+ } catch (error) {
return {
- line: lineTo0BasedLine(gline),
- column: columnToColumn(gcolumn),
+ line: lineToLine(line),
+ column: columnToColumn(column),
+ verified: false,
+ message: unknownToError(error),
};
- } catch (error) {
- console.warn(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(line?: number, column?: number): Location {
+ originalLocation(request: LocationRequest): Location {
+ const { line, column } = request;
+ let mappedPosition: MappedPosition;
try {
- const { line: oline, column: ocolumn } = this.#sourceMap.originalPositionFor({
+ mappedPosition = this.#sourceMap.originalPositionFor({
line: lineTo1BasedLine(line),
column: columnToColumn(column),
});
- console.log(`[sourcemap] <--`, { line, column }, { oline, ocolumn });
+ } catch (error) {
return {
- line: lineTo0BasedLine(oline),
- column: columnToColumn(ocolumn),
+ line: lineToLine(line),
+ column: columnToColumn(column),
+ verified: false,
+ message: unknownToError(error),
};
- } catch (error) {
- console.warn(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(line?: number, column?: number, url?: string): Location {
+ generatedLocation(request: LocationRequest): Location {
+ const { line, column } = request;
return {
line: lineToLine(line),
column: columnToColumn(column),
+ verified: true,
};
}
- originalLocation(line?: number, column?: number): Location {
+ originalLocation(request: LocationRequest): Location {
+ const { line, column } = request;
return {
line: lineToLine(line),
column: columnToColumn(column),
+ verified: true,
};
}
}
@@ -104,10 +145,6 @@ export function SourceMap(url?: string): SourceMap {
const [_, base64] = url.split(",", 2);
const decoded = Buffer.from(base64, "base64url").toString("utf8");
const schema = JSON.parse(decoded);
- // HACK: Bun is sometimes sending invalid mappings
- try {
- schema.mappings = schema.mappings.replace(/[^a-z,;]/gi, "").slice(1);
- } catch {}
const sourceMap = new SourceMapConsumer(schema);
return new ActualSourceMap(sourceMap);
} catch (error) {
@@ -117,17 +154,34 @@ export function SourceMap(url?: string): SourceMap {
}
function lineTo1BasedLine(line?: number): number {
- return line ? line + 1 : 1;
+ return numberIsValid(line) ? line + 1 : 1;
}
function lineTo0BasedLine(line?: number): number {
- return line ? line - 1 : 0;
+ return numberIsValid(line) ? line - 1 : 0;
}
function lineToLine(line?: number): number {
- return line ?? 0;
+ return numberIsValid(line) ? line : 0;
}
function columnToColumn(column?: number): number {
- return column ?? 0;
+ 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);
}