Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
a8fbf48519 Add remote debugging support to VSCode extension
Implement comprehensive remote debugging capabilities for WSL, Docker, and SSH environments with automatic path mapping and configuration support.

Features:
- Remote debugging configuration schema with address/port/path mapping
- Automatic path detection for WSL (/mnt/), Docker (/workspace), SSH scenarios
- Bidirectional path mapping between local and remote file systems
- Enhanced FileDebugSession with remote configuration support
- Pre-configured launch configurations for common remote scenarios
- Comprehensive documentation with setup guides and examples

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 03:52:05 +00:00
6 changed files with 600 additions and 9 deletions

44
.vscode/launch.json generated vendored
View File

@@ -339,6 +339,50 @@
"set print asm-demangle",
],
},
// Remote debugging configurations
{
"type": "bun",
"request": "attach",
"name": "[Remote] Attach to WSL",
"address": "localhost",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/mnt/c/path/to/project",
"skipFiles": ["<node_internals>/**"]
},
{
"type": "bun",
"request": "attach",
"name": "[Remote] Attach to Docker Container",
"address": "localhost",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/workspace",
"skipFiles": ["<node_internals>/**"]
},
{
"type": "bun",
"request": "attach",
"name": "[Remote] Attach to SSH Remote",
"address": "remote.server.com",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/user/project",
"skipFiles": ["<node_internals>/**"]
},
{
"type": "bun",
"request": "launch",
"name": "[Remote] Launch with Path Mapping",
"program": "${file}",
"cwd": "${workspaceFolder}",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/workspace",
"runtime": "bun",
"env": {
"NODE_ENV": "development"
}
},
],
"inputs": [
{

View File

@@ -23,7 +23,7 @@ At its core is the _Bun runtime_, a fast JavaScript runtime designed as a drop-i
- Live in-editor error messages (gif below)
- Test runner codelens
- Debugger support
- Debugger support with **remote debugging** for WSL, Docker, and SSH
- Run scripts from package.json
- Visual lockfile viewer for old binary lockfiles (`bun.lockb`)
@@ -120,5 +120,44 @@ You can use the following configurations to customize the behavior of the Bun ex
// The custom script to call for testing instead of `bun test`
"bun.test.customScript": "bun test",
// Remote debugging settings
"bun.remote.enabled": true, // Enable remote debugging
"bun.remote.autoDetectPaths": true, // Auto-detect path mappings
"bun.remote.defaultPort": 6499 // Default debug port
}
```
## Remote Debugging
The Bun extension supports remote debugging for applications running in WSL, Docker containers, and SSH remote environments. This allows you to debug Bun applications running in different environments while developing locally in VSCode.
### Quick Setup
1. **Start your Bun application with debugging enabled:**
```bash
bun --inspect=0.0.0.0:6499 your-script.js
```
2. **Add a remote attach configuration to your `launch.json`:**
```json
{
"type": "bun",
"request": "attach",
"name": "Attach to Remote",
"address": "localhost",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/workspace"
}
```
3. **Start debugging** by selecting your remote configuration and pressing F5.
### Supported Remote Environments
- **WSL (Windows Subsystem for Linux)**: Debug Bun apps running in WSL from Windows VSCode
- **Docker Containers**: Debug containerized Bun applications with automatic path mapping
- **SSH Remote**: Debug Bun applications running on remote servers
For detailed setup instructions and configuration examples, see [REMOTE-DEBUGGING.md](./REMOTE-DEBUGGING.md).

View File

@@ -0,0 +1,299 @@
# Remote Debugging with Bun VSCode Extension
This document explains how to use the remote debugging features of the Bun VSCode extension for debugging Bun applications in WSL, Docker containers, and SSH remote environments.
## Overview
The Bun VSCode extension now supports remote debugging scenarios including:
- **WSL (Windows Subsystem for Linux)**: Debug Bun applications running in WSL from Windows VSCode
- **Docker Containers**: Debug Bun applications running inside Docker containers
- **SSH Remote**: Debug Bun applications running on remote servers via SSH
## Configuration
### Extension Settings
The extension provides several settings to control remote debugging behavior:
```json
{
"bun.remote.enabled": true, // Enable remote debugging capabilities
"bun.remote.autoDetectPaths": true, // Auto-detect path mappings
"bun.remote.defaultPort": 6499 // Default debug port
}
```
### Launch Configuration Properties
Remote debugging configurations support the following properties:
| Property | Type | Description |
|----------|------|-------------|
| `address` | string | TCP/IP address of remote debugger (default: "localhost") |
| `port` | number | Debug port to connect to (default: 6499) |
| `localRoot` | string | Path to local source code directory |
| `remoteRoot` | string | Path to remote source code directory |
| `skipFiles` | array | Glob patterns for files to skip during debugging |
## Setup Instructions
### WSL Debugging
1. **Start Bun with debugging enabled in WSL:**
```bash
bun --inspect=0.0.0.0:6499 your-script.js
```
2. **Create a launch configuration in VSCode:**
```json
{
"type": "bun",
"request": "attach",
"name": "Attach to WSL",
"address": "localhost",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/mnt/c/path/to/your/project"
}
```
3. **Path mapping is automatic** when `bun.remote.autoDetectPaths` is enabled.
### Docker Container Debugging
1. **Dockerfile setup:**
```dockerfile
FROM oven/bun:latest
WORKDIR /workspace
COPY . .
EXPOSE 6499
CMD ["bun", "--inspect=0.0.0.0:6499", "index.js"]
```
2. **Start container with port forwarding:**
```bash
docker run -p 6499:6499 -v "$(pwd)":/workspace your-app
```
3. **Launch configuration:**
```json
{
"type": "bun",
"request": "attach",
"name": "Attach to Docker",
"address": "localhost",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/workspace"
}
```
### SSH Remote Debugging
1. **Start Bun on remote server:**
```bash
# On remote server
bun --inspect=0.0.0.0:6499 your-script.js
```
2. **Set up SSH tunnel (optional but recommended):**
```bash
# On local machine
ssh -L 6499:localhost:6499 user@remote-server.com
```
3. **Launch configuration:**
```json
{
"type": "bun",
"request": "attach",
"name": "Attach to SSH Remote",
"address": "localhost", // or remote-server.com if no tunnel
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/user/project"
}
```
## Example Launch Configurations
Here are complete launch configuration examples for common scenarios:
### WSL Configuration
```json
{
"type": "bun",
"request": "attach",
"name": "[Remote] Attach to WSL",
"address": "localhost",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/mnt/c/Users/username/project",
"skipFiles": ["<node_internals>/**"]
}
```
### Docker Configuration
```json
{
"type": "bun",
"request": "attach",
"name": "[Remote] Attach to Docker Container",
"address": "localhost",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/workspace",
"skipFiles": ["<node_internals>/**"]
}
```
### SSH Remote Configuration
```json
{
"type": "bun",
"request": "attach",
"name": "[Remote] Attach to SSH Remote",
"address": "remote.server.com",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/home/user/project",
"skipFiles": ["<node_internals>/**"]
}
```
### Launch with Path Mapping
```json
{
"type": "bun",
"request": "launch",
"name": "[Remote] Launch with Path Mapping",
"program": "${file}",
"cwd": "${workspaceFolder}",
"localRoot": "${workspaceFolder}",
"remoteRoot": "/workspace",
"runtime": "bun",
"env": {
"NODE_ENV": "development"
}
}
```
## Path Mapping
Path mapping is crucial for remote debugging to work correctly. The extension automatically maps file paths between your local development environment and the remote execution environment.
### Automatic Path Detection
When `bun.remote.autoDetectPaths` is enabled, the extension attempts to automatically detect appropriate path mappings:
- **WSL**: Converts Windows paths to `/mnt/` paths
- **Docker**: Maps to `/workspace` by default
- **SSH**: Preserves directory structure
### Manual Path Mapping
For complex scenarios, specify explicit path mappings:
```json
{
"localRoot": "/Users/dev/my-project",
"remoteRoot": "/app/source"
}
```
This maps:
- Local: `/Users/dev/my-project/src/index.ts`
- Remote: `/app/source/src/index.ts`
## Troubleshooting
### Common Issues
1. **Breakpoints not hitting:**
- Verify path mapping is correct
- Check that source maps are enabled
- Ensure remote Bun process is running with `--inspect`
2. **Connection refused:**
- Verify the debug port is open and accessible
- Check firewall settings
- Ensure port forwarding is set up correctly for Docker/SSH
3. **Source files not found:**
- Review `localRoot` and `remoteRoot` settings
- Enable `bun.remote.autoDetectPaths` for automatic detection
- Use absolute paths in configuration
### Debug Information
Enable debug logging to troubleshoot issues:
```bash
# Set environment variables for detailed logging
BUN_DEBUG=1 bun --inspect=0.0.0.0:6499 your-script.js
```
## Security Considerations
- **Network binding**: Use `0.0.0.0` only in trusted environments
- **SSH tunneling**: Prefer SSH tunnels over direct remote connections
- **Firewall**: Restrict debug port access to trusted networks
- **Production**: Never enable debugging in production environments
## Advanced Usage
### Multiple Remote Targets
You can set up multiple remote debugging targets:
```json
{
"configurations": [
{
"name": "Dev Container",
"type": "bun",
"request": "attach",
"address": "localhost",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/workspace"
},
{
"name": "Staging Server",
"type": "bun",
"request": "attach",
"address": "staging.example.com",
"port": 6499,
"localRoot": "${workspaceFolder}",
"remoteRoot": "/opt/app"
}
]
}
```
### Environment-Specific Settings
Use VSCode's environment-specific settings for different remote scenarios:
```json
{
"bun.remote.enabled": true,
"bun.remote.autoDetectPaths": true,
"[wsl]": {
"bun.remote.defaultPort": 6499
},
"[ssh-remote]": {
"bun.remote.defaultPort": 9229
}
}
```
## Getting Help
If you encounter issues with remote debugging:
1. Check the VSCode Output panel for error messages
2. Review the Debug Console for connection details
3. Verify your launch configuration matches your remote setup
4. Report issues at: https://github.com/oven-sh/bun/issues

View File

@@ -1,6 +1,6 @@
{
"name": "bun-vscode",
"version": "0.0.29",
"version": "0.0.30",
"author": "oven",
"repository": {
"type": "git",
@@ -85,6 +85,24 @@
"type": "string",
"default": "",
"description": "Custom script to use instead of `bun test`, for example script from `package.json`"
},
"bun.remote.enabled": {
"type": "boolean",
"description": "Enable remote debugging capabilities for WSL, SSH, and Docker environments.",
"scope": "window",
"default": true
},
"bun.remote.autoDetectPaths": {
"type": "boolean",
"description": "Automatically detect and map local and remote paths for debugging.",
"scope": "window",
"default": true
},
"bun.remote.defaultPort": {
"type": "number",
"description": "Default port to use for remote debugging connections.",
"scope": "window",
"default": 6499
}
}
},
@@ -261,6 +279,33 @@
"hot"
],
"default": false
},
"localRoot": {
"type": "string",
"description": "Path to the local directory containing the program being debugged.",
"default": "${workspaceFolder}"
},
"remoteRoot": {
"type": "string",
"description": "Path to the remote directory containing the program being debugged."
},
"address": {
"type": "string",
"description": "TCP/IP address of remote debugger. Default is localhost.",
"default": "localhost"
},
"port": {
"type": "number",
"description": "Debug port to attach to. Default is 6499.",
"default": 6499
},
"skipFiles": {
"type": "array",
"description": "Array of glob patterns to skip while debugging.",
"items": {
"type": "string"
},
"default": []
}
}
},
@@ -270,6 +315,33 @@
"type": "string",
"description": "The URL of the WebSocket inspector to attach to."
},
"address": {
"type": "string",
"description": "TCP/IP address of remote debugger. Default is localhost.",
"default": "localhost"
},
"port": {
"type": "number",
"description": "Debug port to attach to. Default is 6499.",
"default": 6499
},
"localRoot": {
"type": "string",
"description": "Path to the local directory containing the program being debugged.",
"default": "${workspaceFolder}"
},
"remoteRoot": {
"type": "string",
"description": "Path to the remote directory containing the program being debugged."
},
"skipFiles": {
"type": "array",
"description": "Array of glob patterns to skip while debugging.",
"items": {
"type": "string"
},
"default": []
},
"noDebug": {
"type": "boolean",
"description": "If the debugger should be disabled.",

View File

@@ -9,7 +9,7 @@ buildSync({
entryPoints: ["src/extension.ts", "src/web-extension.ts"],
outdir: "dist",
bundle: true,
external: ["vscode"],
external: ["vscode", "@vscode/debugadapter", "ws"],
platform: "node",
format: "cjs",
// The following settings are required to allow for extension debugging

View File

@@ -1,6 +1,7 @@
import { DebugSession, OutputEvent } from "@vscode/debugadapter";
import { tmpdir } from "node:os";
import { join } from "node:path";
import * as path from "node:path";
import * as vscode from "vscode";
import {
type DAP,
@@ -43,6 +44,18 @@ const ATTACH_CONFIGURATION: vscode.DebugConfiguration = {
stopOnEntry: false,
};
const REMOTE_ATTACH_CONFIGURATION: vscode.DebugConfiguration = {
type: "bun",
internalConsoleOptions: "neverOpen",
request: "attach",
name: "Attach to Remote",
address: "localhost",
port: 6499,
localRoot: "${workspaceFolder}",
remoteRoot: "",
stopOnEntry: false,
};
const adapters = new Map<string, FileDebugSession>();
export function registerDebugger(context: vscode.ExtensionContext, factory?: vscode.DebugAdapterDescriptorFactory) {
@@ -174,7 +187,13 @@ async function injectDebugTerminal2() {
class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
provideDebugConfigurations(folder?: vscode.WorkspaceFolder): vscode.ProviderResult<vscode.DebugConfiguration[]> {
return [DEBUG_CONFIGURATION, RUN_CONFIGURATION, ATTACH_CONFIGURATION];
const configs = [DEBUG_CONFIGURATION, RUN_CONFIGURATION, ATTACH_CONFIGURATION];
if (getConfig("remote.enabled")) {
configs.push(REMOTE_ATTACH_CONFIGURATION);
}
return configs;
}
resolveDebugConfiguration(
@@ -186,9 +205,33 @@ class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
const { request } = config;
if (request === "attach") {
target = ATTACH_CONFIGURATION;
// Check if this is a remote attach configuration
if (config.address || config.port || config.localRoot || config.remoteRoot) {
target = REMOTE_ATTACH_CONFIGURATION;
// Handle remote attach URL construction
if (!config.url && config.address && config.port) {
config.url = `ws://${config.address}:${config.port}/`;
}
// Auto-detect path mapping for common remote scenarios
if (getConfig("remote.autoDetectPaths") && !config.remoteRoot) {
config.remoteRoot = this.detectRemoteRoot(folder, config);
}
} else {
target = ATTACH_CONFIGURATION;
}
} else {
target = DEBUG_CONFIGURATION;
// Handle remote launch configurations
if (config.address || config.localRoot || config.remoteRoot) {
config.port = config.port || getConfig("remote.defaultPort");
if (getConfig("remote.autoDetectPaths") && !config.remoteRoot) {
config.remoteRoot = this.detectRemoteRoot(folder, config);
}
}
}
if (config.program === "-" && config.__code) {
@@ -213,6 +256,26 @@ class DebugConfigurationProvider implements vscode.DebugConfigurationProvider {
return config;
}
private detectRemoteRoot(folder: vscode.WorkspaceFolder | undefined, config: vscode.DebugConfiguration): string {
const localRoot = config.localRoot || folder?.uri.fsPath || "";
// WSL path detection
if (process.platform === "win32" && localRoot.includes("\\")) {
// Convert Windows path to WSL path
const wslPath = localRoot.replace(/^([A-Z]):\\/, "/mnt/$1/").replace(/\\/g, "/").toLowerCase();
return wslPath;
}
// Docker container path detection
if (config.address && config.address !== "localhost" && config.address !== "127.0.0.1") {
// Assume container has source mounted at /workspace
return "/workspace";
}
// SSH remote path - use same path structure
return localRoot.replace(/\\/g, "/");
}
}
class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
@@ -230,7 +293,7 @@ class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory
}
}
const adapter = new FileDebugSession(session.id, __untitledName);
const adapter = new FileDebugSession(session.id, __untitledName, configuration);
await adapter.initialize();
return new vscode.DebugAdapterInlineImplementation(adapter);
}
@@ -283,8 +346,11 @@ class FileDebugSession extends DebugSession {
sessionId?: string;
untitledDocPath?: string;
bunEvalPath?: string;
localRoot?: string;
remoteRoot?: string;
pathMappings: Array<{ localRoot: string; remoteRoot: string }> = [];
constructor(sessionId?: string, untitledDocPath?: string) {
constructor(sessionId?: string, untitledDocPath?: string, config?: vscode.DebugConfiguration) {
super();
this.sessionId = sessionId;
this.untitledDocPath = untitledDocPath;
@@ -293,6 +359,50 @@ class FileDebugSession extends DebugSession {
const cwd = vscode.workspace.workspaceFolders?.[0]?.uri?.fsPath ?? process.cwd();
this.bunEvalPath = join(cwd, "[eval]");
}
// Set up path mappings for remote debugging
if (config) {
this.localRoot = config.localRoot;
this.remoteRoot = config.remoteRoot;
if (this.localRoot && this.remoteRoot) {
this.pathMappings.push({
localRoot: this.localRoot,
remoteRoot: this.remoteRoot
});
}
}
}
private mapRemoteToLocal(remotePath: string): string {
if (!remotePath || this.pathMappings.length === 0) {
return remotePath;
}
for (const mapping of this.pathMappings) {
if (remotePath.startsWith(mapping.remoteRoot)) {
return remotePath.replace(mapping.remoteRoot, mapping.localRoot).replace(/\//g, path.sep);
}
}
return remotePath;
}
private mapLocalToRemote(localPath: string): string {
if (!localPath || this.pathMappings.length === 0) {
return localPath;
}
const normalizedLocalPath = localPath.replace(/\\/g, "/");
for (const mapping of this.pathMappings) {
const normalizedLocalRoot = mapping.localRoot.replace(/\\/g, "/");
if (normalizedLocalPath.startsWith(normalizedLocalRoot)) {
return normalizedLocalPath.replace(normalizedLocalRoot, mapping.remoteRoot);
}
}
return localPath;
}
async initialize() {
@@ -328,8 +438,28 @@ class FileDebugSession extends DebugSession {
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) => {
// Apply path mapping for remote debugging
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) => {
// Apply path mapping for remote debugging
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 +475,18 @@ class FileDebugSession extends DebugSession {
if (type === "request") {
const { untitledDocPath, bunEvalPath } = this;
const { command } = message;
if (untitledDocPath && (command === "setBreakpoints" || command === "breakpointLocations")) {
const args = message.arguments as any;
if (args.source?.path === untitledDocPath) {
args.source.path = bunEvalPath;
}
} else if (command === "setBreakpoints" || command === "breakpointLocations") {
// Apply path mapping for remote debugging
const args = message.arguments as any;
if (args.source?.path) {
args.source.path = this.mapLocalToRemote(args.source.path);
}
}
this.adapter.emit("Adapter.request", message);