2020-09-25 20:14:28 +02:00
|
|
|
/* eslint-disable import/no-extraneous-dependencies */
|
|
|
|
import * as fs from 'fs'
|
|
|
|
import * as path from 'path'
|
|
|
|
import * as dotenv from 'dotenv'
|
2022-03-03 17:52:20 +01:00
|
|
|
import { expand as dotenvExpand } from 'dotenv-expand'
|
2020-03-26 13:32:41 +01:00
|
|
|
|
2022-08-11 05:27:48 +02:00
|
|
|
export type Env = { [key: string]: string | undefined }
|
2020-05-26 21:01:57 +02:00
|
|
|
export type LoadedEnvFiles = Array<{
|
|
|
|
path: string
|
|
|
|
contents: string
|
|
|
|
}>
|
2020-03-26 13:32:41 +01:00
|
|
|
|
2023-04-02 15:17:15 +02:00
|
|
|
export let initialEnv: Env | undefined = undefined
|
2020-04-23 21:15:36 +02:00
|
|
|
let combinedEnv: Env | undefined = undefined
|
2020-06-01 23:00:22 +02:00
|
|
|
let cachedLoadedEnvFiles: LoadedEnvFiles = []
|
2022-08-13 18:55:55 +02:00
|
|
|
let previousLoadedEnvFiles: LoadedEnvFiles = []
|
2020-04-23 21:15:36 +02:00
|
|
|
|
2023-05-22 22:29:41 +02:00
|
|
|
export function updateInitialEnv(newEnv: Env) {
|
|
|
|
Object.assign(initialEnv || {}, newEnv)
|
|
|
|
}
|
|
|
|
|
2020-09-25 20:14:28 +02:00
|
|
|
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) => {
|
2023-08-22 14:38:42 +02:00
|
|
|
// 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
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-09-25 20:14:28 +02:00
|
|
|
export function processEnv(
|
|
|
|
loadedEnvFiles: LoadedEnvFiles,
|
|
|
|
dir?: string,
|
2022-08-11 05:27:48 +02:00
|
|
|
log: Log = console,
|
|
|
|
forceReload = false
|
2020-09-25 20:14:28 +02:00
|
|
|
) {
|
2022-08-11 05:27:48 +02:00
|
|
|
if (!initialEnv) {
|
|
|
|
initialEnv = Object.assign({}, process.env)
|
|
|
|
}
|
|
|
|
// only reload env when forceReload is specified
|
|
|
|
if (
|
|
|
|
!forceReload &&
|
|
|
|
(process.env.__NEXT_PROCESSED_ENV || loadedEnvFiles.length === 0)
|
|
|
|
) {
|
2020-05-26 21:01:57 +02:00
|
|
|
return process.env as Env
|
|
|
|
}
|
2023-03-25 10:21:27 +01:00
|
|
|
// flag that we processed the environment values already.
|
2020-05-26 21:01:57 +02:00
|
|
|
process.env.__NEXT_PROCESSED_ENV = 'true'
|
|
|
|
|
2022-08-11 05:27:48 +02:00
|
|
|
const origEnv = Object.assign({}, initialEnv)
|
2020-08-02 22:15:11 +02:00
|
|
|
const parsed: dotenv.DotenvParseOutput = {}
|
|
|
|
|
2020-05-26 21:01:57 +02:00
|
|
|
for (const envFile of loadedEnvFiles) {
|
|
|
|
try {
|
2020-09-25 20:14:28 +02:00
|
|
|
let result: dotenv.DotenvConfigOutput = {}
|
2020-05-26 21:01:57 +02:00
|
|
|
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
|
|
|
|
)
|
|
|
|
) {
|
2020-05-26 21:01:57 +02:00
|
|
|
log.info(`Loaded env from ${path.join(dir || '', envFile.path)}`)
|
|
|
|
}
|
|
|
|
|
2020-08-02 22:15:11 +02:00
|
|
|
for (const key of Object.keys(result.parsed || {})) {
|
|
|
|
if (
|
|
|
|
typeof parsed[key] === 'undefined' &&
|
|
|
|
typeof origEnv[key] === 'undefined'
|
|
|
|
) {
|
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
|
2020-08-02 22:15:11 +02:00
|
|
|
parsed[key] = result.parsed?.[key]!
|
|
|
|
}
|
|
|
|
}
|
2020-05-26 21:01:57 +02:00
|
|
|
} catch (err) {
|
|
|
|
log.error(
|
|
|
|
`Failed to load env from ${path.join(dir || '', envFile.path)}`,
|
|
|
|
err
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2020-08-02 22:15:11 +02:00
|
|
|
return Object.assign(process.env, parsed)
|
2020-05-26 21:01:57 +02:00
|
|
|
}
|
|
|
|
|
2023-07-04 21:28:34 +02:00
|
|
|
export function resetEnv() {
|
|
|
|
if (initialEnv) {
|
|
|
|
replaceProcessEnv(initialEnv)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-26 21:01:57 +02:00
|
|
|
export function loadEnvConfig(
|
|
|
|
dir: string,
|
2020-09-25 20:14:28 +02:00
|
|
|
dev?: boolean,
|
2022-08-11 05:27:48 +02:00
|
|
|
log: Log = console,
|
|
|
|
forceReload = false
|
2020-05-26 21:01:57 +02:00
|
|
|
): {
|
|
|
|
combinedEnv: Env
|
|
|
|
loadedEnvFiles: LoadedEnvFiles
|
|
|
|
} {
|
2022-08-11 05:27:48 +02:00
|
|
|
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
|
2022-08-11 05:27:48 +02:00
|
|
|
cachedLoadedEnvFiles = []
|
2020-04-23 21:15:36 +02:00
|
|
|
|
2020-03-26 13:32:41 +01:00
|
|
|
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`,
|
2020-05-22 19:13:16 +02:00
|
|
|
`.env.${mode}`,
|
2020-03-26 13:32:41 +01:00
|
|
|
'.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 {
|
2020-03-31 18:27:46 +02:00
|
|
|
const stats = fs.statSync(dotEnvPath)
|
|
|
|
|
|
|
|
// make sure to only attempt to read files
|
|
|
|
if (!stats.isFile()) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2020-03-26 13:32:41 +01:00
|
|
|
const contents = fs.readFileSync(dotEnvPath, 'utf8')
|
2020-06-01 23:00:22 +02:00
|
|
|
cachedLoadedEnvFiles.push({
|
2020-05-26 21:01:57 +02:00
|
|
|
path: envFile,
|
|
|
|
contents,
|
|
|
|
})
|
2021-09-16 18:06:57 +02:00
|
|
|
} catch (err: any) {
|
2020-03-26 13:32:41 +01:00
|
|
|
if (err.code !== 'ENOENT') {
|
2020-04-23 21:17:05 +02:00
|
|
|
log.error(`Failed to load env from ${envFile}`, err)
|
2020-03-26 13:32:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-08-11 05:27:48 +02:00
|
|
|
combinedEnv = processEnv(cachedLoadedEnvFiles, dir, log, forceReload)
|
2020-06-01 23:00:22 +02:00
|
|
|
return { combinedEnv, loadedEnvFiles: cachedLoadedEnvFiles }
|
2020-03-26 13:32:41 +01:00
|
|
|
}
|