Skip to content

Commit 5d7ff50

Browse files
src: add config file support
1 parent 9ce1fff commit 5d7ff50

18 files changed

+386
-0
lines changed

doc/api/cli.md

+28
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,34 @@ added: v23.6.0
911911
912912
Enable experimental import support for `.node` addons.
913913

914+
### `--experimental-config-file`
915+
916+
<!-- YAML
917+
added: REPLACEME
918+
-->
919+
920+
> Stability: 1.0 - Early development
921+
922+
Use this flag to specify a configuration file that will be loaded and parsed
923+
before the application starts.
924+
Node.js will read the configuration file and apply the settings as
925+
[`NODE_OPTIONS`][].
926+
The configuration file should be a JSON file
927+
with the following structure:
928+
929+
```json
930+
{
931+
"version": 0,
932+
"experimental_transform_types": true
933+
}
934+
```
935+
936+
Currently the only supported version is 0.
937+
The configuration file cannot be used in conjuction with `--env-file`.
938+
If multiple keys are present in the configuration file, only the first one
939+
will be considered and the followin will be ignored.
940+
Unknown keys will be ignored.
941+
914942
### `--experimental-eventsource`
915943

916944
<!-- YAML

doc/node.1

+3
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,9 @@ Interpret the entry point as a URL.
166166
.It Fl -experimental-addon-modules
167167
Enable experimental addon module support.
168168
.
169+
.It Fl -experimental-config-file
170+
Enable support for experimental config file
171+
.
169172
.It Fl -experimental-import-meta-resolve
170173
Enable experimental ES modules support for import.meta.resolve().
171174
.

lib/internal/process/pre_execution.js

