mirror of
https://github.com/oven-sh/bun
synced 2026-02-03 07:28:53 +00:00
Compare commits
7 Commits
claude/nod
...
dap3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eeeef5aaf0 | ||
|
|
f9b966c13f | ||
|
|
d2ad4da1a0 | ||
|
|
eb4ef364f2 | ||
|
|
d0e2679fb5 | ||
|
|
764437eb6d | ||
|
|
824655e1cb |
Binary file not shown.
@@ -1,143 +0,0 @@
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`remoteObjectToString 1`] = `"undefined"`;
|
||||
|
||||
exports[`remoteObjectToString 2`] = `"null"`;
|
||||
|
||||
exports[`remoteObjectToString 3`] = `"true"`;
|
||||
|
||||
exports[`remoteObjectToString 4`] = `"false"`;
|
||||
|
||||
exports[`remoteObjectToString 5`] = `"0"`;
|
||||
|
||||
exports[`remoteObjectToString 6`] = `"1"`;
|
||||
|
||||
exports[`remoteObjectToString 7`] = `"3.141592653589793"`;
|
||||
|
||||
exports[`remoteObjectToString 8`] = `"-2.718281828459045"`;
|
||||
|
||||
exports[`remoteObjectToString 9`] = `"NaN"`;
|
||||
|
||||
exports[`remoteObjectToString 10`] = `"Infinity"`;
|
||||
|
||||
exports[`remoteObjectToString 11`] = `"-Infinity"`;
|
||||
|
||||
exports[`remoteObjectToString 12`] = `"0n"`;
|
||||
|
||||
exports[`remoteObjectToString 13`] = `"1n"`;
|
||||
|
||||
exports[`remoteObjectToString 14`] = `"10000000000000n"`;
|
||||
|
||||
exports[`remoteObjectToString 15`] = `"-10000000000000n"`;
|
||||
|
||||
exports[`remoteObjectToString 16`] = `""""`;
|
||||
|
||||
exports[`remoteObjectToString 17`] = `"" ""`;
|
||||
|
||||
exports[`remoteObjectToString 18`] = `""Hello""`;
|
||||
|
||||
exports[`remoteObjectToString 19`] = `""Hello World""`;
|
||||
|
||||
exports[`remoteObjectToString 20`] = `"Array(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 21`] = `"Array(3) [1, 2, 3]"`;
|
||||
|
||||
exports[`remoteObjectToString 22`] = `"Array(4) ["a", 1, null, undefined]"`;
|
||||
|
||||
exports[`remoteObjectToString 23`] = `"Array(2) [1, Array]"`;
|
||||
|
||||
exports[`remoteObjectToString 24`] = `"Array(1) [Array]"`;
|
||||
|
||||
exports[`remoteObjectToString 25`] = `"{}"`;
|
||||
|
||||
exports[`remoteObjectToString 26`] = `"{a: 1}"`;
|
||||
|
||||
exports[`remoteObjectToString 27`] = `"{a: 1, b: 2, c: 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 28`] = `"{a: Object}"`;
|
||||
|
||||
exports[`remoteObjectToString 29`] = `
|
||||
"ƒ() {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 30`] = `
|
||||
"ƒ namedFunction() {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 31`] = `
|
||||
"class {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 32`] = `
|
||||
"class namedClass {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 33`] = `
|
||||
"class namedClass {
|
||||
a() {
|
||||
}
|
||||
b = 1;
|
||||
c = [
|
||||
null,
|
||||
undefined,
|
||||
"a",
|
||||
{
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3
|
||||
}
|
||||
];
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 34`] = `"Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)"`;
|
||||
|
||||
exports[`remoteObjectToString 35`] = `"Invalid Date"`;
|
||||
|
||||
exports[`remoteObjectToString 36`] = `"/(?:)/"`;
|
||||
|
||||
exports[`remoteObjectToString 37`] = `"/abc/"`;
|
||||
|
||||
exports[`remoteObjectToString 38`] = `"/abc/g"`;
|
||||
|
||||
exports[`remoteObjectToString 39`] = `"/abc/"`;
|
||||
|
||||
exports[`remoteObjectToString 40`] = `"Set(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 41`] = `"Set(3) [1, 2, 3]"`;
|
||||
|
||||
exports[`remoteObjectToString 42`] = `"WeakSet(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 43`] = `"WeakSet(3) [{a: 1}, {b: 2}, {c: 3}]"`;
|
||||
|
||||
exports[`remoteObjectToString 44`] = `"Map(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 45`] = `"Map(3) {"a" => 1, "b" => 2, "c" => 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 46`] = `"WeakMap(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 47`] = `"WeakMap(3) {{a: 1} => 1, {b: 2} => 2, {c: 3} => 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 48`] = `"Symbol()"`;
|
||||
|
||||
exports[`remoteObjectToString 49`] = `"Symbol(namedSymbol)"`;
|
||||
|
||||
exports[`remoteObjectToString 50`] = `"Error"`;
|
||||
|
||||
exports[`remoteObjectToString 51`] = `"TypeError: This is a TypeError"`;
|
||||
|
||||
exports[`remoteObjectToString 52`] = `"Headers {append: ƒ, delete: ƒ, get: ƒ, getAll: ƒ, has: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 53`] = `"Headers {a: "1", append: ƒ, b: "2", delete: ƒ, get: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 54`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, cache: "default", …}"`;
|
||||
|
||||
exports[`remoteObjectToString 55`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, cache: "default", …}"`;
|
||||
|
||||
exports[`remoteObjectToString 56`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, clone: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 57`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, clone: ƒ, …}"`;
|
||||
@@ -1,99 +0,0 @@
|
||||
console.log(
|
||||
undefined,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
1,
|
||||
Math.PI,
|
||||
-Math.E,
|
||||
NaN,
|
||||
Infinity,
|
||||
-Infinity,
|
||||
BigInt(0),
|
||||
BigInt(1),
|
||||
BigInt("10000000000000"),
|
||||
BigInt("-10000000000000"),
|
||||
"",
|
||||
" ",
|
||||
"Hello",
|
||||
"Hello World",
|
||||
[],
|
||||
[1, 2, 3],
|
||||
["a", 1, null, undefined],
|
||||
[1, [2, [3, [4, [5, [6, [7, [8, [9, [10]]]]]]]]]],
|
||||
[[[[[]]]]],
|
||||
{},
|
||||
{ a: 1 },
|
||||
{ a: 1, b: 2, c: 3 },
|
||||
{ a: { b: { c: { d: { e: { f: { g: { h: { i: { j: 10 } } } } } } } } } },
|
||||
function () {},
|
||||
function namedFunction() {},
|
||||
class {},
|
||||
class namedClass {},
|
||||
class namedClass {
|
||||
a() {}
|
||||
b = 1;
|
||||
c = [
|
||||
null,
|
||||
undefined,
|
||||
"a",
|
||||
{
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
},
|
||||
];
|
||||
},
|
||||
new Date(0),
|
||||
new Date(NaN),
|
||||
new RegExp(),
|
||||
new RegExp("abc"),
|
||||
new RegExp("abc", "g"),
|
||||
/abc/,
|
||||
new Set(),
|
||||
new Set([1, 2, 3]),
|
||||
new WeakSet(),
|
||||
new WeakSet([{ a: 1 }, { b: 2 }, { c: 3 }]),
|
||||
new Map(),
|
||||
new Map([
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
]),
|
||||
new WeakMap(),
|
||||
new WeakMap([
|
||||
[{ a: 1 }, 1],
|
||||
[{ b: 2 }, 2],
|
||||
[{ c: 3 }, 3],
|
||||
]),
|
||||
Symbol(),
|
||||
Symbol("namedSymbol"),
|
||||
new Error(),
|
||||
new TypeError("This is a TypeError"),
|
||||
//"a".repeat(10000),
|
||||
//["a"].fill("a", 0, 10000),
|
||||
new Headers(),
|
||||
new Headers({
|
||||
a: "1",
|
||||
b: "2",
|
||||
}),
|
||||
new Request("https://example.com/"),
|
||||
new Request("https://example.com/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
a: "1",
|
||||
b: "2",
|
||||
},
|
||||
body: '{"example":true}',
|
||||
}),
|
||||
new Response(),
|
||||
new Response('{"example":true}', {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {
|
||||
a: "1",
|
||||
b: "2",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -1,62 +0,0 @@
|
||||
import { beforeAll, afterAll, test, expect } from "bun:test";
|
||||
import type { JSC } from "../../bun-inspector-protocol";
|
||||
import { WebSocketInspector } from "../../bun-inspector-protocol";
|
||||
import type { PipedSubprocess } from "bun";
|
||||
import { spawn } from "bun";
|
||||
import { remoteObjectToString } from "./preview";
|
||||
|
||||
let subprocess: PipedSubprocess | undefined;
|
||||
let objects: JSC.Runtime.RemoteObject[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
subprocess = spawn({
|
||||
cwd: import.meta.dir,
|
||||
cmd: [process.argv0, "--inspect-wait=0", "fixtures/preview.js"],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
});
|
||||
const decoder = new TextDecoder();
|
||||
let url: URL;
|
||||
for await (const chunk of subprocess!.stdout) {
|
||||
const text = decoder.decode(chunk);
|
||||
if (text.includes("ws://")) {
|
||||
url = new URL(/(ws:\/\/.*)/.exec(text)![0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
objects = await new Promise((resolve, reject) => {
|
||||
const inspector = new WebSocketInspector({
|
||||
url,
|
||||
listener: {
|
||||
["Inspector.connected"]: () => {
|
||||
inspector.send("Inspector.enable");
|
||||
inspector.send("Runtime.enable");
|
||||
inspector.send("Console.enable");
|
||||
inspector.send("Debugger.enable");
|
||||
inspector.send("Debugger.resume");
|
||||
inspector.send("Inspector.initialized");
|
||||
},
|
||||
["Inspector.disconnected"]: error => {
|
||||
reject(error);
|
||||
},
|
||||
["Console.messageAdded"]: ({ message }) => {
|
||||
const { parameters } = message;
|
||||
resolve(parameters!);
|
||||
inspector.close();
|
||||
},
|
||||
},
|
||||
});
|
||||
inspector.start();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
subprocess?.kill();
|
||||
});
|
||||
|
||||
test("remoteObjectToString", () => {
|
||||
for (const object of objects) {
|
||||
expect(remoteObjectToString(object)).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
@@ -1,2 +1,3 @@
|
||||
export type * from "./protocol";
|
||||
export * from "./debugger/adapter";
|
||||
export type * from "./src/protocol";
|
||||
export * from "./src/debugger/adapter";
|
||||
export * from "./src/debugger/signal";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "bun-debug-adapter-protocol",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.4",
|
||||
"ws": "^8.13.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Protocol, Type } from "../protocol/schema.d.ts";
|
||||
import type { Protocol, Type } from "../src/protocol/schema";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import type { DAP } from "..";
|
||||
// @ts-ignore: FIXME - there is something wrong with the types
|
||||
import type { JSC, InspectorListener } from "../../bun-inspector-protocol";
|
||||
import { WebSocketInspector } from "../../bun-inspector-protocol";
|
||||
import type { DAP } from "../protocol";
|
||||
import type { JSC } from "../../../bun-inspector-protocol/src/protocol";
|
||||
import type { InspectorEventMap } from "../../../bun-inspector-protocol/src/inspector";
|
||||
// @ts-ignore
|
||||
import { WebSocketInspector, remoteObjectToString } from "../../../bun-inspector-protocol/index";
|
||||
import type { ChildProcess } from "node:child_process";
|
||||
import { spawn, spawnSync } from "node:child_process";
|
||||
import capabilities from "./capabilities";
|
||||
import { Location, SourceMap } from "./sourcemap";
|
||||
import { remoteObjectToString } from "./preview";
|
||||
import { compare, parse } from "semver";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
type InitializeRequest = DAP.InitializeRequest & {
|
||||
supportsConfigurationDoneRequest?: boolean;
|
||||
@@ -21,6 +22,7 @@ type LaunchRequest = DAP.LaunchRequest & {
|
||||
env?: Record<string, string>;
|
||||
inheritEnv?: boolean;
|
||||
watch?: boolean | "hot";
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
type AttachRequest = DAP.AttachRequest & {
|
||||
@@ -46,6 +48,7 @@ type Source = DAP.Source & {
|
||||
type Breakpoint = DAP.Breakpoint & {
|
||||
id: number;
|
||||
breakpointId: string;
|
||||
generatedLocation: JSC.Debugger.Location;
|
||||
source: Source;
|
||||
};
|
||||
|
||||
@@ -71,23 +74,34 @@ type Variable = DAP.Variable & {
|
||||
};
|
||||
|
||||
type IDebugAdapter = {
|
||||
[E in keyof DAP.EventMap]?: (event: DAP.EventMap[E]) => void;
|
||||
[E in keyof DAP.EventMap]?: (event: DAP.EventMap[E]) => void | Promise<void>;
|
||||
} & {
|
||||
[R in keyof DAP.RequestMap]?: (
|
||||
request: DAP.RequestMap[R],
|
||||
) => void | DAP.ResponseMap[R] | Promise<void | DAP.ResponseMap[R]>;
|
||||
) => void | DAP.ResponseMap[R] | Promise<DAP.ResponseMap[R]> | Promise<void>;
|
||||
};
|
||||
|
||||
export type DebugAdapterOptions = {
|
||||
sendToAdapter(message: DAP.Request | DAP.Response | DAP.Event): Promise<void>;
|
||||
export type DebugAdapterEventMap = InspectorEventMap & {
|
||||
[E in keyof DAP.EventMap as E extends string ? `Adapter.${E}` : never]: [DAP.EventMap[E]];
|
||||
} & {
|
||||
"Adapter.request": [DAP.Request];
|
||||
"Adapter.response": [DAP.Response];
|
||||
"Adapter.event": [DAP.Event];
|
||||
"Adapter.error": [Error];
|
||||
} & {
|
||||
"Process.requested": [unknown];
|
||||
"Process.spawned": [ChildProcess];
|
||||
"Process.exited": [number | Error | null, string | null];
|
||||
"Process.stdout": [string];
|
||||
"Process.stderr": [string];
|
||||
};
|
||||
|
||||
// This adapter only support single-threaded debugging,
|
||||
// which means that there is only one thread at a time.
|
||||
const threadId = 1;
|
||||
const isDebug = process.env.NODE_ENV === "development";
|
||||
|
||||
export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
#sendToAdapter: DebugAdapterOptions["sendToAdapter"];
|
||||
export class DebugAdapter extends EventEmitter<DebugAdapterEventMap> implements IDebugAdapter {
|
||||
#inspector: WebSocketInspector;
|
||||
#sourceId: number;
|
||||
#pendingSources: Map<string, ((source: Source) => void)[]>;
|
||||
@@ -102,12 +116,17 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
#initialized?: InitializeRequest;
|
||||
#launched?: LaunchRequest;
|
||||
#connected?: boolean;
|
||||
#terminated?: boolean;
|
||||
#url?: URL;
|
||||
|
||||
constructor({ sendToAdapter }: DebugAdapterOptions) {
|
||||
this.#inspector = new WebSocketInspector({ listener: this });
|
||||
this.#sendToAdapter = sendToAdapter;
|
||||
constructor(url?: string | URL) {
|
||||
super();
|
||||
this.#inspector = new WebSocketInspector(url);
|
||||
const emit = this.#inspector.emit.bind(this.#inspector);
|
||||
this.#inspector.emit = (event, ...args) => {
|
||||
let sent = false;
|
||||
sent ||= emit(event, ...args);
|
||||
sent ||= this.emit(event, ...(args as any));
|
||||
return sent;
|
||||
};
|
||||
this.#sourceId = 1;
|
||||
this.#pendingSources = new Map();
|
||||
this.#sources = new Map();
|
||||
@@ -119,81 +138,200 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
this.#variables = [{ name: "", value: "", type: undefined, variablesReference: 0 }];
|
||||
}
|
||||
|
||||
async accept(message: DAP.Request | DAP.Response | DAP.Event): Promise<void> {
|
||||
const { type } = message;
|
||||
|
||||
switch (type) {
|
||||
case "request":
|
||||
return this.#acceptRequest(message);
|
||||
}
|
||||
|
||||
throw new Error(`Not supported: ${type}`);
|
||||
get url(): string {
|
||||
return this.#inspector.url;
|
||||
}
|
||||
|
||||
async #acceptRequest(request: DAP.Request): Promise<void> {
|
||||
const { seq, command, arguments: args } = request;
|
||||
|
||||
let response;
|
||||
try {
|
||||
if (!(command! in this)) {
|
||||
throw new Error(`Not supported: ${command}`);
|
||||
}
|
||||
response = await this[command as keyof this](args);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const { message } = unknownToError(error);
|
||||
return this.#sendToAdapter({
|
||||
type: "response",
|
||||
success: false,
|
||||
message,
|
||||
request_seq: seq,
|
||||
seq: 0,
|
||||
command,
|
||||
});
|
||||
}
|
||||
|
||||
return this.#sendToAdapter({
|
||||
type: "response",
|
||||
success: true,
|
||||
request_seq: seq,
|
||||
seq: 0,
|
||||
command,
|
||||
body: response,
|
||||
});
|
||||
start(url?: string): Promise<boolean> {
|
||||
return this.#inspector.start(url);
|
||||
}
|
||||
|
||||
async #send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
|
||||
method: M,
|
||||
params?: JSC.RequestMap[M],
|
||||
): Promise<JSC.ResponseMap[M]> {
|
||||
/**
|
||||
* Sends a request to the JavaScript inspector.
|
||||
* @param method the method name
|
||||
* @param params the method parameters
|
||||
* @returns the response
|
||||
* @example
|
||||
* const { result, wasThrown } = await adapter.send("Runtime.evaluate", {
|
||||
* expression: "1 + 1",
|
||||
* });
|
||||
* console.log(result.value); // 2
|
||||
*/
|
||||
async send<M extends keyof JSC.ResponseMap>(method: M, params?: JSC.RequestMap[M]): Promise<JSC.ResponseMap[M]> {
|
||||
return this.#inspector.send(method, params);
|
||||
}
|
||||
|
||||
async #emit<E extends keyof DAP.EventMap>(name: E, body?: DAP.EventMap[E]): Promise<void> {
|
||||
await this.#sendToAdapter({
|
||||
/**
|
||||
* Emits an event. For the adapter to work, you must:
|
||||
* - emit `Adapter.request` when the client sends a request to the adapter.
|
||||
* - listen to `Adapter.response` to receive responses from the adapter.
|
||||
* - listen to `Adapter.event` to receive events from the adapter.
|
||||
* @param event the event name
|
||||
* @param args the event arguments
|
||||
* @returns if the event was sent to a listener
|
||||
*/
|
||||
emit<E extends keyof DebugAdapterEventMap>(event: E, ...args: DebugAdapterEventMap[E] | []): boolean {
|
||||
if (isDebug && event !== "Adapter.event" && event !== "Inspector.event") {
|
||||
console.log(event, ...args);
|
||||
}
|
||||
|
||||
let sent = super.emit(event, ...(args as any));
|
||||
|
||||
if (!(event in this)) {
|
||||
return sent;
|
||||
}
|
||||
|
||||
let result: unknown;
|
||||
try {
|
||||
// @ts-ignore
|
||||
result = this[event as keyof this](...(args as any));
|
||||
} catch (cause) {
|
||||
sent ||= this.emit("Adapter.error", unknownToError(cause));
|
||||
return sent;
|
||||
}
|
||||
|
||||
if (result instanceof Promise) {
|
||||
result.catch(cause => {
|
||||
this.emit("Adapter.error", unknownToError(cause));
|
||||
});
|
||||
}
|
||||
|
||||
return sent;
|
||||
}
|
||||
|
||||
#emit<E extends keyof DAP.EventMap>(event: E, body?: DAP.EventMap[E]): void {
|
||||
this.emit("Adapter.event", {
|
||||
type: "event",
|
||||
seq: 0,
|
||||
event: name,
|
||||
event,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
async ["Adapter.request"](request: DAP.Request): Promise<void> {
|
||||
const { command, arguments: args } = request;
|
||||
|
||||
if (!(command in this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
let result: unknown;
|
||||
try {
|
||||
// @ts-ignore
|
||||
result = await this[command as keyof this](args);
|
||||
} catch (cause) {
|
||||
const error = unknownToError(cause);
|
||||
this.emit("Adapter.error", error);
|
||||
|
||||
const { message } = error;
|
||||
this.emit("Adapter.response", {
|
||||
type: "response",
|
||||
command,
|
||||
success: false,
|
||||
message,
|
||||
request_seq: request.seq,
|
||||
seq: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("Adapter.response", {
|
||||
type: "response",
|
||||
command,
|
||||
success: true,
|
||||
request_seq: request.seq,
|
||||
seq: 0,
|
||||
body: result,
|
||||
});
|
||||
}
|
||||
|
||||
["Adapter.event"](event: DAP.Event): void {
|
||||
const { event: name, body } = event;
|
||||
this.emit(`Adapter.${name}` as keyof DebugAdapterEventMap, body);
|
||||
}
|
||||
|
||||
async #spawn(options: {
|
||||
command: string;
|
||||
args?: string[];
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
strictEnv?: boolean;
|
||||
isDebugee?: boolean;
|
||||
}): Promise<boolean> {
|
||||
const { command, args = [], cwd, env = {}, strictEnv, isDebugee } = options;
|
||||
const request = {
|
||||
command,
|
||||
args,
|
||||
cwd,
|
||||
env: strictEnv ? env : { ...process.env, ...env },
|
||||
};
|
||||
this.emit("Process.requested", request);
|
||||
|
||||
let subprocess: ChildProcess;
|
||||
try {
|
||||
subprocess = spawn(command, args, {
|
||||
...request,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
} catch (cause) {
|
||||
this.emit("Process.exited", new Error("Failed to spawn process", { cause }), null);
|
||||
return false;
|
||||
}
|
||||
|
||||
subprocess.on("spawn", () => {
|
||||
this.emit("Process.spawned", subprocess);
|
||||
|
||||
if (isDebugee) {
|
||||
this.#emit("process", {
|
||||
name: `${command} ${args.join(" ")}`,
|
||||
systemProcessId: subprocess.pid,
|
||||
isLocalProcess: true,
|
||||
startMethod: "launch",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
subprocess.on("exit", (code, signal) => {
|
||||
this.emit("Process.exited", code, signal);
|
||||
|
||||
if (isDebugee) {
|
||||
this.#emit("exited", {
|
||||
exitCode: code ?? -1,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
subprocess.stdout?.on("data", data => {
|
||||
this.emit("Process.stdout", data.toString());
|
||||
});
|
||||
|
||||
subprocess.stderr?.on("data", data => {
|
||||
this.emit("Process.stderr", data.toString());
|
||||
});
|
||||
|
||||
return new Promise(resolve => {
|
||||
subprocess.on("spawn", () => resolve(true));
|
||||
subprocess.on("exit", () => resolve(false));
|
||||
subprocess.on("error", () => resolve(false));
|
||||
});
|
||||
}
|
||||
|
||||
initialize(request: InitializeRequest): DAP.InitializeResponse {
|
||||
const { clientID, supportsConfigurationDoneRequest } = (this.#initialized = request);
|
||||
|
||||
this.#send("Inspector.enable");
|
||||
this.#send("Runtime.enable");
|
||||
this.#send("Console.enable");
|
||||
this.#send("Debugger.enable");
|
||||
this.#send("Debugger.setAsyncStackTraceDepth", { depth: 200 });
|
||||
this.#send("Debugger.setPauseOnDebuggerStatements", { enabled: true });
|
||||
this.#send("Debugger.setBlackboxBreakpointEvaluations", { blackboxBreakpointEvaluations: true });
|
||||
this.#send("Debugger.setBreakpointsActive", { active: true });
|
||||
this.send("Inspector.enable");
|
||||
this.send("Runtime.enable");
|
||||
this.send("Console.enable");
|
||||
this.send("Debugger.enable");
|
||||
this.send("Debugger.setAsyncStackTraceDepth", { depth: 200 });
|
||||
this.send("Debugger.setPauseOnDebuggerStatements", { enabled: true });
|
||||
this.send("Debugger.setBlackboxBreakpointEvaluations", { blackboxBreakpointEvaluations: true });
|
||||
this.send("Debugger.setBreakpointsActive", { active: true });
|
||||
|
||||
// If the client will not send a `configurationDone` request, then we need to
|
||||
// tell the debugger that everything is ready.
|
||||
if (!supportsConfigurationDoneRequest && clientID !== "vscode") {
|
||||
this.#send("Inspector.initialized");
|
||||
this.send("Inspector.initialized");
|
||||
}
|
||||
|
||||
// Tell the client what capabilities this adapter supports.
|
||||
@@ -204,16 +342,16 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
// If the client requested that `noDebug` mode be enabled,
|
||||
// then we need to disable all breakpoints and pause on statements.
|
||||
if (this.#launched?.noDebug) {
|
||||
this.#send("Debugger.setBreakpointsActive", { active: false });
|
||||
this.#send("Debugger.setPauseOnExceptions", { state: "none" });
|
||||
this.#send("Debugger.setPauseOnDebuggerStatements", { enabled: false });
|
||||
this.#send("Debugger.setPauseOnMicrotasks", { enabled: false });
|
||||
this.#send("Debugger.setPauseForInternalScripts", { shouldPause: false });
|
||||
this.#send("Debugger.setPauseOnAssertions", { enabled: false });
|
||||
this.send("Debugger.setBreakpointsActive", { active: false });
|
||||
this.send("Debugger.setPauseOnExceptions", { state: "none" });
|
||||
this.send("Debugger.setPauseOnDebuggerStatements", { enabled: false });
|
||||
this.send("Debugger.setPauseOnMicrotasks", { enabled: false });
|
||||
this.send("Debugger.setPauseForInternalScripts", { shouldPause: false });
|
||||
this.send("Debugger.setPauseOnAssertions", { enabled: false });
|
||||
}
|
||||
|
||||
// Tell the debugger that everything is ready.
|
||||
this.#send("Inspector.initialized");
|
||||
this.send("Inspector.initialized");
|
||||
}
|
||||
|
||||
async launch(request: DAP.LaunchRequest): Promise<void> {
|
||||
@@ -229,16 +367,17 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
category: "stderr",
|
||||
output: `Failed to start debugger.\n${message}`,
|
||||
});
|
||||
this.#emit("terminated");
|
||||
this.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
async #launch(request: LaunchRequest): Promise<void> {
|
||||
/*
|
||||
if (this.#process?.exitCode === null) {
|
||||
throw new Error("Another program is already running. Did you terminate the last session?");
|
||||
}
|
||||
|
||||
const { program, runtime = "bun", args = [], cwd, env = {}, inheritEnv = true, watch = true } = request;
|
||||
const { program, runtime = "bun", args = [], cwd, env = {}, inheritEnv = true, watch = false } = request;
|
||||
if (!program) {
|
||||
throw new Error("No program specified. Did you set the 'program' property in your launch.json?");
|
||||
}
|
||||
@@ -247,66 +386,36 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
throw new Error("Program must be a JavaScript or TypeScript file.");
|
||||
}
|
||||
|
||||
const argz = ["--inspect-wait=0", ...args];
|
||||
if (watch) {
|
||||
argz.push(watch === "hot" ? "--hot" : "--watch");
|
||||
const finalArgs = [...args];
|
||||
const isTest = isTestJavaScript(program);
|
||||
if (isTest) {
|
||||
finalArgs.unshift("test");
|
||||
}
|
||||
console.log(argz);
|
||||
|
||||
const subprocess = spawn(runtime, [...argz, program], {
|
||||
stdio: ["ignore", "pipe", "pipe", "pipe"],
|
||||
cwd,
|
||||
env: inheritEnv ? { ...process.env, ...env } : env,
|
||||
});
|
||||
if (watch) {
|
||||
finalArgs.push(watch === "hot" ? "--hot" : "--watch");
|
||||
}
|
||||
|
||||
subprocess.on("spawn", () => {
|
||||
this.#process = subprocess;
|
||||
this.#emit("process", {
|
||||
name: program,
|
||||
systemProcessId: subprocess.pid,
|
||||
isLocalProcess: true,
|
||||
startMethod: "launch",
|
||||
});
|
||||
});
|
||||
const finalEnv = inheritEnv
|
||||
? {
|
||||
...process.env,
|
||||
...env,
|
||||
}
|
||||
: {
|
||||
...env,
|
||||
};
|
||||
|
||||
subprocess.on("exit", code => {
|
||||
this.#emit("exited", {
|
||||
exitCode: code ?? -1,
|
||||
});
|
||||
this.#process = undefined;
|
||||
});
|
||||
finalEnv["BUN_INSPECT"] = `1${this.#url}`;
|
||||
finalEnv["BUN_INSPECT_NOTIFY"] = `unix://${this.#inspector.unix}`;
|
||||
|
||||
const stdout: string[] = [];
|
||||
subprocess.stdout!.on("data", data => {
|
||||
if (!this.#url) {
|
||||
const text = data.toString();
|
||||
stdout.push(text);
|
||||
const url = (this.#url = parseUrlMaybe(text));
|
||||
this.#inspector.start(url);
|
||||
} else if (stdout.length) {
|
||||
stdout.length = 0;
|
||||
}
|
||||
});
|
||||
if (true) {
|
||||
finalEnv["FORCE_COLOR"] = "1";
|
||||
} else {
|
||||
// https://github.com/microsoft/vscode/issues/571
|
||||
finalEnv["NO_COLOR"] = "1";
|
||||
}
|
||||
|
||||
const stderr: string[] = [];
|
||||
subprocess.stderr!.on("data", data => {
|
||||
if (!this.#url) {
|
||||
stderr.push(data.toString());
|
||||
} else if (stderr.length) {
|
||||
stderr.length = 0;
|
||||
}
|
||||
});
|
||||
|
||||
const start = new Promise<undefined>(resolve => {
|
||||
subprocess.on("spawn", () => resolve(undefined));
|
||||
});
|
||||
|
||||
const exitOrError = new Promise<number | string | Error>(resolve => {
|
||||
subprocess.on("exit", (code, signal) => resolve(code ?? signal ?? -1));
|
||||
subprocess.on("error", resolve);
|
||||
});
|
||||
|
||||
const reason = await Promise.race([start, exitOrError]);
|
||||
let reason = undefined;
|
||||
|
||||
if (reason instanceof Error) {
|
||||
const { message } = reason;
|
||||
@@ -317,11 +426,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
throw new Error(`Program exited with code ${reason} before the debugger could attached.`);
|
||||
}
|
||||
|
||||
for (let retries = 0; !this.#url && retries < 10; retries++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100 * retries));
|
||||
}
|
||||
|
||||
if (this.#url) {
|
||||
if (await this.#start()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -334,43 +439,56 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
|
||||
const { stdout: version } = spawnSync(runtime, ["--version"], { stdio: "pipe", encoding: "utf-8" });
|
||||
|
||||
if (parse(version, true) && compare("0.8.0", version, true)) {
|
||||
throw new Error(
|
||||
`Bun v${version.trim()} does not have debugger support. Please upgrade to v0.8 or later by running: \`bun upgrade\``,
|
||||
);
|
||||
const minVersion = "0.8.2";
|
||||
if (parse(version, true) && compare(minVersion, version, true)) {
|
||||
throw new Error(`This extension requires Bun v${minVersion} or later. Please upgrade by running: bun upgrade`);
|
||||
}
|
||||
|
||||
for (const message of stderr) {
|
||||
this.#emit("output", {
|
||||
category: "stderr",
|
||||
output: message,
|
||||
source: {
|
||||
path: program,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const message of stdout) {
|
||||
this.#emit("output", {
|
||||
category: "stdout",
|
||||
output: message,
|
||||
source: {
|
||||
path: program,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("Program started, but the debugger could not be attached.");
|
||||
throw new Error("Program started, but the debugger could not be attached.");*/
|
||||
}
|
||||
|
||||
attach(request: AttachRequest): void {
|
||||
async #start(url?: string | URL): Promise<boolean> {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const ok = await this.#inspector.start(url);
|
||||
if (ok) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100 * i));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async attach(request: DAP.AttachRequest): Promise<void> {
|
||||
try {
|
||||
await this.#attach(request);
|
||||
} catch (error) {
|
||||
// Some clients, like VSCode, will show a system-level popup when a `launch` request fails.
|
||||
// Instead, we want to show the error as a sidebar notification.
|
||||
const { message } = unknownToError(error);
|
||||
this.#emit("output", {
|
||||
category: "stderr",
|
||||
output: `Failed to start debugger.\n${message}`,
|
||||
});
|
||||
this.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
async #attach(request: AttachRequest): Promise<void> {
|
||||
const { url } = request;
|
||||
this.#inspector.start(parseUrl(url));
|
||||
|
||||
if (await this.#start(url)) {
|
||||
this.configurationDone();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error("Failed to attach to program.");
|
||||
}
|
||||
|
||||
terminate(): void {
|
||||
this.#terminated = true;
|
||||
this.#process?.kill();
|
||||
this.#emit("terminated");
|
||||
}
|
||||
|
||||
disconnect(request: DAP.DisconnectRequest): void {
|
||||
@@ -387,7 +505,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
const { source } = request;
|
||||
|
||||
const { scriptId } = await this.#getSource(sourceToId(source));
|
||||
const { scriptSource } = await this.#send("Debugger.getScriptSource", { scriptId });
|
||||
const { scriptSource } = await this.send("Debugger.getScriptSource", { scriptId });
|
||||
|
||||
return {
|
||||
content: scriptSource,
|
||||
@@ -406,27 +524,27 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
}
|
||||
|
||||
async pause(): Promise<void> {
|
||||
await this.#send("Debugger.pause");
|
||||
await this.send("Debugger.pause");
|
||||
this.#stopped = "pause";
|
||||
}
|
||||
|
||||
async continue(): Promise<void> {
|
||||
await this.#send("Debugger.resume");
|
||||
await this.send("Debugger.resume");
|
||||
this.#stopped = undefined;
|
||||
}
|
||||
|
||||
async next(): Promise<void> {
|
||||
await this.#send("Debugger.stepNext");
|
||||
await this.send("Debugger.stepNext");
|
||||
this.#stopped = "step";
|
||||
}
|
||||
|
||||
async stepIn(): Promise<void> {
|
||||
await this.#send("Debugger.stepInto");
|
||||
await this.send("Debugger.stepInto");
|
||||
this.#stopped = "step";
|
||||
}
|
||||
|
||||
async stepOut(): Promise<void> {
|
||||
await this.#send("Debugger.stepOut");
|
||||
await this.send("Debugger.stepOut");
|
||||
this.#stopped = "step";
|
||||
}
|
||||
|
||||
@@ -439,7 +557,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
this.#generatedLocation(source, endLine ?? line + 1, endColumn),
|
||||
]);
|
||||
|
||||
const { locations } = await this.#send("Debugger.getBreakpointLocations", {
|
||||
const { locations } = await this.send("Debugger.getBreakpointLocations", {
|
||||
start,
|
||||
end,
|
||||
});
|
||||
@@ -468,20 +586,20 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
if (!numberIsValid(line)) {
|
||||
return 0;
|
||||
}
|
||||
if (this.#initialized?.linesStartAt1) {
|
||||
return line - 1;
|
||||
if (!this.#initialized?.linesStartAt1) {
|
||||
return line;
|
||||
}
|
||||
return line;
|
||||
return line - 1;
|
||||
}
|
||||
|
||||
#columnTo0BasedColumn(column?: number): number {
|
||||
if (!numberIsValid(column)) {
|
||||
return 0;
|
||||
}
|
||||
if (this.#initialized?.columnsStartAt1) {
|
||||
return column - 1;
|
||||
if (!this.#initialized?.columnsStartAt1) {
|
||||
return column;
|
||||
}
|
||||
return column;
|
||||
return column - 1;
|
||||
}
|
||||
|
||||
#originalLocation(
|
||||
@@ -505,17 +623,17 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
}
|
||||
|
||||
#lineFrom0BasedLine(line?: number): number {
|
||||
if (this.#initialized?.linesStartAt1) {
|
||||
return numberIsValid(line) ? line + 1 : 1;
|
||||
if (!this.#initialized?.linesStartAt1) {
|
||||
return numberIsValid(line) ? line : 0;
|
||||
}
|
||||
return numberIsValid(line) ? line : 0;
|
||||
return numberIsValid(line) ? line + 1 : 1;
|
||||
}
|
||||
|
||||
#columnFrom0BasedColumn(column?: number): number {
|
||||
if (this.#initialized?.columnsStartAt1) {
|
||||
return numberIsValid(column) ? column + 1 : 1;
|
||||
if (!this.#initialized?.columnsStartAt1) {
|
||||
return numberIsValid(column) ? column : 0;
|
||||
}
|
||||
return numberIsValid(column) ? column : 0;
|
||||
return numberIsValid(column) ? column + 1 : 1;
|
||||
}
|
||||
|
||||
async setBreakpoints(request: DAP.SetBreakpointsRequest): Promise<DAP.SetBreakpointsResponse> {
|
||||
@@ -524,17 +642,27 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
const source = await this.#getSource(sourceId);
|
||||
|
||||
const oldBreakpoints = this.#getBreakpoints(sourceId);
|
||||
console.log("OLD BREAKPOINTS", oldBreakpoints);
|
||||
|
||||
const breakpoints = await Promise.all(
|
||||
requests!.map(async ({ line, column, ...options }) => {
|
||||
const breakpoint = this.#getBreakpoint(sourceId, line, column);
|
||||
if (breakpoint) {
|
||||
return breakpoint;
|
||||
const location = this.#generatedLocation(source, line, column);
|
||||
console.log("NEW BREAKPOINT", location);
|
||||
|
||||
for (const breakpoint of oldBreakpoints) {
|
||||
const { generatedLocation } = breakpoint;
|
||||
if (
|
||||
location.lineNumber === generatedLocation.lineNumber &&
|
||||
location.columnNumber === generatedLocation.columnNumber
|
||||
) {
|
||||
console.log("SAME BREAKPOINT");
|
||||
return breakpoint;
|
||||
}
|
||||
}
|
||||
|
||||
const location = this.#generatedLocation(source, line, column);
|
||||
console.log("CREATE BREAKPOINT");
|
||||
try {
|
||||
const { breakpointId, actualLocation } = await this.#send("Debugger.setBreakpoint", {
|
||||
const { breakpointId, actualLocation } = await this.send("Debugger.setBreakpoint", {
|
||||
location,
|
||||
options: breakpointOptions(options),
|
||||
});
|
||||
@@ -545,6 +673,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
breakpointId,
|
||||
source,
|
||||
verified: true,
|
||||
generatedLocation: location,
|
||||
...originalLocation,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -560,6 +689,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
source,
|
||||
verified: false,
|
||||
message,
|
||||
generatedLocation: location,
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -569,7 +699,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
oldBreakpoints.map(async ({ breakpointId }) => {
|
||||
const isRemoved = !breakpoints.filter(({ breakpointId: id }) => breakpointId === id).length;
|
||||
if (isRemoved) {
|
||||
await this.#send("Debugger.removeBreakpoint", {
|
||||
await this.send("Debugger.removeBreakpoint", {
|
||||
breakpointId,
|
||||
});
|
||||
this.#removeBreakpoint(breakpointId);
|
||||
@@ -595,18 +725,13 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
return breakpoints;
|
||||
}
|
||||
|
||||
#getBreakpoint(sourceId: string | number, line?: number, column?: number): Breakpoint | undefined {
|
||||
for (const breakpoint of this.#getBreakpoints(sourceId)) {
|
||||
if (isSameLocation(breakpoint, { line, column })) {
|
||||
return breakpoint;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
#addBreakpoint(breakpoint: Breakpoint): Breakpoint {
|
||||
this.#breakpoints.push(breakpoint);
|
||||
|
||||
// For now, remove the column from breakpoints because
|
||||
// it can be inaccurate and causes weird rendering issues in VSCode.
|
||||
breakpoint.column = this.#lineFrom0BasedLine(0);
|
||||
|
||||
this.#emit("breakpoint", {
|
||||
reason: "changed",
|
||||
breakpoint,
|
||||
@@ -643,7 +768,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.#send("Debugger.addSymbolicBreakpoint", {
|
||||
await this.send("Debugger.addSymbolicBreakpoint", {
|
||||
symbol: name,
|
||||
caseSensitive: true,
|
||||
isRegex: false,
|
||||
@@ -671,7 +796,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
oldBreakpoints.map(async ({ name }) => {
|
||||
const isRemoved = !breakpoints.filter(({ name: n }) => name === n).length;
|
||||
if (isRemoved) {
|
||||
await this.#send("Debugger.removeSymbolicBreakpoint", {
|
||||
await this.send("Debugger.removeSymbolicBreakpoint", {
|
||||
symbol: name,
|
||||
caseSensitive: true,
|
||||
isRegex: false,
|
||||
@@ -723,7 +848,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
filterIds.push(...filterOptions.map(({ filterId }) => filterId));
|
||||
}
|
||||
|
||||
await this.#send("Debugger.setPauseOnExceptions", {
|
||||
await this.send("Debugger.setPauseOnExceptions", {
|
||||
state: exceptionFiltersToPauseOnExceptionsState(filterIds),
|
||||
});
|
||||
}
|
||||
@@ -752,7 +877,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
async #evaluate(expression: string, callFrameId?: string): Promise<JSC.Runtime.EvaluateResponse> {
|
||||
const method = callFrameId ? "Debugger.evaluateOnCallFrame" : "Runtime.evaluate";
|
||||
|
||||
return this.#send(method, {
|
||||
return this.send(method, {
|
||||
callFrameId,
|
||||
expression: sanitizeExpression(expression),
|
||||
generatePreview: true,
|
||||
@@ -773,13 +898,6 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
}
|
||||
|
||||
["Inspector.connected"](): void {
|
||||
if (this.#connected) {
|
||||
this.restart();
|
||||
return;
|
||||
}
|
||||
|
||||
this.#connected = true;
|
||||
|
||||
this.#emit("output", {
|
||||
category: "debug console",
|
||||
output: "Debugger attached.\n",
|
||||
@@ -788,18 +906,13 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
this.#emit("initialized");
|
||||
}
|
||||
|
||||
["Inspector.disconnected"](error?: Error): void {
|
||||
if (this.#connected && this.#process?.exitCode === null) {
|
||||
this.#url = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
async ["Inspector.disconnected"](error?: Error): Promise<void> {
|
||||
this.#emit("output", {
|
||||
category: "debug console",
|
||||
output: "Debugger detached.\n",
|
||||
});
|
||||
|
||||
if (error && !this.#terminated) {
|
||||
if (error) {
|
||||
const { message } = error;
|
||||
this.#emit("output", {
|
||||
category: "stderr",
|
||||
@@ -854,6 +967,11 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
["Debugger.scriptFailedToParse"](event: JSC.Debugger.ScriptFailedToParseEvent): void {
|
||||
const { url, errorMessage, errorLine } = event;
|
||||
|
||||
// If no url is present, the script is from a `evaluate` request.
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#emit("output", {
|
||||
category: "stderr",
|
||||
output: errorMessage,
|
||||
@@ -870,7 +988,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
if (reason === "PauseOnNextStatement") {
|
||||
for (const { functionName } of callFrames) {
|
||||
if (functionName === "module code") {
|
||||
this.#send("Debugger.resume");
|
||||
this.send("Debugger.resume");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -885,10 +1003,11 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
this.#addAsyncStackTrace(asyncStackTrace);
|
||||
}
|
||||
|
||||
let hitBreakpointIds: number[] | undefined;
|
||||
// Depending on the reason, the `data` property is set to the reason
|
||||
// why the execution was paused. For example, if the reason is "breakpoint",
|
||||
// the `data` property is set to the breakpoint ID.
|
||||
let hitBreakpointIds: number[] | undefined;
|
||||
|
||||
if (data) {
|
||||
if (reason === "exception") {
|
||||
const remoteObject = data as JSC.Runtime.RemoteObject;
|
||||
@@ -941,10 +1060,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
|
||||
const variables = parameters.map((parameter, i) => {
|
||||
const variable = this.#addVariable(parameter, { name: `${i}` });
|
||||
|
||||
const { value } = variable;
|
||||
output += value + " ";
|
||||
|
||||
output += remoteObjectToString(parameter, true) + " ";
|
||||
return variable;
|
||||
});
|
||||
|
||||
@@ -1301,7 +1417,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { properties, internalProperties } = await this.#send("Runtime.getDisplayableProperties", {
|
||||
const { properties, internalProperties } = await this.send("Runtime.getDisplayableProperties", {
|
||||
objectId,
|
||||
generatePreview: true,
|
||||
});
|
||||
@@ -1319,7 +1435,7 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
|
||||
const hasEntries = type !== "array" && (indexedVariables || namedVariables);
|
||||
if (hasEntries) {
|
||||
const { entries } = await this.#send("Runtime.getCollectionEntries", {
|
||||
const { entries } = await this.send("Runtime.getCollectionEntries", {
|
||||
objectId,
|
||||
fetchStart: offset,
|
||||
fetchCount: count,
|
||||
@@ -1371,7 +1487,6 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.#terminated = true;
|
||||
this.#process?.kill();
|
||||
this.#inspector.close();
|
||||
this.#reset();
|
||||
@@ -1389,8 +1504,6 @@ export class DebugAdapter implements IDebugAdapter, InspectorListener {
|
||||
this.#launched = undefined;
|
||||
this.#initialized = undefined;
|
||||
this.#connected = undefined;
|
||||
this.#terminated = undefined;
|
||||
this.#url = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1498,14 +1611,6 @@ function consoleMessageGroup(type: JSC.Console.ConsoleMessage["type"]): DAP.Outp
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function sourceToPath(source?: DAP.Source): string {
|
||||
const { path } = source ?? {};
|
||||
if (!path) {
|
||||
throw new Error("No source found.");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function sourceToId(source?: DAP.Source): string | number {
|
||||
const { path, sourceReference } = source ?? {};
|
||||
if (path) {
|
||||
@@ -1598,39 +1703,8 @@ function isJavaScript(path: string): boolean {
|
||||
return /\.(c|m)?(j|t)sx?$/.test(path);
|
||||
}
|
||||
|
||||
function parseUrl(hostname?: string, port?: number): URL {
|
||||
hostname ||= "localhost";
|
||||
port ||= 6499;
|
||||
let url: URL;
|
||||
try {
|
||||
if (hostname.includes("://")) {
|
||||
url = new URL(hostname);
|
||||
} else if (hostname.includes(":") && !hostname.startsWith("[")) {
|
||||
url = new URL(`ws://[${hostname}]:${port}/`);
|
||||
} else {
|
||||
url = new URL(`ws://${hostname}:${port}/`);
|
||||
}
|
||||
} catch {
|
||||
throw new Error(`Invalid URL or hostname/port: ${hostname}`);
|
||||
}
|
||||
// HACK: Bun sometimes has issues connecting through "127.0.0.1"
|
||||
if (url.hostname === "localhost" || url.hostname === "127.0.0.1") {
|
||||
url.hostname = "[::1]";
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function parseUrlMaybe(string: string): URL | undefined {
|
||||
const match = /(wss?:\/\/.*)/im.exec(string);
|
||||
if (!match) {
|
||||
return undefined;
|
||||
}
|
||||
const [_, href] = match;
|
||||
try {
|
||||
return parseUrl(href);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
function isTestJavaScript(path: string): boolean {
|
||||
return /\.(test|spec)\.(c|m)?(j|t)sx?$/.test(path);
|
||||
}
|
||||
|
||||
function variablesSortBy(a: DAP.Variable, b: DAP.Variable): number {
|
||||
@@ -1681,8 +1755,6 @@ function consoleLevelToAnsiColor(level: JSC.Console.ConsoleMessage["level"]): st
|
||||
return "\u001b[33m";
|
||||
case "error":
|
||||
return "\u001b[31m";
|
||||
case "debug":
|
||||
return "\u001b[36m";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DAP } from "..";
|
||||
import type { DAP } from "../protocol";
|
||||
|
||||
const capabilities: DAP.Capabilities = {
|
||||
/**
|
||||
87
packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Normal file
87
packages/bun-debug-adapter-protocol/src/debugger/signal.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { Server } from "node:net";
|
||||
import { createServer } from "node:net";
|
||||
import { EventEmitter } from "node:events";
|
||||
|
||||
const isDebug = process.env.NODE_ENV === "development";
|
||||
|
||||
export type UnixSignalEventMap = {
|
||||
"Signal.listening": [string];
|
||||
"Signal.error": [Error];
|
||||
"Signal.received": [string];
|
||||
"Signal.closed": [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Starts a server that listens for signals on a UNIX domain socket.
|
||||
*/
|
||||
export class UnixSignal extends EventEmitter<UnixSignalEventMap> {
|
||||
#path: string;
|
||||
#server: Server;
|
||||
#ready: Promise<void>;
|
||||
|
||||
constructor(path?: string) {
|
||||
super();
|
||||
this.#path = path ? parseUnixPath(path) : randomUnixPath();
|
||||
this.#server = createServer();
|
||||
this.#server.on("listening", () => this.emit("Signal.listening", this.#path));
|
||||
this.#server.on("error", error => this.emit("Signal.error", error));
|
||||
this.#server.on("close", () => this.emit("Signal.closed"));
|
||||
this.#server.on("connection", socket => {
|
||||
socket.on("data", data => {
|
||||
this.emit("Signal.received", data.toString());
|
||||
});
|
||||
});
|
||||
this.#ready = new Promise((resolve, reject) => {
|
||||
this.#server.on("listening", resolve);
|
||||
this.#server.on("error", reject);
|
||||
});
|
||||
this.#server.listen(this.#path);
|
||||
}
|
||||
|
||||
emit<E extends keyof UnixSignalEventMap>(event: E, ...args: UnixSignalEventMap[E]): boolean {
|
||||
if (isDebug) {
|
||||
console.log(event, ...args);
|
||||
}
|
||||
|
||||
return super.emit(event, ...args);
|
||||
}
|
||||
|
||||
/**
|
||||
* The path to the UNIX domain socket.
|
||||
*/
|
||||
get url(): string {
|
||||
return `unix://${this.#path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves when the server is listening or rejects if an error occurs.
|
||||
*/
|
||||
get ready(): Promise<void> {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the server.
|
||||
*/
|
||||
close(): void {
|
||||
this.#server.close();
|
||||
}
|
||||
}
|
||||
|
||||
function randomUnixPath(): string {
|
||||
return join(tmpdir(), `${Math.random().toString(36).slice(2)}.sock`);
|
||||
}
|
||||
|
||||
function parseUnixPath(path: string): string {
|
||||
if (path.startsWith("/")) {
|
||||
return path;
|
||||
}
|
||||
try {
|
||||
const { pathname } = new URL(path);
|
||||
return pathname;
|
||||
} catch {
|
||||
throw new Error(`Invalid UNIX path: ${path}`);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,7 @@ class ActualSourceMap implements SourceMap {
|
||||
|
||||
generatedLocation(request: LocationRequest): Location {
|
||||
const { line, column, url } = request;
|
||||
|
||||
let lineRange: LineRange;
|
||||
try {
|
||||
const source = this.#getSource(url);
|
||||
@@ -68,6 +69,7 @@ class ActualSourceMap implements SourceMap {
|
||||
message: unknownToError(error),
|
||||
};
|
||||
}
|
||||
|
||||
if (!locationIsValid(lineRange)) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
@@ -75,6 +77,7 @@ class ActualSourceMap implements SourceMap {
|
||||
verified: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { line: gline, column: gcolumn } = lineRange;
|
||||
return {
|
||||
line: lineToLine(gline),
|
||||
@@ -85,6 +88,7 @@ class ActualSourceMap implements SourceMap {
|
||||
|
||||
originalLocation(request: LocationRequest): Location {
|
||||
const { line, column } = request;
|
||||
|
||||
let mappedPosition: MappedPosition;
|
||||
try {
|
||||
mappedPosition = this.#sourceMap.originalPositionFor({
|
||||
@@ -99,6 +103,7 @@ class ActualSourceMap implements SourceMap {
|
||||
message: unknownToError(error),
|
||||
};
|
||||
}
|
||||
|
||||
if (!locationIsValid(mappedPosition)) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
@@ -106,6 +111,7 @@ class ActualSourceMap implements SourceMap {
|
||||
verified: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { line: oline, column: ocolumn } = mappedPosition;
|
||||
return {
|
||||
line: lineTo0BasedLine(oline),
|
||||
3761
packages/bun-debug-adapter-protocol/src/protocol/protocol.json
Normal file
3761
packages/bun-debug-adapter-protocol/src/protocol/protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,8 +15,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"types": ["bun-types"],
|
||||
"outDir": "dist",
|
||||
},
|
||||
"include": [".", "../bun-types/index.d.ts", "../bun-inspector-protocol/index"]
|
||||
"include": ["src", "scripts", "../bun-types/index.d.ts", "../bun-inspector-protocol/src"]
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,3 +1,4 @@
|
||||
export type * from "./protocol";
|
||||
export type * from "./inspector";
|
||||
export * from "./inspector/websocket";
|
||||
export type * from "./src/protocol";
|
||||
export type * from "./src/inspector";
|
||||
export * from "./src/util/preview";
|
||||
export * from "./src/inspector/websocket";
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
export default {
|
||||
fetch(request) {
|
||||
console.log(request);
|
||||
debugger;
|
||||
return new Response();
|
||||
},
|
||||
};
|
||||
@@ -1,49 +0,0 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
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,
|
||||
},
|
||||
);
|
||||
@@ -1,196 +0,0 @@
|
||||
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}`);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Protocol, Domain, Property } from "../protocol/schema";
|
||||
import type { Protocol, Domain, Property } from "../src/protocol/schema";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
|
||||
40
packages/bun-inspector-protocol/src/inspector/index.d.ts
vendored
Normal file
40
packages/bun-inspector-protocol/src/inspector/index.d.ts
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { EventEmitter } from "node:events";
|
||||
import type { JSC } from "../protocol";
|
||||
|
||||
export type InspectorEventMap = {
|
||||
[E in keyof JSC.EventMap]: [JSC.EventMap[E]];
|
||||
} & {
|
||||
"Inspector.connecting": [string];
|
||||
"Inspector.connected": [];
|
||||
"Inspector.disconnected": [Error | undefined];
|
||||
"Inspector.error": [Error];
|
||||
"Inspector.pendingRequest": [JSC.Request];
|
||||
"Inspector.request": [JSC.Request];
|
||||
"Inspector.response": [JSC.Response];
|
||||
"Inspector.event": [JSC.Event];
|
||||
};
|
||||
|
||||
/**
|
||||
* A client that can send and receive messages to/from a debugger.
|
||||
*/
|
||||
export interface Inspector extends EventEmitter<InspectorEventMap> {
|
||||
/**
|
||||
* Starts the inspector.
|
||||
*/
|
||||
start(...args: unknown[]): Promise<boolean>;
|
||||
/**
|
||||
* 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]>;
|
||||
/**
|
||||
* If the inspector is closed.
|
||||
*/
|
||||
get closed(): boolean;
|
||||
/**
|
||||
* Closes the inspector.
|
||||
*/
|
||||
close(...args: unknown[]): void;
|
||||
}
|
||||
239
packages/bun-inspector-protocol/src/inspector/websocket.ts
Normal file
239
packages/bun-inspector-protocol/src/inspector/websocket.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
import type { Inspector, InspectorEventMap } from ".";
|
||||
import type { JSC } from "../protocol";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
/**
|
||||
* An inspector that communicates with a debugger over a WebSocket.
|
||||
*/
|
||||
export class WebSocketInspector extends EventEmitter<InspectorEventMap> implements Inspector {
|
||||
#url?: string;
|
||||
#webSocket?: WebSocket;
|
||||
#ready: Promise<boolean> | undefined;
|
||||
#requestId: number;
|
||||
#pendingRequests: JSC.Request[];
|
||||
#pendingResponses: Map<number, (result: unknown) => void>;
|
||||
|
||||
constructor(url?: string | URL) {
|
||||
super();
|
||||
this.#url = url ? String(url) : undefined;
|
||||
this.#requestId = 1;
|
||||
this.#pendingRequests = [];
|
||||
this.#pendingResponses = new Map();
|
||||
}
|
||||
|
||||
get url(): string {
|
||||
return this.#url!;
|
||||
}
|
||||
|
||||
async start(url?: string | URL): Promise<boolean> {
|
||||
if (url) {
|
||||
this.#url = String(url);
|
||||
}
|
||||
|
||||
if (!this.#url) {
|
||||
this.emit("Inspector.error", new Error("Inspector needs a URL, but none was provided"));
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.#connect(this.#url);
|
||||
}
|
||||
|
||||
async #connect(url: string): Promise<boolean> {
|
||||
if (this.#ready) {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
this.close(1001, "Restarting...");
|
||||
this.emit("Inspector.connecting", url);
|
||||
|
||||
let webSocket: WebSocket;
|
||||
try {
|
||||
// @ts-expect-error: Support both Bun and Node.js version of `headers`.
|
||||
webSocket = new WebSocket(url, {
|
||||
headers: {
|
||||
"Ref-Event-Loop": "1",
|
||||
},
|
||||
finishRequest: (request: import("http").ClientRequest) => {
|
||||
request.setHeader("Ref-Event-Loop", "1");
|
||||
request.end();
|
||||
},
|
||||
});
|
||||
} catch (cause) {
|
||||
this.#close(unknownToError(cause));
|
||||
return false;
|
||||
}
|
||||
|
||||
webSocket.addEventListener("open", () => {
|
||||
this.emit("Inspector.connected");
|
||||
|
||||
for (const request of this.#pendingRequests) {
|
||||
if (this.#send(request)) {
|
||||
this.emit("Inspector.request", request);
|
||||
}
|
||||
}
|
||||
|
||||
this.#pendingRequests.length = 0;
|
||||
});
|
||||
|
||||
webSocket.addEventListener("message", ({ data }) => {
|
||||
if (typeof data === "string") {
|
||||
this.#accept(data);
|
||||
}
|
||||
});
|
||||
|
||||
webSocket.addEventListener("error", event => {
|
||||
this.#close(unknownToError(event));
|
||||
});
|
||||
|
||||
webSocket.addEventListener("unexpected-response", () => {
|
||||
this.#close(new Error("WebSocket upgrade failed"));
|
||||
});
|
||||
|
||||
webSocket.addEventListener("close", ({ code, reason }) => {
|
||||
if (code === 1001 || code === 1006) {
|
||||
this.#close();
|
||||
return;
|
||||
}
|
||||
this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
|
||||
});
|
||||
|
||||
this.#webSocket = webSocket;
|
||||
|
||||
const ready = new Promise<boolean>(resolve => {
|
||||
webSocket.addEventListener("open", () => resolve(true));
|
||||
webSocket.addEventListener("close", () => resolve(false));
|
||||
webSocket.addEventListener("error", () => resolve(false));
|
||||
}).finally(() => {
|
||||
this.#ready = undefined;
|
||||
});
|
||||
|
||||
this.#ready = ready;
|
||||
|
||||
return ready;
|
||||
}
|
||||
|
||||
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: params ?? {},
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const done = (result: any) => {
|
||||
this.#pendingResponses.delete(id);
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
|
||||
this.#pendingResponses.set(id, done);
|
||||
if (this.#send(request)) {
|
||||
this.emit("Inspector.request", request);
|
||||
} else {
|
||||
this.emit("Inspector.pendingRequest", request);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#send(request: JSC.Request): boolean {
|
||||
if (this.#webSocket) {
|
||||
const { readyState } = this.#webSocket!;
|
||||
if (readyState === WebSocket.OPEN) {
|
||||
this.#webSocket.send(JSON.stringify(request));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.#pendingRequests.includes(request)) {
|
||||
this.#pendingRequests.push(request);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#accept(message: string): void {
|
||||
let data: JSC.Event | JSC.Response;
|
||||
try {
|
||||
data = JSON.parse(message);
|
||||
} catch (cause) {
|
||||
this.emit("Inspector.error", new Error(`Failed to parse message: ${message}`, { cause }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!("id" in data)) {
|
||||
this.emit("Inspector.event", data);
|
||||
const { method, params } = data;
|
||||
this.emit(method, params);
|
||||
return;
|
||||
}
|
||||
|
||||
this.emit("Inspector.response", data);
|
||||
|
||||
const { id } = data;
|
||||
const resolve = this.#pendingResponses.get(id);
|
||||
if (!resolve) {
|
||||
this.emit("Inspector.error", new Error(`Failed to find matching request for ID: ${id}`));
|
||||
return;
|
||||
}
|
||||
|
||||
this.#pendingResponses.delete(id);
|
||||
if ("error" in data) {
|
||||
const { error } = data;
|
||||
const { message } = error;
|
||||
resolve(new Error(message));
|
||||
} else {
|
||||
const { result } = data;
|
||||
resolve(result);
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
for (const resolve of this.#pendingResponses.values()) {
|
||||
resolve(error ?? new Error("WebSocket closed"));
|
||||
}
|
||||
this.#pendingResponses.clear();
|
||||
if (error) {
|
||||
this.emit("Inspector.error", error);
|
||||
}
|
||||
this.emit("Inspector.disconnected", error);
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
}
|
||||
17428
packages/bun-inspector-protocol/src/protocol/v8/index.d.ts
vendored
Normal file
17428
packages/bun-inspector-protocol/src/protocol/v8/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
14136
packages/bun-inspector-protocol/src/protocol/v8/protocol.json
Normal file
14136
packages/bun-inspector-protocol/src/protocol/v8/protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
import type { JSC } from "../../bun-inspector-protocol";
|
||||
import type { JSC } from "../protocol";
|
||||
|
||||
export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject): string {
|
||||
export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject, topLevel?: boolean): string {
|
||||
const { type, subtype, value, description, className, preview } = remoteObject;
|
||||
switch (type) {
|
||||
case "undefined":
|
||||
@@ -9,6 +9,9 @@ export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject): st
|
||||
case "number":
|
||||
return description ?? JSON.stringify(value);
|
||||
case "string":
|
||||
if (topLevel) {
|
||||
return String(value ?? description);
|
||||
}
|
||||
return JSON.stringify(value ?? description);
|
||||
case "symbol":
|
||||
case "bigint":
|
||||
190
packages/bun-inspector-protocol/test/inspector/websocket.test.ts
Normal file
190
packages/bun-inspector-protocol/test/inspector/websocket.test.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { describe, test, expect, mock, beforeAll, afterAll } from "bun:test";
|
||||
import { WebSocketInspector } from "../../src/inspector/websocket";
|
||||
import type { Server } from "bun";
|
||||
import { serve } from "bun";
|
||||
|
||||
let server: Server;
|
||||
let url: URL;
|
||||
|
||||
describe("WebSocketInspector", () => {
|
||||
test("fails without a URL", () => {
|
||||
const ws = new WebSocketInspector();
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with invalid URL", () => {
|
||||
const ws = new WebSocketInspector("notaurl");
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with valid URL but no server", () => {
|
||||
const ws = new WebSocketInspector("ws://localhost:0/doesnotexist/");
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("fails with invalid upgrade response", () => {
|
||||
const ws = new WebSocketInspector(new URL("/", url));
|
||||
const fn = mock(error => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
ws.on("Inspector.error", fn);
|
||||
expect(ws.start()).resolves.toBeFalse();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("can connect to a server", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(() => {
|
||||
expect(ws.closed).toBe(false);
|
||||
});
|
||||
ws.on("Inspector.connected", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can disconnect from a server", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(() => {
|
||||
expect(ws.closed).toBeTrue();
|
||||
});
|
||||
ws.on("Inspector.disconnected", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
ws.close();
|
||||
expect(fn).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("can connect to a server multiple times", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(() => {
|
||||
expect(ws.closed).toBeFalse();
|
||||
});
|
||||
ws.on("Inspector.connected", fn0);
|
||||
const fn1 = mock(() => {
|
||||
expect(ws.closed).toBeTrue();
|
||||
});
|
||||
ws.on("Inspector.disconnected", fn1);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
ws.close();
|
||||
}
|
||||
expect(fn0).toHaveBeenCalledTimes(3);
|
||||
expect(fn1).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
test("can send a request", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(request => {
|
||||
expect(request).toStrictEqual({
|
||||
id: 1,
|
||||
method: "Debugger.setPauseOnAssertions",
|
||||
params: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.request", fn0);
|
||||
const fn1 = mock(response => {
|
||||
expect(response).toStrictEqual({
|
||||
id: 1,
|
||||
result: {
|
||||
ok: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.response", fn1);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(ws.send("Debugger.setPauseOnAssertions", { enabled: true })).resolves.toMatchObject({ ok: true });
|
||||
expect(fn0).toHaveBeenCalled();
|
||||
expect(fn1).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can send a request before connecting", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn0 = mock(request => {
|
||||
expect(request).toStrictEqual({
|
||||
id: 1,
|
||||
method: "Runtime.enable",
|
||||
params: {},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.pendingRequest", fn0);
|
||||
ws.on("Inspector.request", fn0);
|
||||
const fn1 = mock(response => {
|
||||
expect(response).toStrictEqual({
|
||||
id: 1,
|
||||
result: {
|
||||
ok: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.response", fn1);
|
||||
const request = ws.send("Runtime.enable");
|
||||
expect(ws.start()).resolves.toBe(true);
|
||||
expect(request).resolves.toMatchObject({ ok: true });
|
||||
expect(fn0).toHaveBeenCalledTimes(2);
|
||||
expect(fn1).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
|
||||
test("can receive an event", () => {
|
||||
const ws = new WebSocketInspector(url);
|
||||
const fn = mock(event => {
|
||||
expect(event).toStrictEqual({
|
||||
method: "Debugger.scriptParsed",
|
||||
params: {
|
||||
scriptId: "1",
|
||||
},
|
||||
});
|
||||
});
|
||||
ws.on("Inspector.event", fn);
|
||||
expect(ws.start()).resolves.toBeTrue();
|
||||
expect(ws.send("Debugger.enable")).resolves.toMatchObject({ ok: true });
|
||||
expect(fn).toHaveBeenCalled();
|
||||
ws.close();
|
||||
});
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
server = serve({
|
||||
port: 0,
|
||||
fetch(request, server) {
|
||||
if (request.url.endsWith("/ws") && server.upgrade(request)) {
|
||||
return;
|
||||
}
|
||||
return new Response();
|
||||
},
|
||||
websocket: {
|
||||
message(ws, message) {
|
||||
const { id, method } = JSON.parse(String(message));
|
||||
ws.send(JSON.stringify({ id, result: { ok: true } }));
|
||||
|
||||
if (method === "Debugger.enable") {
|
||||
ws.send(JSON.stringify({ method: "Debugger.scriptParsed", params: { scriptId: "1" } }));
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
const { hostname, port } = server;
|
||||
url = new URL(`ws://${hostname}:${port}/ws`);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
server?.stop(true);
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"module": "ESNext",
|
||||
"target": "ESNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
@@ -13,7 +13,6 @@
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "dist",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [".", "../bun-types/index.d.ts"]
|
||||
}
|
||||
|
||||
1
packages/bun-vscode/.gitignore
vendored
1
packages/bun-vscode/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
extension
|
||||
example/.vscode
|
||||
|
||||
10
packages/bun-vscode/.vscode/launch.json
vendored
10
packages/bun-vscode/.vscode/launch.json
vendored
@@ -5,22 +5,28 @@
|
||||
"name": "Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/example"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "Build (watch)"
|
||||
"preLaunchTask": "Build"
|
||||
},
|
||||
{
|
||||
"name": "Extension (web)",
|
||||
"type": "extensionHost",
|
||||
"debugWebWorkerHost": true,
|
||||
"request": "launch",
|
||||
"env": {
|
||||
"NODE_ENV": "development"
|
||||
},
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"--extensionDevelopmentKind=web",
|
||||
"${workspaceFolder}/example"
|
||||
],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "Build (watch)"
|
||||
"preLaunchTask": "Build"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
7
packages/bun-vscode/.vscode/tasks.json
vendored
7
packages/bun-vscode/.vscode/tasks.json
vendored
@@ -6,13 +6,6 @@
|
||||
"type": "shell",
|
||||
"command": "bun run build",
|
||||
"problemMatcher": "$esbuild"
|
||||
},
|
||||
{
|
||||
"label": "Build (watch)",
|
||||
"type": "shell",
|
||||
"command": "bun run build:watch",
|
||||
"isBackground": true,
|
||||
"problemMatcher": "$esbuild-watch"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
0
packages/bun-vscode/LICENSE
Normal file
0
packages/bun-vscode/LICENSE
Normal file
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
Binary file not shown.
18
packages/bun-vscode/example/.vscode/launch.json
vendored
18
packages/bun-vscode/example/.vscode/launch.json
vendored
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Debug Bun",
|
||||
"program": "${file}",
|
||||
"watch": "hot"
|
||||
},
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Attach to Bun",
|
||||
"url": "ws://localhost:6499/",
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
@@ -1,30 +0,0 @@
|
||||
// @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=
|
||||
@@ -1,63 +0,0 @@
|
||||
// @bun
|
||||
const express = import.meta.require("express");
|
||||
const app = express();
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
app
|
||||
.get("/", (req, res) => {
|
||||
console.log("I am logging a request!??");
|
||||
readFile(import.meta.path, "utf-8").then(data => {
|
||||
console.log(data.length);
|
||||
debugger;
|
||||
res.send("hello world");
|
||||
});
|
||||
})
|
||||
.listen(3000);
|
||||
|
||||
const va = 1;
|
||||
let vb = 2;
|
||||
var vc = 3;
|
||||
|
||||
function fa() {
|
||||
fb();
|
||||
}
|
||||
|
||||
function fb() {
|
||||
fc();
|
||||
}
|
||||
|
||||
function fc() {
|
||||
fd();
|
||||
}
|
||||
|
||||
function fd() {
|
||||
let map = new Map([
|
||||
[1, 2],
|
||||
[2, 3],
|
||||
[3, 4],
|
||||
]);
|
||||
let set = new Set([1, 2, 3, 4, 5]);
|
||||
let arr = [1, 2, 3, 4, 5];
|
||||
let obj = {
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
};
|
||||
function fd1() {
|
||||
let date = new Date();
|
||||
console.log(new Error().stack);
|
||||
debugger;
|
||||
console.log(date);
|
||||
}
|
||||
fd1();
|
||||
}
|
||||
|
||||
Bun.serve({
|
||||
port: 9229,
|
||||
inspector: true,
|
||||
development: true,
|
||||
fetch(request, server) {
|
||||
// console.log(request);
|
||||
return new Response(request.url);
|
||||
},
|
||||
});
|
||||
11
packages/bun-vscode/example/example.test.ts
Normal file
11
packages/bun-vscode/example/example.test.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
|
||||
describe("example", () => {
|
||||
test("it works", () => {
|
||||
expect(1).toBe(1);
|
||||
expect(1).not.toBe(2);
|
||||
expect(() => {
|
||||
throw new Error("error");
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ export default {
|
||||
const coolThing: CoolThing = new SuperCoolThing();
|
||||
coolThing.doCoolThing();
|
||||
debugger;
|
||||
return new Response("HELLO WORLD");
|
||||
return new Response("BAI BAI");
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"name": "example",
|
||||
"dependencies": {
|
||||
"elysia": "^0.6.3",
|
||||
"express": "^4.18.2",
|
||||
@@ -7,5 +9,11 @@
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"mime"
|
||||
]
|
||||
}
|
||||
],
|
||||
"devDependencies": {
|
||||
"bun-types": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
}
|
||||
}
|
||||
22
packages/bun-vscode/example/tsconfig.json
Normal file
22
packages/bun-vscode/example/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"allowJs": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -7,18 +7,14 @@
|
||||
"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": {
|
||||
"@types/vscode": "^1.81.0",
|
||||
"@vscode/debugadapter": "^1.56.0",
|
||||
"@vscode/debugadapter-testsupport": "^1.56.0",
|
||||
"@vscode/vsce": "^2.20.1",
|
||||
"bun-types": "^0.7.3",
|
||||
"typescript": "^5.0.0",
|
||||
"esbuild": "^0.19.2"
|
||||
"esbuild": "^0.19.2",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onLanguage:javascript",
|
||||
@@ -59,14 +55,16 @@
|
||||
"commands": [
|
||||
{
|
||||
"command": "extension.bun.runFile",
|
||||
"title": "Run File",
|
||||
"title": "Run Bun",
|
||||
"shortTitle": "Run",
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(play)"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.debugFile",
|
||||
"title": "Debug File",
|
||||
"title": "Debug Bun",
|
||||
"shortTitle": "Debug",
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(debug-alt)"
|
||||
@@ -169,6 +167,11 @@
|
||||
false,
|
||||
"hot"
|
||||
],
|
||||
"default": false
|
||||
},
|
||||
"debug": {
|
||||
"type": "boolean",
|
||||
"description": "If the process should be started in debug mode.",
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
@@ -177,48 +180,11 @@
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL of the Bun process to attach to.",
|
||||
"default": "ws://localhost:6499/"
|
||||
"description": "The URL of the Bun process to attach to."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Bun: Debug",
|
||||
"program": "${file}"
|
||||
},
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Bun: Attach",
|
||||
"url": "ws://localhost:6499/"
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "Bun: Debug",
|
||||
"description": "A new configuration for 'debugging' a Bun process.",
|
||||
"body": {
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Ask for file name",
|
||||
"program": "^\"\\${file}\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Bun: Attach",
|
||||
"description": "A new configuration for 'attaching' to a running Bun process.",
|
||||
"body": {
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Attach to Bun",
|
||||
"url": "ws://localhost:6499/"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
@@ -231,15 +197,15 @@
|
||||
".lockb"
|
||||
],
|
||||
"icon": {
|
||||
"dark": "src/assets/icon-small.png",
|
||||
"light": "src/assets/icon-small.png"
|
||||
"dark": "assets/icon-small.png",
|
||||
"light": "assets/icon-small.png"
|
||||
}
|
||||
}
|
||||
],
|
||||
"jsonValidation": [
|
||||
{
|
||||
"fileMatch": "package.json",
|
||||
"url": "src/resources/package.json"
|
||||
"url": "assets/package.json"
|
||||
}
|
||||
],
|
||||
"customEditors": [
|
||||
@@ -268,7 +234,7 @@
|
||||
"theme": "dark"
|
||||
},
|
||||
"homepage": "https://bun.sh/",
|
||||
"icon": "src/assets/icon.png",
|
||||
"icon": "assets/icon.png",
|
||||
"keywords": [
|
||||
"bun",
|
||||
"node.js",
|
||||
@@ -279,10 +245,8 @@
|
||||
"license": "MIT",
|
||||
"publisher": "oven",
|
||||
"scripts": {
|
||||
"bundle": "./node_modules/.bin/esbuild 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"
|
||||
"build": "node scripts/build.mjs",
|
||||
"test": "node scripts/test.mjs"
|
||||
},
|
||||
"workspaceTrust": {
|
||||
"request": "never"
|
||||
|
||||
29
packages/bun-vscode/scripts/build.mjs
Normal file
29
packages/bun-vscode/scripts/build.mjs
Normal file
@@ -0,0 +1,29 @@
|
||||
import { buildSync } from "esbuild";
|
||||
import { rmSync, mkdirSync, cpSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
const { pathname } = new URL("..", import.meta.url);
|
||||
process.chdir(pathname);
|
||||
|
||||
buildSync({
|
||||
entryPoints: ["src/extension.ts", "src/web-extension.ts"],
|
||||
outdir: "dist",
|
||||
bundle: true,
|
||||
external: ["vscode"],
|
||||
platform: "node",
|
||||
format: "cjs",
|
||||
});
|
||||
|
||||
rmSync("extension", { recursive: true, force: true });
|
||||
mkdirSync("extension", { recursive: true });
|
||||
cpSync("dist", "extension/dist", { recursive: true });
|
||||
cpSync("assets", "extension/assets", { recursive: true });
|
||||
cpSync("README.md", "extension/README.md");
|
||||
cpSync("LICENSE", "extension/LICENSE");
|
||||
cpSync("package.json", "extension/package.json");
|
||||
|
||||
const cmd = process.isBun ? "bunx" : "npx";
|
||||
spawnSync(cmd, ["vsce", "package"], {
|
||||
cwd: "extension",
|
||||
stdio: "inherit",
|
||||
});
|
||||
21
packages/bun-vscode/scripts/test.mjs
Normal file
21
packages/bun-vscode/scripts/test.mjs
Normal file
@@ -0,0 +1,21 @@
|
||||
import { readdirSync } from "node:fs";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const { pathname } = new URL("..", import.meta.url);
|
||||
process.chdir(pathname);
|
||||
|
||||
let path;
|
||||
for (const filename of readdirSync("extension")) {
|
||||
if (filename.endsWith(".vsix")) {
|
||||
path = `extension/${filename}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
throw new Error("No .vsix file found");
|
||||
}
|
||||
|
||||
spawn("code", ["--new-window", `--install-extension=${path}`, `--extensionDevelopmentPath=${pathname}`, "example"], {
|
||||
stdio: "inherit",
|
||||
});
|
||||
@@ -1,15 +1,16 @@
|
||||
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 { DebugAdapter, UnixSignal } from "../../../bun-debug-adapter-protocol";
|
||||
import { DebugSession } from "@vscode/debugadapter";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
const debugConfiguration: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
request: "launch",
|
||||
name: "Debug Bun",
|
||||
program: "${file}",
|
||||
watch: true,
|
||||
watch: false,
|
||||
};
|
||||
|
||||
const runConfiguration: vscode.DebugConfiguration = {
|
||||
@@ -17,93 +18,157 @@ const runConfiguration: vscode.DebugConfiguration = {
|
||||
request: "launch",
|
||||
name: "Run Bun",
|
||||
program: "${file}",
|
||||
watch: true,
|
||||
debug: false,
|
||||
watch: false,
|
||||
};
|
||||
|
||||
const attachConfiguration: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
request: "attach",
|
||||
name: "Attach to Bun",
|
||||
name: "Attach Bun",
|
||||
url: "ws://localhost:6499/",
|
||||
};
|
||||
|
||||
const debugConfigurations: vscode.DebugConfiguration[] = [debugConfiguration, attachConfiguration];
|
||||
let channels: Record<string, vscode.OutputChannel> = {};
|
||||
let terminal: TerminalDebugSession | undefined;
|
||||
|
||||
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.commands.registerCommand("extension.bun.runFile", RunFileCommand),
|
||||
vscode.commands.registerCommand("extension.bun.debugFile", DebugFileCommand),
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
"bun",
|
||||
{
|
||||
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
|
||||
return debugConfigurations;
|
||||
},
|
||||
},
|
||||
new DebugConfigurationProvider(),
|
||||
vscode.DebugConfigurationProviderTriggerKind.Initial,
|
||||
),
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
"bun",
|
||||
new DebugConfigurationProvider(),
|
||||
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
|
||||
),
|
||||
vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory ?? new InlineDebugAdapterFactory()),
|
||||
(channels["dap"] = vscode.window.createOutputChannel("Debug Adapter Protocol (Bun)")),
|
||||
(channels["jsc"] = vscode.window.createOutputChannel("JavaScript Inspector (Bun)")),
|
||||
(channels["console"] = vscode.window.createOutputChannel("Console (Bun)")),
|
||||
(terminal = new TerminalDebugSession()),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
function RunFileCommand(resource?: vscode.Uri): void {
|
||||
const path = getCurrentPath(resource);
|
||||
if (path) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...runConfiguration,
|
||||
noDebug: true,
|
||||
program: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class BunConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
function DebugFileCommand(resource?: vscode.Uri): void {
|
||||
const path = getCurrentPath(resource);
|
||||
if (path) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...debugConfiguration,
|
||||
program: path,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
|
||||
return [debugConfiguration, runConfiguration, attachConfiguration];
|
||||
}
|
||||
|
||||
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);
|
||||
let target: DebugConfiguration;
|
||||
|
||||
const { request } = config;
|
||||
if (request === "attach") {
|
||||
target = attachConfiguration;
|
||||
} else {
|
||||
target = debugConfiguration;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(target)) {
|
||||
if (config[key] === undefined) {
|
||||
config[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
const adapter = new VSCodeAdapter(_session);
|
||||
createDebugAdapterDescriptor(session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
const { configuration } = session;
|
||||
const { request, url } = configuration;
|
||||
|
||||
if (request === "attach" && url === terminal?.adapter.url) {
|
||||
return new vscode.DebugAdapterInlineImplementation(terminal);
|
||||
}
|
||||
|
||||
const adapter = new FileDebugSession(session.id);
|
||||
return new vscode.DebugAdapterInlineImplementation(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
class FileDebugSession extends DebugSession {
|
||||
readonly adapter: DebugAdapter;
|
||||
readonly signal: UnixSignal;
|
||||
|
||||
constructor(sessionId?: string) {
|
||||
super();
|
||||
const uniqueId = sessionId ?? Math.random().toString(36).slice(2);
|
||||
this.adapter = new DebugAdapter(`ws+unix://${tmpdir()}/${uniqueId}.sock`);
|
||||
this.adapter.on("Adapter.response", response => this.sendResponse(response));
|
||||
this.adapter.on("Adapter.event", event => this.sendEvent(event));
|
||||
this.signal = new UnixSignal();
|
||||
}
|
||||
|
||||
handleMessage(message: DAP.Event | DAP.Request | DAP.Response): void {
|
||||
const { type } = message;
|
||||
if (type === "request") {
|
||||
this.adapter.emit("Adapter.request", message);
|
||||
} else {
|
||||
throw new Error(`Not supported: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.adapter.close();
|
||||
}
|
||||
}
|
||||
|
||||
class TerminalDebugSession extends FileDebugSession {
|
||||
readonly terminal: vscode.Terminal;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.terminal = vscode.window.createTerminal({
|
||||
name: "Bun Terminal",
|
||||
env: {
|
||||
"BUN_INSPECT": `1${this.adapter.url}`,
|
||||
"BUN_INSPECT_NOTIFY": `${this.signal.url}`,
|
||||
},
|
||||
isTransient: true,
|
||||
iconPath: new vscode.ThemeIcon("debug-console"),
|
||||
});
|
||||
this.terminal.show();
|
||||
this.signal.on("Signal.received", () => {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...attachConfiguration,
|
||||
url: this.adapter.url,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isJavaScript(languageId: string): boolean {
|
||||
return (
|
||||
languageId === "javascript" ||
|
||||
@@ -113,41 +178,9 @@ function isJavaScript(languageId: string): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
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();
|
||||
function getCurrentPath(target?: vscode.Uri): string | undefined {
|
||||
if (!target && vscode.window.activeTextEditor) {
|
||||
target = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
return target?.fsPath;
|
||||
}
|
||||
|
||||
@@ -626,6 +626,14 @@ pub const PathLike = union(Tag) {
|
||||
|
||||
pub const Tag = enum { string, buffer, slice_with_underlying_string };
|
||||
|
||||
pub fn estimatedSize(this: *const PathLike) usize {
|
||||
return switch (this.*) {
|
||||
.string => this.string.estimatedSize(),
|
||||
.buffer => this.buffer.slice().len,
|
||||
.slice_with_underlying_string => 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *const PathLike) void {
|
||||
if (this.* == .slice_with_underlying_string) {
|
||||
this.slice_with_underlying_string.deinit();
|
||||
@@ -1059,6 +1067,13 @@ pub const PathOrFileDescriptor = union(Tag) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimatedSize(this: *const PathOrFileDescriptor) usize {
|
||||
return switch (this.*) {
|
||||
.path => this.path.estimatedSize(),
|
||||
.fd => 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toThreadSafe(this: *PathOrFileDescriptor) void {
|
||||
if (this.* == .path) {
|
||||
this.path.toThreadSafe();
|
||||
|
||||
@@ -1253,8 +1253,8 @@ pub const Blob = struct {
|
||||
if (this.store) |store| {
|
||||
size += @sizeOf(Blob.Store);
|
||||
size += switch (store.data) {
|
||||
.bytes => store.data.bytes.stored_name.len,
|
||||
.file => store.data.file.pathlike.path.slice().len,
|
||||
.bytes => store.data.bytes.stored_name.estimatedSize(),
|
||||
.file => store.data.file.pathlike.estimatedSize(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -543,6 +543,15 @@ pub const Arguments = struct {
|
||||
.wait_for_connection = true,
|
||||
.set_breakpoint_on_first_line = true,
|
||||
} };
|
||||
} else if (bun.getenvZ("BUN_INSPECT")) |inspect_value| {
|
||||
ctx.runtime_options.debugger = if (inspect_value.len == 0 or inspect_value[0] == '0')
|
||||
Command.Debugger{ .unspecified = {} }
|
||||
else
|
||||
Command.Debugger{ .enable = .{
|
||||
.path_or_port = inspect_value[1..],
|
||||
.wait_for_connection = inspect_value[0] == '1' or inspect_value[0] == '2',
|
||||
.set_breakpoint_on_first_line = inspect_value[0] == '2',
|
||||
} };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type * as BunType from "bun";
|
||||
|
||||
// We want to avoid dealing with creating a prototype for the inspector class
|
||||
let sendFn_, disconnectFn_;
|
||||
const colors = Bun.enableANSIColors && process.env.NO_COLOR !== "1";
|
||||
|
||||
var debuggerCounter = 1;
|
||||
class DebuggerWithMessageQueue {
|
||||
@@ -31,7 +32,7 @@ function generatePath() {
|
||||
}
|
||||
|
||||
function terminalLink(url) {
|
||||
if (Bun.enableANSIColors) {
|
||||
if (colors) {
|
||||
// bold + hyperlink + reset
|
||||
return "\x1b[1m\x1b]8;;" + url + "\x1b\\" + url + "\x1b]8;;\x1b\\" + "\x1b[22m";
|
||||
}
|
||||
@@ -40,7 +41,7 @@ function terminalLink(url) {
|
||||
}
|
||||
|
||||
function dim(text) {
|
||||
if (Bun.enableANSIColors) {
|
||||
if (colors) {
|
||||
return "\x1b[2m" + text + "\x1b[22m";
|
||||
}
|
||||
|
||||
@@ -63,7 +64,12 @@ class WebSocketListener {
|
||||
start(url: string): BunType.Server {
|
||||
let defaultHostname = "localhost";
|
||||
let usingDefaultPort = false;
|
||||
if (/^[0-9]*$/.test(url)) {
|
||||
let isUnix = false;
|
||||
|
||||
if (url.startsWith("ws+unix://")) {
|
||||
isUnix = true;
|
||||
url = url.slice(10);
|
||||
} else if (/^[0-9]*$/.test(url)) {
|
||||
url = "ws://" + defaultHostname + ":" + url + generatePath();
|
||||
} else if (!url || url.startsWith("/")) {
|
||||
url = "ws://" + defaultHostname + ":" + defaultPort + generatePath();
|
||||
@@ -81,19 +87,21 @@ class WebSocketListener {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
var { hostname, port, pathname } = new URL(url);
|
||||
this.url = pathname.toLowerCase();
|
||||
} catch (e) {
|
||||
console.error("[Inspector]", "Failed to parse url", '"' + url + '"');
|
||||
process.exit(1);
|
||||
if (!isUnix) {
|
||||
try {
|
||||
var { hostname, port, pathname } = new URL(url);
|
||||
this.url = pathname.toLowerCase();
|
||||
} catch (e) {
|
||||
console.error("[Inspector]", "Failed to parse url", '"' + url + '"');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const serveOptions: BunType.WebSocketServeOptions<DebuggerWithMessageQueue> = {
|
||||
hostname,
|
||||
...(isUnix ? { unix: url } : { hostname }),
|
||||
development: false,
|
||||
|
||||
// ts-ignore
|
||||
// @ts-ignore
|
||||
reusePort: false,
|
||||
|
||||
websocket: {
|
||||
@@ -130,16 +138,18 @@ class WebSocketListener {
|
||||
},
|
||||
);
|
||||
|
||||
console.log(
|
||||
"[Inspector]",
|
||||
"Connection #" + connection.count + " opened",
|
||||
"(" +
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
"timeStyle": "long",
|
||||
"dateStyle": "short",
|
||||
}).format(new Date()) +
|
||||
")",
|
||||
);
|
||||
if (!isUnix) {
|
||||
console.log(
|
||||
"[Inspector]",
|
||||
"Connection #" + connection.count + " opened",
|
||||
"(" +
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
"timeStyle": "long",
|
||||
"dateStyle": "short",
|
||||
}).format(new Date()) +
|
||||
")",
|
||||
);
|
||||
}
|
||||
},
|
||||
drain: socket => {
|
||||
const queue = socket.data.messageQueue;
|
||||
@@ -160,16 +170,18 @@ class WebSocketListener {
|
||||
},
|
||||
close: socket => {
|
||||
socket.data.disconnect();
|
||||
console.log(
|
||||
"[Inspector]",
|
||||
"Connection #" + socket.data.count + " closed",
|
||||
"(" +
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
"timeStyle": "long",
|
||||
"dateStyle": "short",
|
||||
}).format(new Date()) +
|
||||
")",
|
||||
);
|
||||
if (!isUnix) {
|
||||
console.log(
|
||||
"[Inspector]",
|
||||
"Connection #" + socket.data.count + " closed",
|
||||
"(" +
|
||||
new Intl.DateTimeFormat(undefined, {
|
||||
"timeStyle": "long",
|
||||
"dateStyle": "short",
|
||||
}).format(new Date()) +
|
||||
")",
|
||||
);
|
||||
}
|
||||
this.activeConnections.delete(socket);
|
||||
},
|
||||
},
|
||||
@@ -186,7 +198,7 @@ class WebSocketListener {
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname === this.url) {
|
||||
if (!this.url || pathname === this.url) {
|
||||
const refHeader = req.headers.get("Ref-Event-Loop");
|
||||
if (
|
||||
server.upgrade(req, {
|
||||
@@ -224,6 +236,9 @@ class WebSocketListener {
|
||||
...serveOptions,
|
||||
port: portNumber++,
|
||||
});
|
||||
if (isUnix) {
|
||||
notify();
|
||||
}
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
}
|
||||
@@ -234,6 +249,9 @@ class WebSocketListener {
|
||||
...serveOptions,
|
||||
port: portNumber,
|
||||
});
|
||||
if (isUnix) {
|
||||
notify();
|
||||
}
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
}
|
||||
@@ -253,10 +271,16 @@ class WebSocketListener {
|
||||
console.write(textToWrite);
|
||||
}
|
||||
|
||||
if (!this.url) {
|
||||
return server;
|
||||
}
|
||||
|
||||
// yellow foreground
|
||||
writeToConsole(dim(`------------------ Bun Inspector ------------------` + "\n"));
|
||||
// reset background
|
||||
writeToConsole("\x1b[49m");
|
||||
if (colors) {
|
||||
// reset background
|
||||
writeToConsole("\x1b[49m");
|
||||
}
|
||||
|
||||
writeToConsole(
|
||||
"Listening at:\n " +
|
||||
@@ -273,6 +297,24 @@ class WebSocketListener {
|
||||
}
|
||||
}
|
||||
|
||||
function notify(): void {
|
||||
const unix = process.env["BUN_INSPECT_NOTIFY"];
|
||||
if (!unix || !unix.startsWith("unix://")) {
|
||||
return;
|
||||
}
|
||||
Bun.connect({
|
||||
unix: unix.slice(7),
|
||||
socket: {
|
||||
open: socket => {
|
||||
socket.end("1");
|
||||
},
|
||||
data: () => {}, // required or it errors
|
||||
},
|
||||
}).finally(() => {
|
||||
// Do nothing
|
||||
});
|
||||
}
|
||||
|
||||
interface Debugger {
|
||||
send(msg: string): void;
|
||||
disconnect(): void;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -31,6 +31,10 @@ pub const PathString = packed struct {
|
||||
return this.toValue().asObjectRef();
|
||||
}
|
||||
|
||||
pub fn estimatedSize(this: *const PathString) usize {
|
||||
return @as(usize, this.len);
|
||||
}
|
||||
|
||||
pub fn toJS(this: PathString, ctx: JSC.C.JSContextRef, _: JSC.C.ExceptionRef) JSC.C.JSValueRef {
|
||||
var zig_str = JSC.ZigString.init(this.slice());
|
||||
zig_str.detectEncoding();
|
||||
|
||||
Reference in New Issue
Block a user