Skip to content

Commit 97aacf7

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

File tree

8 files changed

+268
-0
lines changed

8 files changed

+268
-0
lines changed

doc/api/cli.md

+8
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+
### `--env-file=config`
979+
980+
<!-- YAML
981+
added: REPLACEME
982+
-->
983+
984+
Loads environment variables from a path relative to the current directory.
985+
978986
### `--max-http-header-size=size`
979987

980988
<!-- YAML

src/node.cc

+6
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,11 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
303304
}
304305
#endif
305306

307+
if (env->options()->has_env_file_string) {
308+
std::string path = env->GetCwd() + kPathSeparator + env->options()->env_file;
309+
node::dotenv::LoadFromFile(env, path);
310+
}
311+
306312
// TODO(joyeecheung): move these conditions into JS land and let the
307313
// deserialize main function take precedence. For workers, we need to
308314
// move the pre-execution part into a different file that can be

src/node_dotenv.cc

+115
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#include "env-inl.h"
2+
#include "node_dotenv.h"
3+
#include "node_file.h"
4+
#include "uv.h"
5+
6+
namespace node {
7+
8+
using v8::Isolate;
9+
using v8::NewStringType;
10+
11+
namespace dotenv {
12+
13+
void ParseLine(const std::string_view line, Isolate* isolate, std::shared_ptr<KVStore> store) {
14+
auto equal_index = line.find('=');
15+
16+
if (equal_index == std::string_view::npos) {
17+
return;
18+
}
19+
20+
auto key = line.substr(0, equal_index);
21+
22+
// Remove leading and trailing space characters from key.
23+
while (!key.empty() && key.front() == ' ') key.remove_prefix(1);
24+
while (!key.empty() && key.back() == ' ') key.remove_suffix(1);
25+
26+
// Omit lines with comments
27+
if (key.front() == '#' || key.empty()) {
28+
return;
29+
}
30+
31+
auto value = std::string(line.substr(equal_index + 1));
32+
33+
// Might start and end with `"' characters.
34+
auto quotation_index = value.find_first_of("`\"'");
35+
36+
if (quotation_index == 0) {
37+
auto quote_character = value[quotation_index];
38+
value.erase(0, 1);
39+
40+
auto end_quotation_index = value.find_last_of(quote_character);
41+
42+
// We couldn't find the closing quotation character. Terminate.
43+
if (end_quotation_index == std::string::npos) {
44+
return;
45+
}
46+
47+
value.erase(end_quotation_index);
48+
} else {
49+
auto hash_index = value.find('#');
50+
51+
// Remove any inline comments
52+
if (hash_index != std::string::npos) {
53+
value.erase(hash_index);
54+
}
55+
56+
// Remove any leading/trailing spaces from unquoted values.
57+
while (!value.empty() && value.front() == ' ') value.erase(0, 1);
58+
while (!value.empty() && value.back() == ' ') value.erase(value.size() - 1);
59+
}
60+
61+
store->Set(isolate,
62+
v8::String::NewFromUtf8(
63+
isolate, key.data(), NewStringType::kNormal, key.size())
64+
.ToLocalChecked(),
65+
v8::String::NewFromUtf8(
66+
isolate, value.data(), NewStringType::kNormal, value.size())
67+
.ToLocalChecked());
68+
}
69+
70+
void LoadFromFile(Environment* env, const std::string_view path) {
71+
Isolate* isolate = env->isolate();
72+
auto store = env->env_vars();
73+
uv_fs_t req;
74+
auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
75+
76+
uv_file file = uv_fs_open(nullptr, &req, path.data(), 0, 438, nullptr);
77+
if (req.result < 0) {
78+
// req will be cleaned up by scope leave.
79+
return;
80+
}
81+
uv_fs_req_cleanup(&req);
82+
83+
auto defer_close = OnScopeLeave([file]() {
84+
uv_fs_t close_req;
85+
CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr));
86+
uv_fs_req_cleanup(&close_req);
87+
});
88+
89+
std::string result{};
90+
char buffer[8192];
91+
uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer));
92+
93+
while (true) {
94+
auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr);
95+
if (req.result < 0) {
96+
// req will be cleaned up by scope leave.
97+
return;
98+
}
99+
uv_fs_req_cleanup(&req);
100+
if (r <= 0) {
101+
break;
102+
}
103+
result.append(buf.base, r);
104+
}
105+
106+
using std::string_view_literals::operator""sv;
107+
108+
for (const auto& line : SplitString(result, "\n"sv)) {
109+
ParseLine(line, isolate, store);
110+
}
111+
}
112+
113+
} // namespace dotenv
114+
115+
} // namespace node

