diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index 0ce538e2e98..87cc2a395ba 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -209,3 +209,50 @@ export async function updateConfig(config: vscode.WorkspaceConfiguration) { } } } + +export function substituteVariablesInEnv(env: Env): Env { + const missingDeps = new Set(); + // vscode uses `env:ENV_NAME` for env vars resolution, and it's easier + // to follow the same convention for our dependency tracking + const definedEnvKeys = new Set(Object.keys(env).map(key => `env:${key}`)); + const envWithDeps = Object.fromEntries(Object.entries(env).map(([key, value]) => { + const deps = new Set(); + const depRe = new RegExp(/\${(?.+?)}/g); + let match = undefined; + while ((match = depRe.exec(value))) { + const depName = match.groups!.depName; + deps.add(depName); + // `depName` at this point can have a form of `expression` or + // `prefix:expression` + if (!definedEnvKeys.has(depName)) { + missingDeps.add(depName); + } + } + return [`env:${key}`, { deps: [...deps], value }]; + })); + + const resolved = new Set(); + // TODO: handle missing dependencies + const toResolve = new Set(Object.keys(envWithDeps)); + + let leftToResolveSize; + do { + leftToResolveSize = toResolve.size; + for (const key of toResolve) { + if (envWithDeps[key].deps.every(dep => resolved.has(dep))) { + envWithDeps[key].value = envWithDeps[key].value.replace( + /\${(?.+?)}/g, (_wholeMatch, depName) => { + return envWithDeps[depName].value; + }); + resolved.add(key); + toResolve.delete(key); + } + } + } while (toResolve.size > 0 && toResolve.size < leftToResolveSize); + + const resolvedEnv: Env = {}; + for (const key of Object.keys(env)) { + resolvedEnv[key] = envWithDeps[`env:${key}`].value; + } + return resolvedEnv; +} diff --git a/editors/code/tests/unit/settings.test.ts b/editors/code/tests/unit/settings.test.ts new file mode 100644 index 00000000000..12734d15667 --- /dev/null +++ b/editors/code/tests/unit/settings.test.ts @@ -0,0 +1,41 @@ +import * as assert from 'assert'; +import { Context } from '.'; +import { substituteVariablesInEnv } from '../../src/config'; + +export async function getTests(ctx: Context) { + await ctx.suite('Server Env Settings', suite => { + suite.addTest('Replacing Env Variables', async () => { + const envJson = { + USING_MY_VAR: "${env:MY_VAR} test ${env:MY_VAR}", + MY_VAR: "test" + }; + const expectedEnv = { + USING_MY_VAR: "test test test", + MY_VAR: "test" + }; + const actualEnv = await substituteVariablesInEnv(envJson); + assert.deepStrictEqual(actualEnv, expectedEnv); + }); + + suite.addTest('Circular dependencies remain as is', async () => { + const envJson = { + A_USES_B: "${env:B_USES_A}", + B_USES_A: "${env:A_USES_B}", + C_USES_ITSELF: "${env:C_USES_ITSELF}", + D_USES_C: "${env:C_USES_ITSELF}", + E_IS_ISOLATED: "test", + F_USES_E: "${env:E_IS_ISOLATED}" + }; + const expectedEnv = { + A_USES_B: "${env:B_USES_A}", + B_USES_A: "${env:A_USES_B}", + C_USES_ITSELF: "${env:C_USES_ITSELF}", + D_USES_C: "${env:C_USES_ITSELF}", + E_IS_ISOLATED: "test", + F_USES_E: "test" + }; + const actualEnv = await substituteVariablesInEnv(envJson); + assert.deepStrictEqual(actualEnv, expectedEnv); + }); + }); +}