mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
Add remoteRoot/localRoot mapping for VSCode (#19884)
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com> Co-authored-by: robobun <robobun@oven.sh>
This commit is contained in:
@@ -95,6 +95,9 @@ You can use the following configurations to debug JavaScript and TypeScript file
|
||||
// The URL of the WebSocket inspector to attach to.
|
||||
// This value can be retrieved by using `bun --inspect`.
|
||||
"url": "ws://localhost:6499/",
|
||||
// Optional path mapping for remote debugging
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/app",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
@@ -279,6 +279,14 @@
|
||||
"type": "boolean",
|
||||
"description": "If the debugger should stop on the first line of the program.",
|
||||
"default": false
|
||||
},
|
||||
"localRoot": {
|
||||
"type": "string",
|
||||
"description": "The local path that maps to \"remoteRoot\" when attaching to a remote Bun process."
|
||||
},
|
||||
"remoteRoot": {
|
||||
"type": "string",
|
||||
"description": "The remote path to the code when attaching. File paths reported by Bun that start with this path will be mapped back to 'localRoot'."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DebugSession, OutputEvent } from "@vscode/debugadapter";
|
||||
import { tmpdir } from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { join } from "node:path";
|
||||
import * as vscode from "vscode";
|
||||
import {
|
||||
@@ -220,7 +221,7 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
|
||||
session: vscode.DebugSession,
|
||||
): Promise<vscode.ProviderResult<vscode.DebugAdapterDescriptor>> {
|
||||
const { configuration } = session;
|
||||
const { request, url, __untitledName } = configuration;
|
||||
const { request, url, __untitledName, localRoot, remoteRoot } = configuration;
|
||||
|
||||
if (request === "attach") {
|
||||
for (const [adapterUrl, adapter] of adapters) {
|
||||
@@ -230,7 +231,10 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
|
||||
}
|
||||
}
|
||||
|
||||
const adapter = new FileDebugSession(session.id, __untitledName);
|
||||
const adapter = new FileDebugSession(session.id, __untitledName, {
|
||||
localRoot,
|
||||
remoteRoot,
|
||||
});
|
||||
await adapter.initialize();
|
||||
return new vscode.DebugAdapterInlineImplementation(adapter);
|
||||
}
|
||||
@@ -275,6 +279,11 @@ interface RuntimeExceptionThrownEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface PathMapping {
|
||||
localRoot?: string;
|
||||
remoteRoot?: string;
|
||||
}
|
||||
|
||||
class FileDebugSession extends DebugSession {
|
||||
// If these classes are moved/published, we should make sure
|
||||
// we remove these non-null assertions so consumers of
|
||||
@@ -283,18 +292,60 @@ class FileDebugSession extends DebugSession {
|
||||
sessionId?: string;
|
||||
untitledDocPath?: string;
|
||||
bunEvalPath?: string;
|
||||
localRoot?: string;
|
||||
remoteRoot?: string;
|
||||
#isWindowsRemote = false;
|
||||
|
||||
constructor(sessionId?: string, untitledDocPath?: string) {
|
||||
constructor(sessionId?: string, untitledDocPath?: string, mapping?: PathMapping) {
|
||||
super();
|
||||
this.sessionId = sessionId;
|
||||
this.untitledDocPath = untitledDocPath;
|
||||
|
||||
if (mapping) {
|
||||
this.localRoot = mapping.localRoot;
|
||||
this.remoteRoot = mapping.remoteRoot;
|
||||
if (typeof mapping.remoteRoot === "string") {
|
||||
this.#isWindowsRemote = mapping.remoteRoot.includes("\\");
|
||||
}
|
||||
}
|
||||
|
||||
if (untitledDocPath) {
|
||||
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath ?? process.cwd();
|
||||
this.bunEvalPath = join(cwd, "[eval]");
|
||||
}
|
||||
}
|
||||
|
||||
mapRemoteToLocal(p: string | undefined): string | undefined {
|
||||
if (!p || !this.remoteRoot || !this.localRoot) return p;
|
||||
const remoteModule = this.#isWindowsRemote ? path.win32 : path.posix;
|
||||
let remoteRoot = remoteModule.normalize(this.remoteRoot);
|
||||
if (!remoteRoot.endsWith(remoteModule.sep)) remoteRoot += remoteModule.sep;
|
||||
let target = remoteModule.normalize(p);
|
||||
const starts = this.#isWindowsRemote
|
||||
? target.toLowerCase().startsWith(remoteRoot.toLowerCase())
|
||||
: target.startsWith(remoteRoot);
|
||||
if (starts) {
|
||||
const rel = target.slice(remoteRoot.length);
|
||||
const localRel = rel.split(remoteModule.sep).join(path.sep);
|
||||
return path.join(this.localRoot, localRel);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
mapLocalToRemote(p: string | undefined): string | undefined {
|
||||
if (!p || !this.remoteRoot || !this.localRoot) return p;
|
||||
let localRoot = path.normalize(this.localRoot);
|
||||
if (!localRoot.endsWith(path.sep)) localRoot += path.sep;
|
||||
let localPath = path.normalize(p);
|
||||
if (localPath.startsWith(localRoot)) {
|
||||
const rel = localPath.slice(localRoot.length);
|
||||
const remoteModule = this.#isWindowsRemote ? path.win32 : path.posix;
|
||||
const remoteRel = rel.split(path.sep).join(remoteModule.sep);
|
||||
return remoteModule.join(this.remoteRoot, remoteRel);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
const uniqueId = this.sessionId ?? Math.random().toString(36).slice(2);
|
||||
const url =
|
||||
@@ -307,14 +358,20 @@ class FileDebugSession extends DebugSession {
|
||||
|
||||
if (untitledDocPath) {
|
||||
this.adapter.on("Adapter.response", (response: DebugProtocolResponse) => {
|
||||
if (response.body?.source?.path === bunEvalPath) {
|
||||
response.body.source.path = untitledDocPath;
|
||||
if (response.body?.source?.path) {
|
||||
if (response.body.source.path === bunEvalPath) {
|
||||
response.body.source.path = untitledDocPath;
|
||||
} else {
|
||||
response.body.source.path = this.mapRemoteToLocal(response.body.source.path);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(response.body?.breakpoints)) {
|
||||
for (const bp of response.body.breakpoints) {
|
||||
if (bp.source?.path === bunEvalPath) {
|
||||
bp.source.path = untitledDocPath;
|
||||
bp.verified = true;
|
||||
} else if (bp.source?.path) {
|
||||
bp.source.path = this.mapRemoteToLocal(bp.source.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,14 +379,35 @@ class FileDebugSession extends DebugSession {
|
||||
});
|
||||
|
||||
this.adapter.on("Adapter.event", (event: DebugProtocolEvent) => {
|
||||
if (event.body?.source?.path === bunEvalPath) {
|
||||
event.body.source.path = untitledDocPath;
|
||||
if (event.body?.source?.path) {
|
||||
if (event.body.source.path === bunEvalPath) {
|
||||
event.body.source.path = untitledDocPath;
|
||||
} else {
|
||||
event.body.source.path = this.mapRemoteToLocal(event.body.source.path);
|
||||
}
|
||||
}
|
||||
this.sendEvent(event);
|
||||
});
|
||||
} else {
|
||||
this.adapter.on("Adapter.response", response => this.sendResponse(response));
|
||||
this.adapter.on("Adapter.event", event => this.sendEvent(event));
|
||||
this.adapter.on("Adapter.response", (response: DebugProtocolResponse) => {
|
||||
if (response.body?.source?.path) {
|
||||
response.body.source.path = this.mapRemoteToLocal(response.body.source.path);
|
||||
}
|
||||
if (Array.isArray(response.body?.breakpoints)) {
|
||||
for (const bp of response.body.breakpoints) {
|
||||
if (bp.source?.path) {
|
||||
bp.source.path = this.mapRemoteToLocal(bp.source.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.sendResponse(response);
|
||||
});
|
||||
this.adapter.on("Adapter.event", (event: DebugProtocolEvent) => {
|
||||
if (event.body?.source?.path) {
|
||||
event.body.source.path = this.mapRemoteToLocal(event.body.source.path);
|
||||
}
|
||||
this.sendEvent(event);
|
||||
});
|
||||
}
|
||||
|
||||
this.adapter.on("Adapter.reverseRequest", ({ command, arguments: args }) =>
|
||||
@@ -345,11 +423,15 @@ class FileDebugSession extends DebugSession {
|
||||
if (type === "request") {
|
||||
const { untitledDocPath, bunEvalPath } = this;
|
||||
const { command } = message;
|
||||
if (untitledDocPath && (command === "setBreakpoints" || command === "breakpointLocations")) {
|
||||
if (command === "setBreakpoints" || command === "breakpointLocations") {
|
||||
const args = message.arguments as any;
|
||||
if (args.source?.path === untitledDocPath) {
|
||||
if (untitledDocPath && args.source?.path === untitledDocPath) {
|
||||
args.source.path = bunEvalPath;
|
||||
} else if (args.source?.path) {
|
||||
args.source.path = this.mapLocalToRemote(args.source.path);
|
||||
}
|
||||
} else if (command === "source" && message.arguments?.source?.path) {
|
||||
message.arguments.source.path = this.mapLocalToRemote(message.arguments.source.path);
|
||||
}
|
||||
|
||||
this.adapter.emit("Adapter.request", message);
|
||||
@@ -367,7 +449,7 @@ class TerminalDebugSession extends FileDebugSession {
|
||||
signal!: TCPSocketSignal | UnixSignal;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
super(undefined, undefined);
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
|
||||
Reference in New Issue
Block a user