Skip to content

Commit 769823e

Browse files
authored
src: add built-in .env file support
PR-URL: #48890 Refs: https://github.com/orgs/nodejs/discussions/44975 Reviewed-By: Geoffrey Booth <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Paolo Insogna <[email protected]>
1 parent 6bb400f commit 769823e

19 files changed

+508
-16
lines changed

doc/api/cli.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,6 +984,41 @@ surface on other platforms, but the performance impact may be severe.
984984
This flag is inherited from V8 and is subject to change upstream. It may
985985
disappear in a non-semver-major release.
986986

987+
### `--env-file=config`
988+
989+
> Stability: 1.1 - Active development
990+
991+
<!-- YAML
992+
added: REPLACEME
993+
-->
994+
995+
Loads environment variables from a file relative to the current directory,
996+
making them available to applications on `process.env`. The [environment
997+
variables which configure Node.js][environment_variables], such as `NODE_OPTIONS`,
998+
are parsed and applied. If the same variable is defined in the environment and
999+
in the file, the value from the environment takes precedence.
1000+
1001+
The format of the file should be one line per key-value pair of environment
1002+
variable name and value separated by `=`:
1003+
1004+
```text
1005+
PORT=3000
1006+
```
1007+
1008+
Any text after a `#` is treated as a comment:
1009+
1010+
```text
1011+
# This is a comment
1012+
PORT=3000 # This is also a comment
1013+
```
1014+
1015+
Values can start and end with the following quotes: `\`, `"` or `'`.
1016+
They are omitted from the values.
1017+
1018+
```text
1019+
USERNAME="nodejs" # will result in `nodejs` as the value.
1020+
```
1021+
9871022
### `--max-http-header-size=size`
9881023

