5584e5743a
This PR merges the app renderer worker into the router process. This improves the memory overhead mostly. There're future work to do to get rid of the IPC server for router and app renderer, as they're now merged in one process. Fixes NEXT-1492
165 lines
4.5 KiB
TypeScript
165 lines
4.5 KiB
TypeScript
/* eslint-disable import/no-extraneous-dependencies */
|
|
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
import * as dotenv from 'dotenv'
|
|
import { expand as dotenvExpand } from 'dotenv-expand'
|
|
|
|
export type Env = { [key: string]: string | undefined }
|
|
export type LoadedEnvFiles = Array<{
|
|
path: string
|
|
contents: string
|
|
}>
|
|
|
|
export let initialEnv: Env | undefined = undefined
|
|
let combinedEnv: Env | undefined = undefined
|
|
let cachedLoadedEnvFiles: LoadedEnvFiles = []
|
|
let previousLoadedEnvFiles: LoadedEnvFiles = []
|
|
|
|
export function updateInitialEnv(newEnv: Env) {
|
|
Object.assign(initialEnv || {}, newEnv)
|
|
}
|
|
|
|
type Log = {
|
|
info: (...args: any[]) => void
|
|
error: (...args: any[]) => void
|
|
}
|
|
|
|
function replaceProcessEnv(sourceEnv: Env) {
|
|
Object.keys(process.env).forEach((key) => {
|
|
// Allow mutating internal Next.js env variables after the server has initiated.
|
|
// This is necessary for dynamic things like the IPC server port.
|
|
if (!key.startsWith('__NEXT_PRIVATE')) {
|
|
if (sourceEnv[key] === undefined || sourceEnv[key] === '') {
|
|
delete process.env[key]
|
|
}
|
|
}
|
|
})
|
|
|
|
Object.entries(sourceEnv).forEach(([key, value]) => {
|
|
process.env[key] = value
|
|
})
|
|
}
|
|
|
|
export function processEnv(
|
|
loadedEnvFiles: LoadedEnvFiles,
|
|
dir?: string,
|
|
log: Log = console,
|
|
forceReload = false
|
|
) {
|
|
if (!initialEnv) {
|
|
initialEnv = Object.assign({}, process.env)
|
|
}
|
|
// only reload env when forceReload is specified
|
|
if (
|
|
!forceReload &&
|
|
(process.env.__NEXT_PROCESSED_ENV || loadedEnvFiles.length === 0)
|
|
) {
|
|
return process.env as Env
|
|
}
|
|
// flag that we processed the environment values already.
|
|
process.env.__NEXT_PROCESSED_ENV = 'true'
|
|
|
|
const origEnv = Object.assign({}, initialEnv)
|
|
const parsed: dotenv.DotenvParseOutput = {}
|
|
|
|
for (const envFile of loadedEnvFiles) {
|
|
try {
|
|
let result: dotenv.DotenvConfigOutput = {}
|
|
result.parsed = dotenv.parse(envFile.contents)
|
|
|
|
result = dotenvExpand(result)
|
|
|
|
if (
|
|
result.parsed &&
|
|
!previousLoadedEnvFiles.some(
|
|
(item) =>
|
|
item.contents === envFile.contents && item.path === envFile.path
|
|
)
|
|
) {
|
|
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'
|
|
) {
|
|
// We're being imprecise in the type system - assume parsed[key] can be undefined
|
|
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
|
|
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 resetEnv() {
|
|
if (initialEnv) {
|
|
replaceProcessEnv(initialEnv)
|
|
}
|
|
}
|
|
|
|
export function loadEnvConfig(
|
|
dir: string,
|
|
dev?: boolean,
|
|
log: Log = console,
|
|
forceReload = false
|
|
): {
|
|
combinedEnv: Env
|
|
loadedEnvFiles: LoadedEnvFiles
|
|
} {
|
|
if (!initialEnv) {
|
|
initialEnv = Object.assign({}, process.env)
|
|
}
|
|
// only reload env when forceReload is specified
|
|
if (combinedEnv && !forceReload) {
|
|
return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
|
|
}
|
|
replaceProcessEnv(initialEnv)
|
|
previousLoadedEnvFiles = cachedLoadedEnvFiles
|
|
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: any) {
|
|
if (err.code !== 'ENOENT') {
|
|
log.error(`Failed to load env from ${envFile}`, err)
|
|
}
|
|
}
|
|
}
|
|
combinedEnv = processEnv(cachedLoadedEnvFiles, dir, log, forceReload)
|
|
return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
|
|
}
|