Skip to content

Commit 497298b

Browse files
src: add config file support
1 parent 9ce1fff commit 497298b

18 files changed

+399
-0
lines changed

doc/api/cli.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,48 @@ 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.
925+
The configuration file should be a JSON file
926+
with the following structure:
927+
928+
```json
929+
{
930+
"experimental_transform_types": true,
931+
"import": [
932+
"amaro/transform"
933+
]
934+
}
935+
```
936+
937+
The priority in configuration is as follows:
938+
939+
* NODE\_OPTIONS and command-line options
940+
* Config file
941+
* Dotenv NODE\_OPTIONS
942+
943+
If multiple keys are present in the configuration file,
944+
the last one will override the previous ones.
945+
Unknown keys will be ignored.
946+
947+
It possible to use the official json schema to validate the configuration file,
948+
which may vary depending on the Node.js version.
949+
950+
```json
951+
{
952+
"$schema": "https://nodejs.org/dist/REPLACEME/node_config_json_schema.json",
953+
}
954+
```
955+
914956
### `--experimental-eventsource`
915957

916958
<!-- YAML

doc/node.1

Lines changed: 3 additions & 0 deletions
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
.

doc/node_config_json_schema.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"type": "object",
4+
"properties": {
5+
"experimental_transform_types": {
6+
"type": "boolean"
7+
},
8+
"import": {
9+
"type": "array",
10+
"items": {
11+
"type": "string"
12+
}
13+
}
14+
},
15+
"additionalProperties": false
16+
}

lib/internal/process/pre_execution.js

Lines changed: 8 additions & 0 deletions
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

Lines changed: 2 additions & 0 deletions
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

Lines changed: 21 additions & 0 deletions
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,23 @@ static ExitCode InitializeNodeWithArgsInternal(
884888
per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
885889
}
886890

891+
auto result = per_process::config_reader.GetDataFromArgs(*argv);
892+
if (result.has_value()) {
893+
switch (per_process::config_reader.ParseConfig(result.value())) {
894+
case ConfigReader::ParseResult::Valid:
895+
break;
896+
case ConfigReader::ParseResult::InvalidContent:
897+
errors->push_back(result.value() + ": invalid format");
898+
break;
899+
case ConfigReader::ParseResult::FileError:
900+
errors->push_back(result.value() + ": not found");
901+
break;
902+
default:
903+
UNREACHABLE();
904+
}
905+
per_process::config_reader.AssignNodeOptions(&node_options);
906+
}
907+
887908
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
888909
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
889910
// NODE_OPTIONS environment variable is preferred over the file one.

src/node_options.cc

Lines changed: 3 additions & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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<std::string> ConfigReader::GetDataFromArgs(
16+
const std::vector<std::string>& args) {
17+
constexpr std::string_view flag = "--experimental-config-file";
18+
19+
for (auto it = args.begin(); it != args.end(); ++it) {
20+
if (*it == flag) {
21+
// Case: "--experimental-config-file foo"
22+
if (auto next = std::next(it); next != args.end()) {
23+
return *next;
24+
}
25+
} else if (it->starts_with(flag)) {
26+
// Case: "--experimental-config-file=foo"
27+
if (it->size() > flag.size() && (*it)[flag.size()] == '=') {
28+
return it->substr(flag.size() + 1);
29+
}
30+
}
31+
}
32+
33+
return std::nullopt;
34+
}
35+
36+
ConfigReader::ParseResult ConfigReader::ParseConfig(
37+
const std::string& config_path) {
38+
std::string file_content;
39+
// Read the configuration file
40+
int r = ReadFileSync(&file_content, config_path.c_str());
41+
if (r != 0) {
42+
const char* err = uv_strerror(r);
43+
FPrintF(
44+
stderr, "Cannot read configuration from %s: %s\n", config_path, err);
45+
return ParseResult::FileError;
46+
}
47+
48+
// Parse the configuration file
49+
simdjson::ondemand::parser json_parser;
50+
simdjson::ondemand::document document;
51+
if (json_parser.iterate(file_content).get(document)) {
52+
FPrintF(stderr, "Can't parse %s\n", config_path.c_str());
53+
return ParseResult::InvalidContent;
54+
}
55+
56+
simdjson::ondemand::object main_object;
57+
// If document is not an object, throw an error.
58+
if (auto root_error = document.get_object().get(main_object)) {
59+
if (root_error == simdjson::error_code::INCORRECT_TYPE) {
60+
FPrintF(stderr,
61+
"Root value unexpected not an object for %s\n\n",
62+
config_path.c_str());
63+
} else {
64+
FPrintF(stderr, "Can't parse %s\n", config_path.c_str());
65+
}
66+
return ParseResult::InvalidContent;
67+
}
68+
69+
ConfigReader::Config config;
70+
simdjson::ondemand::value ondemand_value;
71+
simdjson::ondemand::raw_json_string key;
72+
73+
for (auto field : main_object) {
74+
if (field.key().get(key) || field.value().get(ondemand_value)) {
75+
return ParseResult::InvalidContent;
76+
}
77+
if (key == "experimental_transform_types") {
78+
if (ondemand_value.get_bool().get(config.experimental_transform_types)) {
79+
FPrintF(stderr, "Invalid value for experimental_transform_types\n");
80+
return ParseResult::InvalidContent;
81+
}
82+
}
83+
84+
if (key == "import") {
85+
simdjson::ondemand::array raw_imports;
86+
if (ondemand_value.get_array().get(raw_imports)) {
87+
FPrintF(stderr, "Invalid value for import\n");
88+
return ParseResult::InvalidContent;
89+
}
90+
for (auto raw_import : raw_imports) {
91+
std::string_view import;
92+
if (raw_import.get_string(import)) {
93+
FPrintF(stderr, "Invalid value for import\n");
94+
return ParseResult::InvalidContent;
95+
}
96+
config.import.push_back(std::string(import));
97+
}
98+
}
99+
}
100+
101+
config_ = config;
102+
return ParseResult::Valid;
103+
}
104+
105+
void ConfigReader::AssignNodeOptions(std::string* node_options) {
106+
std::string result = "";
107+
if (config_.experimental_transform_types) {
108+
result += "--experimental-transform-types";
109+
}
110+
111+
if(config_.import.size() > 0) {
112+
for (const auto& import : config_.import) {
113+
result += " --import=" + import;
114+
}
115+
}
116+
*node_options = result;
117+
return;
118+
}
119+
} // namespace node

src/node_rc.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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 };
17+
struct Config {
18+
bool experimental_transform_types;
19+
std::vector<std::string> import;
20+
};
21+
ConfigReader::ParseResult ParseConfig(const std::string& config_path);
22+
23+
std::optional<std::string> GetDataFromArgs(
24+
const std::vector<std::string>& args);
25+
26+
void AssignNodeOptions(std::string* node_options);
27+
28+
private:
29+
ConfigReader::Config config_;
30+
};
31+
32+
} // namespace node
33+
34+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
35+
36+
#endif // SRC_NODE_RC_H_
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NODE_OPTIONS="--no-experimental-strip-types"

test/fixtures/rc/empty-object.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
3+
}
4+

test/fixtures/rc/empty.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

test/fixtures/rc/invalid-import.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"import": [1]
3+
}

test/fixtures/rc/loaders.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"import": [
3+
"./test/fixtures/printA.js",
4+
"./test/fixtures/printB.js",
5+
"./test/fixtures/printC.js"
6+
]
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"experimental_transform_types": true,
3+
"experimental_transform_types": false
4+
}

test/fixtures/rc/transform-types.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"experimental_transform_types": true
3+
}

0 commit comments

Comments
 (0)