Skip to content

Commit 0541c35

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

File tree

8 files changed

+308
-0
lines changed

8 files changed

+308
-0
lines changed

doc/api/cli.md

+34
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,40 @@ 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+
> Stability: 1 - Experimental
981+
982+
<!-- YAML
983+
added: REPLACEME
984+
-->
985+
986+
Loads environment variables from a file relative to the current directory.
987+
988+
The format of the file should be one line per key-value pair of environment
989+
variable name and value separated by `=`:
990+
991+
```text
992+
PORT=3000
993+
```
994+
995+
Any text after a `#` is treated as a comment:
996+
997+
```text
998+
# This is a comment
999+
PORT=3000 # This is also a comment
1000+
```
1001+
1002+
If the `NODE_OPTIONS` variable is set, it will be parsed to configure Node.js
1003+
during startup. For example, if the following were saved into a `.env` file:
1004+
1005+
```text
1006+
NODE_OPTIONS="--enable-source-maps --inspect"
1007+
```
1008+
1009+
And run via `node --end-file .env`, the launched Node.js process will run
1010+
with source maps enabled and the default port open.
1011+
9781012
### `--max-http-header-size=size`
9791013

9801014
<!-- 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

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

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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
39+
NODE_OPTIONS="--experimental-permission"

test/parallel/test-dotenv.js

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

0 commit comments

Comments
 (0)