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

BIN
bun.lockb

Binary file not shown.

View File

@@ -9,6 +9,7 @@
"prettier": "^2.4.1", "prettier": "^2.4.1",
"react": "next", "react": "next",
"react-dom": "next", "react-dom": "next",
"source-map-js": "^1.0.2",
"typescript": "^5.0.2" "typescript": "^5.0.2"
}, },
"private": true, "private": true,
@@ -17,7 +18,7 @@
"build-fallback": "esbuild --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js", "build-fallback": "esbuild --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js",
"postinstall": "bash .scripts/postinstall.sh", "postinstall": "bash .scripts/postinstall.sh",
"typecheck": "tsc --noEmit && cd test && bun run typecheck", "typecheck": "tsc --noEmit && cd test && bun run typecheck",
"fmt": "prettier --write --cache './{src,test,bench}/**/*.{mjs,ts,tsx,js,jsx}'", "fmt": "prettier --write --cache './{src,test,bench,packages/{bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}/**/*.{mjs,ts,tsx,js,jsx}'",
"lint": "eslint './**/*.d.ts' --cache", "lint": "eslint './**/*.d.ts' --cache",
"lint:fix": "eslint './**/*.d.ts' --cache --fix" "lint:fix": "eslint './**/*.d.ts' --cache --fix"
}, },

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

@@ -3,19 +3,20 @@
"lib": ["ESNext"], "lib": ["ESNext"],
"module": "esnext", "module": "esnext",
"target": "esnext", "target": "esnext",
"moduleResolution": "bundler", "moduleResolution": "nodenext",
"moduleDetection": "force", "moduleDetection": "force",
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"noEmit": true, "noEmit": true,
"composite": true, "composite": true,
"strict": true,
"downlevelIteration": true, "downlevelIteration": true,
"skipLibCheck": true, "skipLibCheck": true,
"jsx": "preserve",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"inlineSourceMap": true,
"allowJs": true, "allowJs": true,
"types": [ "types": ["bun-types"],
"bun-types" // add Bun global "outDir": "dist",
] },
} "include": [".", "../bun-types/index.d.ts", "../bun-inspector-protocol/index"]
} }

View File

@@ -1,4 +1,5 @@
{ {
"name": "bun-ecosystem-ci",
"private": true, "private": true,
"dependencies": { "dependencies": {
"globby": "^13.1.3" "globby": "^13.1.3"
@@ -11,4 +12,4 @@
"format": "prettier --write src", "format": "prettier --write src",
"test": "bun run src/runner.ts" "test": "bun run src/runner.ts"
} }
} }

View File

@@ -1,4 +1,4 @@
# web-inspector-bun # bun-devtools-frontend
This is the WebKit Web Inspector bundled as standalone assets. This is the WebKit Web Inspector bundled as standalone assets.

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
# bun-inspector-protocol

Binary file not shown.

View File

@@ -0,0 +1,3 @@
export type * from "./protocol";
export type * from "./inspector";
export * from "./inspector/websocket";

View File

@@ -0,0 +1,7 @@
export default {
fetch(request) {
console.log(request);
debugger;
return new Response();
},
};

View File

@@ -0,0 +1,49 @@
import type { JSC } from "..";
/**
* A client that can send and receive messages to/from a debugger.
*/
export abstract class Inspector {
constructor(listener?: InspectorListener);
/**
* Starts the inspector.
*/
start(...args: unknown[]): void;
/**
* Sends a request to the debugger.
*/
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
method: M,
params?: JSC.RequestMap[M],
): Promise<JSC.ResponseMap[M]>;
/**
* Accepts a message from the debugger.
* @param message the unparsed message from the debugger
*/
accept(message: string): void;
/**
* If the inspector is closed.
*/
get closed(): boolean;
/**
* Closes the inspector.
*/
close(...args: unknown[]): void;
}
export type InspectorListener = {
/**
* Defines a handler when a debugger event is received.
*/
[M in keyof JSC.EventMap]?: (event: JSC.EventMap[M]) => void;
} & {
/**
* Defines a handler when the debugger is connected or reconnected.
*/
["Inspector.connected"]?: () => void;
/**
* Defines a handler when the debugger is disconnected.
* @param error the error that caused the disconnect, if any
*/
["Inspector.disconnected"]?: (error?: Error) => void;
};

View File

@@ -0,0 +1,83 @@
import { afterAll, beforeAll, mock, test, expect } from "bun:test";
import type { JSC } from "..";
import type { InspectorListener } from ".";
import { WebSocketInspector } from "./websocket";
import { sleep, spawn } from "bun";
let inspectee: any;
let url: string;
beforeAll(async () => {
const { pathname } = new URL("fixtures/inspectee.js", import.meta.url);
inspectee = spawn({
cmd: [process.argv0, "--inspect", pathname],
stdout: "pipe",
stderr: "pipe",
});
url = await new Promise(async resolve => {
for await (const chunk of inspectee.stdout) {
const text = new TextDecoder().decode(chunk);
const match = /(wss?:\/\/.*:[0-9]+\/.*)/.exec(text);
if (!match) {
continue;
}
const [_, url] = match;
resolve(url);
}
});
});
afterAll(() => {
inspectee?.kill();
});
test(
"WebSocketInspector",
async () => {
const listener: InspectorListener = {
["Inspector.connected"]: mock((...args) => {
expect(args).toBeEmpty();
}),
["Inspector.disconnected"]: mock((error?: Error) => {
expect(error).toBeUndefined();
}),
["Debugger.scriptParsed"]: mock((event: JSC.Debugger.ScriptParsedEvent) => {
expect(event).toMatchObject({
endColumn: expect.any(Number),
endLine: expect.any(Number),
isContentScript: expect.any(Boolean),
module: expect.any(Boolean),
scriptId: expect.any(String),
startColumn: expect.any(Number),
startLine: expect.any(Number),
url: expect.any(String),
});
}),
};
const inspector = new WebSocketInspector({
url,
listener,
});
inspector.start();
inspector.send("Runtime.enable");
inspector.send("Debugger.enable");
//expect(inspector.send("Runtime.enable")).resolves.toBeEmpty();
//expect(inspector.send("Debugger.enable")).resolves.toBeEmpty();
expect(inspector.send("Runtime.evaluate", { expression: "1 + 1" })).resolves.toMatchObject({
result: {
type: "number",
value: 2,
description: "2",
},
wasThrown: false,
});
expect(listener["Inspector.connected"]).toHaveBeenCalled();
expect(listener["Debugger.scriptParsed"]).toHaveBeenCalled();
inspector.close();
expect(inspector.closed).toBeTrue();
expect(listener["Inspector.disconnected"]).toHaveBeenCalled();
},
{
timeout: 100000,
},
);

View File

