Improved support for debug-adapter-protocol (#4186)

* Improve support for \`debug-adapter-protocol\`

* More improvements, fix formatting in debug console

* Fix attaching

* Prepare for source maps

* Start of source map support, breakpoints work

* Source map support

* add some package.jsons

* wip

* Update package.json

* More fixes

* Make source maps safer if exception occurs

* Check bun version if it fails

* Fix console.log formatting

* Fix source maps partly

* More source map fixes

* Prepare for extension

* watch mode with dap

* Improve preview code

* Prepare for extension 2

---------

Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
Ashcon Partovi
2023-08-24 22:53:34 -07:00
committed by GitHub
parent f269432d90
commit 1480889205
73 changed files with 13542 additions and 9216 deletions

View File

@@ -0,0 +1,2 @@
protocol/*/protocol.json linguist-generated=true
protocol/*/index.d.ts linguist-generated=true

View File

@@ -0,0 +1 @@
protocol/*.json

View File

@@ -0,0 +1,3 @@
# bun-debug-adapter-protocol
https://microsoft.github.io/debug-adapter-protocol/overview

Binary file not shown.

View File

@@ -0,0 +1,143 @@
// 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: ƒ, …}"`;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,271 @@
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;

View File

@@ -0,0 +1,99 @@
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",
},
}),
);

View File

@@ -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==

View File

@@ -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";
}
}

View File

@@ -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();
}

View File

@@ -0,0 +1,62 @@
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();
}
});

View File

@@ -0,0 +1,110 @@
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";
}

View File

@@ -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();
}

View File

@@ -0,0 +1,187 @@
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);
}

View File

@@ -0,0 +1,2 @@
export type * from "./protocol";
export * from "./debugger/adapter";

View File

@@ -0,0 +1,8 @@
{
"name": "bun-debug-adapter-protocol",
"dependencies": {
"semver": "^7.5.4",
"ws": "^8.13.0",
"source-map-js": "^1.0.2"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
export type Protocol = {
$schema: string;
title: string;
description: string;
type: "object";
definitions: Record<string, Type>;
};
export type Type = {
description?: string;
} & (
| {
type: "number" | "integer" | "boolean";
}
| {
type: "string";
enum?: string[];
enumDescriptions?: string[];
}
| {
type: "object";
properties?: Record<string, Type>;
required?: string[];
}
| {
type: "array";
items?: Type;
}
| {
type?: undefined;
$ref: string;
}
| {
type?: undefined;
allOf: Type[];
}
);

View File

@@ -0,0 +1,176 @@
import type { Protocol, Type } from "../protocol/schema.d.ts";
import { writeFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
run().catch(console.error);
async function run() {
const cwd = new URL("../protocol/", import.meta.url);
const runner = "Bun" in globalThis ? "bunx" : "npx";
const write = (name: string, data: string) => {
const path = new URL(name, cwd);
writeFileSync(path, data);
spawnSync(runner, ["prettier", "--write", path.pathname], { cwd, stdio: "ignore" });
};
const schema: Protocol = await download(
"https://microsoft.github.io/debug-adapter-protocol/debugAdapterProtocol.json",
);
write("protocol.json", JSON.stringify(schema));
const types = formatProtocol(schema);
write("index.d.ts", `// GENERATED - DO NOT EDIT\n${types}`);
}
function formatProtocol(protocol: Protocol, extraTs?: string): string {
const { definitions } = protocol;
const requestMap = new Map();
const responseMap = new Map();
const eventMap = new Map();
let body = `export namespace DAP {`;
loop: for (const [key, definition] of Object.entries(definitions)) {
if (/[a-z]+Request$/i.test(key)) {
continue;
}
if (/[a-z]+Arguments$/i.test(key)) {
const name = key.replace(/(Request)?Arguments$/, "");
const requestName = `${name}Request`;
requestMap.set(toMethod(name), requestName);
body += formatType(definition, requestName);
continue;
}
if ("allOf" in definition) {
const { allOf } = definition;
for (const type of allOf) {
if (type.type !== "object") {
continue;
}
const { description, properties = {} } = type;
if (/[a-z]+Event$/i.test(key)) {
const { event, body: type = {} } = properties;
if (!event || !("enum" in event)) {
continue;
}
const [eventKey] = event.enum ?? [];
eventMap.set(eventKey, key);
const eventType: Type = {
type: "object",
description,
...type,
};
body += formatType(eventType, key);
continue loop;
}
if (/[a-z]+Response$/i.test(key)) {
const { body: type = {} } = properties;
const bodyType: Type = {
type: "object",
description,
...type,
};
const name = key.replace(/Response$/, "");
responseMap.set(toMethod(name), key);
body += formatType(bodyType, key);
continue loop;
}
}
}
body += formatType(definition, key);
}
for (const [key, name] of responseMap) {
if (requestMap.has(key)) {
continue;
}
const requestName = `${name.replace(/Response$/, "")}Request`;
requestMap.set(key, requestName);
body += formatType({ type: "object", properties: {} }, requestName);
}
body += formatMapType("RequestMap", requestMap);
body += formatMapType("ResponseMap", responseMap);
body += formatMapType("EventMap", eventMap);
if (extraTs) {
body += extraTs;
}
return body + "};";
}
function formatMapType(key: string, typeMap: Map<string, string>): string {
const type: Type = {
type: "object",
required: [...typeMap.keys()],
properties: Object.fromEntries([...typeMap.entries()].map(([key, value]) => [key, { $ref: value }])),
};
return formatType(type, key);
}
function formatType(type: Type, key?: string): string {
const { description, type: kind } = type;
let body = "";
if (key) {
if (description) {
body += `\n${toComment(description)}\n`;
}
body += `export type ${key}=`;
}
if (kind === "boolean") {
body += "boolean";
} else if (kind === "number" || kind === "integer") {
body += "number";
} else if (kind === "string") {
const { enum: choices } = type;
if (choices) {
body += choices.map(value => `"${value}"`).join("|");
} else {
body += "string";
}
} else if (kind === "array") {
const { items } = type;
const itemType = items ? formatType(items) : "unknown";
body += `${itemType}[]`;
} else if (kind === "object") {
const { properties, required } = type;
if (!properties || Object.keys(properties).length === 0) {
body += "{}";
} else {
body += "{";
for (const [key, { description, ...type }] of Object.entries(properties)) {
if (description) {
body += `\n${toComment(description)}`;
}
const delimit = required?.includes(key) ? ":" : "?:";
body += `\n${key}${delimit}${formatType(type)};`;
}
body += "}";
}
} else if ("$ref" in type) {
const { $ref: ref } = type;
body += ref.split("/").pop() || "unknown";
} else if ("allOf" in type) {
const { allOf } = type;
body += allOf.map(type => formatType(type)).join("&");
} else {
body += "unknown";
}
if (key) {
body += ";";
}
return body;
}
function toMethod(name: string): string {
return `${name.substring(0, 1).toLowerCase()}${name.substring(1)}`;
}
function toComment(description?: string): string {
if (!description) {
return "";
}
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
return lines.join("\n");
}
async function download<T>(url: string | URL): Promise<T> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ${url}: ${response.statusText}`);
}
return response.json();
}

View File

@@ -0,0 +1,22 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "nodenext",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"noEmit": true,
"composite": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"inlineSourceMap": true,
"allowJs": true,
"types": ["bun-types"],
"outDir": "dist",
},
"include": [".", "../bun-types/index.d.ts", "../bun-inspector-protocol/index"]
}