rsnext/packages/next-env/index.ts
Shu Ding 5584e5743a
Merge app renderer process (#54143)
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
2023-08-22 14:38:42 +02:00

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 }
}