Skip to content

Commit fae5554

Browse files
anonrigmarco-ippolito
authored andcommitted
src: add process.loadEnvFile and util.parseEnv
PR-URL: nodejs#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]>
1 parent 0f63ee9 commit fae5554

16 files changed

+345
-14
lines changed

doc/api/process.md

+23
Original file line numberDiff line numberDiff line change
@@ -2260,6 +2260,29 @@ process.kill(process.pid, 'SIGHUP');
22602260
When `SIGUSR1` is received by a Node.js process, Node.js will start the
22612261
debugger. See [Signal Events][].
22622262
2263+
## `process.loadEnvFile(path)`
2264+
2265+
<!-- YAML
2266+
added: REPLACEME
2267+
-->
2268+
2269+
> Stability: 1.1 - Active development
2270+
2271+
* `path` {string | URL | Buffer | undefined}. **Default:** `'./.env'`
2272+
2273+
Loads the `.env` file into `process.env`. Usage of `NODE_OPTIONS`
2274+
in the `.env` file will not have any effect on Node.js.
2275+
2276+
```cjs
2277+
const { loadEnvFile } = require('node:process');
2278+
loadEnvFile();
2279+
```
2280+
2281+
```mjs
2282+
import { loadEnvFile } from 'node:process';
2283+
loadEnvFile();
2284+
```
2285+
22632286
## `process.mainModule`
22642287
22652288
<!-- YAML

doc/api/util.md

