mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Improved support for debug-adapter-protocol (#4186)
* Improve support for \`debug-adapter-protocol\` * More improvements, fix formatting in debug console * Fix attaching * Prepare for source maps * Start of source map support, breakpoints work * Source map support * add some package.jsons * wip * Update package.json * More fixes * Make source maps safer if exception occurs * Check bun version if it fails * Fix console.log formatting * Fix source maps partly * More source map fixes * Prepare for extension * watch mode with dap * Improve preview code * Prepare for extension 2 --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"prettier": "^2.4.1",
|
||||
"react": "next",
|
||||
"react-dom": "next",
|
||||
"source-map-js": "^1.0.2",
|
||||
"typescript": "^5.0.2"
|
||||
},
|
||||
"private": true,
|
||||
@@ -17,7 +18,7 @@
|
||||
"build-fallback": "esbuild --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js",
|
||||
"postinstall": "bash .scripts/postinstall.sh",
|
||||
"typecheck": "tsc --noEmit && cd test && bun run typecheck",
|
||||
"fmt": "prettier --write --cache './{src,test,bench}/**/*.{mjs,ts,tsx,js,jsx}'",
|
||||
"fmt": "prettier --write --cache './{src,test,bench,packages/{bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}/**/*.{mjs,ts,tsx,js,jsx}'",
|
||||
"lint": "eslint './**/*.d.ts' --cache",
|
||||
"lint:fix": "eslint './**/*.d.ts' --cache --fix"
|
||||
},
|
||||
|
||||
2
packages/bun-debug-adapter-protocol/.gitattributes
vendored
Normal file
2
packages/bun-debug-adapter-protocol/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
protocol/*/protocol.json linguist-generated=true
|
||||
protocol/*/index.d.ts linguist-generated=true
|
||||
1
packages/bun-debug-adapter-protocol/.gitignore
vendored
Normal file
1
packages/bun-debug-adapter-protocol/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
protocol/*.json
|
||||
3
packages/bun-debug-adapter-protocol/README.md
Normal file
3
packages/bun-debug-adapter-protocol/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# bun-debug-adapter-protocol
|
||||
|
||||
https://microsoft.github.io/debug-adapter-protocol/overview
|
||||
BIN
packages/bun-debug-adapter-protocol/bun.lockb
Executable file
BIN
packages/bun-debug-adapter-protocol/bun.lockb
Executable file
Binary file not shown.
@@ -0,0 +1,143 @@
|
||||
// Bun Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`remoteObjectToString 1`] = `"undefined"`;
|
||||
|
||||
exports[`remoteObjectToString 2`] = `"null"`;
|
||||
|
||||
exports[`remoteObjectToString 3`] = `"true"`;
|
||||
|
||||
exports[`remoteObjectToString 4`] = `"false"`;
|
||||
|
||||
exports[`remoteObjectToString 5`] = `"0"`;
|
||||
|
||||
exports[`remoteObjectToString 6`] = `"1"`;
|
||||
|
||||
exports[`remoteObjectToString 7`] = `"3.141592653589793"`;
|
||||
|
||||
exports[`remoteObjectToString 8`] = `"-2.718281828459045"`;
|
||||
|
||||
exports[`remoteObjectToString 9`] = `"NaN"`;
|
||||
|
||||
exports[`remoteObjectToString 10`] = `"Infinity"`;
|
||||
|
||||
exports[`remoteObjectToString 11`] = `"-Infinity"`;
|
||||
|
||||
exports[`remoteObjectToString 12`] = `"0n"`;
|
||||
|
||||
exports[`remoteObjectToString 13`] = `"1n"`;
|
||||
|
||||
exports[`remoteObjectToString 14`] = `"10000000000000n"`;
|
||||
|
||||
exports[`remoteObjectToString 15`] = `"-10000000000000n"`;
|
||||
|
||||
exports[`remoteObjectToString 16`] = `""""`;
|
||||
|
||||
exports[`remoteObjectToString 17`] = `"" ""`;
|
||||
|
||||
exports[`remoteObjectToString 18`] = `""Hello""`;
|
||||
|
||||
exports[`remoteObjectToString 19`] = `""Hello World""`;
|
||||
|
||||
exports[`remoteObjectToString 20`] = `"Array(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 21`] = `"Array(3) [1, 2, 3]"`;
|
||||
|
||||
exports[`remoteObjectToString 22`] = `"Array(4) ["a", 1, null, undefined]"`;
|
||||
|
||||
exports[`remoteObjectToString 23`] = `"Array(2) [1, Array]"`;
|
||||
|
||||
exports[`remoteObjectToString 24`] = `"Array(1) [Array]"`;
|
||||
|
||||
exports[`remoteObjectToString 25`] = `"{}"`;
|
||||
|
||||
exports[`remoteObjectToString 26`] = `"{a: 1}"`;
|
||||
|
||||
exports[`remoteObjectToString 27`] = `"{a: 1, b: 2, c: 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 28`] = `"{a: Object}"`;
|
||||
|
||||
exports[`remoteObjectToString 29`] = `
|
||||
"ƒ() {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 30`] = `
|
||||
"ƒ namedFunction() {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 31`] = `
|
||||
"class {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 32`] = `
|
||||
"class namedClass {
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 33`] = `
|
||||
"class namedClass {
|
||||
a() {
|
||||
}
|
||||
b = 1;
|
||||
c = [
|
||||
null,
|
||||
undefined,
|
||||
"a",
|
||||
{
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3
|
||||
}
|
||||
];
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`remoteObjectToString 34`] = `"Wed Dec 31 1969 16:00:00 GMT-0800 (Pacific Standard Time)"`;
|
||||
|
||||
exports[`remoteObjectToString 35`] = `"Invalid Date"`;
|
||||
|
||||
exports[`remoteObjectToString 36`] = `"/(?:)/"`;
|
||||
|
||||
exports[`remoteObjectToString 37`] = `"/abc/"`;
|
||||
|
||||
exports[`remoteObjectToString 38`] = `"/abc/g"`;
|
||||
|
||||
exports[`remoteObjectToString 39`] = `"/abc/"`;
|
||||
|
||||
exports[`remoteObjectToString 40`] = `"Set(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 41`] = `"Set(3) [1, 2, 3]"`;
|
||||
|
||||
exports[`remoteObjectToString 42`] = `"WeakSet(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 43`] = `"WeakSet(3) [{a: 1}, {b: 2}, {c: 3}]"`;
|
||||
|
||||
exports[`remoteObjectToString 44`] = `"Map(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 45`] = `"Map(3) {"a" => 1, "b" => 2, "c" => 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 46`] = `"WeakMap(0)"`;
|
||||
|
||||
exports[`remoteObjectToString 47`] = `"WeakMap(3) {{a: 1} => 1, {b: 2} => 2, {c: 3} => 3}"`;
|
||||
|
||||
exports[`remoteObjectToString 48`] = `"Symbol()"`;
|
||||
|
||||
exports[`remoteObjectToString 49`] = `"Symbol(namedSymbol)"`;
|
||||
|
||||
exports[`remoteObjectToString 50`] = `"Error"`;
|
||||
|
||||
exports[`remoteObjectToString 51`] = `"TypeError: This is a TypeError"`;
|
||||
|
||||
exports[`remoteObjectToString 52`] = `"Headers {append: ƒ, delete: ƒ, get: ƒ, getAll: ƒ, has: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 53`] = `"Headers {a: "1", append: ƒ, b: "2", delete: ƒ, get: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 54`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, cache: "default", …}"`;
|
||||
|
||||
exports[`remoteObjectToString 55`] = `"Request {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, cache: "default", …}"`;
|
||||
|
||||
exports[`remoteObjectToString 56`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: null, bodyUsed: false, clone: ƒ, …}"`;
|
||||
|
||||
exports[`remoteObjectToString 57`] = `"Response {arrayBuffer: ƒ, blob: ƒ, body: ReadableStream, bodyUsed: false, clone: ƒ, …}"`;
|
||||
1692
packages/bun-debug-adapter-protocol/debugger/adapter.ts
Normal file
1692
packages/bun-debug-adapter-protocol/debugger/adapter.ts
Normal file
File diff suppressed because it is too large
Load Diff
271
packages/bun-debug-adapter-protocol/debugger/capabilities.ts
Normal file
271
packages/bun-debug-adapter-protocol/debugger/capabilities.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import type { DAP } from "..";
|
||||
|
||||
const capabilities: DAP.Capabilities = {
|
||||
/**
|
||||
* The debug adapter supports the `configurationDone` request.
|
||||
* @see configurationDone
|
||||
*/
|
||||
supportsConfigurationDoneRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports function breakpoints using the `setFunctionBreakpoints` request.
|
||||
* @see setFunctionBreakpoints
|
||||
*/
|
||||
supportsFunctionBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports conditional breakpoints.
|
||||
* @see setBreakpoints
|
||||
* @see setInstructionBreakpoints
|
||||
* @see setFunctionBreakpoints
|
||||
* @see setExceptionBreakpoints
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsConditionalBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports breakpoints that break execution after a specified number of hits.
|
||||
* @see setBreakpoints
|
||||
* @see setInstructionBreakpoints
|
||||
* @see setFunctionBreakpoints
|
||||
* @see setExceptionBreakpoints
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsHitConditionalBreakpoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports a (side effect free) `evaluate` request for data hovers.
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsEvaluateForHovers: true,
|
||||
|
||||
/**
|
||||
* Available exception filter options for the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
exceptionBreakpointFilters: [
|
||||
{
|
||||
filter: "all",
|
||||
label: "Caught Exceptions",
|
||||
default: false,
|
||||
supportsCondition: true,
|
||||
description: "Breaks on all throw errors, even if they're caught later.",
|
||||
conditionDescription: `error.name == "CustomError"`,
|
||||
},
|
||||
{
|
||||
filter: "uncaught",
|
||||
label: "Uncaught Exceptions",
|
||||
default: false,
|
||||
supportsCondition: true,
|
||||
description: "Breaks only on errors or promise rejections that are not handled.",
|
||||
conditionDescription: `error.name == "CustomError"`,
|
||||
},
|
||||
],
|
||||
|
||||
/**
|
||||
* The debug adapter supports stepping back via the `stepBack` and `reverseContinue` requests.
|
||||
* @see stepBack
|
||||
* @see reverseContinue
|
||||
*/
|
||||
supportsStepBack: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports setting a variable to a value.
|
||||
* @see setVariable
|
||||
*/
|
||||
supportsSetVariable: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports restarting a frame.
|
||||
* @see restartFrame
|
||||
*/
|
||||
supportsRestartFrame: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `gotoTargets` request.
|
||||
* @see gotoTargets
|
||||
*/
|
||||
supportsGotoTargetsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `stepInTargets` request.
|
||||
* @see stepInTargets
|
||||
*/
|
||||
supportsStepInTargetsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `completions` request.
|
||||
* @see completions
|
||||
*/
|
||||
supportsCompletionsRequest: false,
|
||||
|
||||
/**
|
||||
* The set of characters that should trigger completion in a REPL.
|
||||
* If not specified, the UI should assume the `.` character.
|
||||
* @see completions
|
||||
*/
|
||||
completionTriggerCharacters: [".", "[", '"', "'"],
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `modules` request.
|
||||
* @see modules
|
||||
*/
|
||||
supportsModulesRequest: false,
|
||||
|
||||
/**
|
||||
* The set of additional module information exposed by the debug adapter.
|
||||
* @see modules
|
||||
*/
|
||||
additionalModuleColumns: [],
|
||||
|
||||
/**
|
||||
* Checksum algorithms supported by the debug adapter.
|
||||
*/
|
||||
supportedChecksumAlgorithms: [],
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `restart` request.
|
||||
* In this case a client should not implement `restart` by terminating
|
||||
* and relaunching the adapter but by calling the `restart` request.
|
||||
* @see restart
|
||||
*/
|
||||
supportsRestartRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports `exceptionOptions` on the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
supportsExceptionOptions: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports a `format` attribute on the `stackTrace`, `variables`, and `evaluate` requests.
|
||||
* @see stackTrace
|
||||
* @see variables
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsValueFormattingOptions: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `exceptionInfo` request.
|
||||
* @see exceptionInfo
|
||||
*/
|
||||
supportsExceptionInfoRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminateDebuggee` attribute on the `disconnect` request.
|
||||
* @see disconnect
|
||||
*/
|
||||
supportTerminateDebuggee: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `suspendDebuggee` attribute on the `disconnect` request.
|
||||
* @see disconnect
|
||||
*/
|
||||
supportSuspendDebuggee: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the delayed loading of parts of the stack,
|
||||
* which requires that both the `startFrame` and `levels` arguments and
|
||||
* the `totalFrames` result of the `stackTrace` request are supported.
|
||||
* @see stackTrace
|
||||
*/
|
||||
supportsDelayedStackTraceLoading: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `loadedSources` request.
|
||||
* @see loadedSources
|
||||
*/
|
||||
supportsLoadedSourcesRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports log points by interpreting the `logMessage` attribute of the `SourceBreakpoint`.
|
||||
* @see setBreakpoints
|
||||
*/
|
||||
supportsLogPoints: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminateThreads` request.
|
||||
* @see terminateThreads
|
||||
*/
|
||||
supportsTerminateThreadsRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `setExpression` request.
|
||||
* @see setExpression
|
||||
*/
|
||||
supportsSetExpression: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `terminate` request.
|
||||
* @see terminate
|
||||
*/
|
||||
supportsTerminateRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports data breakpoints.
|
||||
* @see setDataBreakpoints
|
||||
*/
|
||||
supportsDataBreakpoints: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `readMemory` request.
|
||||
* @see readMemory
|
||||
*/
|
||||
supportsReadMemoryRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `writeMemory` request.
|
||||
* @see writeMemory
|
||||
*/
|
||||
supportsWriteMemoryRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `disassemble` request.
|
||||
* @see disassemble
|
||||
*/
|
||||
supportsDisassembleRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `cancel` request.
|
||||
* @see cancel
|
||||
*/
|
||||
supportsCancelRequest: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `breakpointLocations` request.
|
||||
* @see breakpointLocations
|
||||
*/
|
||||
supportsBreakpointLocationsRequest: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `clipboard` context value in the `evaluate` request.
|
||||
* @see evaluate
|
||||
*/
|
||||
supportsClipboardContext: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports stepping granularities (argument `granularity`) for the stepping requests.
|
||||
* @see stepIn
|
||||
*/
|
||||
supportsSteppingGranularity: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports adding breakpoints based on instruction references.
|
||||
* @see setInstructionBreakpoints
|
||||
*/
|
||||
supportsInstructionBreakpoints: false,
|
||||
|
||||
/**
|
||||
* The debug adapter supports `filterOptions` as an argument on the `setExceptionBreakpoints` request.
|
||||
* @see setExceptionBreakpoints
|
||||
*/
|
||||
supportsExceptionFilterOptions: true,
|
||||
|
||||
/**
|
||||
* The debug adapter supports the `singleThread` property on the execution requests
|
||||
* (`continue`, `next`, `stepIn`, `stepOut`, `reverseContinue`, `stepBack`).
|
||||
*/
|
||||
supportsSingleThreadExecutionRequests: false,
|
||||
};
|
||||
|
||||
export default capabilities;
|
||||
@@ -0,0 +1,99 @@
|
||||
console.log(
|
||||
undefined,
|
||||
null,
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
1,
|
||||
Math.PI,
|
||||
-Math.E,
|
||||
NaN,
|
||||
Infinity,
|
||||
-Infinity,
|
||||
BigInt(0),
|
||||
BigInt(1),
|
||||
BigInt("10000000000000"),
|
||||
BigInt("-10000000000000"),
|
||||
"",
|
||||
" ",
|
||||
"Hello",
|
||||
"Hello World",
|
||||
[],
|
||||
[1, 2, 3],
|
||||
["a", 1, null, undefined],
|
||||
[1, [2, [3, [4, [5, [6, [7, [8, [9, [10]]]]]]]]]],
|
||||
[[[[[]]]]],
|
||||
{},
|
||||
{ a: 1 },
|
||||
{ a: 1, b: 2, c: 3 },
|
||||
{ a: { b: { c: { d: { e: { f: { g: { h: { i: { j: 10 } } } } } } } } } },
|
||||
function () {},
|
||||
function namedFunction() {},
|
||||
class {},
|
||||
class namedClass {},
|
||||
class namedClass {
|
||||
a() {}
|
||||
b = 1;
|
||||
c = [
|
||||
null,
|
||||
undefined,
|
||||
"a",
|
||||
{
|
||||
a: 1,
|
||||
b: 2,
|
||||
c: 3,
|
||||
},
|
||||
];
|
||||
},
|
||||
new Date(0),
|
||||
new Date(NaN),
|
||||
new RegExp(),
|
||||
new RegExp("abc"),
|
||||
new RegExp("abc", "g"),
|
||||
/abc/,
|
||||
new Set(),
|
||||
new Set([1, 2, 3]),
|
||||
new WeakSet(),
|
||||
new WeakSet([{ a: 1 }, { b: 2 }, { c: 3 }]),
|
||||
new Map(),
|
||||
new Map([
|
||||
["a", 1],
|
||||
["b", 2],
|
||||
["c", 3],
|
||||
]),
|
||||
new WeakMap(),
|
||||
new WeakMap([
|
||||
[{ a: 1 }, 1],
|
||||
[{ b: 2 }, 2],
|
||||
[{ c: 3 }, 3],
|
||||
]),
|
||||
Symbol(),
|
||||
Symbol("namedSymbol"),
|
||||
new Error(),
|
||||
new TypeError("This is a TypeError"),
|
||||
//"a".repeat(10000),
|
||||
//["a"].fill("a", 0, 10000),
|
||||
new Headers(),
|
||||
new Headers({
|
||||
a: "1",
|
||||
b: "2",
|
||||
}),
|
||||
new Request("https://example.com/"),
|
||||
new Request("https://example.com/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
a: "1",
|
||||
b: "2",
|
||||
},
|
||||
body: '{"example":true}',
|
||||
}),
|
||||
new Response(),
|
||||
new Response('{"example":true}', {
|
||||
status: 200,
|
||||
statusText: "OK",
|
||||
headers: {
|
||||
a: "1",
|
||||
b: "2",
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
"use strict";
|
||||
export default {
|
||||
fetch(request) {
|
||||
const animal = getAnimal(request.url);
|
||||
const voice = animal.talk();
|
||||
return new Response(voice);
|
||||
},
|
||||
};
|
||||
function getAnimal(query) {
|
||||
switch (query.split("/").pop()) {
|
||||
case "dog":
|
||||
return new Dog();
|
||||
case "cat":
|
||||
return new Cat();
|
||||
}
|
||||
return new Bird();
|
||||
}
|
||||
class Dog {
|
||||
name = "dog";
|
||||
talk() {
|
||||
return "woof";
|
||||
}
|
||||
}
|
||||
class Cat {
|
||||
name = "cat";
|
||||
talk() {
|
||||
return "meow";
|
||||
}
|
||||
}
|
||||
class Bird {
|
||||
name = "bird";
|
||||
talk() {
|
||||
return "chirp";
|
||||
}
|
||||
}
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsicGFja2FnZXMvYnVuLWRlYnVnLWFkYXB0ZXItcHJvdG9jb2wvZGVidWdnZXIvZml4dHVyZXMvd2l0aC1zb3VyY2VtYXAudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImV4cG9ydCBkZWZhdWx0IHtcbiAgZmV0Y2gocmVxdWVzdDogUmVxdWVzdCk6IFJlc3BvbnNlIHtcbiAgICBjb25zdCBhbmltYWwgPSBnZXRBbmltYWwocmVxdWVzdC51cmwpO1xuICAgIGNvbnN0IHZvaWNlID0gYW5pbWFsLnRhbGsoKTtcbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKHZvaWNlKTtcbiAgfSxcbn07XG5cbmZ1bmN0aW9uIGdldEFuaW1hbChxdWVyeTogc3RyaW5nKTogQW5pbWFsIHtcbiAgc3dpdGNoIChxdWVyeS5zcGxpdChcIi9cIikucG9wKCkpIHtcbiAgICBjYXNlIFwiZG9nXCI6XG4gICAgICByZXR1cm4gbmV3IERvZygpO1xuICAgIGNhc2UgXCJjYXRcIjpcbiAgICAgIHJldHVybiBuZXcgQ2F0KCk7XG4gIH1cbiAgcmV0dXJuIG5ldyBCaXJkKCk7XG59XG5cbmludGVyZmFjZSBBbmltYWwge1xuICByZWFkb25seSBuYW1lOiBzdHJpbmc7XG4gIHRhbGsoKTogc3RyaW5nO1xufVxuXG5jbGFzcyBEb2cgaW1wbGVtZW50cyBBbmltYWwge1xuICBuYW1lID0gXCJkb2dcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwid29vZlwiO1xuICB9XG59XG5cbmNsYXNzIENhdCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImNhdFwiO1xuXG4gIHRhbGsoKTogc3RyaW5nIHtcbiAgICByZXR1cm4gXCJtZW93XCI7XG4gIH1cbn1cblxuY2xhc3MgQmlyZCBpbXBsZW1lbnRzIEFuaW1hbCB7XG4gIG5hbWUgPSBcImJpcmRcIjtcblxuICB0YWxrKCk6IHN0cmluZyB7XG4gICAgcmV0dXJuIFwiY2hpcnBcIjtcbiAgfVxufVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFBLGVBQWU7QUFBQSxFQUNiLE1BQU0sU0FBNEI7QUFDaEMsVUFBTSxTQUFTLFVBQVUsUUFBUSxHQUFHO0FBQ3BDLFVBQU0sUUFBUSxPQUFPLEtBQUs7QUFDMUIsV0FBTyxJQUFJLFNBQVMsS0FBSztBQUFBLEVBQzNCO0FBQ0Y7QUFFQSxTQUFTLFVBQVUsT0FBdUI7QUFDeEMsVUFBUSxNQUFNLE1BQU0sR0FBRyxFQUFFLElBQUksR0FBRztBQUFBLElBQzlCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLElBQ2pCLEtBQUs7QUFDSCxhQUFPLElBQUksSUFBSTtBQUFBLEVBQ25CO0FBQ0EsU0FBTyxJQUFJLEtBQUs7QUFDbEI7QUFPQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLElBQXNCO0FBQUEsRUFDMUIsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7QUFFQSxNQUFNLEtBQXVCO0FBQUEsRUFDM0IsT0FBTztBQUFBLEVBRVAsT0FBZTtBQUNiLFdBQU87QUFBQSxFQUNUO0FBQ0Y7IiwKICAibmFtZXMiOiBbXQp9Cg==
|
||||
@@ -0,0 +1,46 @@
|
||||
export default {
|
||||
fetch(request: Request): Response {
|
||||
const animal = getAnimal(request.url);
|
||||
const voice = animal.talk();
|
||||
return new Response(voice);
|
||||
},
|
||||
};
|
||||
|
||||
function getAnimal(query: string): Animal {
|
||||
switch (query.split("/").pop()) {
|
||||
case "dog":
|
||||
return new Dog();
|
||||
case "cat":
|
||||
return new Cat();
|
||||
}
|
||||
return new Bird();
|
||||
}
|
||||
|
||||
interface Animal {
|
||||
readonly name: string;
|
||||
talk(): string;
|
||||
}
|
||||
|
||||
class Dog implements Animal {
|
||||
name = "dog";
|
||||
|
||||
talk(): string {
|
||||
return "woof";
|
||||
}
|
||||
}
|
||||
|
||||
class Cat implements Animal {
|
||||
name = "cat";
|
||||
|
||||
talk(): string {
|
||||
return "meow";
|
||||
}
|
||||
}
|
||||
|
||||
class Bird implements Animal {
|
||||
name = "bird";
|
||||
|
||||
talk(): string {
|
||||
return "chirp";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
fetch(request) {
|
||||
return new Response(a());
|
||||
},
|
||||
};
|
||||
|
||||
function a() {
|
||||
return b();
|
||||
}
|
||||
|
||||
function b() {
|
||||
return c();
|
||||
}
|
||||
|
||||
function c() {
|
||||
function d() {
|
||||
return "hello";
|
||||
}
|
||||
return d();
|
||||
}
|
||||
62
packages/bun-debug-adapter-protocol/debugger/preview.test.ts
Normal file
62
packages/bun-debug-adapter-protocol/debugger/preview.test.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { beforeAll, afterAll, test, expect } from "bun:test";
|
||||
import type { JSC } from "../../bun-inspector-protocol";
|
||||
import { WebSocketInspector } from "../../bun-inspector-protocol";
|
||||
import type { PipedSubprocess } from "bun";
|
||||
import { spawn } from "bun";
|
||||
import { remoteObjectToString } from "./preview";
|
||||
|
||||
let subprocess: PipedSubprocess | undefined;
|
||||
let objects: JSC.Runtime.RemoteObject[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
subprocess = spawn({
|
||||
cwd: import.meta.dir,
|
||||
cmd: [process.argv0, "--inspect-wait=0", "fixtures/preview.js"],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
stdin: "pipe",
|
||||
});
|
||||
const decoder = new TextDecoder();
|
||||
let url: URL;
|
||||
for await (const chunk of subprocess!.stdout) {
|
||||
const text = decoder.decode(chunk);
|
||||
if (text.includes("ws://")) {
|
||||
url = new URL(/(ws:\/\/.*)/.exec(text)![0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
objects = await new Promise((resolve, reject) => {
|
||||
const inspector = new WebSocketInspector({
|
||||
url,
|
||||
listener: {
|
||||
["Inspector.connected"]: () => {
|
||||
inspector.send("Inspector.enable");
|
||||
inspector.send("Runtime.enable");
|
||||
inspector.send("Console.enable");
|
||||
inspector.send("Debugger.enable");
|
||||
inspector.send("Debugger.resume");
|
||||
inspector.send("Inspector.initialized");
|
||||
},
|
||||
["Inspector.disconnected"]: error => {
|
||||
reject(error);
|
||||
},
|
||||
["Console.messageAdded"]: ({ message }) => {
|
||||
const { parameters } = message;
|
||||
resolve(parameters!);
|
||||
inspector.close();
|
||||
},
|
||||
},
|
||||
});
|
||||
inspector.start();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
subprocess?.kill();
|
||||
});
|
||||
|
||||
test("remoteObjectToString", () => {
|
||||
for (const object of objects) {
|
||||
expect(remoteObjectToString(object)).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
110
packages/bun-debug-adapter-protocol/debugger/preview.ts
Normal file
110
packages/bun-debug-adapter-protocol/debugger/preview.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { JSC } from "../../bun-inspector-protocol";
|
||||
|
||||
export function remoteObjectToString(remoteObject: JSC.Runtime.RemoteObject): string {
|
||||
const { type, subtype, value, description, className, preview } = remoteObject;
|
||||
switch (type) {
|
||||
case "undefined":
|
||||
return "undefined";
|
||||
case "boolean":
|
||||
case "number":
|
||||
return description ?? JSON.stringify(value);
|
||||
case "string":
|
||||
return JSON.stringify(value ?? description);
|
||||
case "symbol":
|
||||
case "bigint":
|
||||
return description!;
|
||||
case "function":
|
||||
return description!.replace("function", "ƒ") || "ƒ";
|
||||
}
|
||||
switch (subtype) {
|
||||
case "null":
|
||||
return "null";
|
||||
case "regexp":
|
||||
case "date":
|
||||
case "error":
|
||||
return description!;
|
||||
}
|
||||
if (preview) {
|
||||
return objectPreviewToString(preview);
|
||||
}
|
||||
if (className) {
|
||||
return className;
|
||||
}
|
||||
return description || "Object";
|
||||
}
|
||||
|
||||
export function objectPreviewToString(objectPreview: JSC.Runtime.ObjectPreview): string {
|
||||
const { type, subtype, entries, properties, overflow, description, size } = objectPreview;
|
||||
if (type !== "object") {
|
||||
return remoteObjectToString(objectPreview);
|
||||
}
|
||||
let items: string[];
|
||||
if (entries) {
|
||||
items = entries.map(entryPreviewToString).sort();
|
||||
} else if (properties) {
|
||||
if (isIndexed(subtype)) {
|
||||
items = properties.map(indexedPropertyPreviewToString).sort();
|
||||
} else {
|
||||
items = properties.map(namedPropertyPreviewToString).sort();
|
||||
}
|
||||
} else {
|
||||
items = ["…"];
|
||||
}
|
||||
if (overflow) {
|
||||
items.push("…");
|
||||
}
|
||||
let label: string;
|
||||
if (description === "Object") {
|
||||
label = "";
|
||||
} else if (size === undefined) {
|
||||
label = description!;
|
||||
} else {
|
||||
label = `${description}(${size})`;
|
||||
}
|
||||
if (!items.length) {
|
||||
return label || "{}";
|
||||
}
|
||||
if (label) {
|
||||
label += " ";
|
||||
}
|
||||
if (isIndexed(subtype)) {
|
||||
return `${label}[${items.join(", ")}]`;
|
||||
}
|
||||
return `${label}{${items.join(", ")}}`;
|
||||
}
|
||||
|
||||
function propertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
|
||||
const { type, value, ...preview } = propertyPreview;
|
||||
if (type === "accessor") {
|
||||
return "ƒ";
|
||||
}
|
||||
return remoteObjectToString({ ...preview, type, description: value });
|
||||
}
|
||||
|
||||
function entryPreviewToString(entryPreview: JSC.Runtime.EntryPreview): string {
|
||||
const { key, value } = entryPreview;
|
||||
if (key) {
|
||||
return `${objectPreviewToString(key)} => ${objectPreviewToString(value)}`;
|
||||
}
|
||||
return objectPreviewToString(value);
|
||||
}
|
||||
|
||||
function namedPropertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
|
||||
const { name, valuePreview } = propertyPreview;
|
||||
if (valuePreview) {
|
||||
return `${name}: ${objectPreviewToString(valuePreview)}`;
|
||||
}
|
||||
return `${name}: ${propertyPreviewToString(propertyPreview)}`;
|
||||
}
|
||||
|
||||
function indexedPropertyPreviewToString(propertyPreview: JSC.Runtime.PropertyPreview): string {
|
||||
const { valuePreview } = propertyPreview;
|
||||
if (valuePreview) {
|
||||
return objectPreviewToString(valuePreview);
|
||||
}
|
||||
return propertyPreviewToString(propertyPreview);
|
||||
}
|
||||
|
||||
function isIndexed(type?: JSC.Runtime.RemoteObject["subtype"]): boolean {
|
||||
return type === "array" || type === "set" || type === "weakset";
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { test, expect } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { SourceMap } from "./sourcemap";
|
||||
|
||||
test("works without source map", () => {
|
||||
const sourceMap = getSourceMap("without-sourcemap.js");
|
||||
expect(sourceMap.generatedLocation({ line: 7 })).toEqual({ line: 7, column: 0, verified: true });
|
||||
expect(sourceMap.generatedLocation({ line: 7, column: 2 })).toEqual({ line: 7, column: 2, verified: true });
|
||||
expect(sourceMap.originalLocation({ line: 11 })).toEqual({ line: 11, column: 0, verified: true });
|
||||
expect(sourceMap.originalLocation({ line: 11, column: 2 })).toEqual({ line: 11, column: 2, verified: true });
|
||||
});
|
||||
|
||||
test("works with source map", () => {
|
||||
const sourceMap = getSourceMap("with-sourcemap.js");
|
||||
// FIXME: Columns don't appear to be accurate for `generatedLocation`
|
||||
expect(sourceMap.generatedLocation({ line: 3 })).toMatchObject({ line: 4, verified: true });
|
||||
expect(sourceMap.generatedLocation({ line: 27 })).toMatchObject({ line: 20, verified: true });
|
||||
expect(sourceMap.originalLocation({ line: 32 })).toEqual({ line: 43, column: 4, verified: true });
|
||||
expect(sourceMap.originalLocation({ line: 13 })).toEqual({ line: 13, column: 6, verified: true });
|
||||
});
|
||||
|
||||
function getSourceMap(filename: string): SourceMap {
|
||||
const { pathname } = new URL(`./fixtures/${filename}`, import.meta.url);
|
||||
const source = readFileSync(pathname, "utf-8");
|
||||
const match = source.match(/\/\/# sourceMappingURL=(.*)$/m);
|
||||
if (match) {
|
||||
const [, url] = match;
|
||||
return SourceMap(url);
|
||||
}
|
||||
return SourceMap();
|
||||
}
|
||||
187
packages/bun-debug-adapter-protocol/debugger/sourcemap.ts
Normal file
187
packages/bun-debug-adapter-protocol/debugger/sourcemap.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { LineRange, MappedPosition } from "source-map-js";
|
||||
import { SourceMapConsumer } from "source-map-js";
|
||||
|
||||
export type LocationRequest = {
|
||||
line?: number;
|
||||
column?: number;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type Location = {
|
||||
line: number; // 0-based
|
||||
column: number; // 0-based
|
||||
} & (
|
||||
| {
|
||||
verified: true;
|
||||
}
|
||||
| {
|
||||
verified?: false;
|
||||
message?: string;
|
||||
}
|
||||
);
|
||||
|
||||
export interface SourceMap {
|
||||
generatedLocation(request: LocationRequest): Location;
|
||||
originalLocation(request: LocationRequest): Location;
|
||||
}
|
||||
|
||||
class ActualSourceMap implements SourceMap {
|
||||
#sourceMap: SourceMapConsumer;
|
||||
#sources: string[];
|
||||
|
||||
constructor(sourceMap: SourceMapConsumer) {
|
||||
this.#sourceMap = sourceMap;
|
||||
this.#sources = (sourceMap as any)._absoluteSources;
|
||||
}
|
||||
|
||||
#getSource(url?: string): string {
|
||||
const sources = this.#sources;
|
||||
if (!sources.length) {
|
||||
return "";
|
||||
}
|
||||
if (sources.length === 1 || !url) {
|
||||
return sources[0];
|
||||
}
|
||||
for (const source of sources) {
|
||||
if (url.endsWith(source)) {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
generatedLocation(request: LocationRequest): Location {
|
||||
const { line, column, url } = request;
|
||||
let lineRange: LineRange;
|
||||
try {
|
||||
const source = this.#getSource(url);
|
||||
lineRange = this.#sourceMap.generatedPositionFor({
|
||||
line: lineTo1BasedLine(line),
|
||||
column: columnToColumn(column),
|
||||
source,
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: false,
|
||||
message: unknownToError(error),
|
||||
};
|
||||
}
|
||||
if (!locationIsValid(lineRange)) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: false,
|
||||
};
|
||||
}
|
||||
const { line: gline, column: gcolumn } = lineRange;
|
||||
return {
|
||||
line: lineToLine(gline),
|
||||
column: columnToColumn(gcolumn),
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
|
||||
originalLocation(request: LocationRequest): Location {
|
||||
const { line, column } = request;
|
||||
let mappedPosition: MappedPosition;
|
||||
try {
|
||||
mappedPosition = this.#sourceMap.originalPositionFor({
|
||||
line: lineTo1BasedLine(line),
|
||||
column: columnToColumn(column),
|
||||
});
|
||||
} catch (error) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: false,
|
||||
message: unknownToError(error),
|
||||
};
|
||||
}
|
||||
if (!locationIsValid(mappedPosition)) {
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: false,
|
||||
};
|
||||
}
|
||||
const { line: oline, column: ocolumn } = mappedPosition;
|
||||
return {
|
||||
line: lineTo0BasedLine(oline),
|
||||
column: columnToColumn(ocolumn),
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NoopSourceMap implements SourceMap {
|
||||
generatedLocation(request: LocationRequest): Location {
|
||||
const { line, column } = request;
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
|
||||
originalLocation(request: LocationRequest): Location {
|
||||
const { line, column } = request;
|
||||
return {
|
||||
line: lineToLine(line),
|
||||
column: columnToColumn(column),
|
||||
verified: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSourceMap = new NoopSourceMap();
|
||||
|
||||
export function SourceMap(url?: string): SourceMap {
|
||||
if (!url || !url.startsWith("data:")) {
|
||||
return defaultSourceMap;
|
||||
}
|
||||
try {
|
||||
const [_, base64] = url.split(",", 2);
|
||||
const decoded = Buffer.from(base64, "base64url").toString("utf8");
|
||||
const schema = JSON.parse(decoded);
|
||||
const sourceMap = new SourceMapConsumer(schema);
|
||||
return new ActualSourceMap(sourceMap);
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse source map URL", url);
|
||||
}
|
||||
return defaultSourceMap;
|
||||
}
|
||||
|
||||
function lineTo1BasedLine(line?: number): number {
|
||||
return numberIsValid(line) ? line + 1 : 1;
|
||||
}
|
||||
|
||||
function lineTo0BasedLine(line?: number): number {
|
||||
return numberIsValid(line) ? line - 1 : 0;
|
||||
}
|
||||
|
||||
function lineToLine(line?: number): number {
|
||||
return numberIsValid(line) ? line : 0;
|
||||
}
|
||||
|
||||
function columnToColumn(column?: number): number {
|
||||
return numberIsValid(column) ? column : 0;
|
||||
}
|
||||
|
||||
function locationIsValid(location: Location): location is Location {
|
||||
const { line, column } = location;
|
||||
return numberIsValid(line) && numberIsValid(column);
|
||||
}
|
||||
|
||||
function numberIsValid(number?: number): number is number {
|
||||
return typeof number === "number" && isFinite(number) && number >= 0;
|
||||
}
|
||||
|
||||
function unknownToError(error: unknown): string {
|
||||
if (error instanceof Error) {
|
||||
const { message } = error;
|
||||
return message;
|
||||
}
|
||||
return String(error);
|
||||
}
|
||||
2
packages/bun-debug-adapter-protocol/index.ts
Normal file
2
packages/bun-debug-adapter-protocol/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type * from "./protocol";
|
||||
export * from "./debugger/adapter";
|
||||
8
packages/bun-debug-adapter-protocol/package.json
Normal file
8
packages/bun-debug-adapter-protocol/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "bun-debug-adapter-protocol",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.4",
|
||||
"ws": "^8.13.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
}
|
||||
}
|
||||
2696
packages/bun-debug-adapter-protocol/protocol/index.d.ts
vendored
Normal file
2696
packages/bun-debug-adapter-protocol/protocol/index.d.ts
vendored
Normal file
File diff suppressed because it is too large
Load Diff
37
packages/bun-debug-adapter-protocol/protocol/schema.d.ts
vendored
Normal file
37
packages/bun-debug-adapter-protocol/protocol/schema.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
export type Protocol = {
|
||||
$schema: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: "object";
|
||||
definitions: Record<string, Type>;
|
||||
};
|
||||
|
||||
export type Type = {
|
||||
description?: string;
|
||||
} & (
|
||||
| {
|
||||
type: "number" | "integer" | "boolean";
|
||||
}
|
||||
| {
|
||||
type: "string";
|
||||
enum?: string[];
|
||||
enumDescriptions?: string[];
|
||||
}
|
||||
| {
|
||||
type: "object";
|
||||
properties?: Record<string, Type>;
|
||||
required?: string[];
|
||||
}
|
||||
| {
|
||||
type: "array";
|
||||
items?: Type;
|
||||
}
|
||||
| {
|
||||
type?: undefined;
|
||||
$ref: string;
|
||||
}
|
||||
| {
|
||||
type?: undefined;
|
||||
allOf: Type[];
|
||||
}
|
||||
);
|
||||
176
packages/bun-debug-adapter-protocol/scripts/generate-protocol.ts
Normal file
176
packages/bun-debug-adapter-protocol/scripts/generate-protocol.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import type { Protocol, Type } from "../protocol/schema.d.ts";
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const cwd = new URL("../protocol/", import.meta.url);
|
||||
const runner = "Bun" in globalThis ? "bunx" : "npx";
|
||||
const write = (name: string, data: string) => {
|
||||
const path = new URL(name, cwd);
|
||||
writeFileSync(path, data);
|
||||
spawnSync(runner, ["prettier", "--write", path.pathname], { cwd, stdio: "ignore" });
|
||||
};
|
||||
const schema: Protocol = await download(
|
||||
"https://microsoft.github.io/debug-adapter-protocol/debugAdapterProtocol.json",
|
||||
);
|
||||
write("protocol.json", JSON.stringify(schema));
|
||||
const types = formatProtocol(schema);
|
||||
write("index.d.ts", `// GENERATED - DO NOT EDIT\n${types}`);
|
||||
}
|
||||
|
||||
function formatProtocol(protocol: Protocol, extraTs?: string): string {
|
||||
const { definitions } = protocol;
|
||||
const requestMap = new Map();
|
||||
const responseMap = new Map();
|
||||
const eventMap = new Map();
|
||||
let body = `export namespace DAP {`;
|
||||
loop: for (const [key, definition] of Object.entries(definitions)) {
|
||||
if (/[a-z]+Request$/i.test(key)) {
|
||||
continue;
|
||||
}
|
||||
if (/[a-z]+Arguments$/i.test(key)) {
|
||||
const name = key.replace(/(Request)?Arguments$/, "");
|
||||
const requestName = `${name}Request`;
|
||||
requestMap.set(toMethod(name), requestName);
|
||||
body += formatType(definition, requestName);
|
||||
continue;
|
||||
}
|
||||
if ("allOf" in definition) {
|
||||
const { allOf } = definition;
|
||||
for (const type of allOf) {
|
||||
if (type.type !== "object") {
|
||||
continue;
|
||||
}
|
||||
const { description, properties = {} } = type;
|
||||
if (/[a-z]+Event$/i.test(key)) {
|
||||
const { event, body: type = {} } = properties;
|
||||
if (!event || !("enum" in event)) {
|
||||
continue;
|
||||
}
|
||||
const [eventKey] = event.enum ?? [];
|
||||
eventMap.set(eventKey, key);
|
||||
const eventType: Type = {
|
||||
type: "object",
|
||||
description,
|
||||
...type,
|
||||
};
|
||||
body += formatType(eventType, key);
|
||||
continue loop;
|
||||
}
|
||||
if (/[a-z]+Response$/i.test(key)) {
|
||||
const { body: type = {} } = properties;
|
||||
const bodyType: Type = {
|
||||
type: "object",
|
||||
description,
|
||||
...type,
|
||||
};
|
||||
const name = key.replace(/Response$/, "");
|
||||
responseMap.set(toMethod(name), key);
|
||||
body += formatType(bodyType, key);
|
||||
continue loop;
|
||||
}
|
||||
}
|
||||
}
|
||||
body += formatType(definition, key);
|
||||
}
|
||||
for (const [key, name] of responseMap) {
|
||||
if (requestMap.has(key)) {
|
||||
continue;
|
||||
}
|
||||
const requestName = `${name.replace(/Response$/, "")}Request`;
|
||||
requestMap.set(key, requestName);
|
||||
body += formatType({ type: "object", properties: {} }, requestName);
|
||||
}
|
||||
body += formatMapType("RequestMap", requestMap);
|
||||
body += formatMapType("ResponseMap", responseMap);
|
||||
body += formatMapType("EventMap", eventMap);
|
||||
if (extraTs) {
|
||||
body += extraTs;
|
||||
}
|
||||
return body + "};";
|
||||
}
|
||||
|
||||
function formatMapType(key: string, typeMap: Map<string, string>): string {
|
||||
const type: Type = {
|
||||
type: "object",
|
||||
required: [...typeMap.keys()],
|
||||
properties: Object.fromEntries([...typeMap.entries()].map(([key, value]) => [key, { $ref: value }])),
|
||||
};
|
||||
return formatType(type, key);
|
||||
}
|
||||
|
||||
function formatType(type: Type, key?: string): string {
|
||||
const { description, type: kind } = type;
|
||||
let body = "";
|
||||
if (key) {
|
||||
if (description) {
|
||||
body += `\n${toComment(description)}\n`;
|
||||
}
|
||||
body += `export type ${key}=`;
|
||||
}
|
||||
if (kind === "boolean") {
|
||||
body += "boolean";
|
||||
} else if (kind === "number" || kind === "integer") {
|
||||
body += "number";
|
||||
} else if (kind === "string") {
|
||||
const { enum: choices } = type;
|
||||
if (choices) {
|
||||
body += choices.map(value => `"${value}"`).join("|");
|
||||
} else {
|
||||
body += "string";
|
||||
}
|
||||
} else if (kind === "array") {
|
||||
const { items } = type;
|
||||
const itemType = items ? formatType(items) : "unknown";
|
||||
body += `${itemType}[]`;
|
||||
} else if (kind === "object") {
|
||||
const { properties, required } = type;
|
||||
if (!properties || Object.keys(properties).length === 0) {
|
||||
body += "{}";
|
||||
} else {
|
||||
body += "{";
|
||||
for (const [key, { description, ...type }] of Object.entries(properties)) {
|
||||
if (description) {
|
||||
body += `\n${toComment(description)}`;
|
||||
}
|
||||
const delimit = required?.includes(key) ? ":" : "?:";
|
||||
body += `\n${key}${delimit}${formatType(type)};`;
|
||||
}
|
||||
body += "}";
|
||||
}
|
||||
} else if ("$ref" in type) {
|
||||
const { $ref: ref } = type;
|
||||
body += ref.split("/").pop() || "unknown";
|
||||
} else if ("allOf" in type) {
|
||||
const { allOf } = type;
|
||||
body += allOf.map(type => formatType(type)).join("&");
|
||||
} else {
|
||||
body += "unknown";
|
||||
}
|
||||
if (key) {
|
||||
body += ";";
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
function toMethod(name: string): string {
|
||||
return `${name.substring(0, 1).toLowerCase()}${name.substring(1)}`;
|
||||
}
|
||||
|
||||
function toComment(description?: string): string {
|
||||
if (!description) {
|
||||
return "";
|
||||
}
|
||||
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
async function download<T>(url: string | URL): Promise<T> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to download ${url}: ${response.statusText}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
@@ -3,19 +3,20 @@
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleDetection": "force",
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"composite": true,
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"jsx": "preserve",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"types": [
|
||||
"bun-types" // add Bun global
|
||||
]
|
||||
}
|
||||
"types": ["bun-types"],
|
||||
"outDir": "dist",
|
||||
},
|
||||
"include": [".", "../bun-types/index.d.ts", "../bun-inspector-protocol/index"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "bun-ecosystem-ci",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"globby": "^13.1.3"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# web-inspector-bun
|
||||
# bun-devtools-frontend
|
||||
|
||||
This is the WebKit Web Inspector bundled as standalone assets.
|
||||
|
||||
20
packages/bun-inspector-frontend/tsconfig.json
Normal file
20
packages/bun-inspector-frontend/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"noImplicitAny": false,
|
||||
"outDir": "dist",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [".", "../bun-types/index.d.ts"]
|
||||
}
|
||||
2
packages/bun-inspector-protocol/.gitattributes
vendored
Normal file
2
packages/bun-inspector-protocol/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
protocol/*/protocol.json linguist-generated=true
|
||||
protocol/*/index.d.ts linguist-generated=true
|
||||
2
packages/bun-inspector-protocol/.gitignore
vendored
Normal file
2
packages/bun-inspector-protocol/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
protocol/*.json
|
||||
protocol/v8
|
||||
1
packages/bun-inspector-protocol/README.md
Normal file
1
packages/bun-inspector-protocol/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# bun-inspector-protocol
|
||||
BIN
packages/bun-inspector-protocol/bun.lockb
Executable file
BIN
packages/bun-inspector-protocol/bun.lockb
Executable file
Binary file not shown.
3
packages/bun-inspector-protocol/index.ts
Normal file
3
packages/bun-inspector-protocol/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export type * from "./protocol";
|
||||
export type * from "./inspector";
|
||||
export * from "./inspector/websocket";
|
||||
@@ -0,0 +1,7 @@
|
||||
export default {
|
||||
fetch(request) {
|
||||
console.log(request);
|
||||
debugger;
|
||||
return new Response();
|
||||
},
|
||||
};
|
||||
49
packages/bun-inspector-protocol/inspector/index.d.ts
vendored
Normal file
49
packages/bun-inspector-protocol/inspector/index.d.ts
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { JSC } from "..";
|
||||
|
||||
/**
|
||||
* A client that can send and receive messages to/from a debugger.
|
||||
*/
|
||||
export abstract class Inspector {
|
||||
constructor(listener?: InspectorListener);
|
||||
/**
|
||||
* Starts the inspector.
|
||||
*/
|
||||
start(...args: unknown[]): void;
|
||||
/**
|
||||
* Sends a request to the debugger.
|
||||
*/
|
||||
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
|
||||
method: M,
|
||||
params?: JSC.RequestMap[M],
|
||||
): Promise<JSC.ResponseMap[M]>;
|
||||
/**
|
||||
* Accepts a message from the debugger.
|
||||
* @param message the unparsed message from the debugger
|
||||
*/
|
||||
accept(message: string): void;
|
||||
/**
|
||||
* If the inspector is closed.
|
||||
*/
|
||||
get closed(): boolean;
|
||||
/**
|
||||
* Closes the inspector.
|
||||
*/
|
||||
close(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
export type InspectorListener = {
|
||||
/**
|
||||
* Defines a handler when a debugger event is received.
|
||||
*/
|
||||
[M in keyof JSC.EventMap]?: (event: JSC.EventMap[M]) => void;
|
||||
} & {
|
||||
/**
|
||||
* Defines a handler when the debugger is connected or reconnected.
|
||||
*/
|
||||
["Inspector.connected"]?: () => void;
|
||||
/**
|
||||
* Defines a handler when the debugger is disconnected.
|
||||
* @param error the error that caused the disconnect, if any
|
||||
*/
|
||||
["Inspector.disconnected"]?: (error?: Error) => void;
|
||||
};
|
||||
83
packages/bun-inspector-protocol/inspector/websocket.test.ts
Normal file
83
packages/bun-inspector-protocol/inspector/websocket.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { afterAll, beforeAll, mock, test, expect } from "bun:test";
|
||||
import type { JSC } from "..";
|
||||
import type { InspectorListener } from ".";
|
||||
import { WebSocketInspector } from "./websocket";
|
||||
import { sleep, spawn } from "bun";
|
||||
|
||||
let inspectee: any;
|
||||
let url: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { pathname } = new URL("fixtures/inspectee.js", import.meta.url);
|
||||
inspectee = spawn({
|
||||
cmd: [process.argv0, "--inspect", pathname],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
url = await new Promise(async resolve => {
|
||||
for await (const chunk of inspectee.stdout) {
|
||||
const text = new TextDecoder().decode(chunk);
|
||||
const match = /(wss?:\/\/.*:[0-9]+\/.*)/.exec(text);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
const [_, url] = match;
|
||||
resolve(url);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
inspectee?.kill();
|
||||
});
|
||||
|
||||
test(
|
||||
"WebSocketInspector",
|
||||
async () => {
|
||||
const listener: InspectorListener = {
|
||||
["Inspector.connected"]: mock((...args) => {
|
||||
expect(args).toBeEmpty();
|
||||
}),
|
||||
["Inspector.disconnected"]: mock((error?: Error) => {
|
||||
expect(error).toBeUndefined();
|
||||
}),
|
||||
["Debugger.scriptParsed"]: mock((event: JSC.Debugger.ScriptParsedEvent) => {
|
||||
expect(event).toMatchObject({
|
||||
endColumn: expect.any(Number),
|
||||
endLine: expect.any(Number),
|
||||
isContentScript: expect.any(Boolean),
|
||||
module: expect.any(Boolean),
|
||||
scriptId: expect.any(String),
|
||||
startColumn: expect.any(Number),
|
||||
startLine: expect.any(Number),
|
||||
url: expect.any(String),
|
||||
});
|
||||
}),
|
||||
};
|
||||
const inspector = new WebSocketInspector({
|
||||
url,
|
||||
listener,
|
||||
});
|
||||
inspector.start();
|
||||
inspector.send("Runtime.enable");
|
||||
inspector.send("Debugger.enable");
|
||||
//expect(inspector.send("Runtime.enable")).resolves.toBeEmpty();
|
||||
//expect(inspector.send("Debugger.enable")).resolves.toBeEmpty();
|
||||
expect(inspector.send("Runtime.evaluate", { expression: "1 + 1" })).resolves.toMatchObject({
|
||||
result: {
|
||||
type: "number",
|
||||
value: 2,
|
||||
description: "2",
|
||||
},
|
||||
wasThrown: false,
|
||||
});
|
||||
expect(listener["Inspector.connected"]).toHaveBeenCalled();
|
||||
expect(listener["Debugger.scriptParsed"]).toHaveBeenCalled();
|
||||
inspector.close();
|
||||
expect(inspector.closed).toBeTrue();
|
||||
expect(listener["Inspector.disconnected"]).toHaveBeenCalled();
|
||||
},
|
||||
{
|
||||
timeout: 100000,
|
||||
},
|
||||
);
|
||||
196
packages/bun-inspector-protocol/inspector/websocket.ts
Normal file
196
packages/bun-inspector-protocol/inspector/websocket.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import type { Inspector, InspectorListener } from ".";
|
||||
import { JSC } from "..";
|
||||
import { WebSocket } from "ws";
|
||||
|
||||
export type WebSocketInspectorOptions = {
|
||||
url?: string | URL;
|
||||
listener?: InspectorListener;
|
||||
};
|
||||
|
||||
/**
|
||||
* An inspector that communicates with a debugger over a WebSocket.
|
||||
*/
|
||||
export class WebSocketInspector implements Inspector {
|
||||
#url?: URL;
|
||||
#webSocket?: WebSocket;
|
||||
#requestId: number;
|
||||
#pendingRequests: Map<number, (result: unknown) => void>;
|
||||
#pendingMessages: string[];
|
||||
#listener: InspectorListener;
|
||||
|
||||
constructor({ url, listener }: WebSocketInspectorOptions) {
|
||||
this.#url = url ? new URL(url) : undefined;
|
||||
this.#listener = listener ?? {};
|
||||
this.#requestId = 1;
|
||||
this.#pendingRequests = new Map();
|
||||
this.#pendingMessages = [];
|
||||
}
|
||||
|
||||
start(url?: string | URL): void {
|
||||
if (url) {
|
||||
this.#url = new URL(url);
|
||||
}
|
||||
if (this.#url) {
|
||||
this.#connect();
|
||||
}
|
||||
}
|
||||
|
||||
#connect(): void {
|
||||
if (!this.#url) {
|
||||
return;
|
||||
}
|
||||
this.#webSocket?.close();
|
||||
let webSocket: WebSocket;
|
||||
try {
|
||||
console.log("[jsc] connecting", this.#url.href);
|
||||
webSocket = new WebSocket(this.#url, {
|
||||
headers: {
|
||||
"Ref-Event-Loop": "0",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.#close(unknownToError(error));
|
||||
return;
|
||||
}
|
||||
webSocket.addEventListener("open", () => {
|
||||
console.log("[jsc] connected");
|
||||
for (const message of this.#pendingMessages) {
|
||||
this.#send(message);
|
||||
}
|
||||
this.#pendingMessages.length = 0;
|
||||
this.#listener["Inspector.connected"]?.();
|
||||
});
|
||||
webSocket.addEventListener("message", ({ data }) => {
|
||||
if (typeof data === "string") {
|
||||
this.accept(data);
|
||||
}
|
||||
});
|
||||
webSocket.addEventListener("error", event => {
|
||||
console.log("[jsc] error", event);
|
||||
this.#close(unknownToError(event));
|
||||
});
|
||||
webSocket.addEventListener("unexpected-response", () => {
|
||||
console.log("[jsc] unexpected-response");
|
||||
this.#close(new Error("WebSocket upgrade failed"));
|
||||
});
|
||||
webSocket.addEventListener("close", ({ code, reason }) => {
|
||||
console.log("[jsc] closed", code, reason);
|
||||
if (code === 1001) {
|
||||
this.#close();
|
||||
} else {
|
||||
this.#close(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
|
||||
}
|
||||
});
|
||||
this.#webSocket = webSocket;
|
||||
}
|
||||
|
||||
send<M extends keyof JSC.RequestMap & keyof JSC.ResponseMap>(
|
||||
method: M,
|
||||
params?: JSC.RequestMap[M] | undefined,
|
||||
): Promise<JSC.ResponseMap[M]> {
|
||||
const id = this.#requestId++;
|
||||
const request = { id, method, params };
|
||||
console.log("[jsc] -->", request);
|
||||
return new Promise((resolve, reject) => {
|
||||
const done = (result: any) => {
|
||||
this.#pendingRequests.delete(id);
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
this.#pendingRequests.set(id, done);
|
||||
this.#send(JSON.stringify(request));
|
||||
});
|
||||
}
|
||||
|
||||
#send(message: string): void {
|
||||
if (this.#webSocket) {
|
||||
const { readyState } = this.#webSocket!;
|
||||
if (readyState === WebSocket.OPEN) {
|
||||
this.#webSocket.send(message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!this.#pendingMessages.includes(message)) {
|
||||
this.#pendingMessages.push(message);
|
||||
}
|
||||
}
|
||||
|
||||
accept(message: string): void {
|
||||
let event: JSC.Event | JSC.Response;
|
||||
try {
|
||||
event = JSON.parse(message);
|
||||
} catch (error) {
|
||||
console.error("Failed to parse message:", message);
|
||||
return;
|
||||
}
|
||||
console.log("[jsc] <--", event);
|
||||
if ("id" in event) {
|
||||
const { id } = event;
|
||||
const resolve = this.#pendingRequests.get(id);
|
||||
if (!resolve) {
|
||||
console.error(`Failed to accept response for unknown ID ${id}:`, event);
|
||||
return;
|
||||
}
|
||||
this.#pendingRequests.delete(id);
|
||||
if ("error" in event) {
|
||||
const { error } = event;
|
||||
const { message } = error;
|
||||
resolve(new Error(message));
|
||||
} else {
|
||||
const { result } = event;
|
||||
resolve(result);
|
||||
}
|
||||
} else {
|
||||
const { method, params } = event;
|
||||
try {
|
||||
// @ts-ignore
|
||||
this.#listener[method]?.(params);
|
||||
} catch (error) {
|
||||
console.error(`Failed to accept ${method} event:`, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get closed(): boolean {
|
||||
if (!this.#webSocket) {
|
||||
return true;
|
||||
}
|
||||
const { readyState } = this.#webSocket;
|
||||
switch (readyState) {
|
||||
case WebSocket.CLOSED:
|
||||
case WebSocket.CLOSING:
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
close(code?: number, reason?: string): void {
|
||||
this.#webSocket?.close(code ?? 1001, reason);
|
||||
}
|
||||
|
||||
#close(error?: Error): void {
|
||||
try {
|
||||
this.#listener["Inspector.disconnected"]?.(error);
|
||||
} finally {
|
||||
for (const resolve of this.#pendingRequests.values()) {
|
||||
resolve(error ?? new Error("WebSocket closed"));
|
||||
}
|
||||
this.#pendingRequests.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unknownToError(input: unknown): Error {
|
||||
if (input instanceof Error) {
|
||||
return input;
|
||||
}
|
||||
if (typeof input === "object" && input !== null && "message" in input) {
|
||||
const { message } = input;
|
||||
return new Error(`${message}`);
|
||||
}
|
||||
return new Error(`${input}`);
|
||||
}
|
||||
7
packages/bun-inspector-protocol/package.json
Normal file
7
packages/bun-inspector-protocol/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "bun-inspector-protocol",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
}
|
||||
1
packages/bun-inspector-protocol/protocol/index.d.ts
vendored
Normal file
1
packages/bun-inspector-protocol/protocol/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
export type { JSC } from "./jsc";
|
||||
3695
packages/bun-inspector-protocol/protocol/jsc/index.d.ts
generated
vendored
Normal file
3695
packages/bun-inspector-protocol/protocol/jsc/index.d.ts
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3114
packages/bun-inspector-protocol/protocol/jsc/protocol.json
generated
Normal file
3114
packages/bun-inspector-protocol/protocol/jsc/protocol.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
packages/bun-inspector-protocol/protocol/protocol.d.ts
vendored
Normal file
28
packages/bun-inspector-protocol/protocol/protocol.d.ts
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
// @ts-nocheck
|
||||
// The content of this file is included in each generated protocol file.
|
||||
|
||||
export type Event<T extends keyof EventMap = keyof EventMap> = {
|
||||
readonly method: T;
|
||||
readonly params: EventMap[T];
|
||||
};
|
||||
|
||||
export type Request<T extends keyof RequestMap = keyof RequestMap> = {
|
||||
readonly id: number;
|
||||
readonly method: T;
|
||||
readonly params: RequestMap[T];
|
||||
};
|
||||
|
||||
export type Response<T extends keyof ResponseMap = keyof ResponseMap> = {
|
||||
readonly id: number;
|
||||
} & (
|
||||
| {
|
||||
readonly method?: T;
|
||||
readonly result: ResponseMap[T];
|
||||
}
|
||||
| {
|
||||
readonly error: {
|
||||
readonly code?: string;
|
||||
readonly message: string;
|
||||
};
|
||||
}
|
||||
);
|
||||
58
packages/bun-inspector-protocol/protocol/schema.d.ts
vendored
Normal file
58
packages/bun-inspector-protocol/protocol/schema.d.ts
vendored
Normal file
@@ -0,0 +1,58 @@
|
||||
// Represents the schema of the protocol.json file.
|
||||
|
||||
export type Protocol = {
|
||||
readonly name: string;
|
||||
readonly version: {
|
||||
readonly major: number;
|
||||
readonly minor: number;
|
||||
};
|
||||
readonly domains: readonly Domain[];
|
||||
};
|
||||
|
||||
export type Domain = {
|
||||
readonly domain: string;
|
||||
readonly dependencies?: readonly string[];
|
||||
readonly types: readonly Property[];
|
||||
readonly commands?: readonly Command[];
|
||||
readonly events?: readonly Event[];
|
||||
};
|
||||
|
||||
export type Command = {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly parameters?: readonly Property[];
|
||||
readonly returns?: readonly Property[];
|
||||
};
|
||||
|
||||
export type Event = {
|
||||
readonly name: string;
|
||||
readonly description?: string;
|
||||
readonly parameters: readonly Property[];
|
||||
};
|
||||
|
||||
export type Property = {
|
||||
readonly id?: string;
|
||||
readonly name?: string;
|
||||
readonly description?: string;
|
||||
readonly optional?: boolean;
|
||||
} & (
|
||||
| {
|
||||
readonly type: "array";
|
||||
readonly items?: Property;
|
||||
}
|
||||
| {
|
||||
readonly type: "object";
|
||||
readonly properties?: readonly Property[];
|
||||
}
|
||||
| {
|
||||
readonly type: "string";
|
||||
readonly enum?: readonly string[];
|
||||
}
|
||||
| {
|
||||
readonly type: "boolean" | "number" | "integer";
|
||||
}
|
||||
| {
|
||||
readonly type: undefined;
|
||||
readonly $ref: string;
|
||||
}
|
||||
);
|
||||
202
packages/bun-inspector-protocol/scripts/generate-protocol.ts
Normal file
202
packages/bun-inspector-protocol/scripts/generate-protocol.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import type { Protocol, Domain, Property } from "../protocol/schema";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
run().catch(console.error);
|
||||
|
||||
async function run() {
|
||||
const cwd = new URL("../protocol/", import.meta.url);
|
||||
const runner = "Bun" in globalThis ? "bunx" : "npx";
|
||||
const write = (name: string, data: string) => {
|
||||
const path = new URL(name, cwd);
|
||||
writeFileSync(path, data);
|
||||
spawnSync(runner, ["prettier", "--write", path.pathname], { cwd, stdio: "ignore" });
|
||||
};
|
||||
const base = readFileSync(new URL("protocol.d.ts", cwd), "utf-8");
|
||||
const baseNoComments = base.replace(/\/\/.*/g, "");
|
||||
const jsc = await downloadJsc();
|
||||
write("jsc/protocol.json", JSON.stringify(jsc));
|
||||
write("jsc/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(jsc, baseNoComments));
|
||||
const v8 = await downloadV8();
|
||||
write("v8/protocol.json", JSON.stringify(v8));
|
||||
write("v8/index.d.ts", "// GENERATED - DO NOT EDIT\n" + formatProtocol(v8, baseNoComments));
|
||||
}
|
||||
|
||||
function formatProtocol(protocol: Protocol, extraTs?: string): string {
|
||||
const { name, domains } = protocol;
|
||||
const eventMap = new Map();
|
||||
const commandMap = new Map();
|
||||
let body = `export namespace ${name} {`;
|
||||
for (const { domain, types = [], events = [], commands = [] } of domains) {
|
||||
body += `export namespace ${domain} {`;
|
||||
for (const type of types) {
|
||||
body += formatProperty(type);
|
||||
}
|
||||
for (const { name, description, parameters = [] } of events) {
|
||||
const symbol = `${domain}.${name}`;
|
||||
const title = toTitle(name);
|
||||
eventMap.set(symbol, `${domain}.${title}`);
|
||||
body += formatProperty({
|
||||
id: `${title}Event`,
|
||||
type: "object",
|
||||
description: `${description}\n@event \`${symbol}\``,
|
||||
properties: parameters,
|
||||
});
|
||||
}
|
||||
for (const { name, description, parameters = [], returns = [] } of commands) {
|
||||
const symbol = `${domain}.${name}`;
|
||||
const title = toTitle(name);
|
||||
commandMap.set(symbol, `${domain}.${title}`);
|
||||
body += formatProperty({
|
||||
id: `${title}Request`,
|
||||
type: "object",
|
||||
description: `${description}\n@request \`${symbol}\``,
|
||||
properties: parameters,
|
||||
});
|
||||
body += formatProperty({
|
||||
id: `${title}Response`,
|
||||
type: "object",
|
||||
description: `${description}\n@response \`${symbol}\``,
|
||||
properties: returns,
|
||||
});
|
||||
}
|
||||
body += "};";
|
||||
}
|
||||
for (const type of ["Event", "Request", "Response"]) {
|
||||
const sourceMap = type === "Event" ? eventMap : commandMap;
|
||||
body += formatProperty({
|
||||
id: `${type}Map`,
|
||||
type: "object",
|
||||
properties: [...sourceMap.entries()].map(([name, title]) => ({
|
||||
name: `"${name}"`,
|
||||
type: undefined,
|
||||
$ref: `${title}${type}`,
|
||||
})),
|
||||
});
|
||||
}
|
||||
if (extraTs) {
|
||||
body += extraTs;
|
||||
}
|
||||
return body + "};";
|
||||
}
|
||||
|
||||
function formatProperty(property: Property): string {
|
||||
const { id, description, type, optional } = property;
|
||||
let body = "";
|
||||
if (id) {
|
||||
if (description) {
|
||||
body += `\n${toComment(description)}\n`;
|
||||
}
|
||||
body += `export type ${id}=`;
|
||||
}
|
||||
if (type === "boolean") {
|
||||
body += "boolean";
|
||||
} else if (type === "number" || type === "integer") {
|
||||
body += "number";
|
||||
} else if (type === "string") {
|
||||
const { enum: choices } = property;
|
||||
if (choices) {
|
||||
body += choices.map(value => `"${value}"`).join("|");
|
||||
} else {
|
||||
body += "string";
|
||||
}
|
||||
} else if (type === "array") {
|
||||
const { items } = property;
|
||||
const itemType = items ? formatProperty(items) : "unknown";
|
||||
body += `${itemType}[]`;
|
||||
} else if (type === "object") {
|
||||
const { properties } = property;
|
||||
if (!properties) {
|
||||
body += "Record<string, unknown>";
|
||||
} else if (properties.length === 0) {
|
||||
body += "{}";
|
||||
} else {
|
||||
body += "{";
|
||||
for (const { name, description, ...property } of properties) {
|
||||
if (description) {
|
||||
body += `\n${toComment(description)}`;
|
||||
}
|
||||
const delimit = property.optional ? "?:" : ":";
|
||||
body += `\n${name}${delimit}${formatProperty({ ...property, id: undefined })};`;
|
||||
}
|
||||
body += "}";
|
||||
}
|
||||
} else if ("$ref" in property) {
|
||||
body += property.$ref;
|
||||
} else {
|
||||
body += "unknown";
|
||||
}
|
||||
if (optional) {
|
||||
body += "|undefined";
|
||||
}
|
||||
if (id) {
|
||||
body += ";";
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://github.com/ChromeDevTools/devtools-protocol/tree/master/json
|
||||
*/
|
||||
async function downloadV8(): Promise<Protocol> {
|
||||
const baseUrl = "https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json";
|
||||
const domains = ["Runtime", "Console", "Debugger", "Memory", "HeapProfiler", "Profiler", "Network", "Inspector"];
|
||||
return Promise.all([
|
||||
download<Protocol>(`${baseUrl}/js_protocol.json`),
|
||||
download<Protocol>(`${baseUrl}/browser_protocol.json`),
|
||||
]).then(([js, browser]) => ({
|
||||
name: "V8",
|
||||
version: js.version,
|
||||
domains: [...js.domains, ...browser.domains]
|
||||
.filter(domain => !domains.includes(domain.domain))
|
||||
.sort((a, b) => a.domain.localeCompare(b.domain)),
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @link https://github.com/WebKit/WebKit/tree/main/Source/JavaScriptCore/inspector/protocol
|
||||
*/
|
||||
async function downloadJsc(): Promise<Protocol> {
|
||||
const baseUrl = "https://raw.githubusercontent.com/WebKit/WebKit/main/Source/JavaScriptCore/inspector/protocol";
|
||||
const domains = [
|
||||
"Runtime",
|
||||
"Console",
|
||||
"Debugger",
|
||||
"Heap",
|
||||
"ScriptProfiler",
|
||||
"CPUProfiler",
|
||||
"GenericTypes",
|
||||
"Network",
|
||||
"Inspector",
|
||||
];
|
||||
return {
|
||||
name: "JSC",
|
||||
version: {
|
||||
major: 1,
|
||||
minor: 3,
|
||||
},
|
||||
domains: await Promise.all(domains.map(domain => download<Domain>(`${baseUrl}/${domain}.json`))).then(domains =>
|
||||
domains.sort((a, b) => a.domain.localeCompare(b.domain)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function download<V>(url: string): Promise<V> {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}: ${url}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function toTitle(name: string): string {
|
||||
return name.charAt(0).toUpperCase() + name.slice(1);
|
||||
}
|
||||
|
||||
function toComment(description?: string): string {
|
||||
if (!description) {
|
||||
return "";
|
||||
}
|
||||
const lines = ["/**", ...description.split("\n").map(line => ` * ${line.trim()}`), "*/"];
|
||||
return lines.join("\n");
|
||||
}
|
||||
19
packages/bun-inspector-protocol/tsconfig.json
Normal file
19
packages/bun-inspector-protocol/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["ESNext"],
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleDetection": "force",
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"skipLibCheck": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSourceMap": true,
|
||||
"allowJs": true,
|
||||
"outDir": "dist",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [".", "../bun-types/index.d.ts"]
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"name": "bun-lambda",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"bun-types": "^0.7.0",
|
||||
"jszip": "^3.10.1",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"name": "bun-release-action",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"aws4fetch": "^1.0.17",
|
||||
|
||||
2
packages/bun-vscode/.gitignore
vendored
Normal file
2
packages/bun-vscode/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
extension
|
||||
5
packages/bun-vscode/.vscode/launch.json
vendored
5
packages/bun-vscode/.vscode/launch.json
vendored
@@ -5,10 +5,7 @@
|
||||
"name": "Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}/example"
|
||||
],
|
||||
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/example"],
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"preLaunchTask": "Build (watch)"
|
||||
},
|
||||
|
||||
2
packages/bun-vscode/.vscode/settings.json
vendored
2
packages/bun-vscode/.vscode/settings.json
vendored
@@ -5,5 +5,5 @@
|
||||
"search.exclude": {
|
||||
"out": true // set this to false to include "out" folder in search results
|
||||
},
|
||||
"typescript.tsc.autoDetect": "off",
|
||||
"typescript.tsc.autoDetect": "off"
|
||||
}
|
||||
6
packages/bun-vscode/.vscode/tasks.json
vendored
6
packages/bun-vscode/.vscode/tasks.json
vendored
@@ -4,13 +4,15 @@
|
||||
{
|
||||
"label": "Build",
|
||||
"type": "shell",
|
||||
"command": "bun run build"
|
||||
"command": "bun run build",
|
||||
"problemMatcher": "$esbuild"
|
||||
},
|
||||
{
|
||||
"label": "Build (watch)",
|
||||
"type": "shell",
|
||||
"command": "bun run build:watch",
|
||||
"isBackground": true
|
||||
"isBackground": true,
|
||||
"problemMatcher": "$esbuild-watch"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1 +1,22 @@
|
||||
# Debug Adapter Protocol for Bun
|
||||
# Bun for Visual Studio Code
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
<img align="right" src="https://user-images.githubusercontent.com/709451/182802334-d9c42afe-f35d-4a7b-86ea-9985f73f20c3.png" height="150px" style="float: right; padding: 30px;">
|
||||
|
||||
This extension adds support for using [Bun](https://bun.sh/) with Visual Studio Code. Bun is an all-in-one toolkit for JavaScript and TypeScript apps.
|
||||
|
||||
At its core is the _Bun runtime_, a fast JavaScript runtime designed as a drop-in replacement for Node.js. It's written in Zig and powered by JavaScriptCore under the hood, dramatically reducing startup times and memory usage.
|
||||
|
||||
<div align="center">
|
||||
<a href="https://bun.sh/docs">Documentation</a>
|
||||
<span> • </span>
|
||||
<a href="https://discord.com/invite/CXdq2DP29u">Discord</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/oven-sh/bun/issues/new">Issues</a>
|
||||
<span> • </span>
|
||||
<a href="https://github.com/oven-sh/bun/issues/159">Roadmap</a>
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
* Off-by-one for debug lines
|
||||
* Formatting values in console (some code is wired up)
|
||||
* Play button on debugger actually starting Bun
|
||||
* bun debug or --inspect command added to Bun, not need Bun.serve
|
||||
* Breakpoint actually setting
|
||||
Binary file not shown.
11
packages/bun-vscode/example/.vscode/launch.json
vendored
11
packages/bun-vscode/example/.vscode/launch.json
vendored
@@ -4,16 +4,15 @@
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceFolder}/example.js",
|
||||
"stopOnEntry": true
|
||||
"name": "Debug Bun",
|
||||
"program": "${file}",
|
||||
"watch": "hot"
|
||||
},
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Attach",
|
||||
"program": "${workspaceFolder}/example.js",
|
||||
"stopOnEntry": true
|
||||
"name": "Attach to Bun",
|
||||
"url": "ws://localhost:6499/",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
30
packages/bun-vscode/example/example-sourcemap.js
Normal file
30
packages/bun-vscode/example/example-sourcemap.js
Normal file
@@ -0,0 +1,30 @@
|
||||
// @bun
|
||||
// example.ts
|
||||
var a = function (request) {
|
||||
b(request);
|
||||
};
|
||||
var b = function (request) {
|
||||
c(request);
|
||||
};
|
||||
var c = function (request) {
|
||||
console.log(request);
|
||||
};
|
||||
var example_default = {
|
||||
async fetch(request, server) {
|
||||
a(request);
|
||||
const coolThing = new SuperCoolThing();
|
||||
coolThing.doCoolThing();
|
||||
debugger;
|
||||
return new Response(request.url);
|
||||
},
|
||||
};
|
||||
|
||||
class SuperCoolThing {
|
||||
doCoolThing() {
|
||||
console.log("super cool thing!");
|
||||
}
|
||||
}
|
||||
export { example_default as default };
|
||||
|
||||
//# debugId=9BB0B773A8E4771564756e2164756e21
|
||||
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiZXhhbXBsZS50cyJdLAogICJzb3VyY2VzQ29udGVudCI6IFsKICAgICJpbXBvcnQgdHlwZSB7IFNlcnZlciB9IGZyb20gXCJidW5cIjtcblxuZXhwb3J0IGRlZmF1bHQge1xuICBhc3luYyBmZXRjaChyZXF1ZXN0OiBSZXF1ZXN0LCBzZXJ2ZXI6IFNlcnZlcik6IFByb21pc2U8UmVzcG9uc2U+IHtcbiAgICBhKHJlcXVlc3QpO1xuICAgIGNvbnN0IGNvb2xUaGluZzogQ29vbFRoaW5nID0gbmV3IFN1cGVyQ29vbFRoaW5nKCk7XG4gICAgY29vbFRoaW5nLmRvQ29vbFRoaW5nKCk7XG4gICAgZGVidWdnZXI7XG4gICAgcmV0dXJuIG5ldyBSZXNwb25zZShyZXF1ZXN0LnVybCk7XG4gIH1cbn07XG5cbi8vIGFcbmZ1bmN0aW9uIGEocmVxdWVzdDogUmVxdWVzdCk6IHZvaWQge1xuICBiKHJlcXVlc3QpO1xufVxuXG4vLyBiXG5mdW5jdGlvbiBiKHJlcXVlc3Q6IFJlcXVlc3QpOiB2b2lkIHtcbiAgYyhyZXF1ZXN0KTtcbn1cblxuLy8gY1xuZnVuY3Rpb24gYyhyZXF1ZXN0OiBSZXF1ZXN0KSB7XG4gIGNvbnNvbGUubG9nKHJlcXVlc3QpO1xufVxuXG5pbnRlcmZhY2UgQ29vbFRoaW5nIHtcbiAgZG9Db29sVGhpbmcoKTogdm9pZDtcbn1cblxuY2xhc3MgU3VwZXJDb29sVGhpbmcgaW1wbGVtZW50cyBDb29sVGhpbmcge1xuICBkb0Nvb2xUaGluZygpOiB2b2lkIHtcbiAgICBjb25zb2xlLmxvZyhcInN1cGVyIGNvb2wgdGhpbmchXCIpO1xuICB9XG59XG4iCiAgXSwKICAibWFwcGluZ3MiOiAiOztBQS8vLy8vZkFhQSxJQUFTLFlBQUMsQ0FBQyxTQUF3QjtBQUNqQyxJQUFFLE9BQU87QUFBQTtBQUlYLElBQVMsWUFBQyxDQUFDLFNBQXdCO0FBQ2pDLElBQUUsT0FBTztBQUFBO0FBSVgsSUFBUyxZQUFDLENBQUMsU0FBa0I7QUFDM0IsVUFBUSxJQUFJLE9BQU87QUFBQTtBQXRCckIsSUFBZTtBQUFBLE9BQ1AsTUFBSyxDQUFDLFNBQWtCLFFBQW1DO0FBQy9ELE1BQUUsT0FBTztBQUNULFVBQU0sWUFBdUIsSUFBSTtBQUNqQyxjQUFVLFlBQVk7QUFDdEI7QUFDQSxXQUFPLElBQUksU0FBUyxRQUFRLEdBQUc7QUFBQTtBQUVuQztBQXFCQTtBQUFBLE1BQU0sZUFBb0M7QUFBQSxFQUN4QyxXQUFXLEdBQVM7QUFDbEIsWUFBUSxJQUFJLG1CQUFtQjtBQUFBO0FBRW5DOyIsCiAgImRlYnVnSWQiOiAiOUJCMEI3NzNBOEU0NzcxNTY0NzU2ZTIxNjQ3NTZlMjEiLAogICJuYW1lcyI6IFtdCn0=
|
||||
@@ -5,7 +5,7 @@ import { readFile } from "node:fs/promises";
|
||||
|
||||
app
|
||||
.get("/", (req, res) => {
|
||||
console.log("I am logging a request!");
|
||||
console.log("I am logging a request!??");
|
||||
readFile(import.meta.path, "utf-8").then(data => {
|
||||
console.log(data.length);
|
||||
debugger;
|
||||
@@ -57,7 +57,7 @@ Bun.serve({
|
||||
inspector: true,
|
||||
development: true,
|
||||
fetch(request, server) {
|
||||
console.log(request);
|
||||
// console.log(request);
|
||||
return new Response(request.url);
|
||||
},
|
||||
});
|
||||
|
||||
34
packages/bun-vscode/example/example.ts
Normal file
34
packages/bun-vscode/example/example.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export default {
|
||||
async fetch(request: Request): Promise<Response> {
|
||||
a(request);
|
||||
const coolThing: CoolThing = new SuperCoolThing();
|
||||
coolThing.doCoolThing();
|
||||
debugger;
|
||||
return new Response("HELLO WORLD");
|
||||
},
|
||||
};
|
||||
|
||||
// a
|
||||
function a(request: Request): void {
|
||||
b(request);
|
||||
}
|
||||
|
||||
// b
|
||||
function b(request: Request): void {
|
||||
c(request);
|
||||
}
|
||||
|
||||
// c
|
||||
function c(request: Request) {
|
||||
console.log(request);
|
||||
}
|
||||
|
||||
interface CoolThing {
|
||||
doCoolThing(): void;
|
||||
}
|
||||
|
||||
class SuperCoolThing implements CoolThing {
|
||||
doCoolThing(): void {
|
||||
console.log("BLAH BLAH");
|
||||
}
|
||||
}
|
||||
@@ -1,65 +1,101 @@
|
||||
{
|
||||
"name": "bun-vscode",
|
||||
"version": "0.0.1",
|
||||
"main": "./dist/extension.js",
|
||||
"author": "oven",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/oven-sh/bun"
|
||||
},
|
||||
"main": "dist/extension.js",
|
||||
"dependencies": {
|
||||
"semver": "^7.5.4",
|
||||
"source-map-js": "^1.0.2",
|
||||
"ws": "^8.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.81.0",
|
||||
"@vscode/debugadapter": "^1.56.0",
|
||||
"@vscode/debugadapter-testsupport": "^1.56.0",
|
||||
"bun-types": "^0.7.3",
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^5.0.0",
|
||||
"esbuild": "^0.19.2"
|
||||
},
|
||||
"activationEvents": [
|
||||
"onLanguage:javascript",
|
||||
"onLanguage:javascriptreact",
|
||||
"onLanguage:typescript",
|
||||
"onLanguage:typescriptreact",
|
||||
"workspaceContains:**/.lockb",
|
||||
"onDebugResolve:bun",
|
||||
"onDebugDynamicConfigurations:bun",
|
||||
"onCommand:extension.bun.getProgramName"
|
||||
"onDebugDynamicConfigurations:bun"
|
||||
],
|
||||
"browser": "./dist/web-extension.js",
|
||||
"browser": "dist/web-extension.js",
|
||||
"bugs": {
|
||||
"url": "https://github.com/oven-sh/bun/issues"
|
||||
},
|
||||
"capabilities": {
|
||||
"untrustedWorkspaces": {
|
||||
"supported": false,
|
||||
"description": "This extension needs to be able to run your code using Bun."
|
||||
}
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages",
|
||||
"Debuggers"
|
||||
"Debuggers",
|
||||
"Testing"
|
||||
],
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "Bun",
|
||||
"properties": {
|
||||
"bun.path": {
|
||||
"type": "string",
|
||||
"description": "A path to the `bun` executable. By default, the extension looks for `bun` in the `PATH`, but if set, it will use the specified path instead.",
|
||||
"scope": "window",
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "extension.bun.runFile",
|
||||
"title": "Run File",
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(play)"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.debugFile",
|
||||
"title": "Debug File",
|
||||
"category": "Bun",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(debug-alt)"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"editor/title/run": [
|
||||
{
|
||||
"command": "extension.bun.runEditorContents",
|
||||
"when": "resourceLangId == javascript",
|
||||
"command": "extension.bun.runFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.debugEditorContents",
|
||||
"when": "resourceLangId == javascript",
|
||||
"command": "extension.bun.debugFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
|
||||
"group": "navigation@2"
|
||||
}
|
||||
],
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "extension.bun.debugEditorContents",
|
||||
"when": "resourceLangId == javascript"
|
||||
"command": "extension.bun.runFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.runEditorContents",
|
||||
"when": "resourceLangId == javascript"
|
||||
"command": "extension.bun.debugFile",
|
||||
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
|
||||
}
|
||||
]
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "extension.bun.debugEditorContents",
|
||||
"title": "Debug File",
|
||||
"category": "Bun Debug",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(debug-alt)"
|
||||
},
|
||||
{
|
||||
"command": "extension.bun.runEditorContents",
|
||||
"title": "Run File",
|
||||
"category": "Bun Debug",
|
||||
"enablement": "!inDebugMode",
|
||||
"icon": "$(play)"
|
||||
}
|
||||
],
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "javascript"
|
||||
@@ -85,31 +121,64 @@
|
||||
"typescriptreact"
|
||||
],
|
||||
"runtime": "node",
|
||||
"program": "./dist/adapter.js",
|
||||
"program": "dist/adapter.js",
|
||||
"configurationAttributes": {
|
||||
"launch": {
|
||||
"required": [
|
||||
"program"
|
||||
],
|
||||
"properties": {
|
||||
"runtime": {
|
||||
"type": "string",
|
||||
"description": "The path to Bun.",
|
||||
"default": "bun"
|
||||
},
|
||||
"program": {
|
||||
"type": "string",
|
||||
"description": "The file to run and debug.",
|
||||
"default": "${workspaceFolder}/${command:AskForProgramName}"
|
||||
"description": "The file to debug.",
|
||||
"default": "${file}"
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "The working directory.",
|
||||
"default": "${workspaceFolder}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "The arguments passed to Bun.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "The environment variables passed to Bun.",
|
||||
"default": {}
|
||||
},
|
||||
"inheritEnv": {
|
||||
"type": "boolean",
|
||||
"description": "If environment variables should be inherited from the parent process.",
|
||||
"default": true
|
||||
},
|
||||
"watch": {
|
||||
"type": ["boolean", "string"],
|
||||
"description": "If the process should be restarted when files change.",
|
||||
"enum": [
|
||||
true,
|
||||
false,
|
||||
"hot"
|
||||
],
|
||||
"default": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"attach": {
|
||||
"properties": {
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"description": "The port to attach and debug.",
|
||||
"default": 6499
|
||||
},
|
||||
"hostname": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The hostname to attach and debug.",
|
||||
"default": "localhost"
|
||||
"description": "The URL of the Bun process to attach to.",
|
||||
"default": "ws://localhost:6499/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,12 +188,13 @@
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Bun: Debug",
|
||||
"program": "${workspaceFolder}/${command:AskForProgramName}"
|
||||
"program": "${file}"
|
||||
},
|
||||
{
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Bun: Attach"
|
||||
"name": "Bun: Attach",
|
||||
"url": "ws://localhost:6499/"
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
@@ -135,7 +205,7 @@
|
||||
"type": "bun",
|
||||
"request": "launch",
|
||||
"name": "Ask for file name",
|
||||
"program": "^\"\\${workspaceFolder}/\\${command:AskForProgramName}\""
|
||||
"program": "^\"\\${file}\""
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -145,14 +215,10 @@
|
||||
"type": "bun",
|
||||
"request": "attach",
|
||||
"name": "Attach to Bun",
|
||||
"port": 6499,
|
||||
"hostname": "localhost"
|
||||
"url": "ws://localhost:6499/"
|
||||
}
|
||||
}
|
||||
],
|
||||
"variables": {
|
||||
"AskForProgramName": "extension.bun.getProgramName"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
@@ -165,15 +231,15 @@
|
||||
".lockb"
|
||||
],
|
||||
"icon": {
|
||||
"dark": "./src/assets/icon-small.png",
|
||||
"light": "./src/assets/icon-small.png"
|
||||
"dark": "src/assets/icon-small.png",
|
||||
"light": "src/assets/icon-small.png"
|
||||
}
|
||||
}
|
||||
],
|
||||
"jsonValidation": [
|
||||
{
|
||||
"fileMatch": "package.json",
|
||||
"url": "./src/resources/package.json"
|
||||
"url": "src/resources/package.json"
|
||||
}
|
||||
],
|
||||
"customEditors": [
|
||||
@@ -189,26 +255,40 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"description": "Visual Studio Code extension for Bun.",
|
||||
"description": "The Visual Studio Code extension for Bun.",
|
||||
"displayName": "Bun",
|
||||
"engines": {
|
||||
"vscode": "^1.66.0"
|
||||
"vscode": "^1.81.0"
|
||||
},
|
||||
"extensionKind": [
|
||||
"workspace"
|
||||
],
|
||||
"galleryBanner": {
|
||||
"color": "#C80000",
|
||||
"theme": "dark"
|
||||
},
|
||||
"icon": "./src/assets/icon.png",
|
||||
"homepage": "https://bun.sh/",
|
||||
"icon": "src/assets/icon.png",
|
||||
"keywords": [
|
||||
"bun",
|
||||
"node.js",
|
||||
"javascript",
|
||||
"typescript",
|
||||
"vscode"
|
||||
],
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"publisher": "oven",
|
||||
"scripts": {
|
||||
"build": "bunx esbuild src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs",
|
||||
"build:watch": "bunx esbuild --watch src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs"
|
||||
"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"
|
||||
},
|
||||
"workspaceTrust": {
|
||||
"request": "never"
|
||||
},
|
||||
"dependencies": {
|
||||
"ws": "^8.13.0"
|
||||
}
|
||||
"workspaces": [
|
||||
"../bun-debug-adapter-protocol",
|
||||
"../bun-inspector-protocol"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,118 +0,0 @@
|
||||
import * as vscode from "vscode";
|
||||
import { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode";
|
||||
import { DAPAdapter } from "./dap";
|
||||
import lockfile from "./lockfile";
|
||||
|
||||
export function activateBunDebug(context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
|
||||
lockfile(context);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("extension.bun.runEditorContents", (resource: vscode.Uri) => {
|
||||
let targetResource = resource;
|
||||
if (!targetResource && vscode.window.activeTextEditor) {
|
||||
targetResource = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
if (targetResource) {
|
||||
vscode.debug.startDebugging(
|
||||
undefined,
|
||||
{
|
||||
type: "bun",
|
||||
name: "Run File",
|
||||
request: "launch",
|
||||
program: targetResource.fsPath,
|
||||
},
|
||||
{ noDebug: true },
|
||||
);
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand("extension.bun.debugEditorContents", (resource: vscode.Uri) => {
|
||||
let targetResource = resource;
|
||||
if (!targetResource && vscode.window.activeTextEditor) {
|
||||
targetResource = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
if (targetResource) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
type: "bun",
|
||||
name: "Debug File",
|
||||
request: "launch",
|
||||
program: targetResource.fsPath,
|
||||
stopOnEntry: true,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("extension.bun.getProgramName", config => {
|
||||
return vscode.window.showInputBox({
|
||||
placeHolder: "Please enter the name of a file in the workspace folder",
|
||||
value: "src/index.js",
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
const provider = new BunConfigurationProvider();
|
||||
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("bun", provider));
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
"bun",
|
||||
{
|
||||
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
|
||||
return [
|
||||
{
|
||||
name: "Launch",
|
||||
request: "launch",
|
||||
type: "bun",
|
||||
program: "${file}",
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
|
||||
),
|
||||
);
|
||||
|
||||
if (!factory) {
|
||||
factory = new InlineDebugAdapterFactory();
|
||||
}
|
||||
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory));
|
||||
if ("dispose" in factory) {
|
||||
// @ts-expect-error ???
|
||||
context.subscriptions.push(factory);
|
||||
}
|
||||
}
|
||||
|
||||
class BunConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
resolveDebugConfiguration(
|
||||
folder: WorkspaceFolder | undefined,
|
||||
config: DebugConfiguration,
|
||||
token?: CancellationToken,
|
||||
): ProviderResult<DebugConfiguration> {
|
||||
// if launch.json is missing or empty
|
||||
if (!config.type && !config.request && !config.name) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && editor.document.languageId === "javascript") {
|
||||
config.type = "bun";
|
||||
config.name = "Launch";
|
||||
config.request = "launch";
|
||||
config.program = "${file}";
|
||||
config.stopOnEntry = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.program) {
|
||||
return vscode.window.showInformationMessage("Cannot find a program to debug").then(_ => {
|
||||
return undefined; // abort launch
|
||||
});
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
return new vscode.DebugAdapterInlineImplementation(new DAPAdapter(_session));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,10 @@
|
||||
import * as vscode from "vscode";
|
||||
import { activateBunDebug } from "./activate";
|
||||
|
||||
const runMode: "external" | "server" | "namedPipeServer" | "inline" = "inline";
|
||||
import activateLockfile from "./features/lockfile";
|
||||
import activateDebug from "./features/debug";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
if (runMode === "inline") {
|
||||
activateBunDebug(context);
|
||||
return;
|
||||
}
|
||||
throw new Error(`This extension does not support '${runMode}' mode.`);
|
||||
activateLockfile(context);
|
||||
activateDebug(context);
|
||||
}
|
||||
|
||||
export function deactivate() {
|
||||
// No-op
|
||||
}
|
||||
export function deactivate() {}
|
||||
|
||||
153
packages/bun-vscode/src/features/debug.ts
Normal file
153
packages/bun-vscode/src/features/debug.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
import * as vscode from "vscode";
|
||||
import type { CancellationToken, DebugConfiguration, ProviderResult, WorkspaceFolder } from "vscode";
|
||||
import type { DAP } from "../../../bun-debug-adapter-protocol";
|
||||
import { DebugAdapter } from "../../../bun-debug-adapter-protocol";
|
||||
import { DebugSession } from "@vscode/debugadapter";
|
||||
|
||||
const debugConfiguration: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
request: "launch",
|
||||
name: "Debug Bun",
|
||||
program: "${file}",
|
||||
watch: true,
|
||||
};
|
||||
|
||||
const runConfiguration: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
request: "launch",
|
||||
name: "Run Bun",
|
||||
program: "${file}",
|
||||
watch: true,
|
||||
};
|
||||
|
||||
const attachConfiguration: vscode.DebugConfiguration = {
|
||||
type: "bun",
|
||||
request: "attach",
|
||||
name: "Attach to Bun",
|
||||
url: "ws://localhost:6499/",
|
||||
};
|
||||
|
||||
const debugConfigurations: vscode.DebugConfiguration[] = [debugConfiguration, attachConfiguration];
|
||||
|
||||
export default function (context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("extension.bun.runFile", (resource: vscode.Uri) => {
|
||||
let targetResource = resource;
|
||||
if (!targetResource && vscode.window.activeTextEditor) {
|
||||
targetResource = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
if (targetResource) {
|
||||
vscode.debug.startDebugging(undefined, runConfiguration, {
|
||||
noDebug: true,
|
||||
});
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand("extension.bun.debugFile", (resource: vscode.Uri) => {
|
||||
let targetResource = resource;
|
||||
if (!targetResource && vscode.window.activeTextEditor) {
|
||||
targetResource = vscode.window.activeTextEditor.document.uri;
|
||||
}
|
||||
if (targetResource) {
|
||||
vscode.debug.startDebugging(undefined, {
|
||||
...debugConfiguration,
|
||||
program: targetResource.fsPath,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const provider = new BunConfigurationProvider();
|
||||
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider("bun", provider));
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.debug.registerDebugConfigurationProvider(
|
||||
"bun",
|
||||
{
|
||||
provideDebugConfigurations(folder: WorkspaceFolder | undefined): ProviderResult<DebugConfiguration[]> {
|
||||
return debugConfigurations;
|
||||
},
|
||||
},
|
||||
vscode.DebugConfigurationProviderTriggerKind.Dynamic,
|
||||
),
|
||||
);
|
||||
|
||||
if (!factory) {
|
||||
factory = new InlineDebugAdapterFactory();
|
||||
}
|
||||
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory("bun", factory));
|
||||
if ("dispose" in factory && typeof factory.dispose === "function") {
|
||||
// @ts-ignore
|
||||
context.subscriptions.push(factory);
|
||||
}
|
||||
}
|
||||
|
||||
class BunConfigurationProvider implements vscode.DebugConfigurationProvider {
|
||||
resolveDebugConfiguration(
|
||||
folder: WorkspaceFolder | undefined,
|
||||
config: DebugConfiguration,
|
||||
token?: CancellationToken,
|
||||
): ProviderResult<DebugConfiguration> {
|
||||
if (!config.type && !config.request && !config.name) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && isJavaScript(editor.document.languageId)) {
|
||||
Object.assign(config, debugConfiguration);
|
||||
}
|
||||
}
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
const adapter = new VSCodeAdapter(_session);
|
||||
return new vscode.DebugAdapterInlineImplementation(adapter);
|
||||
}
|
||||
}
|
||||
|
||||
function isJavaScript(languageId: string): boolean {
|
||||
return (
|
||||
languageId === "javascript" ||
|
||||
languageId === "javascriptreact" ||
|
||||
languageId === "typescript" ||
|
||||
languageId === "typescriptreact"
|
||||
);
|
||||
}
|
||||
|
||||
export class VSCodeAdapter extends DebugSession {
|
||||
#adapter: DebugAdapter;
|
||||
#dap: vscode.OutputChannel;
|
||||
|
||||
constructor(session: vscode.DebugSession) {
|
||||
super();
|
||||
this.#dap = vscode.window.createOutputChannel("Debug Adapter Protocol");
|
||||
this.#adapter = new DebugAdapter({
|
||||
sendToAdapter: this.sendMessage.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
sendMessage(message: DAP.Request | DAP.Response | DAP.Event): void {
|
||||
console.log("[dap] -->", message);
|
||||
this.#dap.appendLine("--> " + JSON.stringify(message));
|
||||
|
||||
const { type } = message;
|
||||
if (type === "response") {
|
||||
this.sendResponse(message);
|
||||
} else if (type === "event") {
|
||||
this.sendEvent(message);
|
||||
} else {
|
||||
throw new Error(`Not supported: ${type}`);
|
||||
}
|
||||
}
|
||||
|
||||
handleMessage(message: DAP.Event | DAP.Request | DAP.Response): void {
|
||||
console.log("[dap] <--", message);
|
||||
this.#dap.appendLine("<-- " + JSON.stringify(message));
|
||||
|
||||
this.#adapter.accept(message);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#adapter.close();
|
||||
this.#dap.dispose();
|
||||
}
|
||||
}
|
||||
@@ -52,10 +52,10 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro
|
||||
process.stderr.on("data", (data: Buffer) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
process.on("error", (error) => {
|
||||
process.on("error", error => {
|
||||
reject(error);
|
||||
});
|
||||
process.on("exit", (code) => {
|
||||
process.on("exit", code => {
|
||||
if (code === 0) {
|
||||
resolve(stdout);
|
||||
} else {
|
||||
@@ -65,19 +65,15 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro
|
||||
});
|
||||
}
|
||||
|
||||
export default function(context: vscode.ExtensionContext): void {
|
||||
export default function (context: vscode.ExtensionContext): void {
|
||||
const viewType = "bun.lockb";
|
||||
const provider = new BunLockfileEditorProvider(context);
|
||||
|
||||
vscode.window.registerCustomEditorProvider(
|
||||
viewType,
|
||||
provider,
|
||||
{
|
||||
vscode.window.registerCustomEditorProvider(viewType, provider, {
|
||||
supportsMultipleEditorsPerDocument: true,
|
||||
webviewOptions: {
|
||||
enableFindWidget: true,
|
||||
retainContextWhenHidden: true,
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,308 +0,0 @@
|
||||
import { Socket, createConnection } from "node:net";
|
||||
import { inspect } from "node:util";
|
||||
import type { JSC } from "../types/jsc";
|
||||
export type { JSC };
|
||||
|
||||
export type JSCClientOptions = {
|
||||
url: string | URL;
|
||||
retry?: boolean;
|
||||
onEvent?: (event: JSC.Event) => void;
|
||||
onRequest?: (request: JSC.Request) => void;
|
||||
onResponse?: (response: JSC.Response) => void;
|
||||
onError?: (error: Error) => void;
|
||||
onClose?: (code: number, reason: string) => void;
|
||||
};
|
||||
const headerInvalidNumber = 2147483646;
|
||||
|
||||
// We use non-printable characters to separate messages in the stream.
|
||||
// These should never appear in textual messages.
|
||||
|
||||
// These are non-sequential so that code which just counts up from 0 doesn't accidentally parse them as messages.
|
||||
// 0x12 0x11 0x13 0x14 as a little-endian 32-bit unsigned integer
|
||||
const headerPrefix = "\x14\x13\x11\x12";
|
||||
|
||||
// 0x14 0x12 0x13 0x11 as a little-endian 32-bit unsigned integer
|
||||
const headerSuffixString = "\x11\x13\x12\x14";
|
||||
|
||||
const headerSuffixInt = Buffer.from(headerSuffixString).readInt32LE(0);
|
||||
const headerPrefixInt = Buffer.from(headerPrefix).readInt32LE(0);
|
||||
|
||||
const messageLengthBuffer = new ArrayBuffer(12);
|
||||
const messageLengthDataView = new DataView(messageLengthBuffer);
|
||||
messageLengthDataView.setInt32(0, headerPrefixInt, true);
|
||||
messageLengthDataView.setInt32(8, headerSuffixInt, true);
|
||||
|
||||
function writeJSONMessageToBuffer(message: any) {
|
||||
const asString = JSON.stringify(message);
|
||||
const byteLength = Buffer.byteLength(asString, "utf8");
|
||||
const buffer = Buffer.allocUnsafe(12 + byteLength);
|
||||
buffer.writeInt32LE(headerPrefixInt, 0);
|
||||
buffer.writeInt32LE(byteLength, 4);
|
||||
buffer.writeInt32LE(headerSuffixInt, 8);
|
||||
if (buffer.write(asString, 12, byteLength, "utf8") !== byteLength) {
|
||||
throw new Error("Failed to write message to buffer");
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
let currentMessageLength = 0;
|
||||
const DEBUGGING = true;
|
||||
function extractMessageLengthAndOffsetFromBytes(buffer: Buffer, offset: number) {
|
||||
const bufferLength = buffer.length;
|
||||
while (offset < bufferLength) {
|
||||
const headerStart = buffer.indexOf(headerPrefix, offset, "binary");
|
||||
if (headerStart === -1) {
|
||||
if (DEBUGGING) {
|
||||
console.error("No header found in buffer of length " + bufferLength + " starting at offset " + offset);
|
||||
}
|
||||
return headerInvalidNumber;
|
||||
}
|
||||
|
||||
// [headerPrefix (4), byteLength (4), headerSuffix (4)]
|
||||
if (bufferLength <= headerStart + 12) {
|
||||
if (DEBUGGING) {
|
||||
console.error(
|
||||
"Not enough bytes for header in buffer of length " + bufferLength + " starting at offset " + offset,
|
||||
);
|
||||
}
|
||||
return headerInvalidNumber;
|
||||
}
|
||||
|
||||
const prefix = buffer.readInt32LE(headerStart);
|
||||
const byteLengthInt = buffer.readInt32LE(headerStart + 4);
|
||||
const suffix = buffer.readInt32LE(headerStart + 8);
|
||||
|
||||
if (prefix !== headerPrefixInt || suffix !== headerSuffixInt) {
|
||||
offset = headerStart + 1;
|
||||
currentMessageLength = 0;
|
||||
|
||||
if (DEBUGGING) {
|
||||
console.error(
|
||||
"Invalid header in buffer of length " + bufferLength + " starting at offset " + offset + ": " + prefix,
|
||||
byteLengthInt,
|
||||
suffix,
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (byteLengthInt < 0) {
|
||||
if (DEBUGGING) {
|
||||
console.error(
|
||||
"Invalid byteLength in buffer of length " + bufferLength + " starting at offset " + offset + ": " + prefix,
|
||||
byteLengthInt,
|
||||
suffix,
|
||||
);
|
||||
}
|
||||
|
||||
return headerInvalidNumber;
|
||||
}
|
||||
|
||||
if (byteLengthInt === 0) {
|
||||
// Ignore 0-length messages
|
||||
// Shouldn't happen in practice
|
||||
offset = headerStart + 12;
|
||||
currentMessageLength = 0;
|
||||
|
||||
if (DEBUGGING) {
|
||||
console.error(
|
||||
"Ignoring 0-length message in buffer of length " + bufferLength + " starting at offset " + offset,
|
||||
);
|
||||
console.error({
|
||||
buffer: buffer,
|
||||
string: buffer.toString(),
|
||||
});
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
currentMessageLength = byteLengthInt;
|
||||
|
||||
return headerStart + 12;
|
||||
}
|
||||
|
||||
if (DEBUGGING) {
|
||||
if (bufferLength > 0)
|
||||
console.error("Header not found in buffer of length " + bufferLength + " starting at offset " + offset);
|
||||
}
|
||||
|
||||
return headerInvalidNumber;
|
||||
}
|
||||
|
||||
class StreamingReader {
|
||||
pendingBuffer: Buffer;
|
||||
|
||||
constructor() {
|
||||
this.pendingBuffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
*onMessage(chunk: Buffer) {
|
||||
let buffer: Buffer;
|
||||
if (this.pendingBuffer.length > 0) {
|
||||
this.pendingBuffer = buffer = Buffer.concat([this.pendingBuffer, chunk]);
|
||||
} else {
|
||||
this.pendingBuffer = buffer = chunk;
|
||||
}
|
||||
|
||||
currentMessageLength = 0;
|
||||
|
||||
for (
|
||||
let offset = extractMessageLengthAndOffsetFromBytes(buffer, 0);
|
||||
buffer.length > 0 && offset !== headerInvalidNumber;
|
||||
currentMessageLength = 0, offset = extractMessageLengthAndOffsetFromBytes(buffer, 0)
|
||||
) {
|
||||
const messageLength = currentMessageLength;
|
||||
const start = offset;
|
||||
const end = start + messageLength;
|
||||
offset = end;
|
||||
const messageChunk = buffer.slice(start, end);
|
||||
this.pendingBuffer = buffer = buffer.slice(offset);
|
||||
yield messageChunk.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class JSCClient {
|
||||
#options: JSCClientOptions;
|
||||
#requestId: number;
|
||||
#pendingMessages: Buffer[];
|
||||
#pendingRequests: Map<number, (result: unknown) => void>;
|
||||
#socket: Socket;
|
||||
#ready?: Promise<void>;
|
||||
#reader = new StreamingReader();
|
||||
signal?: AbortSignal;
|
||||
|
||||
constructor(options: JSCClientOptions) {
|
||||
this.#options = options;
|
||||
this.#socket = undefined;
|
||||
this.#requestId = 1;
|
||||
|
||||
this.#pendingMessages = [];
|
||||
this.#pendingRequests = new Map();
|
||||
}
|
||||
|
||||
get ready(): Promise<void> {
|
||||
if (!this.#ready) {
|
||||
this.#ready = this.#connect();
|
||||
}
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
#connect(): Promise<void> {
|
||||
const { url, retry, onError, onResponse, onEvent, onClose } = this.#options;
|
||||
let [host, port] = typeof url === "string" ? url.split(":") : [url.hostname, url.port];
|
||||
if (port == null) {
|
||||
if (host == null) {
|
||||
host = "localhost";
|
||||
port = "9229";
|
||||
} else {
|
||||
port = "9229";
|
||||
}
|
||||
}
|
||||
|
||||
if (host == null) {
|
||||
host = "localhost";
|
||||
}
|
||||
var resolve,
|
||||
reject,
|
||||
promise = new Promise<void>((r1, r2) => {
|
||||
resolve = r1;
|
||||
reject = r2;
|
||||
}),
|
||||
socket: Socket;
|
||||
let didConnect = false;
|
||||
|
||||
this.#socket = socket = createConnection(
|
||||
{
|
||||
host,
|
||||
port: Number(port),
|
||||
},
|
||||
() => {
|
||||
for (const message of this.#pendingMessages) {
|
||||
this.#send(message);
|
||||
}
|
||||
this.#pendingMessages.length = 0;
|
||||
didConnect = true;
|
||||
resolve();
|
||||
},
|
||||
)
|
||||
.once("error", e => {
|
||||
const error = new Error(`Socket error: ${e?.message || e}`);
|
||||
reject(error);
|
||||
})
|
||||
.on("data", buffer => {
|
||||
for (const message of this.#reader.onMessage(buffer)) {
|
||||
let received: JSC.Event | JSC.Response;
|
||||
try {
|
||||
received = JSON.parse(message);
|
||||
} catch {
|
||||
const error = new Error(`Invalid WebSocket data: ${inspect(message)}`);
|
||||
onError?.(error);
|
||||
return;
|
||||
}
|
||||
console.log({ received });
|
||||
if ("id" in received) {
|
||||
onResponse?.(received);
|
||||
if ("error" in received) {
|
||||
const { message, code = "?" } = received.error;
|
||||
const error = new Error(`${message} [code: ${code}]`);
|
||||
error.code = code;
|
||||
onError?.(error);
|
||||
this.#pendingRequests.get(received.id)?.(error);
|
||||
} else {
|
||||
this.#pendingRequests.get(received.id)?.(received.result);
|
||||
}
|
||||
} else {
|
||||
onEvent?.(received);
|
||||
}
|
||||
}
|
||||
})
|
||||
.on("close", hadError => {
|
||||
if (didConnect) {
|
||||
onClose?.(hadError ? 1 : 0, "Socket closed");
|
||||
}
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
#send(message: any): void {
|
||||
const socket = this.#socket;
|
||||
const framed = writeJSONMessageToBuffer(message);
|
||||
if (socket && !socket.connecting) {
|
||||
socket.write(framed);
|
||||
} else {
|
||||
this.#pendingMessages.push(framed);
|
||||
}
|
||||
}
|
||||
|
||||
async fetch<T extends keyof JSC.RequestMap>(
|
||||
method: T,
|
||||
params?: JSC.Request<T>["params"],
|
||||
): Promise<JSC.ResponseMap[T]> {
|
||||
const request: JSC.Request<T> = {
|
||||
id: this.#requestId++,
|
||||
method,
|
||||
params,
|
||||
};
|
||||
this.#options.onRequest?.(request);
|
||||
return new Promise((resolve, reject) => {
|
||||
const done = (result: Error | JSC.ResponseMap[T]) => {
|
||||
this.#pendingRequests.delete(request.id);
|
||||
if (result instanceof Error) {
|
||||
reject(result);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
};
|
||||
this.#pendingRequests.set(request.id, done);
|
||||
this.#send(request);
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
if (this.#socket) this.#socket.end();
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
import * as vscode from "vscode";
|
||||
import { activateBunDebug } from "./activate";
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
activateBunDebug(context);
|
||||
}
|
||||
export function activate(context: vscode.ExtensionContext) {}
|
||||
|
||||
export function deactivate() {
|
||||
// No-op
|
||||
}
|
||||
export function deactivate() {}
|
||||
|
||||
10
packages/bun-vscode/test/fixtures/echo.ts
vendored
10
packages/bun-vscode/test/fixtures/echo.ts
vendored
@@ -1,10 +0,0 @@
|
||||
import { serve } from "bun";
|
||||
|
||||
serve({
|
||||
port: 9229,
|
||||
development: true,
|
||||
fetch(request, server) {
|
||||
return new Response(`Hello, ${request.url}!`);
|
||||
},
|
||||
inspector: true,
|
||||
});
|
||||
@@ -1,123 +0,0 @@
|
||||
import { beforeAll, afterAll, describe, test, expect, mock } from "bun:test";
|
||||
import { Worker } from "node:worker_threads";
|
||||
import { JSCClient, type JSC } from "../src/jsc";
|
||||
|
||||
let worker: Worker;
|
||||
|
||||
beforeAll(async () => {
|
||||
const { pathname } = new URL("./fixtures/echo.ts", import.meta.url);
|
||||
worker = new Worker(pathname, { smol: true });
|
||||
while (true) {
|
||||
try {
|
||||
await fetch("http://localhost:9229/");
|
||||
break;
|
||||
} catch {}
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
worker?.terminate();
|
||||
});
|
||||
|
||||
describe("JSCClient", () => {
|
||||
const onRequest = mock((request: JSC.Request) => {
|
||||
expect(request).toBeInstanceOf(Object);
|
||||
expect(request.id).toBeNumber();
|
||||
expect(request.method).toBeString();
|
||||
if (request.params) {
|
||||
expect(request.params).toBeInstanceOf(Object);
|
||||
} else {
|
||||
expect(request).toBeUndefined();
|
||||
}
|
||||
});
|
||||
const onResponse = mock((response: JSC.Response) => {
|
||||
expect(response).toBeInstanceOf(Object);
|
||||
expect(response.id).toBeNumber();
|
||||
if ("result" in response) {
|
||||
expect(response.result).toBeInstanceOf(Object);
|
||||
} else {
|
||||
expect(response.error).toBeInstanceOf(Object);
|
||||
expect(response.error.message).toBeString();
|
||||
}
|
||||
});
|
||||
const onEvent = mock((event: JSC.Event) => {
|
||||
expect(event).toBeInstanceOf(Object);
|
||||
expect(event.method).toBeString();
|
||||
if (event.params) {
|
||||
expect(event.params).toBeInstanceOf(Object);
|
||||
} else {
|
||||
expect(event).toBeUndefined();
|
||||
}
|
||||
});
|
||||
const onError = mock((error: Error) => {
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
});
|
||||
const client = new JSCClient({
|
||||
url: "ws://localhost:9229/bun:inspect",
|
||||
onRequest,
|
||||
onResponse,
|
||||
onEvent,
|
||||
onError,
|
||||
});
|
||||
test("can connect", () => {
|
||||
expect(client.ready).resolves.toBeUndefined();
|
||||
});
|
||||
test("can send a request", () => {
|
||||
expect(client.fetch("Runtime.evaluate", { expression: "1 + 1" })).resolves.toStrictEqual({
|
||||
result: {
|
||||
type: "number",
|
||||
value: 2,
|
||||
description: "2",
|
||||
},
|
||||
wasThrown: false,
|
||||
});
|
||||
expect(onRequest).toHaveBeenCalled();
|
||||
expect(onRequest.mock.lastCall[0]).toStrictEqual({
|
||||
id: 1,
|
||||
method: "Runtime.evaluate",
|
||||
params: { expression: "1 + 1" },
|
||||
});
|
||||
expect(onResponse).toHaveBeenCalled();
|
||||
expect(onResponse.mock.lastCall[0]).toMatchObject({
|
||||
id: 1,
|
||||
result: {
|
||||
result: {
|
||||
type: "number",
|
||||
value: 2,
|
||||
description: "2",
|
||||
},
|
||||
wasThrown: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
test("can send an invalid request", () => {
|
||||
expect(
|
||||
client.fetch("Runtime.awaitPromise", {
|
||||
promiseObjectId: "this-does-not-exist",
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
name: "Error",
|
||||
message: expect.stringMatching(/promiseObjectId/),
|
||||
});
|
||||
expect(onRequest).toHaveBeenCalled();
|
||||
expect(onRequest.mock.lastCall[0]).toStrictEqual({
|
||||
id: 2,
|
||||
method: "Runtime.awaitPromise",
|
||||
params: {
|
||||
promiseObjectId: "this-does-not-exist",
|
||||
},
|
||||
});
|
||||
expect(onResponse).toHaveBeenCalled();
|
||||
expect(onResponse.mock.lastCall[0]).toMatchObject({
|
||||
id: 2,
|
||||
error: {
|
||||
code: expect.any(Number),
|
||||
message: expect.stringMatching(/promiseObjectId/),
|
||||
},
|
||||
});
|
||||
expect(onError).toHaveBeenCalled();
|
||||
});
|
||||
test("can disconnect", () => {
|
||||
expect(() => client.close()).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -7,16 +7,8 @@
|
||||
"esModuleInterop": true,
|
||||
"isolatedModules": false,
|
||||
"skipLibCheck": true,
|
||||
"types": [
|
||||
"bun-types"
|
||||
]
|
||||
"types": ["bun-types"]
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"test",
|
||||
"types"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
"include": ["src", "test", "types", "../../bun-devtools", "../../bun-debug-adapter-protocol"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
5216
packages/bun-vscode/types/dap.d.ts
vendored
5216
packages/bun-vscode/types/dap.d.ts
vendored
File diff suppressed because it is too large
Load Diff
1999
packages/bun-vscode/types/jsc.d.ts
vendored
1999
packages/bun-vscode/types/jsc.d.ts
vendored
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user