Skip to content

Commit

Permalink
src: add process.loadEnvFile and util.parseEnv
Browse files Browse the repository at this point in the history
PR-URL: #51476
Reviewed-By: Matteo Collina <[email protected]>
Reviewed-By: Moshe Atlow <[email protected]>
Reviewed-By: Geoffrey Booth <[email protected]>
Reviewed-By: Antoine du Hamel <[email protected]>
Reviewed-By: Rafael Gonzaga <[email protected]>
  • Loading branch information
anonrig authored and targos committed Feb 15, 2024
1 parent 438b7fd commit b8ae5c2
Show file tree
Hide file tree
Showing 16 changed files with 345 additions and 14 deletions.
23 changes: 23 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -2260,6 +2260,29 @@ process.kill(process.pid, 'SIGHUP');
When `SIGUSR1` is received by a Node.js process, Node.js will start the
debugger. See [Signal Events][].
## `process.loadEnvFile(path)`
<!-- YAML
added: REPLACEME
-->
> Stability: 1.1 - Active development
* `path` {string | URL | Buffer | undefined}. **Default:** `'./.env'`
Loads the `.env` file into `process.env`. Usage of `NODE_OPTIONS`
in the `.env` file will not have any effect on Node.js.
```cjs
const { loadEnvFile } = require('node:process');
loadEnvFile();
```
```mjs
import { loadEnvFile } from 'node:process';
loadEnvFile();
```
## `process.mainModule`
<!-- YAML
Expand Down
30 changes: 30 additions & 0 deletions doc/api/util.md
Original file line number Diff line number Diff line change
Expand Up @@ -1593,6 +1593,36 @@ $ node negate.js --no-logfile --logfile=test.log --color --no-color
{ logfile: 'test.log', color: false }
```
## `util.parseEnv(content)`
> Stability: 1.1 - Active development
<!-- YAML
added: REPLACEME
-->
* `content` {string}
The raw contents of a `.env` file.
* Returns: {Object}
Given an example `.env` file:
```cjs
const { parseEnv } = require('node:util');

parseEnv('HELLO=world\nHELLO=oh my\n');
// Returns: { HELLO: 'oh my' }
```
```mjs
import { parseEnv } from 'node:util';

