Skip to content

Commit 6a73a72

Browse files
committed
src: add built-in .env file support
1 parent 6c08b1f commit 6a73a72

File tree

8 files changed

+132
-0
lines changed

8 files changed

+132
-0
lines changed

doc/api/cli.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,14 @@ surface on other platforms, but the performance impact may be severe.
975975
This flag is inherited from V8 and is subject to change upstream. It may
976976
disappear in a non-semver-major release.
977977

978+
### `--load-dotenv`
979+
980+
<!-- YAML
981+
added: REPLACEME
982+
-->
983+
984+
Loads environment variables from .env file in the current directory.
985+
978986
### `--max-http-header-size=size`
979987

980988
<!-- YAML

src/node.cc

Lines changed: 5 additions & 0 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

@@ -303,6 +304,10 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
303304
}
304305
#endif
305306

307+
if (env->options()->load_dotenv) {
308+
node::dotenv::LoadFromFile(env->isolate(), env->GetCwd(), env->env_vars());
309+
}
310+
306311
// TODO(joyeecheung): move these conditions into JS land and let the
307312
// deserialize main function take precedence. For workers, we need to
308313
// move the pre-execution part into a different file that can be

src/node_dotenv.cc

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include "node_dotenv.h"
2+
#include "node_file.h"
3+
#include "uv.h"
4+
5+
namespace node {
6+
7+
using v8::Isolate;
8+
using v8::NewStringType;
9+
10+
namespace dotenv {
11+
12+
void LoadFromFile(Isolate* isolate,
13+
const std::string_view src,
14+
std::shared_ptr<KVStore> store) {
15+
std::string path = std::string(src) + kPathSeparator + ".env";
16+
17+
uv_fs_t req;
18+
auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
19+
20+
uv_file file = uv_fs_open(nullptr, &req, path.c_str(), 0, 438, nullptr);
21+
if (req.result < 0) {
22+
// req will be cleaned up by scope leave.
23+
return;
24+
}
25+
uv_fs_req_cleanup(&req);
26+
27+
auto defer_close = OnScopeLeave([file]() {
28+
uv_fs_t close_req;
29+
CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr));
30+
uv_fs_req_cleanup(&close_req);
31+
});
32+
33+
std::string result{};
34+
char buffer[8192];
35+
uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer));
36+
37+
while (true) {
38+
auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr);
39+
if (req.result < 0) {
40+
// req will be cleaned up by scope leave.
41+
return;
42+
}
43+
uv_fs_req_cleanup(&req);
44+
if (r <= 0) {
45+
break;
46+
}
47+
result.append(buf.base, r);
48+
}
49+
50+
using std::string_view_literals::operator""sv;
51+
52+
for (const auto& line : SplitString(result, "\n"sv)) {
53+
auto equal_index = line.find('=');
54+
55+
if (equal_index == std::string_view::npos) {
56+
continue;
57+
}
58+
59+
std::string_view key = line.substr(0, equal_index);
60+
std::string_view value = line.substr(equal_index + 1);
61+
62+
store->Set(isolate,
63+
v8::String::NewFromUtf8(
64+
isolate, key.data(), NewStringType::kNormal, key.size())
65+
.ToLocalChecked(),
66+
v8::String::NewFromUtf8(
67+
isolate, value.data(), NewStringType::kNormal, value.size())
68+
.ToLocalChecked());
69+
}
70+
}
71+
72+
} // namespace dotenv
73+
74+
} // namespace node

src/node_dotenv.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#ifndef SRC_NODE_DOTENV_H_
2+
#define SRC_NODE_DOTENV_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include "util-inl.h"
7+
8+
namespace node {
9+
10+
namespace dotenv {
11+
12+
void LoadFromFile(v8::Isolate* isolate,
13+
const std::string_view path,
14+
std::shared_ptr<KVStore> store);
15+
16+
} // namespace dotenv
17+
18+
} // namespace node
19+
20+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
21+
22+
#endif // SRC_NODE_DOTENV_H_

src/node_options.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
575575
"write warnings to file instead of stderr",
576576
&EnvironmentOptions::redirect_warnings,
577577
kAllowedInEnvvar);
578+
AddOption("--load-dotenv",
579+
"load .env configuration file on startup",
580+
&EnvironmentOptions::load_dotenv);
578581
AddOption("--test",
579582
"launch test runner on startup",
580583
&EnvironmentOptions::test_runner);

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ class EnvironmentOptions : public Options {
158158
#endif // HAVE_INSPECTOR
159159
std::string redirect_warnings;
160160
std::string diagnostic_dir;
161+
bool load_dotenv = false;
161162
bool test_runner = false;
162163
bool test_runner_coverage = false;
163164
std::vector<std::string> test_name_pattern;

test/fixtures/dotenv/valid/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DATABASE_PASSWORD=nodejs

test/parallel/test-dotenv.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('node:assert');
5+
const { spawnSync } = require('node:child_process');
6+
const path = require('node:path');
7+
8+
{
9+
const child = spawnSync(
10+
process.execPath,
11+
['--load-dotenv', '-e', 'console.log(process.env.DATABASE_PASSWORD)'],
12+
{
13+
cwd: path.join(__dirname, '../fixtures/dotenv/valid')
14+
}
15+
);
16+
17+
assert.strictEqual(child.stdout.toString(), 'nodejs\n');
18+
}

0 commit comments

Comments
 (0)