+30
Original file line numberDiff line numberDiff line change
@@ -1593,6 +1593,36 @@ $ node negate.js --no-logfile --logfile=test.log --color --no-color
15931593
{ logfile: 'test.log', color: false }
15941594
```
15951595
1596+
## `util.parseEnv(content)`
1597+
1598+
> Stability: 1.1 - Active development
1599+
1600+
<!-- YAML
1601+
added: REPLACEME
1602+
-->
1603+
1604+
* `content` {string}
1605+
1606+
The raw contents of a `.env` file.
1607+
1608+
* Returns: {Object}
1609+
1610+
Given an example `.env` file:
1611+
1612+
```cjs
1613+
const { parseEnv } = require('node:util');
1614+
1615+
parseEnv('HELLO=world\nHELLO=oh my\n');
1616+
// Returns: { HELLO: 'oh my' }
1617+
```
1618+
1619+
```mjs
1620+
import { parseEnv } from 'node:util';
1621+
1622+
parseEnv('HELLO=world\nHELLO=oh my\n');
1623+
// Returns: { HELLO: 'oh my' }
1624+
```
1625+
15961626
## `util.promisify(original)`
15971627
15981628
<!-- YAML

lib/internal/bootstrap/node.js

+1
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ const rawMethods = internalBinding('process_methods');
172172
process._kill = rawMethods._kill;
173173

174174
const wrapped = perThreadSetup.wrapProcessMethods(rawMethods);
175+
process.loadEnvFile = wrapped.loadEnvFile;
175176
process._rawDebug = wrapped._rawDebug;
176177
process.cpuUsage = wrapped.cpuUsage;
177178
process.resourceUsage = wrapped.resourceUsage;

lib/internal/process/per_thread.js

+17
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const {
4646
validateNumber,
4747
validateObject,
4848
} = require('internal/validators');
49+
const { getValidatedPath } = require('internal/fs/utils');
50+
const { toNamespacedPath } = require('path');
4951
const constants = internalBinding('constants').os.signals;
5052

5153
const kInternal = Symbol('internal properties');
@@ -100,6 +102,7 @@ function wrapProcessMethods(binding) {
100102
memoryUsage: _memoryUsage,
101103
rss,
102104
resourceUsage: _resourceUsage,
105+
loadEnvFile: _loadEnvFile,
103106
} = binding;
104107

105108
function _rawDebug(...args) {
@@ -250,6 +253,19 @@ function wrapProcessMethods(binding) {
250253
};
251254
}
252255

256+
/**
257+
* Loads the `.env` file to process.env.
258+
* @param {string | URL | Buffer | undefined} path
259+
*/
260+
function loadEnvFile(path = undefined) { // Provide optional value so that `loadEnvFile.length` returns 0
261+
if (path != null) {
262+
path = getValidatedPath(path);
263+
_loadEnvFile(toNamespacedPath(path));
264+
} else {
265+
_loadEnvFile();
266+
}
267+
}
268+
253269

254270
return {
255271
_rawDebug,
@@ -258,6 +274,7 @@ function wrapProcessMethods(binding) {
258274
memoryUsage,
259275
kill,
260276
exit,
277+
loadEnvFile,
261278
};
262279
}
263280

lib/util.js

+13
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ const { debuglog } = require('internal/util/debuglog');
6767
const {
6868
validateFunction,
6969
validateNumber,
70+
validateString,
7071
} = require('internal/validators');
7172
const { isBuffer } = require('buffer').Buffer;
7273
const types = require('internal/util/types');
74+
const binding = internalBinding('util');
7375

7476
const {
7577
deprecate,
@@ -371,6 +373,16 @@ function _exceptionWithHostPort(...args) {
371373
return new ExceptionWithHostPort(...args);
372374
}
373375

376+
/**
377+
* Parses the content of a `.env` file.
378+
* @param {string} content
379+
* @returns {Record<string, string>}
380+
*/
381+
function parseEnv(content) {
382+
validateString(content, 'content');
383+
return binding.parseEnv(content);
384+
}
385+
374386
// Keep the `exports =` so that various functions can still be monkeypatched
375387
module.exports = {
376388
_errnoException,
@@ -465,6 +477,7 @@ module.exports = {
465477
return lazyAbortController().aborted;
466478
},
467479
types,
480+
parseEnv,
468481
};
469482

470483
defineLazyProperties(

src/node.cc

+12-3
Original file line numberDiff line numberDiff line change
@@ -871,9 +871,18 @@ static ExitCode InitializeNodeWithArgsInternal(
871871
CHECK(!per_process::v8_initialized);
872872

873873
for (const auto& file_path : file_paths) {
874-
bool path_exists = per_process::dotenv_file.ParsePath(file_path);
875-
876-
if (!path_exists) errors->push_back(file_path + ": not found");
874+
switch (per_process::dotenv_file.ParsePath(file_path)) {
875+
case Dotenv::ParseResult::Valid:
876+
break;
877+
case Dotenv::ParseResult::InvalidContent:
878+
errors->push_back(file_path + ": invalid format");
879+
break;
880+
case Dotenv::ParseResult::FileError:
881+
errors->push_back(file_path + ": not found");
882+
break;
883+
default:
884+
UNREACHABLE();
885+
}
877886
}
878887

879888
per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);

src/node_dotenv.cc

+40-10
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
namespace node {
77

8+
using v8::Local;
89
using v8::NewStringType;
10+
using v8::Object;
911
using v8::String;
1012

1113
std::vector<std::string> Dotenv::GetPathFromArgs(
@@ -64,14 +66,47 @@ void Dotenv::SetEnvironment(node::Environment* env) {
6466
}
6567
}
6668

67-
bool Dotenv::ParsePath(const std::string_view path) {
69+
Local<Object> Dotenv::ToObject(Environment* env) {
70+
Local<Object> result = Object::New(env->isolate());
71+
72+
for (const auto& entry : store_) {
73+
auto key = entry.first;
74+
auto value = entry.second;
75+
76+
result
77+
->Set(
78+
env->context(),
79+
v8::String::NewFromUtf8(
80+
env->isolate(), key.data(), NewStringType::kNormal, key.size())
81+
.ToLocalChecked(),
82+
v8::String::NewFromUtf8(env->isolate(),
83+
value.data(),
84+
NewStringType::kNormal,
85+
value.size())
86+
.ToLocalChecked())
87+
.Check();
88+
}
89+
90+
return result;
91+
}
92+
93+
void Dotenv::ParseContent(const std::string_view content) {
94+
using std::string_view_literals::operator""sv;
95+
auto lines = SplitString(content, "\n"sv);
96+
97+
for (const auto& line : lines) {
98+
ParseLine(line);
99+
}
100+
}
101+
102+
Dotenv::ParseResult Dotenv::ParsePath(const std::string_view path) {
68103
uv_fs_t req;
69104
auto defer_req_cleanup = OnScopeLeave([&req]() { uv_fs_req_cleanup(&req); });
70105

71106
uv_file file = uv_fs_open(nullptr, &req, path.data(), 0, 438, nullptr);
72107
if (req.result < 0) {
73108
// req will be cleaned up by scope leave.
74-
return false;
109+
return ParseResult::FileError;
75110
}
76111
uv_fs_req_cleanup(&req);
77112

@@ -89,7 +124,7 @@ bool Dotenv::ParsePath(const std::string_view path) {
89124
auto r = uv_fs_read(nullptr, &req, file, &buf, 1, -1, nullptr);
90125
if (req.result < 0) {
91126
// req will be cleaned up by scope leave.
92-
return false;
127+
return ParseResult::InvalidContent;
93128
}
94129
uv_fs_req_cleanup(&req);
95130
if (r <= 0) {
@@ -98,13 +133,8 @@ bool Dotenv::ParsePath(const std::string_view path) {
98133
result.append(buf.base, r);
99134
}
100135

101-
using std::string_view_literals::operator""sv;
102-
auto lines = SplitString(result, "\n"sv);
103-
104-
for (const auto& line : lines) {
105-
ParseLine(line);
106-
}
107-
return true;
136+
ParseContent(result);
137+
return ParseResult::Valid;
108138
}
109139

110140
void Dotenv::AssignNodeOptionsIfAvailable(std::string* node_options) {

src/node_dotenv.h

+6-1
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,28 @@
44
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
55

66
#include "util-inl.h"
7+
#include "v8.h"
78

89
#include <map>
910

1011
namespace node {
1112

1213
class Dotenv {
1314
public:
15+
enum ParseResult { Valid, FileError, InvalidContent };
16+
1417
Dotenv() = default;
1518
Dotenv(const Dotenv& d) = default;
1619
Dotenv(Dotenv&& d) noexcept = default;
1720
Dotenv& operator=(Dotenv&& d) noexcept = default;
1821
Dotenv& operator=(const Dotenv& d) = default;
1922
~Dotenv() = default;
2023

21-
bool ParsePath(const std::string_view path);
24+
void ParseContent(const std::string_view content);
25+
ParseResult ParsePath(const std::string_view path);
2226
void AssignNodeOptionsIfAvailable(std::string* node_options);
2327
void SetEnvironment(Environment* env);
28+
v8::Local<v8::Object> ToObject(Environment* env);
2429

2530
static std::vector<std::string> GetPathFromArgs(
2631
const std::vector<std::string>& args);

src/node_process.h

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ class BindingData : public SnapshotableObject {
8484

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

87+
static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args);
88+
8789
private:
8890
// Buffer length in uint32.
8991
static constexpr size_t kHrTimeBufferLength = 3;

src/node_process_methods.cc

+37
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include "env-inl.h"
55
#include "memory_tracker-inl.h"
66
#include "node.h"
7+
#include "node_dotenv.h"
78
#include "node_errors.h"
89
#include "node_external_reference.h"
910
#include "node_internals.h"
@@ -463,6 +464,38 @@ static void ReallyExit(const FunctionCallbackInfo<Value>& args) {
463464
env->Exit(code);
464465
}
465466

467+
static void LoadEnvFile(const v8::FunctionCallbackInfo<v8::Value>& args) {
468+
Environment* env = Environment::GetCurrent(args);
469+
std::string path = ".env";
470+
if (args.Length() == 1) {
471+
Utf8Value path_value(args.GetIsolate(), args[0]);
472+
path = path_value.ToString();
473+
}
474+
475+
THROW_IF_INSUFFICIENT_PERMISSIONS(
476+
env, permission::PermissionScope::kFileSystemRead, path);
477+
478+
Dotenv dotenv{};
479+
480+
switch (dotenv.ParsePath(path)) {
481+
case dotenv.ParseResult::Valid: {
482+
dotenv.SetEnvironment(env);
483+
break;
484+
}
485+
case dotenv.ParseResult::InvalidContent: {
486+
THROW_ERR_INVALID_ARG_TYPE(
487+
env, "Contents of '%s' should be a valid string.", path.c_str());
488+
break;
489+
}
490+
case dotenv.ParseResult::FileError: {
491+
env->ThrowUVException(UV_ENOENT, "Failed to load '%s'.", path.c_str());
492+
break;
493+
}
494+
default:
495+
UNREACHABLE();
496+
}
497+
}
498+
466499
namespace process {
467500

468501
BindingData::BindingData(Realm* realm,
@@ -616,6 +649,8 @@ static void CreatePerIsolateProperties(IsolateData* isolate_data,
616649
SetMethod(isolate, target, "reallyExit", ReallyExit);
617650
SetMethodNoSideEffect(isolate, target, "uptime", Uptime);
618651
SetMethod(isolate, target, "patchProcessObject", PatchProcessObject);
652+
653+
SetMethod(isolate, target, "loadEnvFile", LoadEnvFile);
619654
}
620655

621656
static void CreatePerContextProperties(Local<Object> target,
@@ -653,6 +688,8 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
653688
registry->Register(ReallyExit);
654689
registry->Register(Uptime);
655690
registry->Register(PatchProcessObject);
691+
692+
registry->Register(LoadEnvFile);
656693
}
657694

658695
} // namespace process

0 commit comments

Comments
 (0)