diff --git a/src/bridge.ts b/src/bridge.ts index 0e90dec8..ba3feed8 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -33,6 +33,8 @@ interface ProcessRef { interface RunOptions { pipeOutput?: boolean + env?: NodeJS.ProcessEnv + extendEnv?: boolean } class DenoBridge { @@ -190,8 +192,8 @@ class DenoBridge { return { global: false, path: downloadedPath } } - getEnvironmentVariables() { - const env: Record = {} + getEnvironmentVariables(inputEnv: NodeJS.ProcessEnv = {}) { + const env: NodeJS.ProcessEnv = { ...inputEnv } if (this.denoDir !== undefined) { env.DENO_DIR = this.denoDir @@ -202,20 +204,24 @@ class DenoBridge { // Runs the Deno CLI in the background and returns a reference to the child // process, awaiting its execution. - async run(args: string[], { pipeOutput }: RunOptions = {}) { + async run(args: string[], { pipeOutput, env: inputEnv, extendEnv = true }: RunOptions = {}) { const { path: binaryPath } = await this.getBinaryPath() - const env = this.getEnvironmentVariables() - const options = { env } + const env = this.getEnvironmentVariables(inputEnv) + const options: Options = { env, extendEnv } return DenoBridge.runWithBinary(binaryPath, args, options, pipeOutput) } // Runs the Deno CLI in the background, assigning a reference of the child // process to a `ps` property in the `ref` argument, if one is supplied. - async runInBackground(args: string[], pipeOutput?: boolean, ref?: ProcessRef) { + async runInBackground( + args: string[], + ref?: ProcessRef, + { pipeOutput, env: inputEnv, extendEnv = true }: RunOptions = {}, + ) { const { path: binaryPath } = await this.getBinaryPath() - const env = this.getEnvironmentVariables() - const options = { env } + const env = this.getEnvironmentVariables(inputEnv) + const options: Options = { env, extendEnv } const ps = DenoBridge.runWithBinary(binaryPath, args, options, pipeOutput) if (ref !== undefined) { diff --git a/src/server/server.ts b/src/server/server.ts index 075c8c13..fc27a7f0 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -30,7 +30,7 @@ const prepareServer = ({ port, }: PrepareServerOptions) => { const processRef: ProcessRef = {} - const startIsolate = async (newFunctions: EdgeFunction[]) => { + const startIsolate = async (newFunctions: EdgeFunction[], env: NodeJS.ProcessEnv = {}) => { if (processRef?.ps !== undefined) { await killProcess(processRef.ps) } @@ -60,7 +60,14 @@ const prepareServer = ({ const bootstrapFlags = ['--port', port.toString()] - await deno.runInBackground(['run', ...denoFlags, stage2Path, ...bootstrapFlags], true, processRef) + // We set `extendEnv: false` to avoid polluting the edge function context + // with variables from the user's system, since those will not be available + // in the production environment. + await deno.runInBackground(['run', ...denoFlags, stage2Path, ...bootstrapFlags], processRef, { + pipeOutput: true, + env, + extendEnv: false, + }) const success = await waitForServer(port, processRef.ps) diff --git a/test/bridge.ts b/test/bridge.ts new file mode 100644 index 00000000..05d97d48 --- /dev/null +++ b/test/bridge.ts @@ -0,0 +1,141 @@ +import { Buffer } from 'buffer' +import fs from 'fs' +import { createRequire } from 'module' +import { platform, env } from 'process' +import { PassThrough } from 'stream' + +import test from 'ava' +import nock from 'nock' +import { spy } from 'sinon' +import tmp, { DirectoryResult } from 'tmp-promise' + +import { DenoBridge } from '../src/bridge.js' +import { getPlatformTarget } from '../src/platform.js' + +const require = createRequire(import.meta.url) +const archiver = require('archiver') + +const getMockDenoBridge = function (tmpDir: DirectoryResult, mockBinaryOutput: string) { + const latestVersion = '1.20.3' + const data = new PassThrough() + const archive = archiver('zip', { zlib: { level: 9 } }) + + archive.pipe(data) + archive.append(Buffer.from(mockBinaryOutput.replace(/@@@latestVersion@@@/g, latestVersion)), { + name: platform === 'win32' ? 'deno.exe' : 'deno', + }) + archive.finalize() + + const target = getPlatformTarget() + + nock('https://dl.deno.land').get('/release-latest.txt').reply(200, `v${latestVersion}`) + nock('https://dl.deno.land') + .get(`/release/v${latestVersion}/deno-${target}.zip`) + .reply(200, () => data) + + const beforeDownload = spy() + const afterDownload = spy() + + return new DenoBridge({ + cacheDirectory: tmpDir.path, + onBeforeDownload: beforeDownload, + onAfterDownload: afterDownload, + useGlobal: false, + }) +} + +test.serial('Does not inherit environment variables if `extendEnv` is false', async (t) => { + const tmpDir = await tmp.dir() + const deno = getMockDenoBridge( + tmpDir, + `#!/usr/bin/env sh + + if [ "$1" = "test" ] + then + env + else + echo "deno @@@latestVersion@@@" + fi`, + ) + + // The environment sets some variables so let us see what they are and remove them from the result + const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: false }) + env.TADA = 'TUDU' + const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: false }) + let output = result?.stdout ?? '' + + delete env.TADA + + referenceOutput?.stdout.split('\n').forEach((line) => { + output = output.replace(line.trim(), '') + }) + output = output.trim().replace(/\n+/, '\n') + + t.is(output, 'LULU=LALA') + + await fs.promises.rmdir(tmpDir.path, { recursive: true }) +}) + +test.serial('Does inherit environment variables if `extendEnv` is true', async (t) => { + const tmpDir = await tmp.dir() + const deno = getMockDenoBridge( + tmpDir, + `#!/usr/bin/env sh + + if [ "$1" = "test" ] + then + env + else + echo "deno @@@latestVersion@@@" + fi`, + ) + + // The environment sets some variables so let us see what they are and remove them from the result + const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true }) + env.TADA = 'TUDU' + const result = await deno.run(['test'], { env: { LULU: 'LALA' }, extendEnv: true }) + let output = result?.stdout ?? '' + + delete env.TADA + + referenceOutput?.stdout.split('\n').forEach((line) => { + output = output.replace(line.trim(), '') + }) + output = output.trim().replace(/\n+/, '\n') + + t.is(output, 'LULU=LALA\nTADA=TUDU') + + await fs.promises.rmdir(tmpDir.path, { recursive: true }) +}) + +test.serial('Does inherit environment variables if `extendEnv` is not set', async (t) => { + const tmpDir = await tmp.dir() + const deno = getMockDenoBridge( + tmpDir, + `#!/usr/bin/env sh + + if [ "$1" = "test" ] + then + env + else + echo "deno @@@latestVersion@@@" + fi`, + ) + + // The environment sets some variables so let us see what they are and remove them from the result + const referenceOutput = await deno.run(['test'], { env: {}, extendEnv: true }) + env.TADA = 'TUDU' + const result = await deno.run(['test'], { env: { LULU: 'LALA' } }) + let output = result?.stdout ?? '' + + delete env.TADA + + referenceOutput?.stdout.split('\n').forEach((line) => { + output = output.replace(line.trim(), '') + }) + output = output.trim().replace(/\n+/, '\n') + + t.is(output, 'LULU=LALA\nTADA=TUDU') + + await fs.promises.rmdir(tmpDir.path, { recursive: true }) +}) diff --git a/test/main.ts b/test/main.ts index 25303729..6591b269 100644 --- a/test/main.ts +++ b/test/main.ts @@ -17,7 +17,7 @@ const archiver = require('archiver') test('Downloads the Deno CLI on demand and caches it for subsequent calls', async (t) => { const latestVersion = '1.20.3' - const mockBinaryOutput = `#!/usr/bin/env node\n\nconsole.log("deno ${latestVersion}")` + const mockBinaryOutput = `#!/usr/bin/env sh\n\necho "deno ${latestVersion}"` const data = new PassThrough() const archive = archiver('zip', { zlib: { level: 9 } })