+8
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ function prepareExecution(options) {
116116
initializeSourceMapsHandlers();
117117
initializeDeprecations();
118118

119+
setupConfigFile();
120+
119121
require('internal/dns/utils').initializeDns();
120122

121123
if (isMainThread) {
@@ -312,6 +314,12 @@ function setupSQLite() {
312314
BuiltinModule.allowRequireByUsers('sqlite');
313315
}
314316

317+
function setupConfigFile() {
318+
if (getOptionValue('--experimental-config-file')) {
319+
emitExperimentalWarning('--experimental-config-file');
320+
}
321+
}
322+
315323
function setupQuic() {
316324
if (!getOptionValue('--experimental-quic')) {
317325
return;

node.gyp

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
'src/node_process_events.cc',
131131
'src/node_process_methods.cc',
132132
'src/node_process_object.cc',
133+
'src/node_rc.cc',
133134
'src/node_realm.cc',
134135
'src/node_report.cc',
135136
'src/node_report_module.cc',
@@ -262,6 +263,7 @@
262263
'src/node_platform.h',
263264
'src/node_process.h',
264265
'src/node_process-inl.h',
266+
'src/node_rc.h',
265267
'src/node_realm.h',
266268
'src/node_realm-inl.h',
267269
'src/node_report.h',

src/node.cc

+30
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
#include "node.h"
2323
#include "node_dotenv.h"
24+
#include "node_rc.h"
2425
#include "node_task_runner.h"
2526

2627
// ========== local headers ==========
@@ -150,6 +151,9 @@ namespace per_process {
150151
// Instance is used to store environment variables including NODE_OPTIONS.
151152
node::Dotenv dotenv_file = Dotenv();
152153

154+
// node_rc.h
155+
node::ConfigReader config_reader = ConfigReader();
156+
153157
// node_revert.h
154158
// Bit flag used to track security reverts.
155159
unsigned int reverted_cve = 0;
@@ -884,6 +888,32 @@ static ExitCode InitializeNodeWithArgsInternal(
884888
per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
885889
}
886890

891+
auto result = per_process::config_reader.GetDataFromArgs(*argv);
892+
// Skip if env_files is not empty, as it has already been processed.
893+
if (result.has_value() && !env_files.empty()) {
894+
errors->push_back(
895+
"--experimental-config-file cannot be used with .env files");
896+
return ExitCode::kInvalidCommandLineArgument;
897+
}
898+
if (result.has_value() && env_files.empty()) {
899+
switch (per_process::config_reader.ParseConfig(result.value())) {
900+
case ConfigReader::ParseResult::Valid:
901+
break;
902+
case ConfigReader::ParseResult::InvalidContent:
903+
errors->push_back(result.value() + ": invalid format");
904+
break;
905+
case ConfigReader::ParseResult::FileError:
906+
errors->push_back(result.value() + ": not found");
907+
break;
908+
case ConfigReader::ParseResult::InvalidVersion:
909+
errors->push_back(result.value() + ": invalid version");
910+
break;
911+
default:
912+
UNREACHABLE();
913+
}
914+
per_process::config_reader.AssignNodeOptions(&node_options);
915+
}
916+
887917
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
888918
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
889919
// NODE_OPTIONS environment variable is preferred over the file one.

src/node_options.cc

+3
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
671671
"set environment variables from supplied file",
672672
&EnvironmentOptions::optional_env_file);
673673
Implies("--env-file-if-exists", "[has_env_file_string]");
674+
AddOption("--experimental-config-file",
675+
"set config file from supplied file",
676+
&EnvironmentOptions::experimental_config_file);
674677
AddOption("--test",
675678
"launch test runner on startup",
676679
&EnvironmentOptions::test_runner);

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,7 @@ class EnvironmentOptions : public Options {
256256

257257
bool report_exclude_env = false;
258258
bool report_exclude_network = false;
259+
std::string experimental_config_file;
259260

260261
inline DebugOptions* get_debug_options() { return &debug_options_; }
261262
inline const DebugOptions& debug_options() const { return debug_options_; }

src/node_rc.cc

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#include "node_rc.h"
2+
#include "debug_utils-inl.h"
3+
#include "env-inl.h"
4+
#include "node_errors.h"
5+
#include "node_file.h"
6+
#include "node_internals.h"
7+
#include "simdjson.h"
8+
9+
#include <functional>
10+
#include <map>
11+
#include <string>
12+
13+
namespace node {
14+
15+
std::optional<ConfigReader::ConfigV0> ConfigReader::ParseConfigV0(
16+
simdjson::ondemand::object* main_object) {
17+
ConfigReader::ConfigV0 config;
18+
config.version = 0;
19+
20+
if (auto value = (*main_object)["experimental_transform_types"];
21+
value.error() != simdjson::NO_SUCH_FIELD) {
22+
if (value.get_bool().get(config.experimental_transform_types)) {
23+
FPrintF(stderr, "Invalid value for experimental_transform_types\n");
24+
return std::nullopt;
25+
}
26+
}
27+
28+
return config;
29+
}
30+
31+
ConfigReader::ConfigReader() {
32+
config_parsers_[0] = &ConfigReader::ParseConfigV0;
33+
}
34+
35+
std::optional<std::string> ConfigReader::GetDataFromArgs(
36+
const std::vector<std::string>& args) {
37+
constexpr std::string_view flag = "--experimental-config-file";
38+
39+
for (auto it = args.begin(); it != args.end(); ++it) {
40+
if (*it == flag) {
41+
// Case: "--experimental-config-file foo"
42+
if (auto next = std::next(it); next != args.end()) {
43+
return *next;
44+
}
45+
} else if (it->starts_with(flag)) {
46+
// Case: "--experimental-config-file=foo"
47+
if (it->size() > flag.size() && (*it)[flag.size()] == '=') {
48+
return it->substr(flag.size() + 1);
49+
}
50+
}
51+
}
52+
53+
return std::nullopt;
54+
}
55+
56+
ConfigReader::ParseResult ConfigReader::ParseConfig(
57+
const std::string& config_path) {
58+
std::string file_content;
59+
// Read the configuration file
60+
int r = ReadFileSync(&file_content, config_path.c_str());
61+
if (r != 0) {
62+
const char* err = uv_strerror(r);
63+
FPrintF(
64+
stderr, "Cannot read configuration from %s: %s\n", config_path, err);
65+
return ParseResult::FileError;
66+
}
67+
68+
// Parse the configuration file
69+
simdjson::ondemand::parser json_parser;
70+
simdjson::ondemand::document document;
71+
if (json_parser.iterate(file_content).get(document)) {
72+
FPrintF(stderr, "Can't parse %s\n", config_path.c_str());
73+
return ParseResult::InvalidContent;
74+
}
75+
76+
simdjson::ondemand::object main_object;
77+
// If document is not an object, throw an error.
78+
if (auto root_error = document.get_object().get(main_object)) {
79+
if (root_error == simdjson::error_code::INCORRECT_TYPE) {
80+
FPrintF(stderr,
81+
"Root value unexpected not an object for %s\n\n",
82+
config_path.c_str());
83+
} else {
84+
FPrintF(stderr, "Can't parse %s\n", config_path.c_str());
85+
}
86+
return ParseResult::InvalidContent;
87+
}
88+
89+
// If json object doesn't have "version" field, throw an error.
90+
simdjson::ondemand::number version_field;
91+
if (main_object["version"].get_number().get(version_field)) {
92+
FPrintF(stderr,
93+
"Can't find numeric \"version\" field in %s\n",
94+
config_path.c_str());
95+
return ParseResult::InvalidVersion;
96+
}
97+
98+
// Check if version is an integer
99+
if (!version_field.is_int64()) {
100+
FPrintF(
101+
stderr, "Version field is not an integer in %s\n", config_path.c_str());
102+
return ParseResult::InvalidVersion;
103+
}
104+
105+
uint64_t version = version_field.get_int64();
106+
if (version < 0 || version >= config_parsers_.size()) {
107+
FPrintF(stderr, "Version %" PRIu64 " does not exist\n", version);
108+
return ParseResult::InvalidVersion;
109+
}
110+
111+
// Get the config parser for the specific version
112+
auto config_parser = config_parsers_.at(version_field.get_int64());
113+
auto config = config_parser(&main_object);
114+
if (!config.has_value()) {
115+
return ParseResult::InvalidContent;
116+
}
117+
118+
// save the config for later
119+
config_ = config.value();
120+
return ParseResult::Valid;
121+
}
122+
123+
void ConfigReader::AssignNodeOptions(std::string* node_options) {
124+
if (ConfigV0* config = std::get_if<ConfigReader::ConfigV0>(&config_)) {
125+
std::string result = "";
126+
if (config->experimental_transform_types) {
127+
result += "--experimental-transform-types";
128+
}
129+
*node_options = result;
130+
return;
131+
}
132+
}
133+
} // namespace node

src/node_rc.h

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#ifndef SRC_NODE_RC_H_
2+
#define SRC_NODE_RC_H_
3+
4+
#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
5+
6+
#include <map>
7+
#include <string>
8+
#include <variant>
9+
#include "simdjson.h"
10+
#include "util-inl.h"
11+
12+
namespace node {
13+
14+
class ConfigReader {
15+
public:
16+
enum ParseResult { Valid, FileError, InvalidContent, InvalidVersion };
17+
struct ConfigV0 {
18+
int64_t version;
19+
bool experimental_transform_types;
20+
};
21+
using Config = std::variant<ConfigV0>;
22+
using ConfigParser =
23+
std::function<std::optional<Config>(simdjson::ondemand::object*)>;
24+
25+
ConfigReader();
26+
27+
ConfigReader::ParseResult ParseConfig(const std::string& config_path);
28+
29+
std::optional<std::string> GetDataFromArgs(
30+
const std::vector<std::string>& args);
31+
32+
void AssignNodeOptions(std::string* node_options);
33+
34+
private:
35+
simdjson::ondemand::parser json_parser_;
36+
37+
ConfigReader::Config config_;
38+
39+
static std::optional<ConfigReader::ConfigV0> ParseConfigV0(
40+
simdjson::ondemand::object* main_object);
41+
42+
std::array<ConfigParser, 1> config_parsers_;
43+
};
44+
45+
} // namespace node
46+
47+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
48+
49+
#endif // SRC_NODE_RC_H_

test/fixtures/rc/empty-object.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
3+
}
4+

test/fixtures/rc/empty.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"version": 9999
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"version": "foo"
3+
}
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"version": 0,
3+
"experimental_transform_types": true,
4+
"experimental_transform_types": false
5+
}
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"version": 0,
3+
"version": 9999
4+
}

test/fixtures/rc/transform-types.json

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"version": 0,
3+
"experimental_transform_types": true
4+
}

test/fixtures/rc/version-only.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"version": 0
3+
}

0 commit comments

Comments
 (0)