Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,26 @@ changes:
Behavior is the same as [`--env-file`][], but an error is not thrown if the file
does not exist.

### `--env-file-override-local`

<!-- YAML
added: REPLACEME
-->

By default, when a variable defined in an env file is already set in the
environment, the existing value is preserved. Pass
`--env-file-override-local` together with [`--env-file`][] (or
[`--env-file-if-exists`][]) to make values from the file override existing
environment variables instead.

```bash
BASIC=local node --env-file=.env --env-file-override-local -p 'process.env.BASIC'
# prints the value from .env, not 'local'.
```

To override variables at runtime, use the `override` option of
[`process.loadEnvFile()`][].

### `--env-file=file`

<!-- YAML
Expand Down Expand Up @@ -4391,6 +4411,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`node:ffi`]: ffi.md
[`node:sqlite`]: sqlite.md
[`node:stream/iter`]: stream_iter.md
[`process.loadEnvFile()`]: process.md#processloadenvfilepath-options
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version
Expand Down
40 changes: 38 additions & 2 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -2753,7 +2753,7 @@ process.kill(process.pid, 'SIGHUP');
When `SIGUSR1` is received by a Node.js process, Node.js will start the
debugger. See [Signal Events][].

## `process.loadEnvFile(path)`
## `process.loadEnvFile(path[, options])`

<!-- YAML
added:
Expand All @@ -2767,7 +2767,10 @@ changes:
description: This API is no longer experimental.
-->

* `path` {string | URL | Buffer | undefined}. **Default:** `'./.env'`
* `path` {string | URL | Buffer | undefined} **Default:** `'./.env'`
* `options` {Object}
* `override` {boolean} If `true`, values from the file replace any matching
variable already set in `process.env`. **Default:** `false`.

