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",
|
"prettier": "^2.4.1",
|
||||||
"react": "next",
|
"react": "next",
|
||||||
"react-dom": "next",
|
"react-dom": "next",
|
||||||
|
"source-map-js": "^1.0.2",
|
||||||
"typescript": "^5.0.2"
|
"typescript": "^5.0.2"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
"build-fallback": "esbuild --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js",
|
"build-fallback": "esbuild --target=esnext --bundle src/fallback.ts --format=iife --platform=browser --minify > src/fallback.out.js",
|
||||||
"postinstall": "bash .scripts/postinstall.sh",
|
"postinstall": "bash .scripts/postinstall.sh",
|
||||||
"typecheck": "tsc --noEmit && cd test && bun run typecheck",
|
"typecheck": "tsc --noEmit && cd test && bun run typecheck",
|
||||||
"fmt": "prettier --write --cache './{src,test,bench}/**/*.{mjs,ts,tsx,js,jsx}'",
|
"fmt": "prettier --write --cache './{src,test,bench,packages/{bun-inspector-*,bun-vscode,bun-debug-adapter-protocol}}/**/*.{mjs,ts,tsx,js,jsx}'",
|
||||||
"lint": "eslint './**/*.d.ts' --cache",
|
"lint": "eslint './**/*.d.ts' --cache",
|
||||||
"lint:fix": "eslint './**/*.d.ts' --cache --fix"
|
"lint:fix": "eslint './**/*.d.ts' --cache --fix"
|
||||||
},
|
},
|
||||||
|
|||||||
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"],
|
"lib": ["ESNext"],
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "nodenext",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"composite": true,
|
"composite": true,
|
||||||
|
"strict": true,
|
||||||
"downlevelIteration": true,
|
"downlevelIteration": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"jsx": "preserve",
|
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"inlineSourceMap": true,
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"types": [
|
"types": ["bun-types"],
|
||||||
"bun-types" // add Bun global
|
"outDir": "dist",
|
||||||
]
|
},
|
||||||
}
|
"include": [".", "../bun-types/index.d.ts", "../bun-inspector-protocol/index"]
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"name": "bun-ecosystem-ci",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"globby": "^13.1.3"
|
"globby": "^13.1.3"
|
||||||
@@ -11,4 +12,4 @@
|
|||||||
"format": "prettier --write src",
|
"format": "prettier --write src",
|
||||||
"test": "bun run src/runner.ts"
|
"test": "bun run src/runner.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# web-inspector-bun
|
# bun-devtools-frontend
|
||||||
|
|
||||||
This is the WebKit Web Inspector bundled as standalone assets.
|
This is the WebKit Web Inspector bundled as standalone assets.
|
||||||
|
|
||||||
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": {
|
"devDependencies": {
|
||||||
"bun-types": "^0.7.0",
|
"bun-types": "^0.7.0",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"name": "bun-release-action",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws4fetch": "^1.0.17",
|
"aws4fetch": "^1.0.17",
|
||||||
|
|||||||
2
packages/bun-vscode/.gitignore
vendored
Normal file
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",
|
"name": "Extension",
|
||||||
"type": "extensionHost",
|
"type": "extensionHost",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"args": [
|
"args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/example"],
|
||||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
|
||||||
"${workspaceFolder}/example"
|
|
||||||
],
|
|
||||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||||
"preLaunchTask": "Build (watch)"
|
"preLaunchTask": "Build (watch)"
|
||||||
},
|
},
|
||||||
|
|||||||
4
packages/bun-vscode/.vscode/settings.json
vendored
4
packages/bun-vscode/.vscode/settings.json
vendored
@@ -5,5 +5,5 @@
|
|||||||
"search.exclude": {
|
"search.exclude": {
|
||||||
"out": true // set this to false to include "out" folder in search results
|
"out": true // set this to false to include "out" folder in search results
|
||||||
},
|
},
|
||||||
"typescript.tsc.autoDetect": "off",
|
"typescript.tsc.autoDetect": "off"
|
||||||
}
|
}
|
||||||
|
|||||||
8
packages/bun-vscode/.vscode/tasks.json
vendored
8
packages/bun-vscode/.vscode/tasks.json
vendored
@@ -4,13 +4,15 @@
|
|||||||
{
|
{
|
||||||
"label": "Build",
|
"label": "Build",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "bun run build"
|
"command": "bun run build",
|
||||||
|
"problemMatcher": "$esbuild"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Build (watch)",
|
"label": "Build (watch)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "bun run build:watch",
|
"command": "bun run build:watch",
|
||||||
"isBackground": true
|
"isBackground": true,
|
||||||
|
"problemMatcher": "$esbuild-watch"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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",
|
"type": "bun",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug",
|
"name": "Debug Bun",
|
||||||
"program": "${workspaceFolder}/example.js",
|
"program": "${file}",
|
||||||
"stopOnEntry": true
|
"watch": "hot"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "bun",
|
"type": "bun",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"name": "Attach",
|
"name": "Attach to Bun",
|
||||||
"program": "${workspaceFolder}/example.js",
|
"url": "ws://localhost:6499/",
|
||||||
"stopOnEntry": true
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
app
|
||||||
.get("/", (req, res) => {
|
.get("/", (req, res) => {
|
||||||
console.log("I am logging a request!");
|
console.log("I am logging a request!??");
|
||||||
readFile(import.meta.path, "utf-8").then(data => {
|
readFile(import.meta.path, "utf-8").then(data => {
|
||||||
console.log(data.length);
|
console.log(data.length);
|
||||||
debugger;
|
debugger;
|
||||||
@@ -57,7 +57,7 @@ Bun.serve({
|
|||||||
inspector: true,
|
inspector: true,
|
||||||
development: true,
|
development: true,
|
||||||
fetch(request, server) {
|
fetch(request, server) {
|
||||||
console.log(request);
|
// console.log(request);
|
||||||
return new Response(request.url);
|
return new Response(request.url);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
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",
|
"name": "bun-vscode",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"main": "./dist/extension.js",
|
"author": "oven",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/oven-sh/bun"
|
||||||
|
},
|
||||||
|
"main": "dist/extension.js",
|
||||||
|
"dependencies": {
|
||||||
|
"semver": "^7.5.4",
|
||||||
|
"source-map-js": "^1.0.2",
|
||||||
|
"ws": "^8.13.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/vscode": "^1.81.0",
|
"@types/vscode": "^1.81.0",
|
||||||
"@vscode/debugadapter": "^1.56.0",
|
"@vscode/debugadapter": "^1.56.0",
|
||||||
"@vscode/debugadapter-testsupport": "^1.56.0",
|
"@vscode/debugadapter-testsupport": "^1.56.0",
|
||||||
"bun-types": "^0.7.3",
|
"bun-types": "^0.7.3",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0",
|
||||||
|
"esbuild": "^0.19.2"
|
||||||
},
|
},
|
||||||
"activationEvents": [
|
"activationEvents": [
|
||||||
|
"onLanguage:javascript",
|
||||||
|
"onLanguage:javascriptreact",
|
||||||
|
"onLanguage:typescript",
|
||||||
|
"onLanguage:typescriptreact",
|
||||||
|
"workspaceContains:**/.lockb",
|
||||||
"onDebugResolve:bun",
|
"onDebugResolve:bun",
|
||||||
"onDebugDynamicConfigurations:bun",
|
"onDebugDynamicConfigurations:bun"
|
||||||
"onCommand:extension.bun.getProgramName"
|
|
||||||
],
|
],
|
||||||
"browser": "./dist/web-extension.js",
|
"browser": "dist/web-extension.js",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/oven-sh/bun/issues"
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"untrustedWorkspaces": {
|
||||||
|
"supported": false,
|
||||||
|
"description": "This extension needs to be able to run your code using Bun."
|
||||||
|
}
|
||||||
|
},
|
||||||
"categories": [
|
"categories": [
|
||||||
"Programming Languages",
|
"Programming Languages",
|
||||||
"Debuggers"
|
"Debuggers",
|
||||||
|
"Testing"
|
||||||
],
|
],
|
||||||
"contributes": {
|
"contributes": {
|
||||||
|
"configuration": {
|
||||||
|
"title": "Bun",
|
||||||
|
"properties": {
|
||||||
|
"bun.path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "A path to the `bun` executable. By default, the extension looks for `bun` in the `PATH`, but if set, it will use the specified path instead.",
|
||||||
|
"scope": "window",
|
||||||
|
"default": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "extension.bun.runFile",
|
||||||
|
"title": "Run File",
|
||||||
|
"category": "Bun",
|
||||||
|
"enablement": "!inDebugMode",
|
||||||
|
"icon": "$(play)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"command": "extension.bun.debugFile",
|
||||||
|
"title": "Debug File",
|
||||||
|
"category": "Bun",
|
||||||
|
"enablement": "!inDebugMode",
|
||||||
|
"icon": "$(debug-alt)"
|
||||||
|
}
|
||||||
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
"editor/title/run": [
|
"editor/title/run": [
|
||||||
{
|
{
|
||||||
"command": "extension.bun.runEditorContents",
|
"command": "extension.bun.runFile",
|
||||||
"when": "resourceLangId == javascript",
|
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
|
||||||
"group": "navigation@1"
|
"group": "navigation@1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "extension.bun.debugEditorContents",
|
"command": "extension.bun.debugFile",
|
||||||
"when": "resourceLangId == javascript",
|
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact",
|
||||||
"group": "navigation@2"
|
"group": "navigation@2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commandPalette": [
|
"commandPalette": [
|
||||||
{
|
{
|
||||||
"command": "extension.bun.debugEditorContents",
|
"command": "extension.bun.runFile",
|
||||||
"when": "resourceLangId == javascript"
|
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"command": "extension.bun.runEditorContents",
|
"command": "extension.bun.debugFile",
|
||||||
"when": "resourceLangId == javascript"
|
"when": "resourceLangId == javascript || resourceLangId == javascriptreact || resourceLangId == typescript || resourceLangId == typescriptreact"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"commands": [
|
|
||||||
{
|
|
||||||
"command": "extension.bun.debugEditorContents",
|
|
||||||
"title": "Debug File",
|
|
||||||
"category": "Bun Debug",
|
|
||||||
"enablement": "!inDebugMode",
|
|
||||||
"icon": "$(debug-alt)"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"command": "extension.bun.runEditorContents",
|
|
||||||
"title": "Run File",
|
|
||||||
"category": "Bun Debug",
|
|
||||||
"enablement": "!inDebugMode",
|
|
||||||
"icon": "$(play)"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"breakpoints": [
|
"breakpoints": [
|
||||||
{
|
{
|
||||||
"language": "javascript"
|
"language": "javascript"
|
||||||
@@ -85,31 +121,64 @@
|
|||||||
"typescriptreact"
|
"typescriptreact"
|
||||||
],
|
],
|
||||||
"runtime": "node",
|
"runtime": "node",
|
||||||
"program": "./dist/adapter.js",
|
"program": "dist/adapter.js",
|
||||||
"configurationAttributes": {
|
"configurationAttributes": {
|
||||||
"launch": {
|
"launch": {
|
||||||
"required": [
|
"required": [
|
||||||
"program"
|
"program"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"runtime": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The path to Bun.",
|
||||||
|
"default": "bun"
|
||||||
|
},
|
||||||
"program": {
|
"program": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The file to run and debug.",
|
"description": "The file to debug.",
|
||||||
"default": "${workspaceFolder}/${command:AskForProgramName}"
|
"default": "${file}"
|
||||||
|
},
|
||||||
|
"cwd": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The working directory.",
|
||||||
|
"default": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"args": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "The arguments passed to Bun.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"default": []
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "The environment variables passed to Bun.",
|
||||||
|
"default": {}
|
||||||
|
},
|
||||||
|
"inheritEnv": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "If environment variables should be inherited from the parent process.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"watch": {
|
||||||
|
"type": ["boolean", "string"],
|
||||||
|
"description": "If the process should be restarted when files change.",
|
||||||
|
"enum": [
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
"hot"
|
||||||
|
],
|
||||||
|
"default": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"attach": {
|
"attach": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"port": {
|
"url": {
|
||||||
"type": "integer",
|
|
||||||
"description": "The port to attach and debug.",
|
|
||||||
"default": 6499
|
|
||||||
},
|
|
||||||
"hostname": {
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The hostname to attach and debug.",
|
"description": "The URL of the Bun process to attach to.",
|
||||||
"default": "localhost"
|
"default": "ws://localhost:6499/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,12 +188,13 @@
|
|||||||
"type": "bun",
|
"type": "bun",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Bun: Debug",
|
"name": "Bun: Debug",
|
||||||
"program": "${workspaceFolder}/${command:AskForProgramName}"
|
"program": "${file}"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "bun",
|
"type": "bun",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"name": "Bun: Attach"
|
"name": "Bun: Attach",
|
||||||
|
"url": "ws://localhost:6499/"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"configurationSnippets": [
|
"configurationSnippets": [
|
||||||
@@ -135,7 +205,7 @@
|
|||||||
"type": "bun",
|
"type": "bun",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Ask for file name",
|
"name": "Ask for file name",
|
||||||
"program": "^\"\\${workspaceFolder}/\\${command:AskForProgramName}\""
|
"program": "^\"\\${file}\""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -145,14 +215,10 @@
|
|||||||
"type": "bun",
|
"type": "bun",
|
||||||
"request": "attach",
|
"request": "attach",
|
||||||
"name": "Attach to Bun",
|
"name": "Attach to Bun",
|
||||||
"port": 6499,
|
"url": "ws://localhost:6499/"
|
||||||
"hostname": "localhost"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"variables": {
|
|
||||||
"AskForProgramName": "extension.bun.getProgramName"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"languages": [
|
"languages": [
|
||||||
@@ -165,15 +231,15 @@
|
|||||||
".lockb"
|
".lockb"
|
||||||
],
|
],
|
||||||
"icon": {
|
"icon": {
|
||||||
"dark": "./src/assets/icon-small.png",
|
"dark": "src/assets/icon-small.png",
|
||||||
"light": "./src/assets/icon-small.png"
|
"light": "src/assets/icon-small.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"jsonValidation": [
|
"jsonValidation": [
|
||||||
{
|
{
|
||||||
"fileMatch": "package.json",
|
"fileMatch": "package.json",
|
||||||
"url": "./src/resources/package.json"
|
"url": "src/resources/package.json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"customEditors": [
|
"customEditors": [
|
||||||
@@ -189,26 +255,40 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"description": "Visual Studio Code extension for Bun.",
|
"description": "The Visual Studio Code extension for Bun.",
|
||||||
|
"displayName": "Bun",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.66.0"
|
"vscode": "^1.81.0"
|
||||||
},
|
},
|
||||||
|
"extensionKind": [
|
||||||
|
"workspace"
|
||||||
|
],
|
||||||
"galleryBanner": {
|
"galleryBanner": {
|
||||||
"color": "#C80000",
|
"color": "#C80000",
|
||||||
"theme": "dark"
|
"theme": "dark"
|
||||||
},
|
},
|
||||||
"icon": "./src/assets/icon.png",
|
"homepage": "https://bun.sh/",
|
||||||
|
"icon": "src/assets/icon.png",
|
||||||
|
"keywords": [
|
||||||
|
"bun",
|
||||||
|
"node.js",
|
||||||
|
"javascript",
|
||||||
|
"typescript",
|
||||||
|
"vscode"
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"private": true,
|
|
||||||
"publisher": "oven",
|
"publisher": "oven",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bunx esbuild src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs",
|
"bundle": "./node_modules/.bin/esbuild src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs",
|
||||||
"build:watch": "bunx esbuild --watch src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs"
|
"prebuild": "bun run bundle && rm -rf extension && mkdir -p extension/src && cp -r dist extension/dist && cp -r src/assets extension/src/assets && cp package.json extension && cp README.md extension",
|
||||||
|
"build": "cd extension && vsce package",
|
||||||
|
"build:watch": "./node_modules/.bin/esbuild --watch src/extension.ts src/web-extension.ts --bundle --external:vscode --outdir=dist --platform=node --format=cjs"
|
||||||
},
|
},
|
||||||
"workspaceTrust": {
|
"workspaceTrust": {
|
||||||
"request": "never"
|
"request": "never"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"workspaces": [
|
||||||
"ws": "^8.13.0"
|
"../bun-debug-adapter-protocol",
|
||||||
}
|
"../bun-inspector-protocol"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 * as vscode from "vscode";
|
||||||
import { activateBunDebug } from "./activate";
|
import activateLockfile from "./features/lockfile";
|
||||||
|
import activateDebug from "./features/debug";
|
||||||
const runMode: "external" | "server" | "namedPipeServer" | "inline" = "inline";
|
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
if (runMode === "inline") {
|
activateLockfile(context);
|
||||||
activateBunDebug(context);
|
activateDebug(context);
|
||||||
return;
|
|
||||||
}
|
|
||||||
throw new Error(`This extension does not support '${runMode}' mode.`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deactivate() {
|
export function deactivate() {}
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
|
|||||||
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) => {
|
process.stderr.on("data", (data: Buffer) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
process.on("error", (error) => {
|
process.on("error", error => {
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
process.on("exit", (code) => {
|
process.on("exit", code => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve(stdout);
|
resolve(stdout);
|
||||||
} else {
|
} else {
|
||||||
@@ -65,19 +65,15 @@ function previewLockfile(uri: vscode.Uri, token?: vscode.CancellationToken): Pro
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function(context: vscode.ExtensionContext): void {
|
export default function (context: vscode.ExtensionContext): void {
|
||||||
const viewType = "bun.lockb";
|
const viewType = "bun.lockb";
|
||||||
const provider = new BunLockfileEditorProvider(context);
|
const provider = new BunLockfileEditorProvider(context);
|
||||||
|
|
||||||
vscode.window.registerCustomEditorProvider(
|
vscode.window.registerCustomEditorProvider(viewType, provider, {
|
||||||
viewType,
|
supportsMultipleEditorsPerDocument: true,
|
||||||
provider,
|
webviewOptions: {
|
||||||
{
|
enableFindWidget: true,
|
||||||
supportsMultipleEditorsPerDocument: true,
|
retainContextWhenHidden: true,
|
||||||
webviewOptions: {
|
|
||||||
enableFindWidget: true,
|
|
||||||
retainContextWhenHidden: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
@@ -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 * as vscode from "vscode";
|
||||||
import { activateBunDebug } from "./activate";
|
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext) {
|
export function activate(context: vscode.ExtensionContext) {}
|
||||||
activateBunDebug(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function deactivate() {
|
export function deactivate() {}
|
||||||
// No-op
|
|
||||||
}
|
|
||||||
|
|||||||
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,
|
"esModuleInterop": true,
|
||||||
"isolatedModules": false,
|
"isolatedModules": false,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"types": [
|
"types": ["bun-types"]
|
||||||
"bun-types"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["src", "test", "types", "../../bun-devtools", "../../bun-debug-adapter-protocol"],
|
||||||
"src",
|
"exclude": ["node_modules"]
|
||||||
"test",
|
|
||||||
"types"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
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