Skip to content

Commit 9fd217d

Browse files
marco-ippolitotargos
authored andcommittedMar 11, 2025
src: add config file support
PR-URL: #57016 Refs: #53787 Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Tierney Cyren <hello@bnb.im> Reviewed-By: Michael Dawson <midawson@redhat.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it>
1 parent d07bd79 commit 9fd217d

33 files changed

+1438
-1
lines changed
 

‎Makefile

+1
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ doc: $(NODE_EXE) doc-only ## Build Node.js, and then build the documentation wit
809809

810810
out/doc:
811811
mkdir -p $@
812+
cp doc/node_config_json_schema.json $@
812813

813814
# If it's a source tarball, doc/api already contains the generated docs.
814815
# Just copy everything under doc/api over.

‎doc/api/cli.md

+63
Original file line numberDiff line numberDiff line change
@@ -919,6 +919,69 @@ flows within the application. As such, it is presently recommended to be sure
919919
your application behaviour is unaffected by this change before using it in
920920
production.
921921

922+
### `--experimental-config-file`
923+
924+
<!-- YAML
925+
added: REPLACEME
926+
-->
927+
928+
> Stability: 1.0 - Early development
929+
930+
Use this flag to specify a configuration file that will be loaded and parsed
931+
before the application starts.
932+
Node.js will read the configuration file and apply the settings.
933+
The configuration file should be a JSON file
934+
with the following structure:
935+
936+
```json
937+
{
938+
"$schema": "https://nodejs.org/dist/REPLACEME/docs/node_config_json_schema.json",
939+
"experimental-transform-types": true,
940+
"import": [
941+
"amaro/transform"
942+
],
943+
"disable-warning": "ExperimentalWarning",
944+
"watch-path": "src",
945+
"watch-preserve-output": true
946+
}
947+
```
948+
949+
Only flags that are allowed in [`NODE_OPTIONS`][] are supported.
950+
No-op flags are not supported.
951+
Not all V8 flags are currently supported.
952+
953+
It is possible to use the [official JSON schema](../node_config_json_schema.json)
954+
to validate the configuration file, which may vary depending on the Node.js version.
955+
Each key in the configuration file corresponds to a flag that can be passed
956+
as a command-line argument. The value of the key is the value that would be
957+
passed to the flag.
958+
959+
For example, the configuration file above is equivalent to
960+
the following command-line arguments:
961+
962+
```bash
963+
node --experimental-transform-types --import amaro/transform --disable-warning=ExperimentalWarning --watch-path=src --watch-preserve-output
964+
```
965+
966+
The priority in configuration is as follows:
967+
968+
1. NODE\_OPTIONS and command-line options
969+
2. Configuration file
970+
3. Dotenv NODE\_OPTIONS
971+
972+
Values in the configuration file will not override the values in the environment
973+
variables and command-line options, but will override the values in the `NODE_OPTIONS`
974+
env file parsed by the `--env-file` flag.
975+
976+
If duplicate keys are present in the configuration file, only
977+
the first key will be used.
978+
979+
The configuration parser will throw an error if the configuration file contains
980+
unknown keys or keys that cannot used in `NODE_OPTIONS`.
981+
982+
Node.js will not sanitize or perform validation on the user-provided configuration,
983+
so **NEVER** use untrusted configuration files.
984+
922985
### `--experimental-eventsource`
923986

924987
<!-- 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
.

‎doc/node_config_json_schema.json

+578
Large diffs are not rendered by default.

‎lib/internal/options.js

+49
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
'use strict';
22

3+
const {
4+
ArrayPrototypeMap,
5+
ArrayPrototypeSort,
6+
ObjectFromEntries,
7+
ObjectKeys,
8+
StringPrototypeReplace,
9+
} = primordials;
10+
311
const {
412
getCLIOptionsValues,
513
getCLIOptionsInfo,
614
getEmbedderOptions: getEmbedderOptionsFromBinding,
15+
getEnvOptionsInputType,
716
} = internalBinding('options');
817

918
let warnOnAllowUnauthorized = true;
@@ -28,6 +37,45 @@ function getEmbedderOptions() {
2837
return embedderOptions ??= getEmbedderOptionsFromBinding();
2938
}
3039

40+
function generateConfigJsonSchema() {
41+
const map = getEnvOptionsInputType();
42+
43+
const schema = {
44+
__proto__: null,
45+
$schema: 'https://json-schema.org/draft/2020-12/schema',
46+
additionalProperties: false,
47+
properties: {
48+
__proto__: null,
49+
},
50+
type: 'object',
51+
};
52+
53+
for (const { 0: key, 1: type } of map) {
54+
const keyWithoutPrefix = StringPrototypeReplace(key, '--', '');
55+
if (type === 'array') {
56+
schema.properties[keyWithoutPrefix] = {
57+
__proto__: null,
58+
oneOf: [
59+
{ __proto__: null, type: 'string' },
60+
{ __proto__: null, type: 'array', items: { __proto__: null, type: 'string', minItems: 1 } },
61+
],
62+
};
63+
} else {
64+
schema.properties[keyWithoutPrefix] = { __proto__: null, type };
65+
}
66+
}
67+
68+
// Sort the proerties by key alphabetically.
69+
const sortedKeys = ArrayPrototypeSort(ObjectKeys(schema.properties));
70+
const sortedProperties = ObjectFromEntries(
71+
ArrayPrototypeMap(sortedKeys, (key) => [key, schema.properties[key]]),
72+
);
73+
74+
schema.properties = sortedProperties;
75+
76+
return schema;
77+
}
78+
3179
function refreshOptions() {
3280
optionsDict = undefined;
3381
}
@@ -55,5 +103,6 @@ module.exports = {
55103
getOptionValue,
56104
getAllowUnauthorized,
57105
getEmbedderOptions,
106+
generateConfigJsonSchema,
58107
refreshOptions,
59108
};

‎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+
initializeConfigFileSupport();
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 initializeConfigFileSupport() {
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
@@ -105,6 +105,7 @@
105105
'src/node_buffer.cc',
106106
'src/node_builtins.cc',
107107
'src/node_config.cc',
108+
'src/node_config_file.cc',
108109
'src/node_constants.cc',
109110
'src/node_contextify.cc',
110111
'src/node_credentials.cc',
@@ -230,6 +231,7 @@
230231
'src/node_blob.h',
231232
'src/node_buffer.h',
232233
'src/node_builtins.h',
234+
'src/node_config_file.h',
233235
'src/node_constants.h',
234236
'src/node_context_data.h',
235237
'src/node_contextify.h',

‎src/node.cc

+34
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// USE OR OTHER DEALINGS IN THE SOFTWARE.
2121

2222
#include "node.h"
23+
#include "node_config_file.h"
2324
#include "node_dotenv.h"
2425
#include "node_task_runner.h"
2526

@@ -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_config_file.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,36 @@ static ExitCode InitializeNodeWithArgsInternal(
884888
per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
885889
}
886890

891+
std::string node_options_from_config;
892+
if (auto path = per_process::config_reader.GetDataFromArgs(*argv)) {
893+
switch (per_process::config_reader.ParseConfig(*path)) {
894+
case ParseResult::Valid:
895+
break;
896+
case ParseResult::InvalidContent:
897+
errors->push_back(std::string(*path) + ": invalid content");
898+
break;
899+
case ParseResult::FileError:
900+
errors->push_back(std::string(*path) + ": not found");
901+
break;
902+
default:
903+
UNREACHABLE();
904+
}
905+
node_options_from_config = per_process::config_reader.AssignNodeOptions();
906+
// (@marco-ippolito) Avoid reparsing the env options again
907+
std::vector<std::string> env_argv_from_config =
908+
ParseNodeOptionsEnvVar(node_options_from_config, errors);
909+
910+
// Check the number of flags in NODE_OPTIONS from the config file
911+
// matches the parsed ones. This avoid users from sneaking in
912+
// additional flags.
913+
if (env_argv_from_config.size() !=
914+
per_process::config_reader.GetFlagsSize()) {
915+
errors->emplace_back("The number of NODE_OPTIONS doesn't match "
916+
"the number of flags in the config file");
917+
}
918+
node_options += node_options_from_config;
919+
}
920+
887921
#if !defined(NODE_WITHOUT_NODE_OPTIONS)
888922
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
889923
// NODE_OPTIONS environment variable is preferred over the file one.

‎src/node_config_file.cc

+195
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#include "node_config_file.h"
2+
#include "debug_utils-inl.h"
3+
#include "simdjson.h"
4+
5+
#include <string>
6+
7+
namespace node {
8+
9+
std::optional<std::string_view> ConfigReader::GetDataFromArgs(
10+
const std::vector<std::string>& args) {
11+
constexpr std::string_view flag = "--experimental-config-file";
12+
13+
for (auto it = args.begin(); it != args.end(); ++it) {
14+
if (*it == flag) {
15+
// Case: "--experimental-config-file foo"
16+
if (auto next = std::next(it); next != args.end()) {
17+
return *next;
18+
}
19+
} else if (it->starts_with(flag)) {
20+
// Case: "--experimental-config-file=foo"
21+
if (it->size() > flag.size() && (*it)[flag.size()] == '=') {
22+
return it->substr(flag.size() + 1);
23+
}
24+
}
25+
}
26+
27+
return std::nullopt;
28+
}
29+
30+
ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
31+
std::string file_content;
32+
// Read the configuration file
33+
int r = ReadFileSync(&file_content, config_path.data());
34+
if (r != 0) {
35+
const char* err = uv_strerror(r);
36+
FPrintF(
37+
stderr, "Cannot read configuration from %s: %s\n", config_path, err);
38+
return ParseResult::FileError;
39+
}
40+
41+
// Parse the configuration file
42+
simdjson::ondemand::parser json_parser;
43+
simdjson::ondemand::document document;
44+
if (json_parser.iterate(file_content).get(document)) {
45+
FPrintF(stderr, "Can't parse %s\n", config_path.data());
46+
return ParseResult::InvalidContent;
47+
}
48+
49+
simdjson::ondemand::object main_object;
50+
// If document is not an object, throw an error.
51+
if (auto root_error = document.get_object().get(main_object)) {
52+
if (root_error == simdjson::error_code::INCORRECT_TYPE) {
53+
FPrintF(stderr,
54+
"Root value unexpected not an object for %s\n\n",
55+
config_path.data());
56+
} else {
57+
FPrintF(stderr, "Can't parse %s\n", config_path.data());
58+
}
59+
return ParseResult::InvalidContent;
60+
}
61+
62+
auto env_options_map = options_parser::MapEnvOptionsFlagInputType();
63+
simdjson::ondemand::value ondemand_value;
64+
std::string_view key;
65+
66+
for (auto field : main_object) {
67+
if (field.unescaped_key().get(key) || field.value().get(ondemand_value)) {
68+
return ParseResult::InvalidContent;
69+
}
70+
71+
// The key needs to match the CLI option
72+
std::string prefix = "--";
73+
auto it = env_options_map.find(prefix.append(key));
74+
if (it != env_options_map.end()) {
75+
switch (it->second) {
76+
case options_parser::OptionType::kBoolean: {
77+
bool result;
78+
if (ondemand_value.get_bool().get(result)) {
79+
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
80+
return ParseResult::InvalidContent;
81+
}
82+
flags_.push_back(it->first + "=" + (result ? "true" : "false"));
83+
break;
84+
}
85+
// String array can allow both string and array types
86+
case options_parser::OptionType::kStringList: {
87+
simdjson::ondemand::json_type field_type;
88+
if (ondemand_value.type().get(field_type)) {
89+
return ParseResult::InvalidContent;
90+
}
91+
switch (field_type) {
92+
case simdjson::ondemand::json_type::array: {
93+
std::vector<std::string> result;
94+
simdjson::ondemand::array raw_imports;
95+
if (ondemand_value.get_array().get(raw_imports)) {
96+
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
97+
return ParseResult::InvalidContent;
98+
}
99+
for (auto raw_import : raw_imports) {
100+
std::string_view import;
101+
if (raw_import.get_string(import)) {
102+
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
103+
return ParseResult::InvalidContent;
104+
}
105+
flags_.push_back(it->first + "=" + std::string(import));
106+
}
107+
break;
108+
}
109+
case simdjson::ondemand::json_type::string: {
110+
std::string result;
111+
if (ondemand_value.get_string(result)) {
112+
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
113+
return ParseResult::InvalidContent;
114+
}
115+
flags_.push_back(it->first + "=" + result);
116+
break;
117+
}
118+
default:
119+
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
120+
return ParseResult::InvalidContent;
121+
}
122+
break;
123+
}
124+
case options_parser::OptionType::kString: {
125+
std::string result;
126+
if (ondemand_value.get_string(result)) {
127+
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
128+
return ParseResult::InvalidContent;
129+
}
130+
flags_.push_back(it->first + "=" + result);
131+
break;
132+
}
133+
case options_parser::OptionType::kInteger: {
134+
int64_t result;
135+
if (ondemand_value.get_int64().get(result)) {
136+
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
137+
return ParseResult::InvalidContent;
138+
}
139+
flags_.push_back(it->first + "=" + std::to_string(result));
140+
break;
141+
}
142+
case options_parser::OptionType::kHostPort:
143+
case options_parser::OptionType::kUInteger: {
144+
uint64_t result;
145+
if (ondemand_value.get_uint64().get(result)) {
146+
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
147+
return ParseResult::InvalidContent;
148+
}
149+
flags_.push_back(it->first + "=" + std::to_string(result));
150+
break;
151+
}
152+
case options_parser::OptionType::kNoOp: {
153+
FPrintF(stderr,
154+
"No-op flag %s is currently not supported\n",
155+
it->first.c_str());
156+
return ParseResult::InvalidContent;
157+
break;
158+
}
159+
case options_parser::OptionType::kV8Option: {
160+
FPrintF(stderr,
161+
"V8 flag %s is currently not supported\n",
162+
it->first.c_str());
163+
return ParseResult::InvalidContent;
164+
}
165+
default:
166+
UNREACHABLE();
167+
}
168+
} else {
169+
FPrintF(stderr, "Unknown or not allowed option %s\n", key.data());
170+
return ParseResult::InvalidContent;
171+
}
172+
}
173+
174+
return ParseResult::Valid;
175+
}
176+
177+
std::string ConfigReader::AssignNodeOptions() {
178+
if (flags_.empty()) {
179+
return "";
180+
} else {
181+
DCHECK_GT(flags_.size(), 0);
182+
std::string acc;
183+
acc.reserve(flags_.size() * 2);
184+
for (size_t i = 0; i < flags_.size(); ++i) {
185+
// The space is necessary at the beginning of the string
186+
acc += " " + flags_[i];
187+
}
188+
return acc;
189+
}
190+
}
191+
192+
size_t ConfigReader::GetFlagsSize() {
193+
return flags_.size();
194+
}
195+
} // namespace node

‎src/node_config_file.h

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#ifndef SRC_NODE_CONFIG_FILE_H_
2+
#define SRC_NODE_CONFIG_FILE_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+
// When trying to parse the configuration file, we can have three possible
15+
// results:
16+
// - Valid: The file was successfully parsed and the content is valid.
17+
// - FileError: There was an error reading the file.
18+
// - InvalidContent: The file was read, but the content is invalid.
19+
enum ParseResult { Valid, FileError, InvalidContent };
20+
21+
// ConfigReader is the class that parses the configuration JSON file.
22+
// It reads the file provided by --experimental-config-file and
23+
// extracts the flags.
24+
class ConfigReader {
25+
public:
26+
ParseResult ParseConfig(const std::string_view& config_path);
27+
28+
std::optional<std::string_view> GetDataFromArgs(
29+
const std::vector<std::string>& args);
30+
31+
std::string AssignNodeOptions();
32+
33+
size_t GetFlagsSize();
34+
35+
private:
36+
std::vector<std::string> flags_;
37+
};
38+
39+
} // namespace node
40+
41+
#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS
42+
43+
#endif // SRC_NODE_CONFIG_FILE_H_

‎src/node_options.cc

+95-1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ using v8::Name;
2929
using v8::Null;
3030
using v8::Number;
3131
using v8::Object;
32+
using v8::String;
3233
using v8::Undefined;
3334
using v8::Value;
3435
namespace node {
@@ -681,6 +682,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
681682
"set environment variables from supplied file",
682683
&EnvironmentOptions::optional_env_file);
683684
Implies("--env-file-if-exists", "[has_env_file_string]");
685+
AddOption("--experimental-config-file",
686+
"set config file from supplied file",
687+
&EnvironmentOptions::experimental_config_file);
684688
AddOption("--test",
685689
"launch test runner on startup",
686690
&EnvironmentOptions::test_runner);
@@ -1299,6 +1303,19 @@ std::string GetBashCompletion() {
12991303
return out.str();
13001304
}
13011305

1306+
std::unordered_map<std::string, options_parser::OptionType>
1307+
MapEnvOptionsFlagInputType() {
1308+
std::unordered_map<std::string, options_parser::OptionType> type_map;
1309+
const auto& parser = _ppop_instance;
1310+
for (const auto& item : parser.options_) {
1311+
if (!item.first.empty() && !item.first.starts_with('[') &&
1312+
item.second.env_setting == kAllowedInEnvvar) {
1313+
type_map[item.first] = item.second.type;
1314+
}
1315+
}
1316+
return type_map;
1317+
}
1318+
13021319
struct IterateCLIOptionsScope {
13031320
explicit IterateCLIOptionsScope(Environment* env) {
13041321
// Temporarily act as if the current Environment's/IsolateData's options
@@ -1542,6 +1559,81 @@ void GetEmbedderOptions(const FunctionCallbackInfo<Value>& args) {
15421559
args.GetReturnValue().Set(ret);
15431560
}
15441561

1562+
// This function returns a map containing all the options available
1563+
// as NODE_OPTIONS and their input type
1564+
// Example --experimental-transform types: kBoolean
1565+
// This is used to determine the type of the input for each option
1566+
// to generate the config file json schema
1567+
void GetEnvOptionsInputType(const FunctionCallbackInfo<Value>& args) {
1568+
Isolate* isolate = args.GetIsolate();
1569+
Local<Context> context = isolate->GetCurrentContext();
1570+
Environment* env = Environment::GetCurrent(context);
1571+
1572+
if (!env->has_run_bootstrapping_code()) {
1573+
// No code because this is an assertion.
1574+
return env->ThrowError(
1575+
"Should not query options before bootstrapping is done");
1576+
}
1577+
1578+
Mutex::ScopedLock lock(per_process::cli_options_mutex);
1579+
1580+
Local<Map> flags_map = Map::New(isolate);
1581+
1582+
for (const auto& item : _ppop_instance.options_) {
1583+
if (!item.first.empty() && !item.first.starts_with('[') &&
1584+
item.second.env_setting == kAllowedInEnvvar) {
1585+
std::string type;
1586+
switch (static_cast<int>(item.second.type)) {
1587+
case 0: // No-op
1588+
case 1: // V8 flags
1589+
break; // V8 and NoOp flags are not supported
1590+
1591+
case 2:
1592+
type = "boolean";
1593+
break;
1594+
case 3: // integer
1595+
case 4: // unsigned integer
1596+
case 6: // host port
1597+
type = "number";
1598+
break;
1599+
case 5: // string
1600+
type = "string";
1601+
break;
1602+
case 7: // string array
1603+
type = "array";
1604+
break;
1605+
default:
1606+
UNREACHABLE();
1607+
}
1608+
1609+
if (type.empty()) {
1610+
continue;
1611+
}
1612+
1613+
Local<String> value;
1614+
if (!String::NewFromUtf8(
1615+
isolate, type.data(), v8::NewStringType::kNormal, type.size())
1616+
.ToLocal(&value)) {
1617+
continue;
1618+
}
1619+
1620+
Local<String> field;
1621+
if (!String::NewFromUtf8(isolate,
1622+
item.first.data(),
1623+
v8::NewStringType::kNormal,
1624+
item.first.size())
1625+
.ToLocal(&field)) {
1626+
continue;
1627+
}
1628+
1629+
if (flags_map->Set(context, field, value).IsEmpty()) {
1630+
return;
1631+
}
1632+
}
1633+
}
1634+
args.GetReturnValue().Set(flags_map);
1635+
}
1636+
15451637
void Initialize(Local<Object> target,
15461638
Local<Value> unused,
15471639
Local<Context> context,
@@ -1554,7 +1646,8 @@ void Initialize(Local<Object> target,
15541646
context, target, "getCLIOptionsInfo", GetCLIOptionsInfo);
15551647
SetMethodNoSideEffect(
15561648
context, target, "getEmbedderOptions", GetEmbedderOptions);
1557-
1649+
SetMethodNoSideEffect(
1650+
context, target, "getEnvOptionsInputType", GetEnvOptionsInputType);
15581651
Local<Object> env_settings = Object::New(isolate);
15591652
NODE_DEFINE_CONSTANT(env_settings, kAllowedInEnvvar);
15601653
NODE_DEFINE_CONSTANT(env_settings, kDisallowedInEnvvar);
@@ -1580,6 +1673,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
15801673
registry->Register(GetCLIOptionsValues);
15811674
registry->Register(GetCLIOptionsInfo);
15821675
registry->Register(GetEmbedderOptions);
1676+
registry->Register(GetEnvOptionsInputType);
15831677
}
15841678
} // namespace options_parser
15851679

‎src/node_options.h

+6
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ class EnvironmentOptions : public Options {
258258

259259
bool report_exclude_env = false;
260260
bool report_exclude_network = false;
261+
std::string experimental_config_file;
261262

262263
inline DebugOptions* get_debug_options() { return &debug_options_; }
263264
inline const DebugOptions& debug_options() const { return debug_options_; }
@@ -390,6 +391,7 @@ enum OptionType {
390391
kHostPort,
391392
kStringList,
392393
};
394+
std::unordered_map<std::string, OptionType> MapEnvOptionsFlagInputType();
393395

394396
template <typename Options>
395397
class OptionsParser {
@@ -570,6 +572,10 @@ class OptionsParser {
570572
friend void GetCLIOptionsInfo(
571573
const v8::FunctionCallbackInfo<v8::Value>& args);
572574
friend std::string GetBashCompletion();
575+
friend std::unordered_map<std::string, OptionType>
576+
MapEnvOptionsFlagInputType();
577+
friend void GetEnvOptionsInputType(
578+
const v8::FunctionCallbackInfo<v8::Value>& args);
573579
};
574580

575581
using StringVector = std::vector<std::string>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
NODE_OPTIONS="--no-experimental-strip-types"

‎test/fixtures/rc/empty-object.json

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

‎test/fixtures/rc/empty.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

‎test/fixtures/rc/host-port.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"inspect-port": 65535
3+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"import": "./test/fixtures/printA.js"
3+
}

‎test/fixtures/rc/import.json

+7
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+
}

‎test/fixtures/rc/invalid-import.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"import": [1]
3+
}
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"max-http-header-size": -1
3+
}

‎test/fixtures/rc/no-op.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"http-parser": true
3+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"--test": true
3+
}

‎test/fixtures/rc/numeric.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"max-http-header-size": 4294967295
3+
}
+4
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/sneaky-flag.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"import": "./test/fixtures/printA.js --experimental-transform-types"
3+
}

‎test/fixtures/rc/string.json

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

‎test/fixtures/rc/test.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
const { test } = require('node:test');
2+
const { ok } = require('node:assert');
3+
4+
test('should pass', () => {
5+
ok(true);
6+
});

‎test/fixtures/rc/transform-types.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"experimental-transform-types": true
3+
}

‎test/fixtures/rc/unknown-flag.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"some-unknown-flag": true
3+
}

‎test/fixtures/rc/v8-flag.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"abort-on-uncaught-exception": true
3+
}

‎test/parallel/test-config-file.js

+256
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
'use strict';
2+
3+
const { spawnPromisified } = require('../common');
4+
const fixtures = require('../common/fixtures');
5+
const { match, strictEqual } = require('node:assert');
6+
const { test } = require('node:test');
7+
8+
test('should handle non existing json', async () => {
9+
const result = await spawnPromisified(process.execPath, [
10+
'--experimental-config-file',
11+
'i-do-not-exist.json',
12+
'-p', '"Hello, World!"',
13+
]);
14+
match(result.stderr, /Cannot read configuration from i-do-not-exist\.json: no such file or directory/);
15+
match(result.stderr, /i-do-not-exist\.json: not found/);
16+
strictEqual(result.stdout, '');
17+
strictEqual(result.code, 9);
18+
});
19+
20+
test('should handle empty json', async () => {
21+
const result = await spawnPromisified(process.execPath, [
22+
'--experimental-config-file',
23+
fixtures.path('rc/empty.json'),
24+
'-p', '"Hello, World!"',
25+
]);
26+
match(result.stderr, /Can't parse/);
27+
match(result.stderr, /empty\.json: invalid content/);
28+
strictEqual(result.stdout, '');
29+
strictEqual(result.code, 9);
30+
});
31+
32+
test('should handle empty object json', async () => {
33+
const result = await spawnPromisified(process.execPath, [
34+
'--no-warnings',
35+
'--experimental-config-file',
36+
fixtures.path('rc/empty-object.json'),
37+
'-p', '"Hello, World!"',
38+
]);
39+
strictEqual(result.stderr, '');
40+
match(result.stdout, /Hello, World!/);
41+
strictEqual(result.code, 0);
42+
});
43+
44+
test('should parse boolean flag', async () => {
45+
const result = await spawnPromisified(process.execPath, [
46+
'--experimental-config-file',
47+
fixtures.path('rc/transform-types.json'),
48+
fixtures.path('typescript/ts/transformation/test-enum.ts'),
49+
]);
50+
match(result.stderr, /--experimental-config-file is an experimental feature and might change at any time/);
51+
match(result.stdout, /Hello, TypeScript!/);
52+
strictEqual(result.code, 0);
53+
});
54+
55+
test('should not override a flag declared twice', async () => {
56+
const result = await spawnPromisified(process.execPath, [
57+
'--no-warnings',
58+
'--experimental-config-file',
59+
fixtures.path('rc/override-property.json'),
60+
fixtures.path('typescript/ts/transformation/test-enum.ts'),
61+
]);
62+
strictEqual(result.stderr, '');
63+
strictEqual(result.stdout, 'Hello, TypeScript!\n');
64+
strictEqual(result.code, 0);
65+
});
66+
67+
test('should override env-file', async () => {
68+
const result = await spawnPromisified(process.execPath, [
69+
'--no-warnings',
70+
'--experimental-config-file',
71+
fixtures.path('rc/transform-types.json'),
72+
'--env-file', fixtures.path('dotenv/node-options-no-tranform.env'),
73+
fixtures.path('typescript/ts/transformation/test-enum.ts'),
74+
]);
75+
strictEqual(result.stderr, '');
76+
match(result.stdout, /Hello, TypeScript!/);
77+
strictEqual(result.code, 0);
78+
});
79+
80+
test('should not override NODE_OPTIONS', async () => {
81+
const result = await spawnPromisified(process.execPath, [
82+
'--no-warnings',
83+
'--experimental-config-file',
84+
fixtures.path('rc/transform-types.json'),
85+
fixtures.path('typescript/ts/transformation/test-enum.ts'),
86+
], {
87+
env: {
88+
...process.env,
89+
NODE_OPTIONS: '--no-experimental-transform-types',
90+
},
91+
});
92+
match(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/);
93+
strictEqual(result.stdout, '');
94+
strictEqual(result.code, 1);
95+
});
96+
97+
test('should not ovverride CLI flags', async () => {
98+
const result = await spawnPromisified(process.execPath, [
99+
'--no-warnings',
100+
'--no-experimental-transform-types',
101+
'--experimental-config-file',
102+
fixtures.path('rc/transform-types.json'),
103+
fixtures.path('typescript/ts/transformation/test-enum.ts'),
104+
]);
105+
match(result.stderr, /ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX/);
106+
strictEqual(result.stdout, '');
107+
strictEqual(result.code, 1);
108+
});
109+
110+
test('should parse array flag correctly', async () => {
111+
const result = await spawnPromisified(process.execPath, [
112+
'--no-warnings',
113+
'--experimental-config-file',
114+
fixtures.path('rc/import.json'),
115+
'--eval', 'setTimeout(() => console.log("D"),99)',
116+
]);
117+
strictEqual(result.stderr, '');
118+
strictEqual(result.stdout, 'A\nB\nC\nD\n');
119+
strictEqual(result.code, 0);
120+
});
121+
122+
test('should validate invalid array flag', async () => {
123+
const result = await spawnPromisified(process.execPath, [
124+
'--no-warnings',
125+
'--experimental-config-file',
126+
fixtures.path('rc/invalid-import.json'),
127+
'--eval', 'setTimeout(() => console.log("D"),99)',
128+
]);
129+
match(result.stderr, /invalid-import\.json: invalid content/);
130+
strictEqual(result.stdout, '');
131+
strictEqual(result.code, 9);
132+
});
133+
134+
test('should validate array flag as string', async () => {
135+
const result = await spawnPromisified(process.execPath, [
136+
'--no-warnings',
137+
'--experimental-config-file',
138+
fixtures.path('rc/import-as-string.json'),
139+
'--eval', 'setTimeout(() => console.log("B"),99)',
140+
]);
141+
strictEqual(result.stderr, '');
142+
strictEqual(result.stdout, 'A\nB\n');
143+
strictEqual(result.code, 0);
144+
});
145+
146+
test('should throw at unknown flag', async () => {
147+
const result = await spawnPromisified(process.execPath, [
148+
'--no-warnings',
149+
'--experimental-config-file',
150+
fixtures.path('rc/unknown-flag.json'),
151+
'-p', '"Hello, World!"',
152+
]);
153+
match(result.stderr, /Unknown or not allowed option some-unknown-flag/);
154+
strictEqual(result.stdout, '');
155+
strictEqual(result.code, 9);
156+
});
157+
158+
test('should throw at flag not available in NODE_OPTIONS', async () => {
159+
const result = await spawnPromisified(process.execPath, [
160+
'--no-warnings',
161+
'--experimental-config-file',
162+
fixtures.path('rc/not-node-options-flag.json'),
163+
'-p', '"Hello, World!"',
164+
]);
165+
match(result.stderr, /Unknown or not allowed option --test/);
166+
strictEqual(result.stdout, '');
167+
strictEqual(result.code, 9);
168+
});
169+
170+
test('unsigned flag should be parsed correctly', async () => {
171+
const result = await spawnPromisified(process.execPath, [
172+
'--no-warnings',
173+
'--experimental-config-file',
174+
fixtures.path('rc/numeric.json'),
175+
'-p', 'http.maxHeaderSize',
176+
]);
177+
strictEqual(result.stderr, '');
178+
strictEqual(result.stdout, '4294967295\n');
179+
strictEqual(result.code, 0);
180+
});
181+
182+
test('numeric flag should not allow negative values', async () => {
183+
const result = await spawnPromisified(process.execPath, [
184+
'--no-warnings',
185+
'--experimental-config-file',
186+
fixtures.path('rc/negative-numeric.json'),
187+
'-p', 'http.maxHeaderSize',
188+
]);
189+
match(result.stderr, /Invalid value for --max-http-header-size/);
190+
match(result.stderr, /negative-numeric\.json: invalid content/);
191+
strictEqual(result.stdout, '');
192+
strictEqual(result.code, 9);
193+
});
194+
195+
test('v8 flag should not be allowed in config file', async () => {
196+
const result = await spawnPromisified(process.execPath, [
197+
'--no-warnings',
198+
'--experimental-config-file',
199+
fixtures.path('rc/v8-flag.json'),
200+
'-p', '"Hello, World!"',
201+
]);
202+
match(result.stderr, /V8 flag --abort-on-uncaught-exception is currently not supported/);
203+
strictEqual(result.stdout, '');
204+
strictEqual(result.code, 9);
205+
});
206+
207+
test('string flag should be parsed correctly', async () => {
208+
const result = await spawnPromisified(process.execPath, [
209+
'--no-warnings',
210+
'--test',
211+
'--experimental-config-file',
212+
fixtures.path('rc/string.json'),
213+
fixtures.path('rc/test.js'),
214+
]);
215+
strictEqual(result.stderr, '');
216+
strictEqual(result.stdout, '.\n');
217+
strictEqual(result.code, 0);
218+
});
219+
220+
test('host port flag should be parsed correctly', { skip: !process.features.inspector }, async () => {
221+
const result = await spawnPromisified(process.execPath, [
222+
'--no-warnings',
223+
'--expose-internals',
224+
'--experimental-config-file',
225+
fixtures.path('rc/host-port.json'),
226+
'-p', 'require("internal/options").getOptionValue("--inspect-port").port',
227+
]);
228+
strictEqual(result.stderr, '');
229+
strictEqual(result.stdout, '65535\n');
230+
strictEqual(result.code, 0);
231+
});
232+
233+
test('no op flag should throw', async () => {
234+
const result = await spawnPromisified(process.execPath, [
235+
'--no-warnings',
236+
'--experimental-config-file',
237+
fixtures.path('rc/no-op.json'),
238+
'-p', '"Hello, World!"',
239+
]);
240+
match(result.stderr, /No-op flag --http-parser is currently not supported/);
241+
match(result.stderr, /no-op\.json: invalid content/);
242+
strictEqual(result.stdout, '');
243+
strictEqual(result.code, 9);
244+
});
245+
246+
test('should not allow users to sneak in a flag', async () => {
247+
const result = await spawnPromisified(process.execPath, [
248+
'--no-warnings',
249+
'--experimental-config-file',
250+
fixtures.path('rc/sneaky-flag.json'),
251+
'-p', '"Hello, World!"',
252+
]);
253+
match(result.stderr, /The number of NODE_OPTIONS doesn't match the number of flags in the config file/);
254+
strictEqual(result.stdout, '');
255+
strictEqual(result.code, 9);
256+
});
+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Flags: --no-warnings --expose-internals
2+
3+
'use strict';
4+
5+
const common = require('../common');
6+
7+
common.skipIfInspectorDisabled();
8+
9+
if (!common.hasCrypto) {
10+
common.skip('missing crypto');
11+
}
12+
13+
const { hasOpenSSL3 } = require('../common/crypto');
14+
15+
if (!hasOpenSSL3) {
16+
common.skip('this test requires OpenSSL 3.x');
17+
}
18+
19+
if (!common.hasIntl) {
20+
// A handful of the tests fail when ICU is not included.
21+
common.skip('missing Intl');
22+
}
23+
24+
const {
25+
generateConfigJsonSchema,
26+
} = require('internal/options');
27+
const schemaInDoc = require('../../doc/node_config_json_schema.json');
28+
const assert = require('assert');
29+
30+
const schema = generateConfigJsonSchema();
31+
32+
// This assertion ensures that whenever we add a new env option, we also add it
33+
// to the JSON schema. The function getEnvOptionsInputType() returns all the available
34+
// env options, so we can generate the JSON schema from it and compare it to the
35+
// current JSON schema.
36+
// To regenerate the JSON schema, run:
37+
// out/Release/node --expose-internals tools/doc/generate-json-schema.mjs
38+
// And then run make doc to update the out/doc/node_config_json_schema.json file.
39+
assert.strictEqual(JSON.stringify(schema), JSON.stringify(schemaInDoc), 'JSON schema is outdated.' +
40+
'Run `out/Release/node --expose-internals tools/doc/generate-json-schema.mjs` to update it.');

‎tools/doc/generate-json-schema.mjs

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// Flags: --expose-internals
2+
3+
import internal from 'internal/options';
4+
import { writeFileSync } from 'fs';
5+
6+
const schema = internal.generateConfigJsonSchema();
7+
writeFileSync('doc/node_config_json_schema.json', `${JSON.stringify(schema, null, 2)}\n`);

0 commit comments

Comments
 (0)
Please sign in to comment.