Skip to content

Commit 579c29a

Browse files
Han5991IlyasShabi
andcommitted
src: support override option in process.loadEnvFile
Add an opt-in `override` option to `process.loadEnvFile()` and a matching `--env-file-override-local` CLI flag, allowing values from `.env` files to replace existing variables in `process.env`. By default, existing environment variables continue to take precedence; callers must opt in explicitly. This lets a single process swap env-file contexts at runtime (integration tests, monorepo configurations) without manually parsing files via `util.parseEnv()`. Co-authored-by: Ilyas Shabi <ilyasshabi94@gmail.com> Signed-off-by: sangwook <rewq5991@gmail.com>
1 parent a9b98b2 commit 579c29a

15 files changed

Lines changed: 206 additions & 12 deletions

doc/api/cli.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -937,6 +937,26 @@ changes:
937937
Behavior is the same as [`--env-file`][], but an error is not thrown if the file
938938
does not exist.
939939

940+
### `--env-file-override-local`
941+
942+
<!-- YAML
943+
added: REPLACEME
944+
-->
945+
946+
By default, when a variable defined in an env file is already set in the
947+
environment, the existing value is preserved. Pass
948+
`--env-file-override-local` together with [`--env-file`][] (or
949+
[`--env-file-if-exists`][]) to make values from the file override existing
950+
environment variables instead.
951+
952+
```bash
953+
BASIC=local node --env-file=.env --env-file-override-local -p 'process.env.BASIC'
954+
# prints the value from .env, not 'local'.
955+
```
956+
957+
To override variables at runtime, use the `override` option of
958+
[`process.loadEnvFile()`][].
959+
940960
### `--env-file=file`
941961

942962
<!-- YAML
@@ -4391,6 +4411,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
43914411
[`node:ffi`]: ffi.md
43924412
[`node:sqlite`]: sqlite.md
43934413
[`node:stream/iter`]: stream_iter.md
4414+
[`process.loadEnvFile()`]: process.md#processloadenvfilepath-options
43944415
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
43954416
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
43964417
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version