parseEnv('HELLO=world\nHELLO=oh my\n');
// Returns: { HELLO: 'oh my' }
```
## `util.promisify(original)`
<!-- YAML
Expand Down
1 change: 1 addition & 0 deletions lib/internal/bootstrap/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ const rawMethods = internalBinding('process_methods');
process._kill = rawMethods._kill;

const wrapped = perThreadSetup.wrapProcessMethods(rawMethods);
process.loadEnvFile = wrapped.loadEnvFile;
process._rawDebug = wrapped._rawDebug;
process.cpuUsage = wrapped.cpuUsage;
process.resourceUsage = wrapped.resourceUsage;
Expand Down
17 changes: 17 additions & 0 deletions lib/internal/process/per_thread.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ const {
validateNumber,
validateObject,
} = require('internal/validators');
const { getValidatedPath } = require('internal/fs/utils');
const { toNamespacedPath } = require('path');
const constants = internalBinding('constants').os.signals;

const kInternal = Symbol('internal properties');
Expand Down Expand Up @@ -100,6 +102,7 @@ function wrapProcessMethods(binding) {
memoryUsage: _memoryUsage,
rss,
resourceUsage: _resourceUsage,
loadEnvFile: _loadEnvFile,
} = binding;

function _rawDebug(...args) {
Expand Down Expand Up @@ -250,6 +253,19 @@ function wrapProcessMethods(binding) {
};
}

/**
* Loads the `.env` file to process.env.
* @param {string | URL | Buffer | undefined} path
*/
function loadEnvFile(path = undefined) { // Provide optional value so that `loadEnvFile.length` returns 0
if (path != null) {
path = getValidatedPath(path);
_loadEnvFile(toNamespacedPath(path));
} else {
_loadEnvFile();
}
}


return {
_rawDebug,
Expand All @@ -258,6 +274,7 @@ function wrapProcessMethods(binding) {
memoryUsage,
kill,
exit,
loadEnvFile,
};
}

Expand Down
13 changes: 13 additions & 0 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,11 @@ const { debuglog } = require('internal/util/debuglog');
const {
validateFunction,
validateNumber,
validateString,
} = require('internal/validators');
const { isBuffer } = require('buffer').Buffer;
const types = require('internal/util/types');
const binding = internalBinding('util');

const {
deprecate,
Expand Down Expand Up @@ -371,6 +373,16 @@ function _exceptionWithHostPort(...args) {
return new ExceptionWithHostPort(...args);
}

/**
* Parses the content of a `.env` file.
* @param {string} content
* @returns {Record<string, string>}
*/
function parseEnv(content) {
validateString(content, 'content');
return binding.parseEnv(content);
}

// Keep the `exports =` so that various functions can still be monkeypatched
module.exports = {
_errnoException,
Expand Down Expand Up @@ -424,6 +436,7 @@ module.exports = {
return lazyAbortController().aborted;
},
types,
parseEnv,
};

defineLazyProperties(
Expand Down
15 changes: 12 additions & 3 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -867,9 +867,18 @@ static ExitCode InitializeNodeWithArgsInternal(
CHECK(!per_process::v8_initialized);

for (const auto& file_path : file_paths) {
bool path_exists = per_process::dotenv_file.ParsePath(file_path);

if (!path_exists) errors->push_back(file_path + ": not found");
switch (per_process::dotenv_file.ParsePath(file_path)) {
case Dotenv::ParseResult::Valid:
break;
case Dotenv::ParseResult::InvalidContent:
errors->push_back(file_path + ": invalid format");
break;
case Dotenv::ParseResult::FileError:
errors->push_back(file_path + ": not found");
break;
default:
UNREACHABLE();
}
}

per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
Expand Down
50 changes: 40 additions & 10 deletions src/node_dotenv.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@

namespace node {

using v8::Local;
using v8::NewStringType;
using v8::Object;
using v8::String;

std::vector<std::string> Dotenv::GetPathFromArgs(
Expand Down Expand Up @@ -64,14 +66,47 @@ void Dotenv::SetEnvironment(node::Environment* env) {
}
}

bool Dotenv::ParsePath(const std::string_view path) {
Local<Object> Dotenv::ToObject(Environment* env) {
Local<Object> result = Object::New(env->isolate());

for (const auto& entry : store_) {
auto key = entry.first;
auto value = entry.second;

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

return result;
}

void Dotenv::ParseContent(const std::string_view content) {
using std::string_view_literals::operator""sv;
auto lines = SplitString(content, "\n"sv);

for (const auto& line : lines) {
ParseLine(line);
}
}

Dotenv::ParseResult Dotenv::ParsePath(const std::string_view path) {
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 false;
return ParseResult::FileError;
}
uv_fs_req_cleanup(&req);

Expand All @@ -89,7 +124,7 @@ bool Dotenv::ParsePath(const std::string_view path) {
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 false;
return ParseResult::InvalidContent;
}
uv_fs_req_cleanup(&req);
if (r <= 0) {
Expand All @@ -98,13 +133,8 @@ bool Dotenv::ParsePath(const std::string_view path) {
result.append(buf.base, r);
}

using std::string_view_literals::operator""sv;
auto lines = SplitString(result, "\n"sv);

for (const auto& line : lines) {
ParseLine(line);
}
return true;
ParseContent(result);
return ParseResult::Valid;
}

void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) {
Expand Down
7 changes: 6 additions & 1 deletion src/node_dotenv.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,28 @@
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include "util-inl.h"
#include "v8.h"

#include <map>

namespace node {

class Dotenv {
public:
enum ParseResult { Valid, FileError, InvalidContent };

Dotenv() = default;
Dotenv(const Dotenv& d) = default;
Dotenv(Dotenv&& d) noexcept = default;
Dotenv& operator=(Dotenv&& d) noexcept = default;
Dotenv& operator=(const Dotenv& d) = default;
~Dotenv() = default;

bool ParsePath(const std::string_view path);
void ParseContent(const std::string_view content);
ParseResult ParsePath(const std::string_view path);
void AssignNodeOptionsIfAvailable(std::string* node_options);
void SetEnvironment(Environment* env);
v8::Local<v8::Object> ToObject(Environment* env);

static std::vector<std::string> GetPathFromArgs(
const std::vector<std::string>& args);
Expand Down
2 changes: 2 additions & 0 deletions src/node_process.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ class BindingData : public SnapshotableObject {

static void SlowBigInt(const v8::FunctionCallbackInfo<v8::Value>& args);

static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args);

private:
// Buffer length in uint32.
static constexpr size_t kHrTimeBufferLength = 3;
Expand Down
37 changes: 37 additions & 0 deletions src/node_process_methods.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "env-inl.h"
#include "memory_tracker-inl.h"
#include "node.h"
#include "node_dotenv.h"
#include "node_errors.h"
#include "node_external_reference.h"
#include "node_internals.h"
Expand Down Expand Up @@ -463,6 +464,38 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) {
env->Exit(code);
}

static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
Environment* env = Environment::GetCurrent(args);
std::string path = ".env";
if (args.Length() == 1) {
Utf8Value path_value(args.GetIsolate(), args[0]);
path = path_value.ToString();
}

THROW_IF_INSUFFICIENT_PERMISSIONS(
env, permission::PermissionScope::kFileSystemRead, path);

Dotenv dotenv{};

switch (dotenv.ParsePath(path)) {
case dotenv.ParseResult::Valid: {
dotenv.SetEnvironment(env);
break;
}
case dotenv.ParseResult::InvalidContent: {
THROW_ERR_INVALID_ARG_TYPE(
env, "Contents of '%s' should be a valid string.", path.c_str());
break;
}
case dotenv.ParseResult::FileError: {
env->ThrowUVException(UV_ENOENT, "Failed to load '%s'.", path.c_str());
break;
}
default:
UNREACHABLE();
}
}

namespace process {

BindingData::BindingData(Realm* realm,
Expand Down Expand Up @@ -616,6 +649,8 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
SetMethod(isolate, target, "reallyExit", ReallyExit);
SetMethodNoSideEffect(isolate, target, "uptime", Uptime);
SetMethod(isolate, target, "patchProcessObject", PatchProcessObject);

SetMethod(isolate, target, "loadEnvFile", LoadEnvFile);
}

static void CreatePerContextProperties(Local<Object> target,
Expand Down Expand Up @@ -653,6 +688,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(ReallyExit);
registry->Register(Uptime);
registry->Register(PatchProcessObject);

registry->Register(LoadEnvFile);
}

} // namespace process
Expand Down
Loading

0 comments on commit b8ae5c2

Please sign in to comment.