rsnext/packages/next/lib/load-env-config.ts
JJ Kasper 9dd974dfca
Fix dotenv loading with cascading values (#15799)
Adds additional test cases for cascading env values and corrects behavior

Fixes: https://github.com/vercel/next.js/issues/15744
2020-08-02 20:15:11 +00:00

111 lines
3.1 KiB
TypeScript

import fs from 'fs'
import path from 'path'
import * as log from '../build/output/log'
import dotenvExpand from 'next/dist/compiled/dotenv-expand'
import dotenv, { DotenvConfigOutput } from 'next/dist/compiled/dotenv'
export type Env = { [key: string]: string }
export type LoadedEnvFiles = Array<{
path: string
contents: string
}>
let combinedEnv: Env | undefined = undefined
let cachedLoadedEnvFiles: LoadedEnvFiles = []
export function processEnv(loadedEnvFiles: LoadedEnvFiles, dir?: string) {
// don't reload env if we already have since this breaks escaped
// environment values e.g. \$ENV_FILE_KEY
if (
combinedEnv ||
process.env.__NEXT_PROCESSED_ENV ||
!loadedEnvFiles.length
) {
return process.env as Env
}
// flag that we processed the environment values in case a serverless
// function is re-used or we are running in `next start` mode
process.env.__NEXT_PROCESSED_ENV = 'true'
const origEnv = Object.assign({}, process.env)
const parsed: dotenv.DotenvParseOutput = {}
for (const envFile of loadedEnvFiles) {
try {
let result: DotenvConfigOutput = {}
result.parsed = dotenv.parse(envFile.contents)
result = dotenvExpand(result)
if (result.parsed) {
log.info(`Loaded env from ${path.join(dir || '', envFile.path)}`)
}
for (const key of Object.keys(result.parsed || {})) {
if (
typeof parsed[key] === 'undefined' &&
typeof origEnv[key] === 'undefined'
) {
parsed[key] = result.parsed?.[key]!
}
}
} catch (err) {
log.error(
`Failed to load env from ${path.join(dir || '', envFile.path)}`,
err
)
}
}
return Object.assign(process.env, parsed)
}
export function loadEnvConfig(
dir: string,
dev?: boolean
): {
combinedEnv: Env
loadedEnvFiles: LoadedEnvFiles
} {
// don't reload env if we already have since this breaks escaped
// environment values e.g. \$ENV_FILE_KEY
if (combinedEnv) return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
const isTest = process.env.NODE_ENV === 'test'
const mode = isTest ? 'test' : dev ? 'development' : 'production'
const dotenvFiles = [
`.env.${mode}.local`,
// Don't include `.env.local` for `test` environment
// since normally you expect tests to produce the same
// results for everyone
mode !== 'test' && `.env.local`,
`.env.${mode}`,
'.env',
].filter(Boolean) as string[]
for (const envFile of dotenvFiles) {
// only load .env if the user provided has an env config file
const dotEnvPath = path.join(dir, envFile)
try {
const stats = fs.statSync(dotEnvPath)
// make sure to only attempt to read files
if (!stats.isFile()) {
continue
}
const contents = fs.readFileSync(dotEnvPath, 'utf8')
cachedLoadedEnvFiles.push({
path: envFile,
contents,
})
} catch (err) {
if (err.code !== 'ENOENT') {
log.error(`Failed to load env from ${envFile}`, err)
}
}
}
combinedEnv = processEnv(cachedLoadedEnvFiles, dir)
return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
}