fdacca8abc
This updates to have a separate routing process and separate rendering processes for `pages` and `app` so that we can properly isolate the two since they rely on different react versions. Besides allowing the above mentioned isolation this also helps us control recovering from process crashes easier as pieces are more isolated from one another e.g. an infinite loop during rendering will no longer block the compiler and can be stopped/restarted as needed. In follow-up PRs we will continue to separate out the routing logic from the rendering logic so that each process only loads what is relevant to it helping simplify the flow for requests regardless of type. --------- Co-authored-by: Shu Ding <g@shud.in> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
149 lines
3.9 KiB
TypeScript
149 lines
3.9 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 = []
|
|
|
|
type Log = {
|
|
info: (...args: any[]) => void
|
|
error: (...args: any[]) => void
|
|
}
|
|
|
|
function replaceProcessEnv(sourceEnv: Env) {
|
|
Object.keys(process.env).forEach((key) => {
|
|
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'
|
|
) {
|
|
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,
|
|
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 }
|
|
}
|