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:
Jarred Sumner
2025-07-18 04:19:15 -07:00
committed by GitHub
parent 6ed245b889
commit f380458bae
3 changed files with 105 additions and 12 deletions

View File

@@ -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",
},
],
}

View File

@@ -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'."
}
}
}

View File

@@ -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() {