diff --git a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts index fca2d9677fdf0f..ddf5a9ff253f99 100644 --- a/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts +++ b/packages/bun-debug-adapter-protocol/src/debugger/adapter.ts @@ -2,12 +2,26 @@ import type { InspectorEventMap } from "../../../bun-inspector-protocol/src/insp import type { JSC } from "../../../bun-inspector-protocol/src/protocol"; import type { DAP } from "../protocol"; // @ts-ignore -import type { ChildProcess } from "node:child_process"; -import { spawn } from "node:child_process"; +import { spawn, ChildProcess } from "node:child_process"; import { EventEmitter } from "node:events"; import { WebSocketInspector, remoteObjectToString } from "../../../bun-inspector-protocol/index"; -import { UnixSignal, randomUnixPath } from "./signal"; +import { randomUnixPath, TCPSocketSignal, UnixSignal } from "./signal"; import { Location, SourceMap } from "./sourcemap"; +import { createServer, AddressInfo } from "node:net"; +import * as path from "node:path"; + +export async function getAvailablePort(): Promise { + const server = createServer(); + server.listen(0); + return new Promise((resolve, reject) => { + server.on("listening", () => { + const { port } = server.address() as AddressInfo; + server.close(() => { + resolve(port); + }); + }); + }); +} const capabilities: DAP.Capabilities = { supportsConfigurationDoneRequest: true, @@ -489,36 +503,73 @@ export class DebugAdapter extends EventEmitter implements ...env, }; - const url = `ws+unix://${randomUnixPath()}`; - const signal = new UnixSignal(); + if (process.platform !== "win32") { + // we're on unix + const url = `ws+unix://${randomUnixPath()}`; + const signal = new UnixSignal(); - signal.on("Signal.received", () => { - this.#attach({ url }); - }); + signal.on("Signal.received", () => { + this.#attach({ url }); + }); - this.once("Adapter.terminated", () => { - signal.close(); - }); + this.once("Adapter.terminated", () => { + signal.close(); + }); - const query = stopOnEntry ? "break=1" : "wait=1"; - processEnv["BUN_INSPECT"] = `${url}?${query}`; - processEnv["BUN_INSPECT_NOTIFY"] = signal.url; + const query = stopOnEntry ? "break=1" : "wait=1"; + processEnv["BUN_INSPECT"] = `${url}?${query}`; + processEnv["BUN_INSPECT_NOTIFY"] = signal.url; + + // This is probably not correct, but it's the best we can do for now. + processEnv["FORCE_COLOR"] = "1"; + processEnv["BUN_QUIET_DEBUG_LOGS"] = "1"; + processEnv["BUN_DEBUG_QUIET_LOGS"] = "1"; + + const started = await this.#spawn({ + command: runtime, + args: processArgs, + env: processEnv, + cwd, + isDebugee: true, + }); - // This is probably not correct, but it's the best we can do for now. - processEnv["FORCE_COLOR"] = "1"; - processEnv["BUN_QUIET_DEBUG_LOGS"] = "1"; - processEnv["BUN_DEBUG_QUIET_LOGS"] = "1"; + if (!started) { + throw new Error("Program could not be started."); + } + } else { + // we're on windows + // Create TCPSocketSignal + const url = `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`; // 127.0.0.1 so it resolves correctly on windows + const signal = new TCPSocketSignal(await getAvailablePort()); - const started = await this.#spawn({ - command: runtime, - args: processArgs, - env: processEnv, - cwd, - isDebugee: true, - }); + signal.on("Signal.received", async () => { + this.#attach({ url }); + }); - if (!started) { - throw new Error("Program could not be started."); + this.once("Adapter.terminated", () => { + signal.close(); + }); + + const query = stopOnEntry ? "break=1" : "wait=1"; + processEnv["BUN_INSPECT"] = `${url}?${query}`; + processEnv["BUN_INSPECT_NOTIFY"] = signal.url; // 127.0.0.1 so it resolves correctly on windows + + // This is probably not correct, but it's the best we can do for now. + processEnv["FORCE_COLOR"] = "1"; + processEnv["BUN_QUIET_DEBUG_LOGS"] = "1"; + processEnv["BUN_DEBUG_QUIET_LOGS"] = "1"; + + const started = await this.#spawn({ + command: runtime, + args: processArgs, + env: processEnv, + cwd, + isDebugee: true, + }); + + if (!started) { + throw new Error("Program could not be started."); + } } } @@ -684,6 +735,9 @@ export class DebugAdapter extends EventEmitter implements async breakpointLocations(request: DAP.BreakpointLocationsRequest): Promise { const { line, endLine, column, endColumn, source: source0 } = request; + if (process.platform === "win32") { + source0.path = source0.path ? normalizeWindowsPath(source0.path) : source0.path; + } const source = await this.#getSource(sourceToId(source0)); const { locations } = await this.send("Debugger.getBreakpointLocations", { @@ -788,6 +842,9 @@ export class DebugAdapter extends EventEmitter implements } async #setBreakpointsByUrl(url: string, requests: DAP.SourceBreakpoint[], unsetOld?: boolean): Promise { + if (process.platform === "win32") { + url = url ? normalizeWindowsPath(url) : url; + } const source = this.#getSourceIfPresent(url); // If the source is not loaded, set a placeholder breakpoint at the start of the file. @@ -1161,6 +1218,9 @@ export class DebugAdapter extends EventEmitter implements async gotoTargets(request: DAP.GotoTargetsRequest): Promise { const { source: source0 } = request; + if (process.platform === "win32") { + source0.path = source0.path ? normalizeWindowsPath(source0.path) : source0.path; + } const source = await this.#getSource(sourceToId(source0)); const { breakpoints } = await this.breakpointLocations(request); @@ -1327,7 +1387,7 @@ export class DebugAdapter extends EventEmitter implements // 1. If it has a `path`, the client retrieves the source from the file system. // 2. If it has a `sourceReference`, the client sends a `source` request. // Moreover, the code is usually shown in a read-only editor. - const isUserCode = url.startsWith("/"); + const isUserCode = path.isAbsolute(url); const sourceMap = SourceMap(sourceMapURL); const name = sourceName(url); const presentationHint = sourcePresentationHint(url); @@ -1646,12 +1706,11 @@ export class DebugAdapter extends EventEmitter implements // If the source does not have a path or is a builtin module, // it cannot be retrieved from the file system. - if (typeof sourceId === "number" || !sourceId.startsWith("/")) { + if (typeof sourceId === "number" || !path.isAbsolute(sourceId)) { throw new Error(`Source not found: ${sourceId}`); } // If the source is not present, it may not have been loaded yet. - // In that case, wait for it to be loaded. let resolves = this.#pendingSources.get(sourceId); if (!resolves) { this.#pendingSources.set(sourceId, (resolves = [])); @@ -2107,7 +2166,6 @@ export class DebugAdapter extends EventEmitter implements close(): void { this.#process?.kill(); - // this.#signal?.close(); this.#inspector.close(); this.#reset(); } @@ -2149,10 +2207,10 @@ function titleize(name: string): string { } function sourcePresentationHint(url?: string): DAP.Source["presentationHint"] { - if (!url || !url.startsWith("/")) { + if (!url || !path.isAbsolute(url)) { return "deemphasize"; } - if (url.includes("/node_modules/")) { + if (url.includes("/node_modules/") || url.includes("\\node_modules\\")) { return "normal"; } return "emphasize"; @@ -2163,6 +2221,9 @@ function sourceName(url?: string): string { return "unknown.js"; } if (isJavaScript(url)) { + if (process.platform === "win32") { + url = url.replaceAll("\\", "/"); + } return url.split("/").pop() || url; } return `${url}.js`; @@ -2567,3 +2628,15 @@ let sequence = 1; function nextId(): number { return sequence++; } + +export function getRandomId() { + return Math.random().toString(36).slice(2); +} + +export function normalizeWindowsPath(winPath: string): string { + winPath = path.normalize(winPath); + if (winPath[1] === ":" && (winPath[2] === "\\" || winPath[2] === "/")) { + return (winPath.charAt(0).toUpperCase() + winPath.slice(1)).replaceAll("\\\\", "\\"); + } + return winPath; +} diff --git a/packages/bun-debug-adapter-protocol/src/debugger/signal.ts b/packages/bun-debug-adapter-protocol/src/debugger/signal.ts index 6cd6e5ca3bf64c..db2b029a972b8d 100644 --- a/packages/bun-debug-adapter-protocol/src/debugger/signal.ts +++ b/packages/bun-debug-adapter-protocol/src/debugger/signal.ts @@ -1,5 +1,5 @@ import { EventEmitter } from "node:events"; -import type { Server } from "node:net"; +import type { Server, Socket } from "node:net"; import { createServer } from "node:net"; import { tmpdir } from "node:os"; import { join } from "node:path"; @@ -85,3 +85,75 @@ function parseUnixPath(path: string | URL): string { throw new Error(`Invalid UNIX path: ${path}`); } } + +export type TCPSocketSignalEventMap = { + "Signal.listening": []; + "Signal.error": [Error]; + "Signal.closed": []; + "Signal.received": [string]; +}; + +export class TCPSocketSignal extends EventEmitter { + #port: number; + #server: ReturnType; + #ready: Promise; + + constructor(port: number) { + super(); + this.#port = port; + + this.#server = createServer((socket: Socket) => { + socket.on("data", data => { + this.emit("Signal.received", data.toString()); + }); + + socket.on("error", error => { + this.emit("Signal.error", error); + }); + + socket.on("close", () => { + this.emit("Signal.closed"); + }); + }); + + this.#ready = new Promise((resolve, reject) => { + this.#server.listen(this.#port, () => { + this.emit("Signal.listening"); + resolve(); + }); + this.#server.on("error", reject); + }); + } + + emit(event: E, ...args: TCPSocketSignalEventMap[E]): boolean { + if (isDebug) { + console.log(event, ...args); + } + return super.emit(event, ...args); + } + + /** + * The TCP port. + */ + get port(): number { + return this.#port; + } + + get url(): string { + return `tcp://127.0.0.1:${this.#port}`; + } + + /** + * Resolves when the server is listening or rejects if an error occurs. + */ + get ready(): Promise { + return this.#ready; + } + + /** + * Closes the server. + */ + close(): void { + this.#server.close(); + } +} diff --git a/packages/bun-vscode/bun.lockb b/packages/bun-vscode/bun.lockb index 1f071eb387a7b9..9433b0c6deda1a 100755 Binary files a/packages/bun-vscode/bun.lockb and b/packages/bun-vscode/bun.lockb differ diff --git a/packages/bun-vscode/example/hello.js b/packages/bun-vscode/example/hello.js deleted file mode 100644 index 895e5479044908..00000000000000 --- a/packages/bun-vscode/example/hello.js +++ /dev/null @@ -1,8 +0,0 @@ -console.log("HELLO"); -console.log("HELLO 2"); -console.log("HELLO 3"); -a(); - -function a() { - console.log("HELLO 4"); -} diff --git a/packages/bun-vscode/example/hello.ts b/packages/bun-vscode/example/hello.ts new file mode 100644 index 00000000000000..ccd2773908ef4c --- /dev/null +++ b/packages/bun-vscode/example/hello.ts @@ -0,0 +1,9 @@ +type OS = "Windows"; + +Bun.serve({ + fetch(req: Request) { + return new Response( + `Hello, ${"Windows" as OS}!` + ); + } +}); diff --git a/packages/bun-vscode/package.json b/packages/bun-vscode/package.json index 501257eb0cc5be..cdaecb909984d7 100644 --- a/packages/bun-vscode/package.json +++ b/packages/bun-vscode/package.json @@ -1,6 +1,6 @@ { "name": "bun-vscode", - "version": "0.0.8", + "version": "0.0.13", "author": "oven", "repository": { "type": "git", diff --git a/packages/bun-vscode/scripts/build.mjs b/packages/bun-vscode/scripts/build.mjs index c2281c467a0486..73f88b014c5e57 100644 --- a/packages/bun-vscode/scripts/build.mjs +++ b/packages/bun-vscode/scripts/build.mjs @@ -1,8 +1,12 @@ import { buildSync } from "esbuild"; -import { spawnSync } from "node:child_process"; +import { execSync } from "node:child_process"; import { cpSync, mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; -const { pathname } = new URL("..", import.meta.url); +let { pathname } = new URL("..", import.meta.url); +if (process.platform === "win32") { + pathname = path.normalize(pathname).substring(1); // remove leading slash +} process.chdir(pathname); buildSync({ @@ -26,7 +30,7 @@ cpSync("LICENSE", "extension/LICENSE"); cpSync("package.json", "extension/package.json"); const cmd = process.isBun ? "bunx" : "npx"; -spawnSync(cmd, ["vsce", "package"], { +execSync(`${cmd} vsce package --no-dependencies`, { cwd: "extension", stdio: "inherit", }); diff --git a/packages/bun-vscode/scripts/test.mjs b/packages/bun-vscode/scripts/test.mjs index e844fa2a664c21..105f577ac0e1b6 100644 --- a/packages/bun-vscode/scripts/test.mjs +++ b/packages/bun-vscode/scripts/test.mjs @@ -1,21 +1,25 @@ -import { spawn } from "node:child_process"; +import { exec } from "node:child_process"; import { readdirSync } from "node:fs"; +import path from "node:path"; -const { pathname } = new URL("..", import.meta.url); +let { pathname } = new URL("..", import.meta.url); +if (process.platform === "win32") { + pathname = path.normalize(pathname).substring(1); // remove leading slash +} process.chdir(pathname); -let path; +let extPath; for (const filename of readdirSync("extension")) { if (filename.endsWith(".vsix")) { - path = `extension/${filename}`; + extPath = `extension/${filename}`; break; } } -if (!path) { +if (!extPath) { throw new Error("No .vsix file found"); } -spawn("code", ["--new-window", `--install-extension=${path}`, `--extensionDevelopmentPath=${pathname}`, "example"], { +exec(`code --new-window --install-extension=${path} --extensionDevelopmentPath=${pathname} example`, { stdio: "inherit", }); diff --git a/packages/bun-vscode/src/features/debug.ts b/packages/bun-vscode/src/features/debug.ts index 88434622a1f8be..c6fc47f0c4d3d0 100644 --- a/packages/bun-vscode/src/features/debug.ts +++ b/packages/bun-vscode/src/features/debug.ts @@ -1,8 +1,8 @@ import { DebugSession } from "@vscode/debugadapter"; import { tmpdir } from "node:os"; import * as vscode from "vscode"; -import type { DAP } from "../../../bun-debug-adapter-protocol"; -import { DebugAdapter, UnixSignal } from "../../../bun-debug-adapter-protocol"; +import { DAP, TCPSocketSignal } from "../../../bun-debug-adapter-protocol"; +import { DebugAdapter, getAvailablePort, UnixSignal, getRandomId } from "../../../bun-debug-adapter-protocol"; export const DEBUG_CONFIGURATION: vscode.DebugConfiguration = { type: "bun", @@ -81,7 +81,7 @@ function debugFileCommand(resource?: vscode.Uri) { if (path) debugCommand(path); } -function injectDebugTerminal(terminal: vscode.Terminal): void { +async function injectDebugTerminal(terminal: vscode.Terminal): Promise { if (!getConfig("debugTerminal.enabled")) return; const { name, creationOptions } = terminal; @@ -97,14 +97,16 @@ function injectDebugTerminal(terminal: vscode.Terminal): void { const stopOnEntry = getConfig("debugTerminal.stopOnEntry") === true; const query = stopOnEntry ? "break=1" : "wait=1"; - const { adapter, signal } = new TerminalDebugSession(); + const debugSession = new TerminalDebugSession(); + await debugSession.initialize(); + const { adapter, signal } = debugSession; const debug = vscode.window.createTerminal({ ...creationOptions, name: "JavaScript Debug Terminal", env: { ...env, "BUN_INSPECT": `${adapter.url}?${query}`, - "BUN_INSPECT_NOTIFY": `${signal.url}`, + "BUN_INSPECT_NOTIFY": signal.url, }, }); @@ -153,7 +155,9 @@ class DebugConfigurationProvider implements vscode.DebugConfigurationProvider { } class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory { - createDebugAdapterDescriptor(session: vscode.DebugSession): vscode.ProviderResult { + async createDebugAdapterDescriptor( + session: vscode.DebugSession, + ): Promise> { const { configuration } = session; const { request, url } = configuration; @@ -166,18 +170,28 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory } const adapter = new FileDebugSession(session.id); + await adapter.initialize(); return new vscode.DebugAdapterInlineImplementation(adapter); } } class FileDebugSession extends DebugSession { - readonly adapter: DebugAdapter; + adapter: DebugAdapter; + sessionId?: string; constructor(sessionId?: string) { super(); - const uniqueId = sessionId ?? Math.random().toString(36).slice(2); - const url = `ws+unix://${tmpdir()}/${uniqueId}.sock`; + this.sessionId = sessionId; + } + async initialize() { + const uniqueId = this.sessionId ?? Math.random().toString(36).slice(2); + let url; + if (process.platform === "win32") { + url = `ws://127.0.0.1:${await getAvailablePort()}/${getRandomId()}`; + } else { + url = `ws+unix://${tmpdir()}/${uniqueId}.sock`; + } this.adapter = new DebugAdapter(url); this.adapter.on("Adapter.response", response => this.sendResponse(response)); this.adapter.on("Adapter.event", event => this.sendEvent(event)); @@ -204,11 +218,19 @@ class FileDebugSession extends DebugSession { } class TerminalDebugSession extends FileDebugSession { - readonly signal: UnixSignal; + signal: TCPSocketSignal | UnixSignal; constructor() { super(); - this.signal = new UnixSignal(); + } + + async initialize() { + await super.initialize(); + if (process.platform === "win32") { + this.signal = new TCPSocketSignal(await getAvailablePort()); + } else { + this.signal = new UnixSignal(); + } this.signal.on("Signal.received", () => { vscode.debug.startDebugging(undefined, { ...ATTACH_CONFIGURATION, @@ -222,7 +244,7 @@ class TerminalDebugSession extends FileDebugSession { name: "Bun Terminal", env: { "BUN_INSPECT": `${this.adapter.url}?wait=1`, - "BUN_INSPECT_NOTIFY": `${this.signal.url}`, + "BUN_INSPECT_NOTIFY": this.signal.url, }, isTransient: true, iconPath: new vscode.ThemeIcon("debug-console"), diff --git a/src/js/internal/debugger.ts b/src/js/internal/debugger.ts index 2ac52a7c7ada93..cf79349cd381ea 100644 --- a/src/js/internal/debugger.ts +++ b/src/js/internal/debugger.ts @@ -111,7 +111,7 @@ class Debugger { return; } - throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:', 'ws+unix:', or 'wss:')`); + throw new TypeError(`Unsupported protocol: '${protocol}' (expected 'ws:' or 'ws+unix:')`); } get #websocket(): WebSocketHandler {