From 9066166ea214cbb3714b1114f7216102671b2de4 Mon Sep 17 00:00:00 2001 From: Daniel Tschinder <231804+danez@users.noreply.github.com> Date: Mon, 17 Apr 2023 15:10:26 +0200 Subject: [PATCH] feat: inject environment variables from .env files into edge functions locally (#5620) * feat: inject environment variables from .env files into edge functions locally * chore: test * chore: rename function * chore: fix test on windows --- src/commands/dev/dev-exec.mjs | 5 ++-- src/commands/dev/dev.mjs | 5 ++-- src/commands/functions/functions-create.mjs | 5 ++-- src/commands/functions/functions-serve.mjs | 7 ++--- src/commands/serve/serve.mjs | 5 ++-- src/lib/edge-functions/registry.mjs | 10 +++++-- src/utils/dev.mjs | 30 ++++++++++++++------- tests/integration/100.command.dev.test.cjs | 14 +++++++--- 8 files changed, 54 insertions(+), 27 deletions(-) diff --git a/src/commands/dev/dev-exec.mjs b/src/commands/dev/dev-exec.mjs index 9bd518fa0c1..a63e7f0ca86 100644 --- a/src/commands/dev/dev-exec.mjs +++ b/src/commands/dev/dev-exec.mjs @@ -1,6 +1,6 @@ import execa from 'execa' -import { injectEnvVariables } from '../../utils/dev.mjs' +import { getDotEnvVariables, injectEnvVariables } from '../../utils/dev.mjs' import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' /** @@ -16,7 +16,8 @@ const devExec = async (cmd, options, command) => { env = await getEnvelopeEnv({ api, context: options.context, env, siteInfo }) } - await injectEnvVariables({ devConfig: { ...config.dev }, env, site }) + env = await getDotEnvVariables({ devConfig: { ...config.dev }, env, site }) + injectEnvVariables(env) await execa(cmd, command.args.slice(1), { stdio: 'inherit', diff --git a/src/commands/dev/dev.mjs b/src/commands/dev/dev.mjs index bfaa5084b6a..b922b72ce9b 100644 --- a/src/commands/dev/dev.mjs +++ b/src/commands/dev/dev.mjs @@ -18,7 +18,7 @@ import { normalizeConfig, } from '../../utils/command-helpers.mjs' import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.mjs' -import { getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' +import { getDotEnvVariables, getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' import { startNetlifyGraph, startPollingForAPIAuthentication } from '../../utils/graph.mjs' @@ -96,7 +96,8 @@ const dev = async (options, command) => { log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`) } - await injectEnvVariables({ devConfig, env, site }) + env = await getDotEnvVariables({ devConfig, env, site }) + injectEnvVariables(env) await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state }) const { addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({ diff --git a/src/commands/functions/functions-create.mjs b/src/commands/functions/functions-create.mjs index 7a9f704c452..115173df269 100644 --- a/src/commands/functions/functions-create.mjs +++ b/src/commands/functions/functions-create.mjs @@ -19,7 +19,7 @@ import ora from 'ora' import { fileExistsAsync } from '../../lib/fs.mjs' import { getAddons, getCurrentAddon, getSiteData } from '../../utils/addons/prepare.mjs' import { NETLIFYDEVERR, NETLIFYDEVLOG, NETLIFYDEVWARN, chalk, error, log } from '../../utils/command-helpers.mjs' -import { injectEnvVariables } from '../../utils/dev.mjs' +import { getDotEnvVariables, injectEnvVariables } from '../../utils/dev.mjs' import execa from '../../utils/execa.mjs' import { readRepoURL, validateRepoURL } from '../../utils/read-repo-url.mjs' @@ -549,11 +549,12 @@ const handleOnComplete = async ({ command, onComplete }) => { const { config } = command.netlify if (onComplete) { - await injectEnvVariables({ + const env = await getDotEnvVariables({ devConfig: { ...config.dev }, env: command.netlify.cachedConfig.env, site: command.netlify.site, }) + injectEnvVariables(env) await onComplete.call(command) } } diff --git a/src/commands/functions/functions-serve.mjs b/src/commands/functions/functions-serve.mjs index 9b077fc2157..c14ceb3af37 100644 --- a/src/commands/functions/functions-serve.mjs +++ b/src/commands/functions/functions-serve.mjs @@ -2,7 +2,7 @@ import { join } from 'path' import { startFunctionsServer } from '../../lib/functions/server.mjs' -import { acquirePort, getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' +import { acquirePort, getDotEnvVariables, getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' import { getFunctionsDir } from '../../utils/functions/index.mjs' const DEFAULT_PORT = 9999 @@ -16,11 +16,12 @@ const functionsServe = async (options, command) => { const { api, config, site, siteInfo } = command.netlify const functionsDir = getFunctionsDir({ options, config }, join('netlify', 'functions')) - const { env } = command.netlify.cachedConfig + let { env } = command.netlify.cachedConfig env.NETLIFY_DEV = { sources: ['internal'], value: 'true' } - await injectEnvVariables({ devConfig: { ...config.dev }, env, site }) + env = await getDotEnvVariables({ devConfig: { ...config.dev }, env, site }) + injectEnvVariables(env) const { capabilities, siteUrl, timeouts } = await getSiteInformation({ offline: options.offline, diff --git a/src/commands/serve/serve.mjs b/src/commands/serve/serve.mjs index b14ccb6e28f..00fbdf27e80 100644 --- a/src/commands/serve/serve.mjs +++ b/src/commands/serve/serve.mjs @@ -16,7 +16,7 @@ import { normalizeConfig, } from '../../utils/command-helpers.mjs' import detectServerSettings, { getConfigWithPlugins } from '../../utils/detect-server-settings.mjs' -import { getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' +import { getDotEnvVariables, getSiteInformation, injectEnvVariables } from '../../utils/dev.mjs' import { getEnvelopeEnv, normalizeContext } from '../../utils/env/index.mjs' import { getInternalFunctionsDir } from '../../utils/functions/functions.mjs' import { ensureNetlifyIgnore } from '../../utils/gitignore.mjs' @@ -52,7 +52,8 @@ const serve = async (options, command) => { log(`${NETLIFYDEVLOG} Injecting environment variable values for ${chalk.yellow('all scopes')}`) } - await injectEnvVariables({ devConfig, env, site }) + env = await getDotEnvVariables({ devConfig, env, site }) + injectEnvVariables(env) await promptEditorHelper({ chalk, config, log, NETLIFYDEVLOG, repositoryRoot, state }) const { addonsUrls, capabilities, siteUrl, timeouts } = await getSiteInformation({ diff --git a/src/lib/edge-functions/registry.mjs b/src/lib/edge-functions/registry.mjs index a57ad4090a6..d3979c9caed 100644 --- a/src/lib/edge-functions/registry.mjs +++ b/src/lib/edge-functions/registry.mjs @@ -15,7 +15,7 @@ export class EdgeFunctionsRegistry { * @param {object} opts.config * @param {string} opts.configPath * @param {string[]} opts.directories - * @param {Record} opts.env + * @param {Record} opts.env * @param {() => Promise} opts.getUpdatedConfig * @param {Declaration[]} opts.internalFunctions * @param {string} opts.projectDir @@ -178,6 +178,11 @@ export class EdgeFunctionsRegistry { return edgeFunctions } + /** + * + * @param {Record} envConfig + * @returns {Record} + */ static getEnvironmentVariables(envConfig) { const env = Object.create(null) Object.entries(envConfig).forEach(([key, variable]) => { @@ -185,7 +190,8 @@ export class EdgeFunctionsRegistry { variable.sources.includes('ui') || variable.sources.includes('account') || variable.sources.includes('addons') || - variable.sources.includes('internal') + variable.sources.includes('internal') || + variable.sources.some((source) => source.startsWith('.env')) ) { env[key] = variable.value } diff --git a/src/utils/dev.mjs b/src/utils/dev.mjs index 233c682bd56..b96f941a891 100644 --- a/src/utils/dev.mjs +++ b/src/utils/dev.mjs @@ -137,30 +137,40 @@ const getEnvSourceName = (source) => { return printFn(name) } -// Takes a set of environment variables in the format provided by @netlify/config, augments it with variables from both -// dot-env files and the process itself, and injects into `process.env`. -export const injectEnvVariables = async ({ devConfig, env, site }) => { - const environment = new Map(Object.entries(env)) +/** + * @param {{devConfig: any, env: Record, site: any}} param0 + * @returns {Promise>} + */ +export const getDotEnvVariables = async ({ devConfig, env, site }) => { const dotEnvFiles = await loadDotEnvFiles({ envFiles: devConfig.envFiles, projectDir: site.root }) - dotEnvFiles.forEach(({ env: fileEnv, file }) => { + const newSourceName = `${file} file` + Object.keys(fileEnv).forEach((key) => { - const newSourceName = `${file} file` - const sources = environment.has(key) ? [newSourceName, ...environment.get(key).sources] : [newSourceName] + const sources = key in env ? [newSourceName, ...env[key].sources] : [newSourceName] if (sources.includes('internal')) { return } - environment.set(key, { + env[key] = { sources, value: fileEnv[key], - }) + } }) }) + return env +} + +/** + * Takes a set of environment variables in the format provided by @netlify/config and injects them into `process.env` + * @param {Record} env + * @return {void} + */ +export const injectEnvVariables = (env) => { // eslint-disable-next-line fp/no-loops - for (const [key, variable] of environment) { + for (const [key, variable] of Object.entries(env)) { const existsInProcess = process.env[key] !== undefined const [usedSource, ...overriddenSources] = existsInProcess ? ['process', ...variable.sources] : variable.sources const usedSourceName = getEnvSourceName(usedSource) diff --git a/tests/integration/100.command.dev.test.cjs b/tests/integration/100.command.dev.test.cjs index 0e4c8af9bf8..1d493f11056 100644 --- a/tests/integration/100.command.dev.test.cjs +++ b/tests/integration/100.command.dev.test.cjs @@ -1,4 +1,3 @@ -// Handlers are meant to be async outside tests const path = require('path') // eslint-disable-next-line ava/use-test @@ -1020,6 +1019,10 @@ test('should have only allowed environment variables set', async (t) => { handler: () => new Response(`${JSON.stringify(Deno.env.toObject())}`), name: 'env', }) + .withContentFile({ + content: 'FROM_ENV="YAS"', + path: '.env', + }) await builder.buildAsync() @@ -1040,16 +1043,19 @@ test('should have only allowed environment variables set', async (t) => { ) const envKeys = Object.keys(response) - t.false(envKeys.includes('DENO_DEPLOYMENT_ID')) - // t.true(envKeys.includes('DENO_DEPLOYMENT_ID')) - // t.is(response.DENO_DEPLOYMENT_ID, 'xxx=') t.true(envKeys.includes('DENO_REGION')) t.is(response.DENO_REGION, 'local') + t.true(envKeys.includes('NETLIFY_DEV')) t.is(response.NETLIFY_DEV, 'true') + t.true(envKeys.includes('SECRET_ENV')) t.is(response.SECRET_ENV, 'true') + t.true(envKeys.includes('FROM_ENV')) + t.is(response.FROM_ENV, 'YAS') + + t.false(envKeys.includes('DENO_DEPLOYMENT_ID')) t.false(envKeys.includes('NODE_ENV')) t.false(envKeys.includes('DEPLOY_URL')) t.false(envKeys.includes('URL'))