Skip to content

Commit

Permalink
src: add built-in .env file support
Browse files Browse the repository at this point in the history
  • Loading branch information
anonrig committed Jul 23, 2023
1 parent 6c08b1f commit 5b5dcc9
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 0 deletions.
8 changes: 8 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -975,6 +975,14 @@ surface on other platforms, but the performance impact may be severe.
This flag is inherited from V8 and is subject to change upstream. It may
disappear in a non-semver-major release.

### `--env-file=config`

<!-- YAML
added: REPLACEME
-->

Loads environment variables from a path relative to the current directory.

### `--max-http-header-size=size`

<!-- YAML
Expand Down
6 changes: 6 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
// USE OR OTHER DEALINGS IN THE SOFTWARE.

#include "node.h"
#include "node_dotenv.h"

// ========== local headers ==========

Expand Down Expand Up @@ -303,6 +304,11 @@ MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) {
}
#endif

if (env->options()->has_env_file_string) {
std::string path = env->GetCwd() + kPathSeparator + env->options()->env_file;
node::dotenv::LoadFromFile(env, path);
}

// TODO(joyeecheung): move these conditions into JS land and let the
// deserialize main function take precedence. For workers, we need to
// move the pre-execution part into a different file that can be
Expand Down
121 changes: 121 additions & 0 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#include "env-inl.h"
#include "node_dotenv.h"
#include "node_file.h"
#include "uv.h"

#ifdef __POSIX__
constexpr std::string_view kNewlineCharacter = "\n";
#else
constexpr std::string_view kNewlineCharacter = "\r\n";
#endif

namespace node {

using v8::Isolate;
using v8::NewStringType;

namespace dotenv {

void ParseLine(const std::string_view line, Isolate* isolate, std::shared_ptr<KVStore> store) {
auto equal_index = line.find('=');

if (equal_index == std::string_view::npos) {
return;
}

auto key = line.substr(0, equal_index);

// Remove leading and trailing space characters from key.
while (!key.empty() && key.front() == ' ') key.remove_prefix(1);
while (!key.empty() && key.back() == ' ') key.remove_suffix(1);

// Omit lines with comments
if (key.front() == '#' || key.empty()) {
return;
}

auto value = std::string(line.substr(equal_index + 1));

// Might start and end with `"' characters.
auto quotation_index = value.find_first_of("`\"'");

if (quotation_index == 0) {
auto quote_character = value[quotation_index];
value.erase(0, 1);

auto end_quotation_index = value.find_last_of(quote_character);

// We couldn't find the closing quotation character. Terminate.
if (end_quotation_index == std::string::npos) {
return;
}

value.erase(end_quotation_index);
} else {
auto hash_index = value.find('#');

// Remove any inline comments
if (hash_index != std::string::npos) {
value.erase(hash_index);
}

// Remove any leading/trailing spaces from unquoted values.
while (!value.empty() && value.front() == ' ') value.erase(0, 1);
while (!value.empty() && value.back() == ' ') value.erase(value.size() - 1);
}

store->Set(isolate,
v8::String::NewFromUtf8(
isolate, key.data(), NewStringType::kNormal, key.size())
.ToLocalChecked(),
v8::String::NewFromUtf8(
isolate, value.data(), NewStringType::kNormal, value.size())
.ToLocalChecked());
}

void LoadFromFile(Environment* env, const std::string_view path) {
Isolate* isolate = env->isolate();
auto store = env->env_vars();
uv_fs_t req;
auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });

uv_file file = uv_fs_open(nullptr, &req, path.data(), 0, 438, nullptr);
if (req.result < 0) {
// req will be cleaned up by scope leave.
return;
}
uv_fs_req_cleanup(&req);

auto defer_close = OnScopeLeave([file]() {
uv_fs_t close_req;
CHECK_EQ(0, uv_fs_close(nullptr, &close_req, file, nullptr));
uv_fs_req_cleanup(&close_req);
});

std::string result{};
char buffer[8192];
uv_buf_t buf = uv_buf_init(buffer, sizeof(buffer));

while (true) {
auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr);
if (req.result < 0) {
// req will be cleaned up by scope leave.
return;
}
uv_fs_req_cleanup(&req);
if (r <= 0) {
break;
}
result.append(buf.base, r);
}

using std::string_view_literals::operator""sv;