9891024
<!-- YAML
@@ -2647,6 +2682,7 @@ done
26472682
[debugger]: debugger.md
26482683
[debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications
26492684
[emit_warning]: process.md#processemitwarningwarning-options
2685+
[environment_variables]: #environment-variables
26502686
[filtering tests by name]: test.md#filtering-tests-by-name
26512687
[jitless]: https://v8.dev/blog/jitless
26522688
[libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
'src/node_contextify.cc',
101101
'src/node_credentials.cc',
102102
'src/node_dir.cc',
103+
'src/node_dotenv.cc',
103104
'src/node_env_var.cc',
104105
'src/node_errors.cc',
105106
'src/node_external_reference.cc',
@@ -214,6 +215,7 @@
214215
'src/node_context_data.h',
215216
'src/node_contextify.h',
216217
'src/node_dir.h',
218+
'src/node_dotenv.h',
217219
'src/node_errors.h',
218220
'src/node_exit_code.h',
219221
'src/node_external_reference.h',

src/env.cc

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -684,7 +684,7 @@ void Environment::TryLoadAddon(
684684
}
685685
}
686686

687-
std::string Environment::GetCwd() {
687+
std::string Environment::GetCwd(const std::string& exec_path) {
688688
char cwd[PATH_MAX_BYTES];
689689
size_t size = PATH_MAX_BYTES;
690690
const int err = uv_cwd(cwd, &size);
@@ -696,7 +696,6 @@ std::string Environment::GetCwd() {
696696

697697
// This can fail if the cwd is deleted. In that case, fall back to
698698
// exec_path.
699-
const std::string& exec_path = exec_path_;
700699
return exec_path.substr(0, exec_path.find_last_of(kPathSeparator));
701700
}
702701

@@ -730,7 +729,7 @@ std::unique_ptr<v8::BackingStore> Environment::release_managed_buffer(
730729
return bs;
731730
}
732731

733-
std::string GetExecPath(const std::vector<std::string>& argv) {
732+
std::string Environment::GetExecPath(const std::vector<std::string>& argv) {
734733
char exec_path_buf[2 * PATH_MAX];
735734
size_t exec_path_len = sizeof(exec_path_buf);
736735
std::string exec_path;
@@ -772,7 +771,7 @@ Environment::Environment(IsolateData* isolate_data,
772771
timer_base_(uv_now(isolate_data->event_loop())),
773772
exec_argv_(exec_args),
774773
argv_(args),
775-
exec_path_(GetExecPath(args)),
774+
exec_path_(Environment::GetExecPath(args)),
776775
exit_info_(
777776
isolate_, kExitInfoFieldCount, MAYBE_FIELD_PTR(env_info, exit_info)),
778777
should_abort_on_uncaught_toggle_(
@@ -1922,7 +1921,7 @@ size_t Environment::NearHeapLimitCallback(void* data,
19221921

19231922
std::string dir = env->options()->diagnostic_dir;
19241923
if (dir.empty()) {
1925-
dir = env->GetCwd();
1924+
dir = Environment::GetCwd(env->exec_path_);
19261925
}
19271926
DiagnosticFilename name(env, "Heap", "heapsnapshot");
19281927
std::string filename = dir + kPathSeparator + (*name);

src/env.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,9 @@ class Environment : public MemoryRetainer {
588588

589589
SET_MEMORY_INFO_NAME(Environment)
590590

591+
static std::string GetExecPath(const std::vector<std::string>& argv);
592+
static std::string GetCwd(const std::string& exec_path);
593+
591594
inline size_t SelfSize() const override;
592595
bool IsRootNode() const override { return true; }
593596
void MemoryInfo(MemoryTracker* tracker) const override;
@@ -604,8 +607,6 @@ class Environment : public MemoryRetainer {
604607
// Should be called before InitializeInspector()
605608
void InitializeDiagnostics();
606609

607-
std::string GetCwd();
608-
609610
#if HAVE_INSPECTOR
610611
// If the environment is created for a worker, pass parent_handle and
611612
// the ownership if transferred into the Environment.

src/heap_utils.cc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,9 @@ void TriggerHeapSnapshot(const FunctionCallbackInfo<Value>& args) {
456456
if (filename_v->IsUndefined()) {
457457
DiagnosticFilename name(env, "Heap", "heapsnapshot");
458458
THROW_IF_INSUFFICIENT_PERMISSIONS(
459-
env, permission::PermissionScope::kFileSystemWrite, env->GetCwd());
459+
env,
460+
permission::PermissionScope::kFileSystemWrite,
461+
Environment::GetCwd(env->exec_path()));
460462
if (WriteSnapshot(env, *name, options).IsNothing()) return;
461463
if (String::NewFromUtf8(isolate, *name).ToLocal(&filename_v)) {
462464
args.GetReturnValue().Set(filename_v);

src/inspector_profiler.cc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,8 @@ void StartProfilers(Environment* env) {
431431
if (env->options()->cpu_prof) {
432432
const std::string& dir = env->options()->cpu_prof_dir;
433433
env->set_cpu_prof_interval(env->options()->cpu_prof_interval);
434-
env->set_cpu_prof_dir(dir.empty() ? env->GetCwd() : dir);
434+
env->set_cpu_prof_dir(dir.empty() ? Environment::GetCwd(env->exec_path())
435+
: dir);
435436
if (env->options()->cpu_prof_name.empty()) {
436437
DiagnosticFilename filename(env, "CPU", "cpuprofile");
437438
env->set_cpu_prof_name(*filename);
@@ -446,7 +447,8 @@ void StartProfilers(Environment* env) {
446447
if (env->options()->heap_prof) {
447448
const std::string& dir = env->options()->heap_prof_dir;
448449
env->set_heap_prof_interval(env->options()->heap_prof_interval);
449-
env->set_heap_prof_dir(dir.empty() ? env->GetCwd() : dir);
450+
env->set_heap_prof_dir(dir.empty() ? Environment::GetCwd(env->exec_path())
451+
: dir);
450452
if (env->options()->heap_prof_name.empty()) {
451453
DiagnosticFilename filename(env, "Heap", "heapprofile");
452454
env->set_heap_prof_name(*filename);

src/node.cc

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

2222
#include "node.h"
23+
#include "node_dotenv.h"
2324

2425
// ========== local headers ==========
2526

@@ -140,6 +141,10 @@ using v8::Value;
140141

141142
namespace per_process {
142143

144+
// node_dotenv.h
145+
// Instance is used to store environment variables including NODE_OPTIONS.
146+
node::Dotenv dotenv_file = Dotenv();
147+
143148
// node_revert.h
144149
// Bit flag used to track security reverts.
145150
unsigned int reverted_cve = 0;
@@ -305,6 +310,10 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
305310
}
306311
#endif
307312

313+
if (env->options()->has_env_file_string) {
314+
per_process::dotenv_file.SetEnvironment(env);
315+
}
316+
308317
// TODO(joyeecheung): move these conditions into JS land and let the
309318
// deserialize main function take precedence. For workers, we need to
310319
// move the pre-execution part into a different file that can be
@@ -831,11 +840,22 @@ static ExitCode InitializeNodeWithArgsInternal(
831840

832841
HandleEnvOptions(per_process::cli_options->per_isolate->per_env);
833842

843+
std::string node_options;
844+
auto file_path = node::Dotenv::GetPathFromArgs(*argv);
845+
846+
if (file_path.has_value()) {
847+
auto cwd = Environment::GetCwd(Environment::GetExecPath(*argv));
848+
std::string path = cwd + kPathSeparator + file_path.value();
849+
CHECK(!per_process::v8_initialized);
850+
per_process::dotenv_file.ParsePath(path);
851+
per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
852+
}
853+
834854
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
835855
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
836-
std::string node_options;
837-
838-
if (credentials::SafeGetenv("NODE_OPTIONS", &node_options)) {
856+
// NODE_OPTIONS environment variable is preferred over the file one.
857+
if (credentials::SafeGetenv("NODE_OPTIONS", &node_options) ||
858+
!node_options.empty()) {
839859
std::vector<std::string> env_argv =
840860
ParseNodeOptionsEnvVar(node_options, errors);
841861

src/node_credentials.cc

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ bool SafeGetenv(const char* key,
123123
}
124124

125125
fail:
126-
text->clear();
127126
return false;
128127
}
129128

src/node_dotenv.cc

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
#include "node_dotenv.h"
2+
#include "env-inl.h"
3+
#include "node_file.h"
4+
#include "uv.h"
5+
6+
namespace node {
7+
8+
using v8::NewStringType;
9+
using v8::String;
10+
11+
std::optional<std::string> Dotenv::GetPathFromArgs(
12+
const std::vector<std::string>& args) {
13+
std::string_view flag = "--env-file";
14+
// Match the last `--env-file`
15+
// This is required to imitate the default behavior of Node.js CLI argument
16+
// matching.
17+
auto path =
18+
std::find_if(args.rbegin(), args.rend(), [&flag](const std::string& arg) {
19+
return strncmp(arg.c_str(), flag.data(), flag.size()) == 0;
20+
});
21+
22+
if (path == args.rend()) {
23+
return std::nullopt;
24+
}
25+
26+
auto equal_char = path->find('=');
27+
28+
if (equal_char != std::string::npos) {
29+
return path->substr(equal_char + 1);
30+
}
31+
32+
auto next_arg = std::prev(path);
33+
34+
if (next_arg == args.rend()) {
35+
return std::nullopt;
36+
}
37+
38+
return *next_arg;
39+
}
40+
41+
void Dotenv::SetEnvironment(node::Environment* env) {
42+
if (store_.empty()) {
43+
return;
44+
}
45+
46+
auto isolate = env->isolate();
47+
48+
for (const auto& entry : store_) {
49+
auto key = entry.first;
50+
auto value = entry.second;
51+
env->env_vars()->Set(
52+
isolate,
53+
v8::String::NewFromUtf8(
54+
isolate, key.data(), NewStringType::kNormal, key.size())
55+
.ToLocalChecked(),
56+
v8::String::NewFromUtf8(
57+
isolate, value.data(), NewStringType::kNormal, value.size())
58+
.ToLocalChecked());
59+
}
60+
}
61+
62+
void Dotenv::ParsePath(const std::string_view path) {
63+
uv_fs_t req;
64+
auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
65+
66+
uv_file file = uv_fs_open(nullptr, &req, path.data(), 0, 438, nullptr);
67+
if (req.result < 0) {
68+
// req will be cleaned up by scope leave.
69+
return;
70+
}
71+
uv_fs_req_cleanup(&req);
72+
73+
auto defer_close = OnScopeLeave([file]() {
74+
uv_fs_t close_req;
75+
CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr));
76+
uv_fs_req_cleanup(&close_req);
77+
});
78+
79+
std::string result{};
80+
char buffer[8192];
81+
uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer));
82+
83+
while (true) {
84+
auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr);
85+
if (req.result < 0) {
86+
// req will be cleaned up by scope leave.
87+
return;
88+
}
89+
uv_fs_req_cleanup(&req);
90+
if (r <= 0) {
91+
break;
92+
}
93+
result.append(buf.base, r);
94+
}
95+
96+
using std::string_view_literals::operator""sv;
97+
auto lines = SplitString(result, "\n"sv);
98+
99+
for (const auto& line : lines) {
100+
ParseLine(line);
101+
}
102+
}
103+
104+
void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) {
105+
auto match = store_.find("NODE_OPTIONS");
106+
107+
if (match != store_.end()) {
108+
*node_options = match->second;
109+
}
110+
}
111+
112+
void Dotenv::ParseLine(const std::string_view line) {
113+
auto equal_index = line.find('=');
114+
115+
if (equal_index == std::string_view::npos) {
116+
return;
117+
}
118+
119+
auto key = line.substr(0, equal_index);
120+
121+
// Remove leading and trailing space characters from key.
122+
while (!key.empty() && std::isspace(key.front())) key.remove_prefix(1);
123+
while (!key.empty() && std::isspace(key.back())) key.remove_suffix(1);
124+
125+
// Omit lines with comments
126+
if (key.front() == '#' || key.empty()) {
127+
return;
128+
}
129+
130+
auto value = std::string(line.substr(equal_index + 1));
131+
132+
// Might start and end with `"' characters.
133+
auto quotation_index = value.find_first_of("`\"'");
134+
135+
if (quotation_index == 0) {
136+
auto quote_character = value[quotation_index];
137+
value.erase(0, 1);
138+
139+
auto end_quotation_index = value.find_last_of(quote_character);
140+
141+
// We couldn't find the closing quotation character. Terminate.
142+
if (end_quotation_index == std::string::npos) {
143+
return;
144+
}
145+
146+
value.erase(end_quotation_index);
147+
} else {
148+
auto hash_index = value.find('#');
149+
150+
// Remove any inline comments
151+
if (hash_index != std::string::npos) {
152+
value.erase(hash_index);
153+
}
154+
155+
// Remove any leading/trailing spaces from unquoted values.
156+
while (!value.empty() && std::isspace(value.front())) value.erase(0, 1);
157+
while (!value.empty() && std::isspace(value.back()))
158+
value.erase(value.size() - 1);
159+
}
160+
161+
store_.emplace(key, value);
162+
}
163+
164+
} // namespace node

0 commit comments

Comments
 (0)