Skip to content

Commit bf7ff36

Browse files
anonrigUlisesGascon
authored andcommitted
src: add built-in .env file support
PR-URL: #48890 Refs: https://github.com/orgs/nodejs/discussions/44975 Reviewed-By: Geoffrey Booth <webadmin@geoffreybooth.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it>
1 parent 21949c4 commit bf7ff36

19 files changed

+508
-16
lines changed

doc/api/cli.md

+36
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,41 @@ surface on other platforms, but the performance impact may be severe.
980980
This flag is inherited from V8 and is subject to change upstream. It may
981981
disappear in a non-semver-major release.
982982

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

9851020
<!-- YAML
@@ -2643,6 +2678,7 @@ done
26432678
[debugger]: debugger.md
26442679
[debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications
26452680
[emit_warning]: process.md#processemitwarningwarning-options
2681+
[environment_variables]: #environment-variables
26462682
[filtering tests by name]: test.md#filtering-tests-by-name
26472683
[jitless]: https://v8.dev/blog/jitless
26482684
[libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html

node.gyp

+2
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

+4-5
Original file line numberDiff line numberDiff line change
@@ -610,7 +610,7 @@ void Environment::TryLoadAddon(
610610
}
611611
}
612612

613-
std::string Environment::GetCwd() {
613+
std::string Environment::GetCwd(const std::string& exec_path) {
614614
char cwd[PATH_MAX_BYTES];
615615
size_t size = PATH_MAX_BYTES;
616616
const int err = uv_cwd(cwd, &size);
@@ -622,7 +622,6 @@ std::string Environment::GetCwd() {
622622

623623
// This can fail if the cwd is deleted. In that case, fall back to
624624
// exec_path.
625-
const std::string& exec_path = exec_path_;
626625
return exec_path.substr(0, exec_path.find_last_of(kPathSeparator));
627626
}
628627

@@ -656,7 +655,7 @@ std::unique_ptr<v8::BackingStore> Environment::release_managed_buffer(
656655
return bs;
657656
}
658657

659-
std::string GetExecPath(const std::vector<std::string>& argv) {
658+
std::string Environment::GetExecPath(const std::vector<std::string>& argv) {
660659
char exec_path_buf[2 * PATH_MAX];
661660
size_t exec_path_len = sizeof(exec_path_buf);
662661
std::string exec_path;
@@ -698,7 +697,7 @@ Environment::Environment(IsolateData* isolate_data,
698697
timer_base_(uv_now(isolate_data->event_loop())),
699698
exec_argv_(exec_args),
700699
argv_(args),
701-
exec_path_(GetExecPath(args)),
700+
exec_path_(Environment::GetExecPath(args)),
702701
exit_info_(
703702
isolate_, kExitInfoFieldCount, MAYBE_FIELD_PTR(env_info, exit_info)),
704703
should_abort_on_uncaught_toggle_(
@@ -1848,7 +1847,7 @@ size_t Environment::NearHeapLimitCallback(void* data,
18481847

18491848
std::string dir = env->options()->diagnostic_dir;
18501849
if (dir.empty()) {
1851-
dir = env->GetCwd();
1850+
dir = Environment::GetCwd(env->exec_path_);
18521851
}
18531852
DiagnosticFilename name(env, "Heap", "heapsnapshot");
18541853
std::string filename = dir + kPathSeparator + (*name);

src/env.h

+3-2
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,9 @@ class Environment : public MemoryRetainer {
564564

565565
SET_MEMORY_INFO_NAME(Environment)
566566

567+
static std::string GetExecPath(const std::vector<std::string>& argv);
568+
static std::string GetCwd(const std::string& exec_path);
569+
567570
inline size_t SelfSize() const override;
568571
bool IsRootNode() const override { return true; }
569572
void MemoryInfo(MemoryTracker* tracker) const override;
@@ -580,8 +583,6 @@ class Environment : public MemoryRetainer {
580583
// Should be called before InitializeInspector()
581584
void InitializeDiagnostics();
582585

583-
std::string GetCwd();
584-
585586
#if HAVE_INSPECTOR
586587
// If the environment is created for a worker, pass parent_handle and
587588
// the ownership if transferred into the Environment.

src/heap_utils.cc

+3-1
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

+4-2
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

+23-3
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

@@ -138,6 +139,10 @@ using v8::Value;
138139

139140
namespace per_process {
140141

142+
// node_dotenv.h
143+
// Instance is used to store environment variables including NODE_OPTIONS.
144+
node::Dotenv dotenv_file = Dotenv();
145+
141146
// node_revert.h
142147
// Bit flag used to track security reverts.
143148
unsigned int reverted_cve = 0;
@@ -303,6 +308,10 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
303308
}
304309
#endif
305310

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

830839
HandleEnvOptions(per_process::cli_options->per_isolate->per_env);
831840

841+
std::string node_options;
842+
auto file_path = node::Dotenv::GetPathFromArgs(*argv);
843+
844+
if (file_path.has_value()) {
845+
auto cwd = Environment::GetCwd(Environment::GetExecPath(*argv));
846+
std::string path = cwd + kPathSeparator + file_path.value();
847+
CHECK(!per_process::v8_initialized);
848+
per_process::dotenv_file.ParsePath(path);
849+
per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
850+
}
851+
832852
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
833853
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
834-
std::string node_options;
835-
836-
if (credentials::SafeGetenv("NODE_OPTIONS", &node_options)) {
854+
// NODE_OPTIONS environment variable is preferred over the file one.
855+
if (credentials::SafeGetenv("NODE_OPTIONS", &node_options) ||
856+
!node_options.empty()) {
837857
std::vector<std::string> env_argv =
838858
ParseNodeOptionsEnvVar(node_options, errors);
839859

src/node_credentials.cc

-1
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

+164
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)