diff --git a/src/node.cc b/src/node.cc index 7ca3e14ee06c3a..8c906b994e70d8 100644 --- a/src/node.cc +++ b/src/node.cc @@ -20,6 +20,7 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. #include "node.h" +#include "node_dotenv.h" // ========== local headers ========== @@ -303,6 +304,10 @@ MaybeLocal StartExecution(Environment* env, StartExecutionCallback cb) { } #endif + if (env->options()->load_dotenv) { + node::dotenv::LoadFromFile(env->isolate(), env->GetCwd(), env->env_vars()); + } + // 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 diff --git a/src/node_dotenv.cc b/src/node_dotenv.cc new file mode 100644 index 00000000000000..ace2c88322cac7 --- /dev/null +++ b/src/node_dotenv.cc @@ -0,0 +1,73 @@ +#include "node_dotenv.h" +#include "uv.h" + +namespace node { + +using v8::Isolate; +using v8::NewStringType; + +namespace dotenv { + +void LoadFromFile(Isolate* isolate, + const std::string_view src, + std::shared_ptr store) { + std::string path = std::string(src) + "/.env"; + + uv_fs_t req; + auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); }); + + uv_file file = uv_fs_open(nullptr, &req, path.c_str(), 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, "\n"sv)) { + auto equal_index = line.find('='); + + if (equal_index == std::string_view::npos) { + continue; + } + + std::string_view key = line.substr(0, equal_index); + std::string_view value = line.substr(equal_index + 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()); + } +} + +} // namespace dotenv + +} // namespace node diff --git a/src/node_dotenv.h b/src/node_dotenv.h new file mode 100644 index 00000000000000..42d62f65504f5c --- /dev/null +++ b/src/node_dotenv.h @@ -0,0 +1,22 @@ +#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(v8::Isolate* isolate, + const std::string_view path, + std::shared_ptr store); + +} // namespace dotenv + +} // namespace node + +#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS + +#endif // SRC_NODE_DOTENV_H_ diff --git a/src/node_options.cc b/src/node_options.cc index c02752464c4ab5..406ea4711e8005 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -575,6 +575,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "write warnings to file instead of stderr", &EnvironmentOptions::redirect_warnings, kAllowedInEnvvar); + AddOption("--load-dotenv", + "load .env configuration file on startup", + &EnvironmentOptions::load_dotenv); AddOption("--test", "launch test runner on startup", &EnvironmentOptions::test_runner); diff --git a/src/node_options.h b/src/node_options.h index bb8b68894b4430..192b543f82035d 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -158,6 +158,7 @@ class EnvironmentOptions : public Options { #endif // HAVE_INSPECTOR std::string redirect_warnings; std::string diagnostic_dir; + bool load_dotenv = false; bool test_runner = false; bool test_runner_coverage = false; std::vector test_name_pattern; diff --git a/test/fixtures/dotenv/valid/.env b/test/fixtures/dotenv/valid/.env new file mode 100644 index 00000000000000..1cbeab17e36b4d --- /dev/null +++ b/test/fixtures/dotenv/valid/.env @@ -0,0 +1 @@ +DATABASE_PASSWORD=nodejs diff --git a/test/parallel/test-dotenv.js b/test/parallel/test-dotenv.js new file mode 100644 index 00000000000000..e2a83b5e26bc56 --- /dev/null +++ b/test/parallel/test-dotenv.js @@ -0,0 +1,18 @@ +'use strict'; + +require('../common'); +const assert = require('node:assert'); +const { spawnSync } = require('node:child_process'); +const path = require('node:path'); + +{ + const child = spawnSync( + process.execPath, + ['--load-dotenv', '-e', 'console.log(process.env.DATABASE_PASSWORD)'], + { + cwd: path.join(__dirname, '../fixtures/dotenv/valid') + } + ); + + assert.strictEqual(child.stdout.toString(), 'nodejs\n'); +}