@@ -0,0 +1,196 @@
import type { Inspector, InspectorListener } from ".";
import { JSC } from "..";
import { WebSocket } from "ws";
export type WebSocketInspectorOptions = {
url?: string | URL;
listener?: InspectorListener;
};
/**
* An inspector that communicates with a debugger over a WebSocket.
*/
export class WebSocketInspector implements Inspector {
#url?: URL;
#webSocket?: WebSocket;
#requestId: number;
#pendingRequests: Map<number, (result: unknown) => void>;
#pendingMessages: string[];
#listener: InspectorListener;
constructor({ url, listener }: WebSocketInspectorOptions) {
this.#url = url ? new URL(url) : undefined;
this.#listener = listener ?? {};
this.#requestId = 1;
this.#pendingRequests = new Map();
this.#pendingMessages = [];
}
start(url?: string | URL): void {
if (url) {
this.#url = new URL(url);
}
if (this.#url) {
this.#connect();
}
}
#connect(): void {
if (!this.#url) {
return;
}
this.#webSocket?.close();
let webSocket: WebSocket;
try {
console.log("[jsc] connecting", this.#url.href);
webSocket = new WebSocket(this.#url, {
headers: {
"Ref-Event-Loop": "0",
},
});
} catch (error) {
this.#close(unknownToError(error));
return;
}
webSocket.addEventListener("open", () => {
console.log("[jsc] connected");
for (const message of this.#pendingMessages) {
this.#send(message);
}
this.#pendingMessages.length = 0;
this.#listener["Inspector.connected"]?.();
});
webSocket.addEventListener("message", ({ data }) => {
if (typeof data === "string") {
this.accept(data);
}
});
webSocket.addEventListener("error", event => {
console.log("[jsc] error", event);
this.#close(unknownToError(event));
});
webSocket.addEventListener("unexpected-response", () => {
console.log("[jsc] unexpected-response");
this.#close(new Error("WebSocket upgrade failed"));
});
webSocket.addEventListener("close", ({ code, reason }) => {
console.log("[jsc] closed", code, reason);
if (code === 1001) {
this.#close();
} else {
this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
}
});
this.#webSocket = webSocket;
}
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
method: M,
params?: JSC.RequestMap[M] | undefined,
): Promise<JSC.ResponseMap[M]> {
const id = this.#requestId++;
const request = { id, method, params };
console.log("[jsc] -->", request);
return new Promise((resolve, reject) => {
const done = (result: any) => {
this.#pendingRequests.delete(id);
if (result instanceof Error) {
reject(result);
} else {
resolve(result);
}
};
this.#pendingRequests.set(id, done);
this.#send(JSON.stringify(request));
});
}
#send(message: string): void {
if (this.#webSocket) {
const { readyState } = this.#webSocket!;
if (readyState === WebSocket.OPEN) {
this.#webSocket.send(message);
}
return;
}
if (!this.#pendingMessages.includes(message)) {
this.#pendingMessages.push(message);
}
}
accept(message: string): void {
let event: JSC.Event | JSC.Response;
try {
event = JSON.parse(message);
} catch (error) {
console.error("Failed to parse message:", message);
return;
}
console.log("[jsc] <--", event);
if ("id" in event) {
const { id } = event;
const resolve = this.#pendingRequests.get(id);
if (!resolve) {
console.error(`Failed to accept response for unknown ID ${id}:`, event);
return;
}
this.#pendingRequests.delete(id);
if ("error" in event) {
const { error } = event;
const { message } = error;
resolve(new Error(message));
} else {
const { result } = event;
resolve(result);
}
} else {
const { method, params } = event;
try {
// @ts-ignore
this.#listener[method]?.(params);
} catch (error) {
console.error(`Failed to accept ${method} event:`, error);
return;
}
}
}
get closed(): boolean {
if (!this.#webSocket) {
return true;
}
const { readyState } = this.#webSocket;
switch (readyState) {
case WebSocket.CLOSED:
case WebSocket.CLOSING:
return true;
}
return false;
}
close(code?: number, reason?: string): void {
this.#webSocket?.close(code ?? 1001, reason);
}
#close(error?: Error): void {
try {
this.#listener["Inspector.disconnected"]?.(error);
} finally {
for (const resolve of this.#pendingRequests.values()) {
resolve(error ?? new Error("WebSocket closed"));
}
this.#pendingRequests.clear();
}
}
}
function unknownToError(input: unknown): Error {
if (input instanceof Error) {
return input;
}
if (typeof input === "object" && input !== null && "message" in input) {
const { message } = input;
return new Error(`${message}`);
}
return new Error(`${input}`);
}

View File

@@ -0,0 +1,7 @@
{
"name": "bun-inspector-protocol",
"version": "0.0.1",
"dependencies": {
"ws": "^8.13.0"
}
}

View File

@@ -0,0 +1 @@
export type { JSC } from "./jsc";

3695
packages/bun-inspector-protocol/protocol/jsc/index.d.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
// @ts-nocheck
// The content of this file is included in each generated protocol file.
export type Event<T extends keyof EventMap = keyof EventMap> = {
readonly method: T;
readonly params: EventMap[T];
};
export type Request<T extends keyof RequestMap = keyof RequestMap> = {
readonly id: number;
readonly method: T;
readonly params: RequestMap[T];
};
export type Response<T extends keyof ResponseMap = keyof ResponseMap> = {
readonly id: number;
} & (
| {
readonly method?: T;
readonly result: ResponseMap[T];
}
| {
readonly error: {
readonly code?: string;
readonly message: string;
};
}
);

View File

@@ -0,0 +1,58 @@
// Represents the schema of the protocol.json file.
export type Protocol = {
readonly name: string;
readonly version: {
readonly major: number;
readonly minor: number;
};
readonly domains: readonly Domain[];
};
export type Domain = {
readonly domain: string;
readonly dependencies?: readonly string[];
readonly types: readonly Property[];
readonly commands?: readonly Command[];
readonly events?: readonly Event[];
};
export type Command = {
readonly name: string;
readonly description?: string;
readonly parameters?: readonly Property[];
readonly returns?: readonly Property[];
};
export type Event = {
readonly name: string;
readonly description?: string;
readonly parameters: readonly Property[];
};
export type Property = {
readonly id?: string;
readonly name?: string;
readonly description?: string;
readonly optional?: boolean;
} & (
| {
readonly type: "array";
readonly items?: Property;
}
| {
readonly type: "object";
readonly properties?: readonly Property[];
}
| {
readonly type: "string";
readonly enum?: readonly string[];
}
| {
readonly type: "boolean" | "number" | "integer";
}
| {
readonly type: undefined;
readonly $ref: string;
}
);

View File