doc/api/process.md

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2753,7 +2753,7 @@ process.kill(process.pid, 'SIGHUP');
27532753
When `SIGUSR1` is received by a Node.js process, Node.js will start the
27542754
debugger. See [Signal Events][].
27552755
2756-
## `process.loadEnvFile(path)`
2756+
## `process.loadEnvFile(path[, options])`
27572757
27582758
<!-- YAML
27592759
added:
@@ -2767,7 +2767,10 @@ changes:
27672767
description: This API is no longer experimental.
27682768
-->
27692769
2770-
* `path` {string | URL | Buffer | undefined}. **Default:** `'./.env'`
2770+
* `path` {string | URL | Buffer | undefined} **Default:** `'./.env'`
2771+
* `options` {Object}
2772+
* `override` {boolean} If `true`, values from the file replace any matching
2773+
variable already set in `process.env`. **Default:** `false`.
27712774
27722775
Loads the `.env` file into `process.env`. Usage of `NODE_OPTIONS`
27732776
in the `.env` file will not have any effect on Node.js.
@@ -2782,6 +2785,38 @@ import { loadEnvFile } from 'node:process';
27822785
loadEnvFile();
27832786
```
27842787
2788+
By default, an env file does not override variables that are already set.
2789+
Pass `override: true` to replace them with the values from the file:
2790+
2791+
```cjs
2792+
const { loadEnvFile } = require('node:process');
2793+
// process.env.API_KEY === 'local-key'
2794+
loadEnvFile('.env', { override: true });
2795+
// process.env.API_KEY now matches the value from .env
2796+
```
2797+
2798+
```mjs
2799+
import { loadEnvFile } from 'node:process';
2800+
// process.env.API_KEY === 'local-key'
2801+
loadEnvFile('.env', { override: true });
2802+
// process.env.API_KEY now matches the value from .env
2803+
```
2804+
2805+
When called with options only, the default `'./.env'` path is used:
2806+
2807+
```cjs
2808+
const { loadEnvFile } = require('node:process');
2809+
loadEnvFile({ override: true });
2810+
```
2811+
2812+
```mjs
2813+
import { loadEnvFile } from 'node:process';
2814+
loadEnvFile({ override: true });
2815+
```
2816+
2817+
The same behavior is available at startup via the
2818+
[`--env-file-override-local`][] flag.
2819+
27852820
## `process.mainModule`
27862821
27872822
<!-- YAML
@@ -4613,6 +4648,7 @@ cases:
46134648
[`'exit'`]: #event-exit
46144649
[`'message'`]: child_process.md#event-message
46154650
[`'uncaughtException'`]: #event-uncaughtexception
4651+
[`--env-file-override-local`]: cli.md#--env-file-override-local
46164652
[`--no-deprecation`]: cli.md#--no-deprecation
46174653
[`--permission`]: cli.md#--permission
46184654
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode

doc/node.1

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,14 @@ node --entry-url 'data:text/javascript,console.log("Hello")'
571571
Behavior is the same as \fB--env-file\fR, but an error is not thrown if the file
572572
does not exist.
573573
.
574+
.It Fl -env-file-override-local
575+
Override existing environment variables with values from files supplied via
576+
\fB--env-file\fR or \fB--env-file-if-exists\fR. Without this flag, existing
577+
variables take precedence.
578+
.Bd -literal
579+
BASIC=local node --env-file=.env --env-file-override-local -p 'process.env.BASIC'
580+
.Ed
581+
.
574582
.It Fl -env-file Ns = Ns Ar file
575583
Loads environment variables from a file relative to the current directory,
576584
making them available to applications on \fBprocess.env\fR. The environment

lib/internal/process/per_thread.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@ const {
4545
ERR_WORKER_UNSUPPORTED_OPERATION,
4646
},
4747
} = require('internal/errors');
48-
const { emitExperimentalWarning } = require('internal/util');
48+
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
49+
const { isArrayBufferView } = require('internal/util/types');
4950
const format = require('internal/util/inspect').format;
5051
const {
5152
validateArray,
53+
validateBoolean,
5254
validateNumber,
5355
validateObject,
5456
validateString,
@@ -60,6 +62,7 @@ const execveDiagnosticChannel = dc.channel('process.execve');
6062
const constants = internalBinding('constants').os.signals;
6163

6264
let getValidatedPath; // We need to lazy load it because of the circular dependency.
65+
let URLClass;
6366

6467
const kInternal = Symbol('internal properties');
6568

@@ -352,14 +355,29 @@ function wrapProcessMethods(binding) {
352355
/**
353356
* Loads the `.env` file to process.env.
354357
* @param {string | URL | Buffer | undefined} path
358+
* @param {{ override?: boolean }} [options]
355359
*/
356-
function loadEnvFile(path = undefined) { // Provide optional value so that `loadEnvFile.length` returns 0
360+
function loadEnvFile(path = undefined, options = kEmptyObject) {
361+
// Provide optional value so that `loadEnvFile.length` returns 0
362+
if (arguments.length === 1 &&
363+
path !== null && path !== undefined &&
364+
typeof path === 'object' &&
365+
!isArrayBufferView(path)) {
366+
URLClass ??= require('internal/url').URL;
367+
if (!(path instanceof URLClass)) {
368+
options = path;
369+
path = undefined;
370+
}
371+
}
372+
validateObject(options, 'options');
373+
const { override = false } = options;
374+
validateBoolean(override, 'options.override');
357375
if (path != null) {
358376
getValidatedPath ??= require('internal/fs/utils').getValidatedPath;
359377
path = getValidatedPath(path);
360-
_loadEnvFile(path);
378+
_loadEnvFile(path, override);
361379
} else {
362-
_loadEnvFile();
380+
_loadEnvFile(undefined, override);
363381
}
364382
}
365383

src/node.cc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ MaybeLocal<Value> StartExecution(Environment* env,
337337
// Without it env is not updated when restarting child process.
338338
// Child process has --watch flag removed, so it will load the file.
339339
if (env->options()->has_env_file_string && !env->options()->watch_mode) {
340-
per_process::dotenv_file.SetEnvironment(env);
340+
per_process::dotenv_file.SetEnvironment(
341+
env, env->options()->env_file_override_local);
341342
}
342343

343344
// TODO(joyeecheung): move these conditions into JS land and let the

src/node_dotenv.cc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ std::vector<Dotenv::env_file_data> Dotenv::GetDataFromArgs(
6666
return env_files;
6767
}
6868

69-
Maybe<void> Dotenv::SetEnvironment(node::Environment* env) {
69+
Maybe<void> Dotenv::SetEnvironment(node::Environment* env, bool override) {
7070
auto context = env->context();
7171
auto env_vars = env->env_vars();
7272

7373
for (const auto& entry : store_) {
7474
auto existing = env_vars->Get(entry.first.data());
75-
if (!existing.has_value()) {
75+
if (override || !existing.has_value()) {
7676
Local<Value> name;
7777
Local<Value> val;
7878
if (!ToV8Value(context, entry.first).ToLocal(&name) ||

src/node_dotenv.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ class Dotenv {
2828
void ParseContent(const std::string_view content);
2929
ParseResult ParsePath(const std::string_view path);
3030
void AssignNodeOptionsIfAvailable(std::string* node_options) const;
31-
v8::Maybe<void> SetEnvironment(Environment* env);
31+
v8::Maybe<void> SetEnvironment(Environment* env, bool override = false);
3232
v8::MaybeLocal<v8::Object> ToObject(Environment* env) const;
3333

3434
static std::vector<env_file_data> GetDataFromArgs(

src/node_options.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
899899
"set environment variables from supplied file",
900900
&EnvironmentOptions::optional_env_file);
901901
Implies("--env-file-if-exists", "[has_env_file_string]");
902+
AddOption("--env-file-override-local",
903+
"override environment variables already set on the machine with "
904+
"values from files supplied via --env-file or --env-file-if-exists",
905+
&EnvironmentOptions::env_file_override_local);
906+
Implies("--env-file-override-local", "[has_env_file_string]");
902907
AddOption("--experimental-config-file",
903908
"set config file path",
904909
&EnvironmentOptions::experimental_config_file_path,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ class EnvironmentOptions : public Options {
194194
std::vector<std::string> env_file;
195195
std::vector<std::string> optional_env_file;
196196
bool has_env_file_string = false;
197+
bool env_file_override_local = false;
197198
bool test_runner = false;
198199
uint64_t test_runner_concurrency = 0;
199200
uint64_t test_runner_timeout = 0;

src/node_process_methods.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,20 +619,22 @@ static void Execve(const FunctionCallbackInfo<Value>& args) {
619619
static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
620620
Environment* env = Environment::GetCurrent(args);
621621
std::string path = ".env";
622-
if (args.Length() == 1) {
622+
if (args.Length() >= 1 && !args[0]->IsUndefined()) {
623623
BufferValue path_value(args.GetIsolate(), args[0]);
624624
ToNamespacedPath(env, &path_value);
625625
path = path_value.ToString();
626626
}
627627

628+
bool override = args.Length() >= 2 && args[1]->IsTrue();
629+
628630
THROW_IF_INSUFFICIENT_PERMISSIONS(
629631
env, permission::PermissionScope::kFileSystemRead, path);
630632

631633
Dotenv dotenv{};
632634

633635
switch (dotenv.ParsePath(path)) {
634636
case dotenv.ParseResult::Valid: {
635-
USE(dotenv.SetEnvironment(env));
637+
USE(dotenv.SetEnvironment(env, override));
636638
break;
637639
}
638640
case dotenv.ParseResult::InvalidContent: {

0 commit comments

Comments
 (0)