From 7b14ee8ec3b3992d1f1d87610845a5ef8aa36d1c Mon Sep 17 00:00:00 2001 From: Tulsi Sapkota Date: Thu, 13 Nov 2025 15:09:18 +1100 Subject: [PATCH 1/3] feat: support env in path mapping --- README.md | 13 +++++++ src/extension.ts | 55 +++++++++++++++++++----------- src/test/envResolver.ts | 75 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 src/test/envResolver.ts diff --git a/README.md b/README.md index b9ffe129..01cc8b76 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,19 @@ To make VS Code map the files on the server to the right files on your local mac } ``` +### Environment Variables in Path Mappings + +You can use environment variables in `pathMappings` using the `${env:VARIABLE_NAME}` syntax. This is useful for dynamic paths that vary between environments or users: + +```json +"pathMappings": { + "${env:DOCKER_WEB_ROOT}": "${workspaceFolder}", + "/app": "${env:PROJECT_PATH}" +} +``` + +The environment variables are resolved when the debug session starts. If a variable is not defined, the literal string (e.g., `${env:UNDEFINED_VAR}`) will be kept as-is. + Please also note that setting any of the CLI debugging options will not work with remote host debugging, because the script is always launched locally. If you want to debug a CLI script on a remote host, you need to launch it manually from the command line. ## Proxy support diff --git a/src/extension.ts b/src/extension.ts index a9514e25..03d0263a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,6 +4,18 @@ import { LaunchRequestArguments } from './phpDebug' import * as which from 'which' import * as path from 'path' +/** + * Resolves environment variables in a string + * Supports: ${env:VAR_NAME} + */ +export function resolveEnvVariables(value: string): string { + // Replace ${env:VAR_NAME} with environment variable values + return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar) => { + const envValue = process.env[envVar] + return envValue !== undefined ? envValue : match + }) +} + export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.debug.registerDebugConfigurationProvider('php', { @@ -71,29 +83,32 @@ export function activate(context: vscode.ExtensionContext) { } } } - if (folder && folder.uri.scheme !== 'file') { - // replace - if (debugConfiguration.pathMappings) { - for (const key in debugConfiguration.pathMappings) { - debugConfiguration.pathMappings[key] = debugConfiguration.pathMappings[key].replace( - '${workspaceFolder}', - folder.uri.toString() - ) + if (debugConfiguration.pathMappings) { + const resolvedMappings: { [index: string]: string } = {} + for (const [serverPath, localPath] of Object.entries(debugConfiguration.pathMappings)) { + const resolvedServerPath = resolveEnvVariables(serverPath) + let resolvedLocalPath = resolveEnvVariables(localPath) + + if (folder && folder.uri.scheme !== 'file') { + resolvedLocalPath = resolvedLocalPath.replace('${workspaceFolder}', folder.uri.toString()) } + + resolvedMappings[resolvedServerPath] = resolvedLocalPath } - // The following path are currently NOT mapped - /* - debugConfiguration.skipEntryPaths = debugConfiguration.skipEntryPaths?.map(v => - v.replace('${workspaceFolder}', folder.uri.toString()) - ) - debugConfiguration.skipFiles = debugConfiguration.skipFiles?.map(v => - v.replace('${workspaceFolder}', folder.uri.toString()) - ) - debugConfiguration.ignore = debugConfiguration.ignore?.map(v => - v.replace('${workspaceFolder}', folder.uri.toString()) - ) - */ + debugConfiguration.pathMappings = resolvedMappings } + // The following path are currently NOT mapped + /* + debugConfiguration.skipEntryPaths = debugConfiguration.skipEntryPaths?.map(v => + v.replace('${workspaceFolder}', folder.uri.toString()) + ) + debugConfiguration.skipFiles = debugConfiguration.skipFiles?.map(v => + v.replace('${workspaceFolder}', folder.uri.toString()) + ) + debugConfiguration.ignore = debugConfiguration.ignore?.map(v => + v.replace('${workspaceFolder}', folder.uri.toString()) + ) + */ return debugConfiguration }, }) diff --git a/src/test/envResolver.ts b/src/test/envResolver.ts new file mode 100644 index 00000000..92856f60 --- /dev/null +++ b/src/test/envResolver.ts @@ -0,0 +1,75 @@ +import { assert } from 'chai' +import { describe, it, beforeEach, afterEach } from 'mocha' + +// Inline the function for testing without vscode dependency +function resolveEnvVariables(value: string): string { + return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar) => { + const envValue = process.env[envVar] + return envValue !== undefined ? envValue : match + }) +} + +describe('Environment Variable Resolution', () => { + let originalEnv: NodeJS.ProcessEnv + + beforeEach(() => { + originalEnv = { ...process.env } + }) + + afterEach(() => { + process.env = originalEnv + }) + + it('should resolve ${env:VAR_NAME} with existing environment variable', () => { + process.env.TEST_VAR = '/test/path' + const result = resolveEnvVariables('${env:TEST_VAR}/subdir') + assert.equal(result, '/test/path/subdir') + }) + + it('should keep ${env:VAR_NAME} if environment variable does not exist', () => { + delete process.env.NONEXISTENT_VAR + const result = resolveEnvVariables('${env:NONEXISTENT_VAR}/subdir') + assert.equal(result, '${env:NONEXISTENT_VAR}/subdir') + }) + + it('should resolve multiple environment variables', () => { + process.env.VAR1 = '/path1' + process.env.VAR2 = '/path2' + const result = resolveEnvVariables('${env:VAR1}/${env:VAR2}') + assert.equal(result, '/path1//path2') + }) + + it('should handle text without environment variables', () => { + const result = resolveEnvVariables('/var/www/html') + assert.equal(result, '/var/www/html') + }) + + it('should handle environment variables with underscores and numbers', () => { + process.env.MY_VAR_123 = '/custom/path' + const result = resolveEnvVariables('${env:MY_VAR_123}') + assert.equal(result, '/custom/path') + }) + + it('should handle empty environment variable value', () => { + process.env.EMPTY_VAR = '' + const result = resolveEnvVariables('${env:EMPTY_VAR}/test') + assert.equal(result, '/test') + }) + + it('should handle mixed content', () => { + process.env.DOCKER_ROOT = '/var/www/html' + const result = resolveEnvVariables('prefix/${env:DOCKER_ROOT}/suffix') + assert.equal(result, 'prefix//var/www/html/suffix') + }) + + it('should handle pathMapping use case', () => { + process.env.DOCKER_WEB_ROOT = '/var/www/html' + process.env.LOCAL_PROJECT = '/Users/developer/myproject' + + const serverPath = '${env:DOCKER_WEB_ROOT}' + const localPath = '${env:LOCAL_PROJECT}' + + assert.equal(resolveEnvVariables(serverPath), '/var/www/html') + assert.equal(resolveEnvVariables(localPath), '/Users/developer/myproject') + }) +}) From 8fa60f24a2b203a596c3cf5bb521300a1f5ec1f1 Mon Sep 17 00:00:00 2001 From: Tulsi Sapkota Date: Thu, 13 Nov 2025 16:55:43 +1100 Subject: [PATCH 2/3] fix: lint --- src/extension.ts | 2 +- src/test/envResolver.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 03d0263a..f9667438 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -10,7 +10,7 @@ import * as path from 'path' */ export function resolveEnvVariables(value: string): string { // Replace ${env:VAR_NAME} with environment variable values - return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar) => { + return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar: string) => { const envValue = process.env[envVar] return envValue !== undefined ? envValue : match }) diff --git a/src/test/envResolver.ts b/src/test/envResolver.ts index 92856f60..6de41805 100644 --- a/src/test/envResolver.ts +++ b/src/test/envResolver.ts @@ -3,7 +3,7 @@ import { describe, it, beforeEach, afterEach } from 'mocha' // Inline the function for testing without vscode dependency function resolveEnvVariables(value: string): string { - return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar) => { + return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar: string) => { const envValue = process.env[envVar] return envValue !== undefined ? envValue : match }) From 55bba1b27dac31fa65b8ada27cbea3fee2edfba6 Mon Sep 17 00:00:00 2001 From: Tulsi Sapkota Date: Thu, 13 Nov 2025 17:02:51 +1100 Subject: [PATCH 3/3] feat: move envResolver to a different file --- src/envResolver.ts | 11 +++++++++++ src/extension.ts | 13 +------------ src/test/envResolver.ts | 9 +-------- 3 files changed, 13 insertions(+), 20 deletions(-) create mode 100644 src/envResolver.ts diff --git a/src/envResolver.ts b/src/envResolver.ts new file mode 100644 index 00000000..8e4c70b9 --- /dev/null +++ b/src/envResolver.ts @@ -0,0 +1,11 @@ +/** + * Resolves environment variables in a string + * Supports: ${env:VAR_NAME} + */ +export function resolveEnvVariables(value: string): string { + // Replace ${env:VAR_NAME} with environment variable values + return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar: string) => { + const envValue = process.env[envVar] + return envValue !== undefined ? envValue : match + }) +} diff --git a/src/extension.ts b/src/extension.ts index f9667438..1020c6fd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,18 +3,7 @@ import { WorkspaceFolder, DebugConfiguration, CancellationToken } from 'vscode' import { LaunchRequestArguments } from './phpDebug' import * as which from 'which' import * as path from 'path' - -/** - * Resolves environment variables in a string - * Supports: ${env:VAR_NAME} - */ -export function resolveEnvVariables(value: string): string { - // Replace ${env:VAR_NAME} with environment variable values - return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar: string) => { - const envValue = process.env[envVar] - return envValue !== undefined ? envValue : match - }) -} +import { resolveEnvVariables } from './envResolver' export function activate(context: vscode.ExtensionContext) { context.subscriptions.push( diff --git a/src/test/envResolver.ts b/src/test/envResolver.ts index 6de41805..1c3f47fd 100644 --- a/src/test/envResolver.ts +++ b/src/test/envResolver.ts @@ -1,13 +1,6 @@ import { assert } from 'chai' import { describe, it, beforeEach, afterEach } from 'mocha' - -// Inline the function for testing without vscode dependency -function resolveEnvVariables(value: string): string { - return value.replace(/\$\{env:([^}]+)\}/g, (match, envVar: string) => { - const envValue = process.env[envVar] - return envValue !== undefined ? envValue : match - }) -} +import { resolveEnvVariables } from '../envResolver' describe('Environment Variable Resolution', () => { let originalEnv: NodeJS.ProcessEnv