src/node_dotenv.h

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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(Environment* env,
13+
const std::string_view path);
14+
15+
} // namespace dotenv
16+
17+
} // namespace node
18+
19+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
20+
21+
#endif // SRC_NODE_DOTENV_H_

src/node_options.cc

+5
Original file line numberDiff line numberDiff line change
@@ -575,6 +575,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
575575
"write warnings to file instead of stderr",
576576
&EnvironmentOptions::redirect_warnings,
577577
kAllowedInEnvvar);
578+
AddOption("[has_env_file_string]", "", &EnvironmentOptions::has_env_file_string);
579+
AddOption("--env-file",
580+
"load .env configuration file on startup (default: .env)",
581+
&EnvironmentOptions::env_file);
582+
Implies("--env-file", "[has_env_file_string]");
578583
AddOption("--test",
579584
"launch test runner on startup",
580585
&EnvironmentOptions::test_runner);

src/node_options.h

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ class EnvironmentOptions : public Options {
158158
#endif // HAVE_INSPECTOR
159159
std::string redirect_warnings;
160160
std::string diagnostic_dir;
161+
std::string env_file;
162+
bool has_env_file_string = false;
161163
bool test_runner = false;
162164
bool test_runner_coverage = false;
163165
std::vector<std::string> test_name_pattern;

test/fixtures/dotenv/valid.env

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
BASIC=basic
2+
3+
# previous line intentionally left blank
4+
AFTER_LINE=after_line
5+
EMPTY=
6+
EMPTY_SINGLE_QUOTES=''
7+
EMPTY_DOUBLE_QUOTES=""
8+
EMPTY_BACKTICKS=``
9+
SINGLE_QUOTES='single_quotes'
10+
SINGLE_QUOTES_SPACED=' single quotes '
11+
DOUBLE_QUOTES="double_quotes"
12+
DOUBLE_QUOTES_SPACED=" double quotes "
13+
DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes'
14+
DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET="{ port: $MONGOLAB_PORT}"
15+
SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes"
16+
BACKTICKS_INSIDE_SINGLE='`backticks` work inside single quotes'
17+
BACKTICKS_INSIDE_DOUBLE="`backticks` work inside double quotes"
18+
BACKTICKS=`backticks`
19+
BACKTICKS_SPACED=` backticks `
20+
DOUBLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" work inside backticks`
21+
SINGLE_QUOTES_INSIDE_BACKTICKS=`single 'quotes' work inside backticks`
22+
DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" and single 'quotes' work inside backticks`
23+
EXPAND_NEWLINES="expand\nnew\nlines"
24+
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
25+
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
26+
# COMMENTS=work
27+
INLINE_COMMENTS=inline comments # work #very #well
28+
INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work
29+
INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work
30+
INLINE_COMMENTS_BACKTICKS=`inline comments outside of #backticks` # work
31+
INLINE_COMMENTS_SPACE=inline comments start with a#number sign. no space required.
32+
EQUAL_SIGNS=equals==
33+
RETAIN_INNER_QUOTES={"foo": "bar"}
34+
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
35+
RETAIN_INNER_QUOTES_AS_BACKTICKS=`{"foo": "bar's"}`
36+
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
37+
38+
SPACED_KEY = parsed

test/parallel/test-dotenv.js

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Flags: --env-file test/fixtures/dotenv/valid.env
2+
'use strict';
3+
4+
require('../common');
5+
const assert = require('node:assert');
6+
7+
assert.strictEqual(process.env.BASIC, 'basic', 'sets basic environment variable');
8+
9+
assert.strictEqual(process.env.AFTER_LINE, 'after_line', 'reads after a skipped line');
10+
11+
assert.strictEqual(process.env.EMPTY, '', 'defaults empty values to empty string');
12+
13+
assert.strictEqual(process.env.EMPTY_SINGLE_QUOTES, '', 'defaults empty values to empty string');
14+
15+
assert.strictEqual(process.env.EMPTY_DOUBLE_QUOTES, '', 'defaults empty values to empty string');
16+
17+
assert.strictEqual(process.env.EMPTY_BACKTICKS, '', 'defaults empty values to empty string');
18+
19+
assert.strictEqual(process.env.SINGLE_QUOTES, 'single_quotes', 'escapes single quoted values');
20+
21+
assert.strictEqual(process.env.SINGLE_QUOTES_SPACED, ' single quotes ', 'respects surrounding spaces in single quotes');
22+
23+
assert.strictEqual(process.env.DOUBLE_QUOTES, 'double_quotes', 'escapes double quoted values');
24+
25+
assert.strictEqual(process.env.DOUBLE_QUOTES_SPACED, ' double quotes ', 'respects surrounding spaces in double quotes');
26+
27+
assert.strictEqual(process.env.DOUBLE_QUOTES_INSIDE_SINGLE, 'double "quotes" work inside single quotes', 'respects double quotes inside single quotes');
28+
29+
assert.strictEqual(process.env.DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET, '{ port: $MONGOLAB_PORT}', 'respects spacing for badly formed brackets');
30+
31+
assert.strictEqual(process.env.SINGLE_QUOTES_INSIDE_DOUBLE, "single 'quotes' work inside double quotes", 'respects single quotes inside double quotes');
32+
33+
assert.strictEqual(process.env.BACKTICKS_INSIDE_SINGLE, '`backticks` work inside single quotes', 'respects backticks inside single quotes');
34+
35+
assert.strictEqual(process.env.BACKTICKS_INSIDE_DOUBLE, '`backticks` work inside double quotes', 'respects backticks inside double quotes');
36+
37+
assert.strictEqual(process.env.BACKTICKS, 'backticks');
38+
39+
assert.strictEqual(process.env.BACKTICKS_SPACED, ' backticks ');
40+
41+
assert.strictEqual(process.env.DOUBLE_QUOTES_INSIDE_BACKTICKS, 'double "quotes" work inside backticks', 'respects double quotes inside backticks');
42+
43+
assert.strictEqual(process.env.SINGLE_QUOTES_INSIDE_BACKTICKS, "single 'quotes' work inside backticks", 'respects single quotes inside backticks');
44+
45+
assert.strictEqual(process.env.DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS, "double \"quotes\" and single 'quotes' work inside backticks", 'respects single quotes inside backticks');
46+
47+
assert.strictEqual(process.env.INLINE_COMMENTS, 'inline comments', 'ignores inline comments');
48+
49+
assert.strictEqual(process.env.INLINE_COMMENTS_SINGLE_QUOTES, 'inline comments outside of #singlequotes', 'ignores inline comments and respects # character inside of single quotes');
50+
51+
assert.strictEqual(process.env.INLINE_COMMENTS_DOUBLE_QUOTES, 'inline comments outside of #doublequotes', 'ignores inline comments and respects # character inside of double quotes');
52+
53+
assert.strictEqual(process.env.INLINE_COMMENTS_BACKTICKS, 'inline comments outside of #backticks', 'ignores inline comments and respects # character inside of backticks');
54+
55+
assert.strictEqual(process.env.INLINE_COMMENTS_SPACE, 'inline comments start with a', 'treats # character as start of comment');
56+
57+
assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==', 'respects equals signs in values');
58+
59+
assert.strictEqual(process.env.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes');
60+
61+
assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==', 'respects equals signs in values');
62+
63+
assert.strictEqual(process.env.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes');
64+
65+
assert.strictEqual(process.env.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}', 'retains inner quotes');
66+
67+
assert.strictEqual(process.env.RETAIN_INNER_QUOTES_AS_BACKTICKS, '{"foo": "bar\'s"}', 'retains inner quotes');
68+
69+
assert.strictEqual(process.env.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string', 'retains spaces in string');
70+
71+
assert.strictEqual(process.env.USERNAME, '[email protected]', 'parses email addresses completely');
72+
73+
assert.strictEqual(process.env.SPACED_KEY, 'parsed', 'parses keys and values surrounded by spaces');

0 commit comments

Comments
 (0)