Skip to content

Commit

Permalink
feat: Implement envFile support (#837)
Browse files Browse the repository at this point in the history
* Implement envFile support.

* Test envfile.

* Add extra test for missing env file.

* README and CHANGELOG.
  • Loading branch information
zobo authored Aug 30, 2022
1 parent 8fa60e3 commit 922add3
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 1 deletion.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ module.exports = {
'prefer-rest-params': 'off',
'@typescript-eslint/no-inferrable-types': ['error', { ignoreParameters: true }],
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-unsafe-assignment': 'off',
/*
"@typescript-eslint/indent": "off",
"@typescript-eslint/member-delimiter-style": [
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/).

## [1.28.0]

- Support for envFile.
- Migrated from tslint to eslint.

## [1.27.0]

- Variable paging with VSCode indexedVariables.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ Options specific to CLI debugging:
- `runtimeArgs`: Additional arguments to pass to the PHP binary
- `externalConsole`: Launches the script in an external console window instead of the debug console (default: `false`)
- `env`: Environment variables to pass to the script
- `envFile`: Optional path to a file containing environment variable definitions

## Features

Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@vscode/debugadapter": "^1.57.0",
"@vscode/debugprotocol": "^1.55.1",
"@xmldom/xmldom": "^0.8.2",
"dotenv": "^16.0.1",
"file-url": "^3.0.0",
"iconv-lite": "^0.6.3",
"minimatch": "^5.1.0",
Expand Down Expand Up @@ -230,6 +231,10 @@
"description": "Environment variables passed to the program.",
"default": {}
},
"envFile": {
"type": "string",
"description": "Absolute path to a file containing environment variable definitions."
},
"hostname": {
"type": "string",
"description": "Address to bind to when listening for Xdebug or Unix domain socket (start with unix://)",
Expand Down
66 changes: 66 additions & 0 deletions src/envfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as fs from 'fs'
import { LaunchRequestArguments } from './phpDebug'
import * as dotenv from 'dotenv'

/**
* Returns the user-configured portion of the environment variables.
*/
export function getConfiguredEnvironment(args: LaunchRequestArguments): { [key: string]: string } {
if (args.envFile) {
try {
return merge(readEnvFile(args.envFile), args.env || {})
} catch (e) {
throw new Error('Failed reading envFile')
}
}
return args.env || {}
}

function readEnvFile(file: string): { [key: string]: string } {
if (!fs.existsSync(file)) {
return {}
}
const buffer = stripBOM(fs.readFileSync(file, 'utf8'))
const env = dotenv.parse(Buffer.from(buffer))
return env
}

function stripBOM(s: string): string {
if (s && s[0] === '\uFEFF') {
s = s.substring(1)
}
return s
}

function merge(...vars: { [key: string]: string }[]): { [key: string]: string } {
if (process.platform === 'win32') {
return caseInsensitiveMerge(...vars)
}
return Object.assign({}, ...vars) as { [key: string]: string }
}

/**
* Performs a case-insenstive merge of the list of objects.
*/
function caseInsensitiveMerge<V>(...objs: ReadonlyArray<Readonly<{ [key: string]: V }> | undefined | null>) {
if (objs.length === 0) {
return {}
}
const out: { [key: string]: V } = {}
const caseMapping: { [key: string]: string } = Object.create(null) // prototype-free object
for (const obj of objs) {
if (!obj) {
continue
}
for (const key of Object.keys(obj)) {
const normalized = key.toLowerCase()
if (caseMapping[normalized]) {
out[caseMapping[normalized]] = obj[key]
} else {
caseMapping[normalized] = key
out[key] = obj[key]
}
}
}
return out
}
5 changes: 4 additions & 1 deletion src/phpDebug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import * as semver from 'semver'
import { LogPointManager } from './logpoint'
import { ProxyConnect } from './proxyConnect'
import { randomUUID } from 'crypto'
import { getConfiguredEnvironment } from './envfile'

if (process.env['VSCODE_NLS_CONFIG']) {
try {
Expand Down Expand Up @@ -100,6 +101,8 @@ export interface LaunchRequestArguments extends VSCodeDebugProtocol.LaunchReques
runtimeArgs?: string[]
/** Optional environment variables to pass to the debuggee. The string valued properties of the 'environmentVariables' are used as key/value pairs. */
env?: { [key: string]: string }
/** Absolute path to a file containing environment variable definitions. */
envFile?: string
/** If true launch the target in an external console. */
externalConsole?: boolean
/** Maximum allowed parallel debugging sessions */
Expand Down Expand Up @@ -268,7 +271,7 @@ class PhpDebugSession extends vscode.DebugSession {
const program = args.program ? [args.program] : []
const cwd = args.cwd || process.cwd()
const env = Object.fromEntries(
Object.entries({ ...process.env, ...args.env }).map(v => [
Object.entries({ ...process.env, ...getConfiguredEnvironment(args) }).map(v => [
v[0],
v[1]?.replace('${port}', port.toString()),
])
Expand Down
34 changes: 34 additions & 0 deletions src/test/envfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { assert } from 'chai'
import { describe, it } from 'mocha'
import { getConfiguredEnvironment } from '../envfile'

describe('EnvFile', () => {
it('should work without envfile', () => {
const ret = getConfiguredEnvironment({ env: { TEST: 'TEST' } })
assert.deepEqual(ret, { TEST: 'TEST' })
})
it('should work with missing envfile', () => {
const ret = getConfiguredEnvironment({ env: { TEST: 'TEST' }, envFile: 'NONEXISTINGFILE' })
assert.deepEqual(ret, { TEST: 'TEST' })
})
it('should merge envfile', () => {
const ret = getConfiguredEnvironment({ env: { TEST: 'TEST' }, envFile: 'testproject/envfile' })
assert.deepEqual(ret, { TEST: 'TEST', TEST1: 'VALUE1', Test2: 'Value2' })
})
;(process.platform === 'win32' ? it : it.skip)('should merge envfile on win32', () => {
const ret = getConfiguredEnvironment({ env: { TEST1: 'TEST' }, envFile: 'testproject/envfile' })
assert.deepEqual(ret, { TEST1: 'TEST', Test2: 'Value2' })
})
;(process.platform === 'win32' ? it : it.skip)('should merge envfile on win32 case insensitive', () => {
const ret = getConfiguredEnvironment({ env: { Test1: 'TEST' }, envFile: 'testproject/envfile' })
assert.deepEqual(ret, { TEST1: 'TEST', Test2: 'Value2' })
})
;(process.platform !== 'win32' ? it : it.skip)('should merge envfile on unix', () => {
const ret = getConfiguredEnvironment({ env: { TEST1: 'TEST' }, envFile: 'testproject/envfile' })
assert.deepEqual(ret, { TEST1: 'TEST', Test2: 'Value2' })
})
;(process.platform !== 'win32' ? it : it.skip)('should merge envfile on unix case insensitive', () => {
const ret = getConfiguredEnvironment({ env: { Test1: 'TEST' }, envFile: 'testproject/envfile' })
assert.deepEqual(ret, { Test1: 'TEST', TEST1: 'VALUE1', Test2: 'Value2' })
})
})
2 changes: 2 additions & 0 deletions testproject/envfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST1=VALUE1
Test2=Value2

0 comments on commit 922add3

Please sign in to comment.