rsnext/packages/next-env/index.ts

166 lines
4.5 KiB
TypeScript
Raw Normal View History

/* 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 = []
2022-08-13 18:55:55 +02:00
let previousLoadedEnvFiles: LoadedEnvFiles = []
export function updateInitialEnv(newEnv: Env) {
Object.assign(initialEnv || {}, newEnv)
}
type Log = {
info: (...args: any[]) => void
error: (...args: any[]) => void
}
Do not re-assign `process.env` (#46914) ## Checklist - [ ] Related issues linked using `fixes #number` - no related issue exists, happy to open one if desired - [x] Tests added - not sure if specific tests are needed? there is an integration test for environment variables, and Next.js relies a lot on passing information through environment variables; i'd expect everything to break if this change broke environment variable handling - [x] Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md - no new errors, does not apply ### What? Re-assigning `process.env` substitutes the "magic object" that sets environment variables at the process level with an ordinary JavaScript object. This causes environment variables that are set after `process.env` is re-assigned to not be visible to native add-ons. See [this Node.js issue][issue] and [this reproduction case][repro] for details. [issue]: https://github.com/nodejs/node/issues/46996 [repro]: https://github.com/unflxw/nodejs-process-env-addons-repro ### Why? In general, paraphrasing the maintainer in the Node.js issue, re-assigning `process.env` is not a thing you should do. More specifically, I'm trying to use Next.js' experimental OpenTelemetry support with AppSignal's Node.js integration, which also uses OpenTelemetry. The AppSignal Node.js package sets environment variables in order to configure a long-running process, which is then launched through a native add-on. Because of the re-assignment of `process.env` that occurs early in Next.js' lifecycle process, by the time the AppSignal Node.js package sets environment variables, it's setting them in an ordinary JavaScript object that Next.js left in the global `process` object, not in the magic one created by the Node.js runtime. This means that these environment variables are not _actually_ being set for the process at the OS level, and therefore they're also not set for the native add-on, or for the long-running process it spawns. ### How? A `replaceProcessEnv` function is implemented that takes an environment object as an argument, and applies the difference between that environment object and the current environment to the existing `process.env` object. This function is used instead of re-assigning `process.env`. Co-authored-by: JJ Kasper <jj@jjsweb.site>
2023-03-09 23:41:50 +01:00
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]
}
Do not re-assign `process.env` (#46914) ## Checklist - [ ] Related issues linked using `fixes #number` - no related issue exists, happy to open one if desired - [x] Tests added - not sure if specific tests are needed? there is an integration test for environment variables, and Next.js relies a lot on passing information through environment variables; i'd expect everything to break if this change broke environment variable handling - [x] Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md - no new errors, does not apply ### What? Re-assigning `process.env` substitutes the "magic object" that sets environment variables at the process level with an ordinary JavaScript object. This causes environment variables that are set after `process.env` is re-assigned to not be visible to native add-ons. See [this Node.js issue][issue] and [this reproduction case][repro] for details. [issue]: https://github.com/nodejs/node/issues/46996 [repro]: https://github.com/unflxw/nodejs-process-env-addons-repro ### Why? In general, paraphrasing the maintainer in the Node.js issue, re-assigning `process.env` is not a thing you should do. More specifically, I'm trying to use Next.js' experimental OpenTelemetry support with AppSignal's Node.js integration, which also uses OpenTelemetry. The AppSignal Node.js package sets environment variables in order to configure a long-running process, which is then launched through a native add-on. Because of the re-assignment of `process.env` that occurs early in Next.js' lifecycle process, by the time the AppSignal Node.js package sets environment variables, it's setting them in an ordinary JavaScript object that Next.js left in the global `process` object, not in the magic one created by the Node.js runtime. This means that these environment variables are not _actually_ being set for the process at the OS level, and therefore they're also not set for the native add-on, or for the long-running process it spawns. ### How? A `replaceProcessEnv` function is implemented that takes an environment object as an argument, and applies the difference between that environment object and the current environment to the existing `process.env` object. This function is used instead of re-assigning `process.env`. Co-authored-by: JJ Kasper <jj@jjsweb.site>
2023-03-09 23:41:50 +01:00
}
})
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)
2022-08-13 18:55:55 +02:00
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'
) {
chore: enable typescript-eslint's recommended and stylistic configs internally (#52948) Spinning out from #37151 and my draft PR #52845, this enables the two basic recommended rulesets from [typescript-eslint](https://typescript-eslint.io) for the Next.js monorepo source code: * [`plugin:@typescript-eslint/recommended`](https://typescript-eslint.io/linting/configs#recommended): Our base recommended rules that detect common bugs or _(non-stylistic)_ TypeScript bad practices * [`plugin:@typescript-eslint/stylistic`](https://typescript-eslint.io/linting/configs#stylistic): Our base starting stylistic recommended for keeping codebases visually consistent and avoiding out-of-practice visual constructs The process I used is pretty standard (see https://github.com/typescript-eslint/typescript-eslint/issues/6760 for other repos it was done on): 1. Enable those base recommended presets 2. Remove any rule settings that are now redundant 3. Reconfigure any rule whose default settings didn't seem to make sense for this codebase 4. Add a `// Todo: ...` comment, and under it, add a disable for any rule that immediately reported a lot of complaints Note that this only enables the presets internally. It doesn't impact what end-users of published packages such as Next.js or `create-next-app` experience. That's a separate task in #52845. I also didn't fix any existing warning from the `canary` branch. Would you like me to do that? My preference would be a separate PR to get it in more quickly. Any code changes are commented inline. --------- Co-authored-by: Steven <steven@ceriously.com>
2023-08-01 01:32:54 +02:00
// 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 }
}
Do not re-assign `process.env` (#46914) ## Checklist - [ ] Related issues linked using `fixes #number` - no related issue exists, happy to open one if desired - [x] Tests added - not sure if specific tests are needed? there is an integration test for environment variables, and Next.js relies a lot on passing information through environment variables; i'd expect everything to break if this change broke environment variable handling - [x] Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md - no new errors, does not apply ### What? Re-assigning `process.env` substitutes the "magic object" that sets environment variables at the process level with an ordinary JavaScript object. This causes environment variables that are set after `process.env` is re-assigned to not be visible to native add-ons. See [this Node.js issue][issue] and [this reproduction case][repro] for details. [issue]: https://github.com/nodejs/node/issues/46996 [repro]: https://github.com/unflxw/nodejs-process-env-addons-repro ### Why? In general, paraphrasing the maintainer in the Node.js issue, re-assigning `process.env` is not a thing you should do. More specifically, I'm trying to use Next.js' experimental OpenTelemetry support with AppSignal's Node.js integration, which also uses OpenTelemetry. The AppSignal Node.js package sets environment variables in order to configure a long-running process, which is then launched through a native add-on. Because of the re-assignment of `process.env` that occurs early in Next.js' lifecycle process, by the time the AppSignal Node.js package sets environment variables, it's setting them in an ordinary JavaScript object that Next.js left in the global `process` object, not in the magic one created by the Node.js runtime. This means that these environment variables are not _actually_ being set for the process at the OS level, and therefore they're also not set for the native add-on, or for the long-running process it spawns. ### How? A `replaceProcessEnv` function is implemented that takes an environment object as an argument, and applies the difference between that environment object and the current environment to the existing `process.env` object. This function is used instead of re-assigning `process.env`. Co-authored-by: JJ Kasper <jj@jjsweb.site>
2023-03-09 23:41:50 +01:00
replaceProcessEnv(initialEnv)
2022-08-13 18:55:55 +02:00
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 }
}