@@ -0,0 +1,202 @@
import type { Protocol, Domain, Property } from "../protocol/schema";
import { readFileSync, 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 base = readFileSync(new URL("protocol.d.ts", cwd), "utf-8");
const baseNoComments = base.replace(/\/\/.*/g, "");
const jsc = await downloadJsc();
write("jsc/protocol.json", JSON.stringify(jsc));
write("jsc/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(jsc, baseNoComments));
const v8 = await downloadV8();
write("v8/protocol.json", JSON.stringify(v8));
write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));
}
function formatProtocol(protocol: Protocol, extraTs?: string): string {
const { name, domains } = protocol;
const eventMap = new Map();
const commandMap = new Map();
let body = `export namespace ${name} {`;
for (const { domain, types = [], events = [], commands = [] } of domains) {
body += `export namespace ${domain} {`;
for (const type of types) {
body += formatProperty(type);
}
for (const { name, description, parameters = [] } of events) {
const symbol = `${domain}.${name}`;
const title = toTitle(name);
eventMap.set(symbol, `${domain}.${title}`);
body += formatProperty({
id: `${title}Event`,
type: "object",
description: `${description}\n@event \`${symbol}\``,
properties: parameters,
});
}
for (const { name, description, parameters = [], returns = [] } of commands) {
const symbol = `${domain}.${name}`;
const title = toTitle(name);
commandMap.set(symbol, `${domain}.${title}`);
body += formatProperty({
id: `${title}Request`,
type: "object",
description: `${description}\n@request \`${symbol}\``,
properties: parameters,
});
body += formatProperty({
id: `${title}Response`,
type: "object",
description: `${description}\n@response \`${symbol}\``,
properties: returns,
});
}
body += "};";
}
for (const type of ["Event", "Request", "Response"]) {
const sourceMap = type === "Event" ? eventMap : commandMap;
body += formatProperty({
id: `${type}Map`,
type: "object",
properties: [...sourceMap.entries()].map(([name, title]) => ({
name: `"${name}"`,
type: undefined,
$ref: `${title}${type}`,
})),
});
}
if (extraTs) {
body += extraTs;
}
return body + "};";
}
function formatProperty(property: Property): string {
const { id, description, type, optional } = property;
let body = "";
if (id) {
if (description) {
body += `\n${toComment(description)}\n`;
}
body += `export type ${id}=`;
}
if (type === "boolean") {
body += "boolean";
} else if (type === "number" || type === "integer") {
body += "number";
} else if (type === "string") {
const { enum: choices } = property;
if (choices) {
body += choices.map(value => `"${value}"`).join("|");
} else {
body += "string";
}
} else if (type === "array") {
const { items } = property;
const itemType = items ? formatProperty(items) : "unknown";
body += `${itemType}[]`;
} else if (type === "object") {
const { properties } = property;
if (!properties) {
body += "Record<string, unknown>";
} else if (properties.length === 0) {
body += "{}";
} else {
body += "{";
for (const { name, description, ...property } of properties) {
if (description) {
body += `\n${toComment(description)}`;
}
const delimit = property.optional ? "?:" : ":";
body += `\n${name}${delimit}${formatProperty({ ...property, id: undefined })};`;
}
body += "}";
}
} else if ("$ref" in property) {
body += property.$ref;
} else {
body += "unknown";
}
if (optional) {
body += "|undefined";
}
if (id) {
body += ";";
}
return body;
}
/**
* @link https://github.com/ChromeDevTools/devtools-protocol/tree/master/json
*/
async function downloadV8(): Promise<Protocol> {
const baseUrl = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json";
const domains = ["Runtime", "Console", "Debugger", "Memory", "HeapProfiler", "Profiler", "Network", "Inspector"];
return Promise.all([
download<Protocol>(`${baseUrl}/js_protocol.json`),
download<Protocol>(`${baseUrl}/browser_protocol.json`),
]).then(([js, browser]) => ({
name: "V8",
version: js.version,
domains: [...js.domains, ...browser.domains]
.filter(domain => !domains.includes(domain.domain))
.sort((a, b) => a.domain.localeCompare(b.domain)),
}));
}
/**
* @link https://github.com/WebKit/WebKit/tree/main/Source/JavaScriptCore/inspector/protocol
*/
async function downloadJsc(): Promise<Protocol> {
const baseUrl = "https://raw.githubusercontent.com/WebKit/WebKit/main/Source/JavaScriptCore/inspector/protocol";
const domains = [
"Runtime",
"Console",
"Debugger",
"Heap",
"ScriptProfiler",
"CPUProfiler",
"GenericTypes",
"Network",
"Inspector",
];
return {
name: "JSC",
version: {
major: 1,
minor: 3,
},
domains: await Promise.all(domains.map(domain => download<Domain>(`${baseUrl}/${domain}.json`))).then(domains =>
domains.sort((a, b) => a.domain.localeCompare(b.domain)),
),
};
}
async function download<V>(url: string): Promise<V> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${response.status}: ${url}`);
}
return response.json();
}
function toTitle(name: string): string {
return name.charAt(0).toUpperCase() + name.slice(1);
}
function toComment(description?: string): string {
if (!description) {
return "";
}
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
return lines.join("\n");
}

View File

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

View File

@@ -1,4 +1,6 @@
{ {
"name": "bun-lambda",
"private": true,
"devDependencies": { "devDependencies": {
"bun-types": "^0.7.0", "bun-types": "^0.7.0",
"jszip": "^3.10.1", "jszip": "^3.10.1",

View File

@@ -1,4 +1,5 @@
{ {
"name": "bun-release-action",
"private": true, "private": true,
"dependencies": { "dependencies": {
"aws4fetch": "^1.0.17", "aws4fetch": "^1.0.17",

2
packages/bun-vscode/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules
extension

View File

@@ -5,10 +5,7 @@
"name": "Extension", "name": "Extension",
"type": "extensionHost", "type": "extensionHost",
"request": "launch", "request": "launch",
"args": [ "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/example"],
"--extensionDevelopmentPath=${workspaceFolder}",
"${workspaceFolder}/example"
],
"outFiles": ["${workspaceFolder}/dist/**/*.js"], "outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "Build (watch)" "preLaunchTask": "Build (watch)"
}, },

View File

@@ -5,5 +5,5 @@
"search.exclude": { "search.exclude": {
"out": true // set this to false to include "out" folder in search results "out": true // set this to false to include "out" folder in search results
}, },
"typescript.tsc.autoDetect": "off", "typescript.tsc.autoDetect": "off"
} }

View File

@@ -4,13 +4,15 @@
{ {
"label": "Build", "label": "Build",
"type": "shell", "type": "shell",
"command": "bun run build" "command": "bun run build",
"problemMatcher": "$esbuild"
}, },
{ {
"label": "Build (watch)", "label": "Build (watch)",
"type": "shell", "type": "shell",
"command": "bun run build:watch", "command": "bun run build:watch",
"isBackground": true "isBackground": true,
"problemMatcher": "$esbuild-watch"
} }
] ]
} }

View File

@@ -1 +1,22 @@
# Debug Adapter Protocol for Bun # Bun for Visual Studio Code
![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/oven.vscode-bun)
![Visual Studio Marketplace Installs](https://img.shields.io/visual-studio-marketplace/i/oven.vscode-bun)
![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/oven.vscode-bun)
<img align="right" src="https://user-images.githubusercontent.com/709451/182802334-d9c42afe-f35d-4a7b-86ea-9985f73f20c3.png" height="150px" style="float: right; padding: 30px;">
This extension adds support for using [Bun](https://bun.sh/) with Visual Studio Code. Bun is an all-in-one toolkit for JavaScript and TypeScript apps.
At its core is the _Bun runtime_, a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage.
<div align="center">
<a href="https://bun.sh/docs">Documentation</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://discord.com/invite/CXdq2DP29u">Discord</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/oven-sh/bun/issues/new">Issues</a>
<span>&nbsp;&nbsp;•&nbsp;&nbsp;</span>
<a href="https://github.com/oven-sh/bun/issues/159">Roadmap</a>
<br/>
</div>

View File

@@ -1,5 +0,0 @@
* Off-by-one for debug lines
* Formatting values in console (some code is wired up)
* Play button on debugger actually starting Bun
* bun debug or --inspect command added to Bun, not need Bun.serve
* Breakpoint actually setting

Binary file not shown.

View File

@@ -4,16 +4,15 @@
{ {
"type": "bun", "type": "bun",
"request": "launch", "request": "launch",
"name": "Debug", "name": "Debug Bun",
"program": "${workspaceFolder}/example.js", "program": "${file}",
"stopOnEntry": true "watch": "hot"
}, },
{ {
"type": "bun", "type": "bun",
"request": "attach", "request": "attach",
"name": "Attach", "name": "Attach to Bun",
"program": "${workspaceFolder}/example.js", "url": "ws://localhost:6499/",
"stopOnEntry": true
} }
] ]
} }

View File

@@ -0,0 +1,30 @@
// @bun
// example.ts
var a = function (request) {
b(request);
};
var b = function (request) {
c(request);
};
var c = function (request) {
console.log(request);
};
var example_default = {
async fetch(request, server) {
a(request);
const coolThing = new SuperCoolThing();
coolThing.doCoolThing();
debugger;
return new Response(request.url);
},
};
class SuperCoolThing {
doCoolThing() {
console.log("super cool thing!");
}
}
export { example_default as default };
//# debugId=9BB0B773A8E4771564756e2164756e21
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiZXhhbXBsZS50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsKICAgICJpbXBvcnQgdHlwZSB7IFNlcnZlciB9IGZyb20gXCJidW5cIjtcblxuZXhwb3J0IGRlZmF1bHQge1xuICBhc3luYyBmZXRjaChyZXF1ZXN0OiBSZXF1ZXN0LCBzZXJ2ZXI6IFNlcnZlcik6IFByb21pc2U8UmVzcG9uc2U+IHtcbiAgICBhKHJlcXVlc3QpO1xuICAgIGNvbnN0IGNvb2xUaGluZzogQ29vbFRoaW5nID0gbmV3IFN1cGVyQ29vbFRoaW5nKCk7XG4gICAgY29vbFRoaW5nLmRvQ29vbFRoaW5nKCk7XG4gICAgZGVidWdnZXI7XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShyZXF1ZXN0LnVybCk7XG4gIH1cbn07XG5cbi8vIGFcbmZ1bmN0aW9uIGEocmVxdWVzdDogUmVxdWVzdCk6IHZvaWQge1xuICBiKHJlcXVlc3QpO1xufVxuXG4vLyBiXG5mdW5jdGlvbiBiKHJlcXVlc3Q6IFJlcXVlc3QpOiB2b2lkIHtcbiAgYyhyZXF1ZXN0KTtcbn1cblxuLy8gY1xuZnVuY3Rpb24gYyhyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gIGNvbnNvbGUubG9nKHJlcXVlc3QpO1xufVxuXG5pbnRlcmZhY2UgQ29vbFRoaW5nIHtcbiAgZG9Db29sVGhpbmcoKTogdm9pZDtcbn1cblxuY2xhc3MgU3VwZXJDb29sVGhpbmcgaW1wbGVtZW50cyBDb29sVGhpbmcge1xuICBkb0Nvb2xUaGluZygpOiB2b2lkIHtcbiAgICBjb25zb2xlLmxvZyhcInN1cGVyIGNvb2wgdGhpbmchXCIpO1xuICB9XG59XG4iCiAgXSwKICAibWFwcGluZ3MiOiAiOztBQS8vLy8vZkFhQSxJQUFTLFlBQUMsQ0FBQyxTQUF3QjtBQUNqQyxJQUFFLE9BQU87QUFBQTtBQUlYLElBQVMsWUFBQyxDQUFDLFNBQXdCO0FBQ2pDLElBQUUsT0FBTztBQUFBO0FBSVgsSUFBUyxZQUFDLENBQUMsU0FBa0I7QUFDM0IsVUFBUSxJQUFJLE9BQU87QUFBQTtBQXRCckIsSUFBZTtBQUFBLE9BQ1AsTUFBSyxDQUFDLFNBQWtCLFFBQW1DO0FBQy9ELE1BQUUsT0FBTztBQUNULFVBQU0sWUFBdUIsSUFBSTtBQUNqQyxjQUFVLFlBQVk7QUFDdEI7QUFDQSxXQUFPLElBQUksU0FBUyxRQUFRLEdBQUc7QUFBQTtBQUVuQztBQXFCQTtBQUFBLE1BQU0sZUFBb0M7QUFBQSxFQUN4QyxXQUFXLEdBQVM7QUFDbEIsWUFBUSxJQUFJLG1CQUFtQjtBQUFBO0FBRW5DOyIsCiAgImRlYnVnSWQiOiAiOUJCMEI3NzNBOEU0NzcxNTY0NzU2ZTIxNjQ3NTZlMjEiLAogICJuYW1lcyI6IFtdCn0=

View File

@@ -5,7 +5,7 @@ import { readFile } from "node:fs/promises";
app app
.get("/", (req, res) => { .get("/", (req, res) => {
console.log("I am logging a request!"); console.log("I am logging a request!??");
readFile(import.meta.path, "utf-8").then(data => { readFile(import.meta.path, "utf-8").then(data => {
console.log(data.length); console.log(data.length);
debugger; debugger;
@@ -57,7 +57,7 @@ Bun.serve({
inspector: true, inspector: true,
development: true, development: true,
fetch(request, server) { fetch(request, server) {
console.log(request); // console.log(request);
return new Response(request.url); return new Response(request.url);
}, },
}); });

View File

@@ -0,0 +1,34 @@
export default {
async fetch(request: Request): Promise<Response> {
a(request);
const coolThing: CoolThing = new SuperCoolThing();
coolThing.doCoolThing();
debugger;
return new Response("HELLO WORLD");
},
};
// a
function a(request: Request): void {
b(request);
}
// b
function b(request: Request): void {
c(request);
}
// c
function c(request: Request) {
console.log(request);
}
interface CoolThing {
doCoolThing(): void;
}
class SuperCoolThing implements CoolThing {
doCoolThing(): void {
console.log("BLAH BLAH");
}
}

View File

@@ -1,65 +1,101 @@
{ {
"name": "bun-vscode", "name": "bun-vscode",
"version": "0.0.1", "version": "0.0.1",
"main": "./dist/extension.js", "author": "oven",
"repository": {
"type": "git",
"url": "https://github.com/oven-sh/bun"
},
"main": "dist/extension.js",
"dependencies": {
"semver": "^7.5.4",
"source-map-js": "^1.0.2",
"ws": "^8.13.0"
},
"devDependencies": { "devDependencies": {
"@types/vscode": "^1.81.0", "@types/vscode": "^1.81.0",
"@vscode/debugadapter": "^1.56.0", "@vscode/debugadapter": "^1.56.0",
"@vscode/debugadapter-testsupport": "^1.56.0", "@vscode/debugadapter-testsupport": "^1.56.0",
"bun-types": "^0.7.3", "bun-types": "^0.7.3",
"typescript": "^5.0.0" "typescript": "^5.0.0",
"esbuild": "^0.19.2"
}, },
"activationEvents": [ "activationEvents": [
"onLanguage:javascript",
"onLanguage:javascriptreact",
"onLanguage:typescript",
"onLanguage:typescriptreact",
"workspaceContains:**/.lockb",
"onDebugResolve:bun", "onDebugResolve:bun",
"onDebugDynamicConfigurations:bun", "onDebugDynamicConfigurations:bun"
"onCommand:extension.bun.getProgramName"
], ],
"browser": "./dist/web-extension.js", "browser": "dist/web-extension.js",
"bugs": {
"url": "https://github.com/oven-sh/bun/issues"
},
"capabilities": {
"untrustedWorkspaces": {
"supported": false,
"description": "This extension needs to be able to run your code using Bun."
}
},
"categories": [ "categories": [
"Programming Languages", "Programming Languages",
"Debuggers" "Debuggers",
"Testing"
], ],
"contributes": { "contributes": {
"configuration": {
"title": "Bun",
"properties": {
"bun.path": {
"type": "string",
"description": "A path to the `bun` executable. By default, the extension looks for `bun` in the `PATH`, but if set, it will use the specified path instead.",
"scope": "window",
"default": null
}
}
},
"commands": [
{
"command": "extension.bun.runFile",
"title": "Run File",
"category": "Bun",
"enablement": "!inDebugMode",
"icon": "$(play)"
},
{
"command": "extension.bun.debugFile",
"title": "Debug File",
"category": "Bun",
"enablement": "!inDebugMode",
"icon": "$(debug-alt)"
}
],
"menus": { "menus": {
"editor/title/run": [ "editor/title/run": [
{ {
"command": "extension.bun.runEditorContents", "command": "extension.bun.runFile",
"when": "resourceLangId == javascript", "when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
"group": "navigation@1" "group": "navigation@1"
}, },
{ {
"command": "extension.bun.debugEditorContents", "command": "extension.bun.debugFile",
"when": "resourceLangId == javascript", "when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
"group": "navigation@2" "group": "navigation@2"
} }
], ],
"commandPalette": [ "commandPalette": [
{ {
"command": "extension.bun.debugEditorContents", "command": "extension.bun.runFile",
"when": "resourceLangId == javascript" "when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
}, },
{ {
"command": "extension.bun.runEditorContents", "command": "extension.bun.debugFile",
"when": "resourceLangId == javascript" "when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
} }
] ]
}, },
"commands": [
{
"command": "extension.bun.debugEditorContents",
"title": "Debug File",
"category": "Bun Debug",
"enablement": "!inDebugMode",
"icon": "$(debug-alt)"
},
{
"command": "extension.bun.runEditorContents",
"title": "Run File",
"category": "Bun Debug",
"enablement": "!inDebugMode",
"icon": "$(play)"
}
],
"breakpoints": [ "breakpoints": [
{ {
"language": "javascript" "language": "javascript"
@@ -85,31 +121,64 @@
"typescriptreact" "typescriptreact"
], ],
"runtime": "node", "runtime": "node",
"program": "./dist/adapter.js", "program": "dist/adapter.js",
"configurationAttributes": { "configurationAttributes": {
"launch": { "launch": {
"required": [ "required": [
"program" "program"
], ],
"properties": { "properties": {
"runtime": {
"type": "string",
"description": "The path to Bun.",
"default": "bun"
},
"program": { "program": {
"type": "string", "type": "string",
"description": "The file to run and debug.", "description": "The file to debug.",
"default": "${workspaceFolder}/${command:AskForProgramName}" "default": "${file}"
},
"cwd": {
"type": "string",
"description": "The working directory.",
"default": "${workspaceFolder}"
},
"args": {
"type": "array",
"description": "The arguments passed to Bun.",
"items": {
"type": "string"
},
"default": []
},
"env": {
"type": "object",
"description": "The environment variables passed to Bun.",
"default": {}
},
"inheritEnv": {
"type": "boolean",
"description": "If environment variables should be inherited from the parent process.",
"default": true
},
"watch": {
"type": ["boolean", "string"],
"description": "If the process should be restarted when files change.",
"enum": [
true,
false,
"hot"
],
"default": true
} }
} }
}, },
"attach": { "attach": {
"properties": { "properties": {
"port": { "url": {
"type": "integer",
"description": "The port to attach and debug.",
"default": 6499
},
"hostname": {
"type": "string", "type": "string",
"description": "The hostname to attach and debug.", "description": "The URL of the Bun process to attach to.",
"default": "localhost" "default": "ws://localhost:6499/"
} }
} }
} }
@@ -119,12 +188,13 @@
"type": "bun", "type": "bun",
"request": "launch", "request": "launch",
"name": "Bun: Debug", "name": "Bun: Debug",
"program": "${workspaceFolder}/${command:AskForProgramName}" "program": "${file}"
}, },
{ {
"type": "bun", "type": "bun",
"request": "attach", "request": "attach",
"name": "Bun: Attach" "name": "Bun: Attach",
"url": "ws://localhost:6499/"
} }
], ],
"configurationSnippets": [ "configurationSnippets": [
@@ -135,7 +205,7 @@
"type": "bun", "type": "bun",
"request": "launch", "request": "launch",
"name": "Ask for file name", "name": "Ask for file name",
"program": "^\"\\${workspaceFolder}/\\${command:AskForProgramName}\"" "program": "^\"\\${file}\""
} }
}, },
{ {
@@ -145,14 +215,10 @@
"type": "bun", "type": "bun",
"request": "attach", "request": "attach",
"name": "Attach to Bun", "name": "Attach to Bun",
"port": 6499, "url": "ws://localhost:6499/"
"hostname": "localhost"
} }
} }
], ]
"variables": {
"AskForProgramName": "extension.bun.getProgramName"
}
} }
], ],
"languages": [ "languages": [
@@ -165,15 +231,15 @@
".lockb" ".lockb"
], ],
"icon": { "icon": {
"dark": "./src/assets/icon-small.png", "dark": "src/assets/icon-small.png",
"light": "./src/assets/icon-small.png" "light": "src/assets/icon-small.png"
} }
} }
], ],
"jsonValidation": [ "jsonValidation": [
{ {
"fileMatch": "package.json", "fileMatch": "package.json",
"url": "./src/resources/package.json" "url": "src/resources/package.json"
} }
], ],
"customEditors": [ "customEditors": [
@@ -189,26 +255,40 @@
} }
] ]
}, },
"description": "Visual Studio Code extension for Bun.", "description": "The Visual Studio Code extension for Bun.",
"displayName": "Bun",
"engines": { "engines": {
"vscode": "^1.66.0" "vscode": "^1.81.0"
}, },
"extensionKind": [
"workspace"
],
"galleryBanner": { "galleryBanner": {
"color": "#C80000", "color": "#C80000",
"theme": "dark" "theme": "dark"
}, },
"icon": "./src/assets/icon.png", "homepage": "https://bun.sh/",
"icon": "src/assets/icon.png",
"keywords": [
"bun",
"node.js",
"javascript",
"typescript",
"vscode"
],
"license": "MIT", "license": "MIT",
"private": true,
"publisher": "oven", "publisher": "oven",
"scripts": { "scripts": {
"build": "bunx esbuild src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs", "bundle": "./node_modules/.bin/esbuild src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs",
"build:watch": "bunx esbuild --watch src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs" "prebuild": "bun run bundle && rm -rf extension && mkdir -p extension/src && cp -r dist extension/dist && cp -r src/assets extension/src/assets && cp package.json extension && cp README.md extension",
"build": "cd extension && vsce package",
"build:watch": "./node_modules/.bin/esbuild --watch src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs"
}, },
"workspaceTrust": { "workspaceTrust": {
"request": "never" "request": "never"
}, },
"dependencies": { "workspaces": [
"ws": "^8.13.0" "../bun-debug-adapter-protocol",
} "../bun-inspector-protocol"
]
} }

View File

@@ -1,118 +0,0 @@
import * as vscode from "vscode";
import { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode";
import { DAPAdapter } from "./dap";
import lockfile from "./lockfile";
export function activateBunDebug(context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
lockfile(context);
context.subscriptions.push(
vscode.commands.registerCommand("extension.bun.runEditorContents", (resource: vscode.Uri) => {
let targetResource = resource;
if (!targetResource && vscode.window.activeTextEditor) {
targetResource = vscode.window.activeTextEditor.document.uri;
}
if (targetResource) {
vscode.debug.startDebugging(
undefined,
{
type: "bun",
name: "Run File",
request: "launch",
program: targetResource.fsPath,
},
{ noDebug: true },
);
}
}),
vscode.commands.registerCommand("extension.bun.debugEditorContents", (resource: vscode.Uri) => {
let targetResource = resource;
if (!targetResource && vscode.window.activeTextEditor) {
targetResource = vscode.window.activeTextEditor.document.uri;
}
if (targetResource) {
vscode.debug.startDebugging(undefined, {
type: "bun",
name: "Debug File",
request: "launch",
program: targetResource.fsPath,
stopOnEntry: true,
});
}
}),
);
context.subscriptions.push(
vscode.commands.registerCommand("extension.bun.getProgramName", config => {
return vscode.window.showInputBox({
placeHolder: "Please enter the name of a file in the workspace folder",
value: "src/index.js",
});
}),
);
const provider = new BunConfigurationProvider();
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("bun", provider));
context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider(
"bun",
{
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
return [
{
name: "Launch",
request: "launch",
type: "bun",
program: "${file}",
},
];
},
},
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
),
);
if (!factory) {
factory = new InlineDebugAdapterFactory();
}
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory));
if ("dispose" in factory) {
// @ts-expect-error ???
context.subscriptions.push(factory);
}
}
class BunConfigurationProvider implements vscode.DebugConfigurationProvider {
resolveDebugConfiguration(
folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
token?: CancellationToken,
): ProviderResult<DebugConfiguration> {
// if launch.json is missing or empty
if (!config.type && !config.request && !config.name) {
const editor = vscode.window.activeTextEditor;
if (editor && editor.document.languageId === "javascript") {
config.type = "bun";
config.name = "Launch";
config.request = "launch";
config.program = "${file}";
config.stopOnEntry = true;
}
}
if (!config.program) {
return vscode.window.showInformationMessage("Cannot find a program to debug").then(_ => {
return undefined; // abort launch
});
}
return config;
}
}
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
return new vscode.DebugAdapterInlineImplementation(new DAPAdapter(_session));
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,10 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import { activateBunDebug } from "./activate"; import activateLockfile from "./features/lockfile";
import activateDebug from "./features/debug";
const runMode: "external" | "server" | "namedPipeServer" | "inline" = "inline";
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {
if (runMode === "inline") { activateLockfile(context);
activateBunDebug(context); activateDebug(context);
return;
}
throw new Error(`This extension does not support '${runMode}' mode.`);
} }
export function deactivate() { export function deactivate() {}
// No-op
}

View File

@@ -0,0 +1,153 @@
import * as vscode from "vscode";
import type { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode";
import type { DAP } from "../../../bun-debug-adapter-protocol";
import { DebugAdapter } from "../../../bun-debug-adapter-protocol";
import { DebugSession } from "@vscode/debugadapter";
const debugConfiguration: vscode.DebugConfiguration = {
type: "bun",
request: "launch",
name: "Debug Bun",
program: "${file}",
watch: true,
};
const runConfiguration: vscode.DebugConfiguration = {
type: "bun",
request: "launch",
name: "Run Bun",
program: "${file}",
watch: true,
};
const attachConfiguration: vscode.DebugConfiguration = {
type: "bun",
request: "attach",
name: "Attach to Bun",
url: "ws://localhost:6499/",
};
const debugConfigurations: vscode.DebugConfiguration[] = [debugConfiguration, attachConfiguration];
export default function (context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
context.subscriptions.push(
vscode.commands.registerCommand("extension.bun.runFile", (resource: vscode.Uri) => {
let targetResource = resource;
if (!targetResource && vscode.window.activeTextEditor) {
targetResource = vscode.window.activeTextEditor.document.uri;
}
if (targetResource) {
vscode.debug.startDebugging(undefined, runConfiguration, {
noDebug: true,
});
}
}),
vscode.commands.registerCommand("extension.bun.debugFile", (resource: vscode.Uri) => {
let targetResource = resource;
if (!targetResource && vscode.window.activeTextEditor) {
targetResource = vscode.window.activeTextEditor.document.uri;
}
if (targetResource) {
vscode.debug.startDebugging(undefined, {
...debugConfiguration,
program: targetResource.fsPath,
});
}
}),
);
const provider = new BunConfigurationProvider();
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("bun", provider));
context.subscriptions.push(
vscode.debug.registerDebugConfigurationProvider(
"bun",
{
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
return debugConfigurations;
},
},
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
),
);
if (!factory) {
factory = new InlineDebugAdapterFactory();
}
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory));
if ("dispose" in factory && typeof factory.dispose === "function") {
// @ts-ignore
context.subscriptions.push(factory);
}
}
class BunConfigurationProvider implements vscode.DebugConfigurationProvider {
resolveDebugConfiguration(
folder: WorkspaceFolder | undefined,
config: DebugConfiguration,
token?: CancellationToken,
): ProviderResult<DebugConfiguration> {
if (!config.type && !config.request && !config.name) {
const editor = vscode.window.activeTextEditor;
if (editor && isJavaScript(editor.document.languageId)) {
Object.assign(config, debugConfiguration);
}
}
return config;
}
}
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
const adapter = new VSCodeAdapter(_session);
return new vscode.DebugAdapterInlineImplementation(adapter);
}
}
function isJavaScript(languageId: string): boolean {
return (
languageId === "javascript" ||
languageId === "javascriptreact" ||
languageId === "typescript" ||
languageId === "typescriptreact"
);
}
export class VSCodeAdapter extends DebugSession {
#adapter: DebugAdapter;
#dap: vscode.OutputChannel;
constructor(session: vscode.DebugSession) {
super();
this.#dap = vscode.window.createOutputChannel("Debug Adapter Protocol");
this.#adapter = new DebugAdapter({
sendToAdapter: this.sendMessage.bind(this),
});
}
sendMessage(message: DAP.Request | DAP.Response | DAP.Event): void {
console.log("[dap] -->", message);
this.#dap.appendLine("--> " + JSON.stringify(message));
const { type } = message;
if (type === "response") {
this.sendResponse(message);
} else if (type === "event") {
this.sendEvent(message);
} else {
throw new Error(`Not supported: ${type}`);
}
}
handleMessage(message: DAP.Event | DAP.Request | DAP.Response): void {
console.log("[dap] <--", message);
this.#dap.appendLine("<-- " + JSON.stringify(message));
this.#adapter.accept(message);
}
dispose() {
this.#adapter.close();
this.#dap.dispose();
}
}

View File

@@ -52,10 +52,10 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro
process.stderr.on("data", (data: Buffer) => { process.stderr.on("data", (data: Buffer) => {
stderr += data.toString(); stderr += data.toString();
}); });
process.on("error", (error) => { process.on("error", error => {
reject(error); reject(error);
}); });
process.on("exit", (code) => { process.on("exit", code => {
if (code === 0) { if (code === 0) {
resolve(stdout); resolve(stdout);
} else { } else {
@@ -65,19 +65,15 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro
}); });
} }
export default function(context: vscode.ExtensionContext): void { export default function (context: vscode.ExtensionContext): void {
const viewType = "bun.lockb"; const viewType = "bun.lockb";
const provider = new BunLockfileEditorProvider(context); const provider = new BunLockfileEditorProvider(context);
vscode.window.registerCustomEditorProvider( vscode.window.registerCustomEditorProvider(viewType, provider, {
viewType, supportsMultipleEditorsPerDocument: true,
provider, webviewOptions: {
{ enableFindWidget: true,
supportsMultipleEditorsPerDocument: true, retainContextWhenHidden: true,
webviewOptions: {
enableFindWidget: true,
retainContextWhenHidden: true,
},
}, },
); });
} }

View File

@@ -1,308 +0,0 @@
import { Socket, createConnection } from "node:net";
import { inspect } from "node:util";
import type { JSC } from "../types/jsc";
export type { JSC };
export type JSCClientOptions = {
url: string | URL;
retry?: boolean;
onEvent?: (event: JSC.Event) => void;
onRequest?: (request: JSC.Request) => void;
onResponse?: (response: JSC.Response) => void;
onError?: (error: Error) => void;
onClose?: (code: number, reason: string) => void;
};
const headerInvalidNumber = 2147483646;
// We use non-printable characters to separate messages in the stream.
// These should never appear in textual messages.
// These are non-sequential so that code which just counts up from 0 doesn't accidentally parse them as messages.
// 0x12 0x11 0x13 0x14 as a little-endian 32-bit unsigned integer
const headerPrefix = "\x14\x13\x11\x12";
// 0x14 0x12 0x13 0x11 as a little-endian 32-bit unsigned integer
const headerSuffixString = "\x11\x13\x12\x14";
const headerSuffixInt = Buffer.from(headerSuffixString).readInt32LE(0);
const headerPrefixInt = Buffer.from(headerPrefix).readInt32LE(0);
const messageLengthBuffer = new ArrayBuffer(12);
const messageLengthDataView = new DataView(messageLengthBuffer);
messageLengthDataView.setInt32(0, headerPrefixInt, true);
messageLengthDataView.setInt32(8, headerSuffixInt, true);
function writeJSONMessageToBuffer(message: any) {
const asString = JSON.stringify(message);
const byteLength = Buffer.byteLength(asString, "utf8");
const buffer = Buffer.allocUnsafe(12 + byteLength);
buffer.writeInt32LE(headerPrefixInt, 0);
buffer.writeInt32LE(byteLength, 4);
buffer.writeInt32LE(headerSuffixInt, 8);
if (buffer.write(asString, 12, byteLength, "utf8") !== byteLength) {
throw new Error("Failed to write message to buffer");
}
return buffer;
}
let currentMessageLength = 0;
const DEBUGGING = true;
function extractMessageLengthAndOffsetFromBytes(buffer: Buffer, offset: number) {
const bufferLength = buffer.length;
while (offset < bufferLength) {
const headerStart = buffer.indexOf(headerPrefix, offset, "binary");
if (headerStart === -1) {
if (DEBUGGING) {
console.error("No header found in buffer of length " + bufferLength + " starting at offset " + offset);
}
return headerInvalidNumber;
}
// [headerPrefix (4), byteLength (4), headerSuffix (4)]
if (bufferLength <= headerStart + 12) {
if (DEBUGGING) {
console.error(
"Not enough bytes for header in buffer of length " + bufferLength + " starting at offset " + offset,
);
}
return headerInvalidNumber;
}
const prefix = buffer.readInt32LE(headerStart);
const byteLengthInt = buffer.readInt32LE(headerStart + 4);
const suffix = buffer.readInt32LE(headerStart + 8);
if (prefix !== headerPrefixInt || suffix !== headerSuffixInt) {
offset = headerStart + 1;
currentMessageLength = 0;
if (DEBUGGING) {
console.error(
"Invalid header in buffer of length " + bufferLength + " starting at offset " + offset + ": " + prefix,
byteLengthInt,
suffix,
);
}
continue;
}
if (byteLengthInt < 0) {
if (DEBUGGING) {
console.error(
"Invalid byteLength in buffer of length " + bufferLength + " starting at offset " + offset + ": " + prefix,
byteLengthInt,
suffix,
);
}
return headerInvalidNumber;
}
if (byteLengthInt === 0) {
// Ignore 0-length messages
// Shouldn't happen in practice
offset = headerStart + 12;
currentMessageLength = 0;
if (DEBUGGING) {
console.error(
"Ignoring 0-length message in buffer of length " + bufferLength + " starting at offset " + offset,
);
console.error({
buffer: buffer,
string: buffer.toString(),
});
}
continue;
}
currentMessageLength = byteLengthInt;
return headerStart + 12;
}
if (DEBUGGING) {
if (bufferLength > 0)
console.error("Header not found in buffer of length " + bufferLength + " starting at offset " + offset);
}
return headerInvalidNumber;
}
class StreamingReader {
pendingBuffer: Buffer;
constructor() {
this.pendingBuffer = Buffer.alloc(0);
}
*onMessage(chunk: Buffer) {
let buffer: Buffer;
if (this.pendingBuffer.length > 0) {
this.pendingBuffer = buffer = Buffer.concat([this.pendingBuffer, chunk]);
} else {
this.pendingBuffer = buffer = chunk;
}
currentMessageLength = 0;
for (
let offset = extractMessageLengthAndOffsetFromBytes(buffer, 0);
buffer.length > 0 && offset !== headerInvalidNumber;
currentMessageLength = 0, offset = extractMessageLengthAndOffsetFromBytes(buffer, 0)
) {
const messageLength = currentMessageLength;
const start = offset;
const end = start + messageLength;
offset = end;
const messageChunk = buffer.slice(start, end);
this.pendingBuffer = buffer = buffer.slice(offset);
yield messageChunk.toString();
}
}
}
export class JSCClient {
#options: JSCClientOptions;
#requestId: number;
#pendingMessages: Buffer[];
#pendingRequests: Map<number, (result: unknown) => void>;
#socket: Socket;
#ready?: Promise<void>;
#reader = new StreamingReader();
signal?: AbortSignal;
constructor(options: JSCClientOptions) {
this.#options = options;
this.#socket = undefined;
this.#requestId = 1;
this.#pendingMessages = [];
this.#pendingRequests = new Map();
}
get ready(): Promise<void> {
if (!this.#ready) {
this.#ready = this.#connect();
}
return this.#ready;
}
#connect(): Promise<void> {
const { url, retry, onError, onResponse, onEvent, onClose } = this.#options;
let [host, port] = typeof url === "string" ? url.split(":") : [url.hostname, url.port];
if (port == null) {
if (host == null) {
host = "localhost";
port = "9229";
} else {
port = "9229";
}
}
if (host == null) {
host = "localhost";
}
var resolve,
reject,
promise = new Promise<void>((r1, r2) => {
resolve = r1;
reject = r2;
}),
socket: Socket;
let didConnect = false;
this.#socket = socket = createConnection(
{
host,
port: Number(port),
},
() => {
for (const message of this.#pendingMessages) {
this.#send(message);
}
this.#pendingMessages.length = 0;
didConnect = true;
resolve();
},
)
.once("error", e => {
const error = new Error(`Socket error: ${e?.message || e}`);
reject(error);
})
.on("data", buffer => {
for (const message of this.#reader.onMessage(buffer)) {
let received: JSC.Event | JSC.Response;
try {
received = JSON.parse(message);
} catch {
const error = new Error(`Invalid WebSocket data: ${inspect(message)}`);
onError?.(error);
return;
}
console.log({ received });
if ("id" in received) {
onResponse?.(received);
if ("error" in received) {
const { message, code = "?" } = received.error;
const error = new Error(`${message} [code: ${code}]`);
error.code = code;
onError?.(error);
this.#pendingRequests.get(received.id)?.(error);
} else {
this.#pendingRequests.get(received.id)?.(received.result);
}
} else {
onEvent?.(received);
}
}
})
.on("close", hadError => {
if (didConnect) {
onClose?.(hadError ? 1 : 0, "Socket closed");
}
});
return promise;
}
#send(message: any): void {
const socket = this.#socket;
const framed = writeJSONMessageToBuffer(message);
if (socket && !socket.connecting) {
socket.write(framed);
} else {
this.#pendingMessages.push(framed);
}
}
async fetch<T extends keyof JSC.RequestMap>(
method: T,
params?: JSC.Request<T>["params"],
): Promise<JSC.ResponseMap[T]> {
const request: JSC.Request<T> = {
id: this.#requestId++,
method,
params,
};
this.#options.onRequest?.(request);
return new Promise((resolve, reject) => {
const done = (result: Error | JSC.ResponseMap[T]) => {
this.#pendingRequests.delete(request.id);
if (result instanceof Error) {
reject(result);
} else {
resolve(result);
}
};
this.#pendingRequests.set(request.id, done);
this.#send(request);
});
}
close(): void {
if (this.#socket) this.#socket.end();
}
}

View File

@@ -1,10 +1,5 @@
import * as vscode from "vscode"; import * as vscode from "vscode";
import { activateBunDebug } from "./activate";
export function activate(context: vscode.ExtensionContext) { export function activate(context: vscode.ExtensionContext) {}
activateBunDebug(context);
}
export function deactivate() { export function deactivate() {}
// No-op
}

View File

@@ -1,10 +0,0 @@
import { serve } from "bun";
serve({
port: 9229,
development: true,
fetch(request, server) {
return new Response(`Hello, ${request.url}!`);
},
inspector: true,
});

View File

@@ -1,123 +0,0 @@
import { beforeAll, afterAll, describe, test, expect, mock } from "bun:test";
import { Worker } from "node:worker_threads";
import { JSCClient, type JSC } from "../src/jsc";
let worker: Worker;
beforeAll(async () => {
const { pathname } = new URL("./fixtures/echo.ts", import.meta.url);
worker = new Worker(pathname, { smol: true });
while (true) {
try {
await fetch("http://localhost:9229/");
break;
} catch {}
}
});
afterAll(() => {
worker?.terminate();
});
describe("JSCClient", () => {
const onRequest = mock((request: JSC.Request) => {
expect(request).toBeInstanceOf(Object);
expect(request.id).toBeNumber();
expect(request.method).toBeString();
if (request.params) {
expect(request.params).toBeInstanceOf(Object);
} else {
expect(request).toBeUndefined();
}
});
const onResponse = mock((response: JSC.Response) => {
expect(response).toBeInstanceOf(Object);
expect(response.id).toBeNumber();
if ("result" in response) {
expect(response.result).toBeInstanceOf(Object);
} else {
expect(response.error).toBeInstanceOf(Object);
expect(response.error.message).toBeString();
}
});
const onEvent = mock((event: JSC.Event) => {
expect(event).toBeInstanceOf(Object);
expect(event.method).toBeString();
if (event.params) {
expect(event.params).toBeInstanceOf(Object);
} else {
expect(event).toBeUndefined();
}
});
const onError = mock((error: Error) => {
expect(error).toBeInstanceOf(Error);
});
const client = new JSCClient({
url: "ws://localhost:9229/bun:inspect",
onRequest,
onResponse,
onEvent,
onError,
});
test("can connect", () => {
expect(client.ready).resolves.toBeUndefined();
});
test("can send a request", () => {
expect(client.fetch("Runtime.evaluate", { expression: "1 + 1" })).resolves.toStrictEqual({
result: {
type: "number",
value: 2,
description: "2",
},
wasThrown: false,
});
expect(onRequest).toHaveBeenCalled();
expect(onRequest.mock.lastCall[0]).toStrictEqual({
id: 1,
method: "Runtime.evaluate",
params: { expression: "1 + 1" },
});
expect(onResponse).toHaveBeenCalled();
expect(onResponse.mock.lastCall[0]).toMatchObject({
id: 1,
result: {
result: {
type: "number",
value: 2,
description: "2",
},
wasThrown: false,
},
});
});
test("can send an invalid request", () => {
expect(
client.fetch("Runtime.awaitPromise", {
promiseObjectId: "this-does-not-exist",
}),
).rejects.toMatchObject({
name: "Error",
message: expect.stringMatching(/promiseObjectId/),
});
expect(onRequest).toHaveBeenCalled();
expect(onRequest.mock.lastCall[0]).toStrictEqual({
id: 2,
method: "Runtime.awaitPromise",
params: {
promiseObjectId: "this-does-not-exist",
},
});
expect(onResponse).toHaveBeenCalled();
expect(onResponse.mock.lastCall[0]).toMatchObject({
id: 2,
error: {
code: expect.any(Number),
message: expect.stringMatching(/promiseObjectId/),
},
});
expect(onError).toHaveBeenCalled();
});
test("can disconnect", () => {
expect(() => client.close()).not.toThrow();
});
});

View File

@@ -7,16 +7,8 @@
"esModuleInterop": true, "esModuleInterop": true,
"isolatedModules": false, "isolatedModules": false,
"skipLibCheck": true, "skipLibCheck": true,
"types": [ "types": ["bun-types"]
"bun-types"
]
}, },
"include": [ "include": ["src", "test", "types", "../../bun-devtools", "../../bun-debug-adapter-protocol"],
"src", "exclude": ["node_modules"]
"test",
"types"
],
"exclude": [
"node_modules"
]
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff