Skip to content

Commit 272828d

Browse files
src: add config file support
1 parent 9ce1fff commit 272828d

16 files changed

+334
-0
lines changed

doc/api/cli.md

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

916956
<!-- 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: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
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+
},
9+
"additionalProperties": false
10+
}

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: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
85+
config_ = config;
86+
return ParseResult::Valid;
87+
}
88+
89+
void ConfigReader::AssignNodeOptions(std::string* node_options) {
90+
std::string result = "";
91+
if (config_.experimental_transform_types) {
92+
result += "--experimental-transform-types";
93+
}
94+
*node_options = result;
95+
return;
96+
}
97+
} // namespace node

src/node_rc.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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+
};
20+
ConfigReader::ParseResult ParseConfig(const std::string& config_path);
21+
22+
std::optional<std::string> GetDataFromArgs(
23+
const std::vector<std::string>& args);
24+
25+
void AssignNodeOptions(std::string* node_options);
26+
27+
private:
28+
ConfigReader::Config config_;
29+
};
30+
31+
} // namespace node
32+
33+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
34+
35+
#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+
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)