Loads the `.env` file into `process.env`. Usage of `NODE_OPTIONS`
in the `.env` file will not have any effect on Node.js.
Expand All @@ -2782,6 +2785,38 @@ import { loadEnvFile } from 'node:process';
loadEnvFile();
```

By default, an env file does not override variables that are already set.
Pass `override: true` to replace them with the values from the file:

```cjs
const { loadEnvFile } = require('node:process');
// process.env.API_KEY === 'local-key'
loadEnvFile('.env', { override: true });
// process.env.API_KEY now matches the value from .env
```

```mjs
import { loadEnvFile } from 'node:process';
// process.env.API_KEY === 'local-key'
loadEnvFile('.env', { override: true });
// process.env.API_KEY now matches the value from .env
```

When called with options only, the default `'./.env'` path is used:

```cjs
const { loadEnvFile } = require('node:process');
loadEnvFile({ override: true });
```

```mjs
import { loadEnvFile } from 'node:process';
loadEnvFile({ override: true });
```

The same behavior is available at startup via the
[`--env-file-override-local`][] flag.

## `process.mainModule`

<!-- YAML
Expand Down Expand Up @@ -4613,6 +4648,7 @@ cases:
[`'exit'`]: #event-exit
[`'message'`]: child_process.md#event-message
[`'uncaughtException'`]: #event-uncaughtexception
[`--env-file-override-local`]: cli.md#--env-file-override-local
[`--no-deprecation`]: cli.md#--no-deprecation
[`--permission`]: cli.md#--permission
[`--unhandled-rejections`]: cli.md#--unhandled-rejectionsmode
Expand Down
8 changes: 8 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,14 @@ node --entry-url 'data:text/javascript,console.log("Hello")'
Behavior is the same as \fB--env-file\fR, but an error is not thrown if the file
does not exist.
.
.It Fl -env-file-override-local
Override existing environment variables with values from files supplied via
\fB--env-file\fR or \fB--env-file-if-exists\fR. Without this flag, existing
variables take precedence.
.Bd -literal
BASIC=local node --env-file=.env --env-file-override-local -p 'process.env.BASIC'
.Ed
.
.It Fl -env-file Ns = Ns Ar file
Loads environment variables from a file relative to the current directory,
making them available to applications on \fBprocess.env\fR. The environment
Expand Down
26 changes: 22 additions & 4 deletions lib/internal/process/per_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,12 @@ const {
ERR_WORKER_UNSUPPORTED_OPERATION,
},
} = require('internal/errors');
const { emitExperimentalWarning } = require('internal/util');
const { emitExperimentalWarning, kEmptyObject } = require('internal/util');
const { isArrayBufferView } = require('internal/util/types');
const format = require('internal/util/inspect').format;
const {
validateArray,
validateBoolean,
validateNumber,
validateObject,
validateString,
Expand All @@ -60,6 +62,7 @@ const execveDiagnosticChannel = dc.channel('process.execve');
const constants = internalBinding('constants').os.signals;

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

const kInternal = Symbol('internal properties');

Expand Down Expand Up @@ -352,14 +355,29 @@ function wrapProcessMethods(binding) {
/**
* Loads the `.env` file to process.env.
* @param {string | URL | Buffer | undefined} path
* @param {{ override?: boolean }} [options]
*/
function loadEnvFile(path = undefined) { // Provide optional value so that `loadEnvFile.length` returns 0
function loadEnvFile(path = undefined, options = kEmptyObject) {
// Provide optional value so that `loadEnvFile.length` returns 0
if (arguments.length === 1 &&
path !== null && path !== undefined &&
typeof path === 'object' &&
!isArrayBufferView(path)) {
URLClass ??= require('internal/url').URL;
if (!(path instanceof URLClass)) {
options = path;
path = undefined;
}
}
validateObject(options, 'options');
const { override = false } = options;
validateBoolean(override, 'options.override');
if (path != null) {
getValidatedPath ??= require('internal/fs/utils').getValidatedPath;
path = getValidatedPath(path);
_loadEnvFile(path);
_loadEnvFile(path, override);
} else {
_loadEnvFile();
_loadEnvFile(undefined, override);
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -337,7 +337,8 @@ MaybeLocal<Value> StartExecution(Environment* env,
// Without it env is not updated when restarting child process.
// Child process has --watch flag removed, so it will load the file.
if (env->options()->has_env_file_string && !env->options()->watch_mode) {
per_process::dotenv_file.SetEnvironment(env);
per_process::dotenv_file.SetEnvironment(
env, env->options()->env_file_override_local);
}

// TODO(joyeecheung): move these conditions into JS land and let the
Expand Down
4 changes: 2 additions & 2 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,13 @@ std::vector<Dotenv::env_file_data> Dotenv::GetDataFromArgs(
return env_files;
}

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

for (const auto& entry : store_) {
auto existing = env_vars->Get(entry.first.data());
if (!existing.has_value()) {
if (override || !existing.has_value()) {
Local<Value> name;
Local<Value> val;
if (!ToV8Value(context, entry.first).ToLocal(&name) ||
Expand Down
2 changes: 1 addition & 1 deletion src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Dotenv {
void ParseContent(const std::string_view content);
ParseResult ParsePath(const std::string_view path);
void AssignNodeOptionsIfAvailable(std::string* node_options) const;
v8::Maybe<void> SetEnvironment(Environment* env);
v8::Maybe<void> SetEnvironment(Environment* env, bool override = false);
v8::MaybeLocal<v8::Object> ToObject(Environment* env) const;

static std::vector<env_file_data> GetDataFromArgs(
Expand Down
5 changes: 5 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"set environment variables from supplied file",
&EnvironmentOptions::optional_env_file);
Implies("--env-file-if-exists", "[has_env_file_string]");
AddOption("--env-file-override-local",
"override environment variables already set on the machine with "
"values from files supplied via --env-file or --env-file-if-exists",
&EnvironmentOptions::env_file_override_local);
Implies("--env-file-override-local", "[has_env_file_string]");
AddOption("--experimental-config-file",
"set config file path",
&EnvironmentOptions::experimental_config_file_path,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class EnvironmentOptions : public Options {
std::vector<std::string> env_file;
std::vector<std::string> optional_env_file;
bool has_env_file_string = false;
bool env_file_override_local = false;
bool test_runner = false;
uint64_t test_runner_concurrency = 0;
uint64_t test_runner_timeout = 0;
Expand Down
6 changes: 4 additions & 2 deletions src/node_process_methods.cc
Original file line number Diff line number Diff line change
Expand Up @@ -619,20 +619,22 @@ static void Execve(const FunctionCallbackInfo<Value>& args) {
static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
std::string path = ".env";
if (args.Length() == 1) {
if (args.Length() >= 1 && !args[0]->IsUndefined()) {
BufferValue path_value(args.GetIsolate(), args[0]);
ToNamespacedPath(env, &path_value);
path = path_value.ToString();
}

bool override = args.Length() >= 2 && args[1]->IsTrue();

THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kFileSystemRead, path);

Dotenv dotenv{};

switch (dotenv.ParsePath(path)) {
case dotenv.ParseResult::Valid: {
USE(dotenv.SetEnvironment(env));
USE(dotenv.SetEnvironment(env, override));
break;
}
case dotenv.ParseResult::InvalidContent: {
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/dotenv/assert-basic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

const assert = require('node:assert');

assert.strictEqual(process.env.BASIC, process.env.EXPECTED);
7 changes: 7 additions & 0 deletions test/fixtures/dotenv/load-env-file-no-override.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

const assert = require('node:assert');

assert.strictEqual(process.env.BASIC, 'Original value');
process.loadEnvFile(process.argv[2]);
assert.strictEqual(process.env.BASIC, 'Original value');
7 changes: 7 additions & 0 deletions test/fixtures/dotenv/load-env-file-options-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

const assert = require('node:assert');

assert.strictEqual(process.env.BASIC, 'Original value');
process.loadEnvFile({ override: true });
assert.strictEqual(process.env.BASIC, 'basic');
7 changes: 7 additions & 0 deletions test/fixtures/dotenv/load-env-file-override.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use strict';

const assert = require('node:assert');

assert.strictEqual(process.env.BASIC, 'Original value');
process.loadEnvFile(process.argv[2], { override: true });
assert.strictEqual(process.env.BASIC, 'basic');
76 changes: 76 additions & 0 deletions test/parallel/test-process-load-env-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,79 @@ describe('process.loadEnvFile()', () => {
assert.strictEqual(child.code, 0);
});
});

describe('process.loadEnvFile(path, options)', () => {
const noOverrideFixture = fixtures.path('dotenv/load-env-file-no-override.js');
const overrideFixture = fixtures.path('dotenv/load-env-file-override.js');
const optionsOnlyFixture = fixtures.path('dotenv/load-env-file-options-only.js');
const assertBasicFixture = fixtures.path('dotenv/assert-basic.js');

it('does not override an existing env var by default', async () => {
const child = await common.spawnPromisified(
process.execPath,
[ noOverrideFixture, validEnvFilePath ],
{ env: { ...process.env, BASIC: 'Original value' } },
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});

it('overrides an existing env var when override: true', async () => {
const child = await common.spawnPromisified(
process.execPath,
[ overrideFixture, validEnvFilePath ],
{ env: { ...process.env, BASIC: 'Original value' } },
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});

it('supports passing options only', async () => {
const child = await common.spawnPromisified(
process.execPath,
[ optionsOnlyFixture ],
{
cwd: fixtures.path('dotenv/'),
env: { ...process.env, BASIC: 'Original value' },
},
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});

it('throws when options is not an object', () => {
assert.throws(() => {
process.loadEnvFile(validEnvFilePath, 'not-an-object');
}, { code: 'ERR_INVALID_ARG_TYPE', message: /options/ });
});

it('throws when options.override is not a boolean', () => {
assert.throws(() => {
process.loadEnvFile(validEnvFilePath, { override: 'yes' });
}, { code: 'ERR_INVALID_ARG_TYPE', message: /options\.override/ });
});

it('--env-file-override-local overrides existing env vars from --env-file', async () => {
const child = await common.spawnPromisified(
process.execPath,
[
`--env-file=${validEnvFilePath}`,
'--env-file-override-local',
assertBasicFixture,
],
{ env: { ...process.env, BASIC: 'Original value', EXPECTED: 'basic' } },
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});

it('--env-file alone keeps existing env vars', async () => {
const child = await common.spawnPromisified(
process.execPath,
[ `--env-file=${validEnvFilePath}`, assertBasicFixture ],
{ env: { ...process.env, BASIC: 'Original value', EXPECTED: 'Original value' } },
);
assert.strictEqual(child.stderr, '');
assert.strictEqual(child.code, 0);
});
});
Loading