Skip to content

Commit 8028f5a

Browse files
src: add config file support
1 parent 9ce1fff commit 8028f5a

18 files changed

+418
-0
lines changed

doc/api/cli.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -911,6 +911,59 @@ 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+
Each key in the configuration file corresponds to a flag that can be passed
938+
as a command-line argument. The value of the key is the value that would be
939+
passed to the flag.
940+
941+
For example, the configuration file above is equivalent to
942+
the following command-line arguments:
943+
944+
```bash
945+
node --experimental-transform-types --import amaro/transform
946+
```
947+
948+
The priority in configuration is as follows:
949+
950+
* NODE\_OPTIONS and command-line options
951+
* Config file
952+
* Dotenv NODE\_OPTIONS
953+
954+
If multiple keys are present in the configuration file,
955+
the last one will override the previous ones.
956+
Unknown keys will be ignored.
957+
958+
It possible to use the official json schema to validate the configuration file,
959+
which may vary depending on the Node.js version.
960+
961+
```json
962+
{
963+
"$schema": "https://nodejs.org/dist/REPLACEME/node_config_json_schema.json",
964+
}
965+
```
966+
914967
### `--experimental-eventsource`
915968

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