Skip to content

Add remoteRoot/localRoot mapping for VSCode #19884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/bun-vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
],
}
Expand Down
8 changes: 8 additions & 0 deletions packages/bun-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,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'."
}
}
}
Expand Down
106 changes: 94 additions & 12 deletions packages/bun-vscode/src/features/debug.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand All @@ -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 =
Expand All @@ -307,29 +358,56 @@ 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);
}
}
}
this.sendResponse(response);
});

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 }) =>
Expand All @@ -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);
Expand All @@ -367,7 +449,7 @@ class TerminalDebugSession extends FileDebugSession {
signal!: TCPSocketSignal | UnixSignal;

constructor() {
super();
super(undefined, undefined);
}

async initialize() {
Expand Down
1 change: 0 additions & 1 deletion src/cli/audit_command.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ const HeaderBuilder = http.HeaderBuilder;
const MutableString = bun.MutableString;
const URL = @import("../url.zig").URL;
const logger = bun.logger;
const semver = @import("../semver.zig");
const libdeflate = @import("../deps/libdeflate.zig");

const VulnerabilityInfo = struct {
Expand Down
1 change: 0 additions & 1 deletion test/regression/issue/18547.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ test("18547", async () => {
expect(request.cookies.get("sessionToken")).toEqual("123456");
expect(clone.cookies.get("sessionToken")).toEqual("654321");


return new Response("OK");
},
},
Expand Down