for (const auto& line : SplitString(result, kNewlineCharacter)) {
ParseLine(line, isolate, store);
}
}

} // namespace dotenv

} // namespace node
21 changes: 21 additions & 0 deletions src/node_dotenv.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef SRC_NODE_DOTENV_H_
#define SRC_NODE_DOTENV_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include "util-inl.h"

namespace node {

namespace dotenv {

void LoadFromFile(Environment* env,
const std::string_view path);

} // namespace dotenv

} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#endif // SRC_NODE_DOTENV_H_
5 changes: 5 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,11 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"write warnings to file instead of stderr",
&EnvironmentOptions::redirect_warnings,
kAllowedInEnvvar);
AddOption("[has_env_file_string]", "", &EnvironmentOptions::has_env_file_string);
AddOption("--env-file",
"load .env configuration file on startup (default: .env)",
&EnvironmentOptions::env_file);
Implies("--env-file", "[has_env_file_string]");
AddOption("--test",
"launch test runner on startup",
&EnvironmentOptions::test_runner);
Expand Down
2 changes: 2 additions & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ class EnvironmentOptions : public Options {
#endif // HAVE_INSPECTOR
std::string redirect_warnings;
std::string diagnostic_dir;
std::string env_file;
bool has_env_file_string = false;
bool test_runner = false;
bool test_runner_coverage = false;
std::vector<std::string> test_name_pattern;
Expand Down
38 changes: 38 additions & 0 deletions test/fixtures/dotenv/valid.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
BASIC=basic

# previous line intentionally left blank
AFTER_LINE=after_line
EMPTY=
EMPTY_SINGLE_QUOTES=''
EMPTY_DOUBLE_QUOTES=""
EMPTY_BACKTICKS=``
SINGLE_QUOTES='single_quotes'
SINGLE_QUOTES_SPACED=' single quotes '
DOUBLE_QUOTES="double_quotes"
DOUBLE_QUOTES_SPACED=" double quotes "
DOUBLE_QUOTES_INSIDE_SINGLE='double "quotes" work inside single quotes'
DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET="{ port: $MONGOLAB_PORT}"
SINGLE_QUOTES_INSIDE_DOUBLE="single 'quotes' work inside double quotes"
BACKTICKS_INSIDE_SINGLE='`backticks` work inside single quotes'
BACKTICKS_INSIDE_DOUBLE="`backticks` work inside double quotes"
BACKTICKS=`backticks`
BACKTICKS_SPACED=` backticks `
DOUBLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" work inside backticks`
SINGLE_QUOTES_INSIDE_BACKTICKS=`single 'quotes' work inside backticks`
DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS=`double "quotes" and single 'quotes' work inside backticks`
EXPAND_NEWLINES="expand\nnew\nlines"
DONT_EXPAND_UNQUOTED=dontexpand\nnewlines
DONT_EXPAND_SQUOTED='dontexpand\nnewlines'
# COMMENTS=work
INLINE_COMMENTS=inline comments # work #very #well
INLINE_COMMENTS_SINGLE_QUOTES='inline comments outside of #singlequotes' # work
INLINE_COMMENTS_DOUBLE_QUOTES="inline comments outside of #doublequotes" # work
INLINE_COMMENTS_BACKTICKS=`inline comments outside of #backticks` # work
INLINE_COMMENTS_SPACE=inline comments start with a#number sign. no space required.
EQUAL_SIGNS=equals==
RETAIN_INNER_QUOTES={"foo": "bar"}
RETAIN_INNER_QUOTES_AS_STRING='{"foo": "bar"}'
RETAIN_INNER_QUOTES_AS_BACKTICKS=`{"foo": "bar's"}`
TRIM_SPACE_FROM_UNQUOTED= some spaced out string
USERNAME=therealnerdybeast@example.tld
SPACED_KEY = parsed
73 changes: 73 additions & 0 deletions test/parallel/test-dotenv.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Flags: --env-file test/fixtures/dotenv/valid.env
'use strict';

require('../common');
const assert = require('node:assert');

assert.strictEqual(process.env.BASIC, 'basic', 'sets basic environment variable');

assert.strictEqual(process.env.AFTER_LINE, 'after_line', 'reads after a skipped line');

assert.strictEqual(process.env.EMPTY, '', 'defaults empty values to empty string');

assert.strictEqual(process.env.EMPTY_SINGLE_QUOTES, '', 'defaults empty values to empty string');

assert.strictEqual(process.env.EMPTY_DOUBLE_QUOTES, '', 'defaults empty values to empty string');

assert.strictEqual(process.env.EMPTY_BACKTICKS, '', 'defaults empty values to empty string');

assert.strictEqual(process.env.SINGLE_QUOTES, 'single_quotes', 'escapes single quoted values');

assert.strictEqual(process.env.SINGLE_QUOTES_SPACED, ' single quotes ', 'respects surrounding spaces in single quotes');

assert.strictEqual(process.env.DOUBLE_QUOTES, 'double_quotes', 'escapes double quoted values');

assert.strictEqual(process.env.DOUBLE_QUOTES_SPACED, ' double quotes ', 'respects surrounding spaces in double quotes');

assert.strictEqual(process.env.DOUBLE_QUOTES_INSIDE_SINGLE, 'double "quotes" work inside single quotes', 'respects double quotes inside single quotes');

assert.strictEqual(process.env.DOUBLE_QUOTES_WITH_NO_SPACE_BRACKET, '{ port: $MONGOLAB_PORT}', 'respects spacing for badly formed brackets');

assert.strictEqual(process.env.SINGLE_QUOTES_INSIDE_DOUBLE, "single 'quotes' work inside double quotes", 'respects single quotes inside double quotes');

assert.strictEqual(process.env.BACKTICKS_INSIDE_SINGLE, '`backticks` work inside single quotes', 'respects backticks inside single quotes');

assert.strictEqual(process.env.BACKTICKS_INSIDE_DOUBLE, '`backticks` work inside double quotes', 'respects backticks inside double quotes');

assert.strictEqual(process.env.BACKTICKS, 'backticks');

assert.strictEqual(process.env.BACKTICKS_SPACED, ' backticks ');

assert.strictEqual(process.env.DOUBLE_QUOTES_INSIDE_BACKTICKS, 'double "quotes" work inside backticks', 'respects double quotes inside backticks');

assert.strictEqual(process.env.SINGLE_QUOTES_INSIDE_BACKTICKS, "single 'quotes' work inside backticks", 'respects single quotes inside backticks');

assert.strictEqual(process.env.DOUBLE_AND_SINGLE_QUOTES_INSIDE_BACKTICKS, "double \"quotes\" and single 'quotes' work inside backticks", 'respects single quotes inside backticks');

assert.strictEqual(process.env.INLINE_COMMENTS, 'inline comments', 'ignores inline comments');

assert.strictEqual(process.env.INLINE_COMMENTS_SINGLE_QUOTES, 'inline comments outside of #singlequotes', 'ignores inline comments and respects # character inside of single quotes');

assert.strictEqual(process.env.INLINE_COMMENTS_DOUBLE_QUOTES, 'inline comments outside of #doublequotes', 'ignores inline comments and respects # character inside of double quotes');

assert.strictEqual(process.env.INLINE_COMMENTS_BACKTICKS, 'inline comments outside of #backticks', 'ignores inline comments and respects # character inside of backticks');

assert.strictEqual(process.env.INLINE_COMMENTS_SPACE, 'inline comments start with a', 'treats # character as start of comment');

assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==', 'respects equals signs in values');

assert.strictEqual(process.env.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes');

assert.strictEqual(process.env.EQUAL_SIGNS, 'equals==', 'respects equals signs in values');

assert.strictEqual(process.env.RETAIN_INNER_QUOTES, '{"foo": "bar"}', 'retains inner quotes');

assert.strictEqual(process.env.RETAIN_INNER_QUOTES_AS_STRING, '{"foo": "bar"}', 'retains inner quotes');

assert.strictEqual(process.env.RETAIN_INNER_QUOTES_AS_BACKTICKS, '{"foo": "bar\'s"}', 'retains inner quotes');

assert.strictEqual(process.env.TRIM_SPACE_FROM_UNQUOTED, 'some spaced out string', 'retains spaces in string');

assert.strictEqual(process.env.USERNAME, 'therealnerdybeast@example.tld', 'parses email addresses completely');

assert.strictEqual(process.env.SPACED_KEY, 'parsed', 'parses keys and values surrounded by spaces');

0 comments on commit 5b5dcc9

Please sign in to comment.