src: support namespace options in configuration file

PR-URL: https://github.com/nodejs/node/pull/58073
Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com>
Reviewed-By: Giovanni Bucci <github@puskin.it>
Reviewed-By: Daniel Lemire <daniel@lemire.me>
This commit is contained in:
Pietro Marchini 2025-06-06 14:47:05 +02:00 committed by GitHub
parent 708fd1945b
commit c1f090dc76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1040 additions and 297 deletions

View File

@ -933,11 +933,19 @@ in the `$schema` must be replaced with the version of Node.js you are using.
],
"watch-path": "src",
"watch-preserve-output": true
},
"testRunner": {
"test-isolation": "process"
}
}
```
In the `nodeOptions` field, only flags that are allowed in [`NODE_OPTIONS`][] are supported.
The configuration file supports namespace-specific options:
* The `nodeOptions` field contains CLI flags that are allowed in [`NODE_OPTIONS`][].
* Namespace fields like `testRunner` contain configuration specific to that subsystem.
No-op flags are not supported.
Not all V8 flags are currently supported.
@ -951,7 +959,7 @@ For example, the configuration file above is equivalent to
the following command-line arguments:
```bash
node --import amaro/strip --watch-path=src --watch-preserve-output
node --import amaro/strip --watch-path=src --watch-preserve-output --test-isolation=process
```
The priority in configuration is as follows:
@ -964,11 +972,10 @@ Values in the configuration file will not override the values in the environment
variables and command-line options, but will override the values in the `NODE_OPTIONS`
env file parsed by the `--env-file` flag.
If duplicate keys are present in the configuration file, only
the first key will be used.
Keys cannot be duplicated within the same or different namespaces.
The configuration parser will throw an error if the configuration file contains
unknown keys or keys that cannot used in `NODE_OPTIONS`.
unknown keys or keys that cannot be used in a namespace.
Node.js will not sanitize or perform validation on the user-provided configuration,
so **NEVER** use untrusted configuration files.

View File

@ -584,6 +584,135 @@
}
},
"type": "object"
},
"testRunner": {
"type": "object",
"additionalProperties": false,
"properties": {
"experimental-test-coverage": {
"type": "boolean"
},
"experimental-test-module-mocks": {
"type": "boolean"
},
"test-concurrency": {
"type": "number"
},
"test-coverage-branches": {
"type": "number"
},
"test-coverage-exclude": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-coverage-functions": {
"type": "number"
},
"test-coverage-include": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-coverage-lines": {
"type": "number"
},
"test-force-exit": {
"type": "boolean"
},
"test-global-setup": {
"type": "string"
},
"test-isolation": {
"type": "string"
},
"test-name-pattern": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-only": {
"type": "boolean"
},
"test-reporter": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-reporter-destination": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-shard": {
"type": "string"
},
"test-skip-pattern": {
"oneOf": [
{
"type": "string"
},
{
"items": {
"type": "string",
"minItems": 1
},
"type": "array"
}
]
},
"test-timeout": {
"type": "number"
},
"test-update-snapshots": {
"type": "boolean"
}
}
}
},
"type": "object"

View File

@ -13,6 +13,7 @@ const {
getCLIOptionsInfo,
getEmbedderOptions: getEmbedderOptionsFromBinding,
getEnvOptionsInputType,
getNamespaceOptionsInputType,
} = internalBinding('options');
let warnOnAllowUnauthorized = true;
@ -38,7 +39,22 @@ function getEmbedderOptions() {
}
function generateConfigJsonSchema() {
const map = getEnvOptionsInputType();
const envOptionsMap = getEnvOptionsInputType();
const namespaceOptionsMap = getNamespaceOptionsInputType();
function createPropertyForType(type) {
if (type === 'array') {
return {
__proto__: null,
oneOf: [
{ __proto__: null, type: 'string' },
{ __proto__: null, items: { __proto__: null, type: 'string', minItems: 1 }, type: 'array' },
],
};
}
return { __proto__: null, type };
}
const schema = {
__proto__: null,
@ -60,24 +76,43 @@ function generateConfigJsonSchema() {
type: 'object',
};
const nodeOptions = schema.properties.nodeOptions.properties;
// Get the root properties object for adding namespaces
const rootProperties = schema.properties;
const nodeOptions = rootProperties.nodeOptions.properties;
for (const { 0: key, 1: type } of map) {
// Add env options to nodeOptions (backward compatibility)
for (const { 0: key, 1: type } of envOptionsMap) {
const keyWithoutPrefix = StringPrototypeReplace(key, '--', '');
if (type === 'array') {
nodeOptions[keyWithoutPrefix] = {
__proto__: null,
oneOf: [
{ __proto__: null, type: 'string' },
{ __proto__: null, items: { __proto__: null, type: 'string', minItems: 1 }, type: 'array' },
],
};
} else {
nodeOptions[keyWithoutPrefix] = { __proto__: null, type };
}
nodeOptions[keyWithoutPrefix] = createPropertyForType(type);
}
// Sort the proerties by key alphabetically.
// Add namespace properties at the root level
for (const { 0: namespace, 1: optionsMap } of namespaceOptionsMap) {
// Create namespace object at the root level
rootProperties[namespace] = {
__proto__: null,
type: 'object',
additionalProperties: false,
properties: { __proto__: null },
};
const namespaceProperties = rootProperties[namespace].properties;
// Add all options for this namespace
for (const { 0: optionName, 1: optionType } of optionsMap) {
const keyWithoutPrefix = StringPrototypeReplace(optionName, '--', '');
namespaceProperties[keyWithoutPrefix] = createPropertyForType(optionType);
}
// Sort the namespace properties alphabetically
const sortedNamespaceKeys = ArrayPrototypeSort(ObjectKeys(namespaceProperties));
const sortedNamespaceProperties = ObjectFromEntries(
ArrayPrototypeMap(sortedNamespaceKeys, (key) => [key, namespaceProperties[key]]),
);
rootProperties[namespace].properties = sortedNamespaceProperties;
}
// Sort the top-level properties by key alphabetically
const sortedKeys = ArrayPrototypeSort(ObjectKeys(nodeOptions));
const sortedProperties = ObjectFromEntries(
ArrayPrototypeMap(sortedKeys, (key) => [key, nodeOptions[key]]),
@ -85,6 +120,14 @@ function generateConfigJsonSchema() {
schema.properties.nodeOptions.properties = sortedProperties;
// Also sort the root level properties
const sortedRootKeys = ArrayPrototypeSort(ObjectKeys(rootProperties));
const sortedRootProperties = ObjectFromEntries(
ArrayPrototypeMap(sortedRootKeys, (key) => [key, rootProperties[key]]),
);
schema.properties = sortedRootProperties;
return schema;
}

View File

@ -98,7 +98,13 @@ let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
});
const kIsolatedProcessName = Symbol('kIsolatedProcessName');
const kFilterArgs = ['--test', '--experimental-test-coverage', '--watch'];
const kFilterArgs = [
'--test',
'--experimental-test-coverage',
'--watch',
'--experimental-default-config-file',
'--experimental-config-file',
];
const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'];
const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];

View File

@ -910,7 +910,7 @@ static ExitCode InitializeNodeWithArgsInternal(
default:
UNREACHABLE();
}
node_options_from_config = per_process::config_reader.AssignNodeOptions();
node_options_from_config = per_process::config_reader.GetNodeOptions();
// (@marco-ippolito) Avoid reparsing the env options again
std::vector<std::string> env_argv_from_config =
ParseNodeOptionsEnvVar(node_options_from_config, errors);
@ -956,7 +956,19 @@ static ExitCode InitializeNodeWithArgsInternal(
#endif
if (!(flags & ProcessInitializationFlags::kDisableCLIOptions)) {
const ExitCode exit_code =
// Parse the options coming from the config file.
// This is done before parsing the command line options
// as the cli flags are expected to override the config file ones.
std::vector<std::string> extra_argv =
per_process::config_reader.GetNamespaceFlags();
// [0] is expected to be the program name, fill it in from the real argv.
extra_argv.insert(extra_argv.begin(), argv->at(0));
// Parse the extra argv coming from the config file
ExitCode exit_code = ProcessGlobalArgsInternal(
&extra_argv, nullptr, errors, kDisallowedInEnvvar);
if (exit_code != ExitCode::kNoFailure) return exit_code;
// Parse options coming from the command line.
exit_code =
ProcessGlobalArgsInternal(argv, exec_argv, errors, kDisallowedInEnvvar);
if (exit_code != ExitCode::kNoFailure) return exit_code;
}

View File

@ -37,121 +37,171 @@ std::optional<std::string_view> ConfigReader::GetDataFromArgs(
return std::nullopt;
}
ParseResult ConfigReader::ParseNodeOptions(
simdjson::ondemand::object* node_options_object) {
auto env_options_map = options_parser::MapEnvOptionsFlagInputType();
simdjson::ondemand::value ondemand_value;
std::string_view key;
ParseResult ConfigReader::ProcessOptionValue(
const std::pair<std::string, options_parser::OptionType>& option_info,
simdjson::ondemand::value* option_value,
std::vector<std::string>* output) {
const std::string& option_name = option_info.first;
const options_parser::OptionType option_type = option_info.second;
for (auto field : *node_options_object) {
if (field.unescaped_key().get(key) || field.value().get(ondemand_value)) {
switch (option_type) {
case options_parser::OptionType::kBoolean: {
bool result;
if (option_value->get_bool().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
return ParseResult::InvalidContent;
}
if (result) {
// If the value is true, we need to set the flag
output->push_back(option_name);
}
break;
}
// String array can allow both string and array types
case options_parser::OptionType::kStringList: {
simdjson::ondemand::json_type field_type;
if (option_value->type().get(field_type)) {
return ParseResult::InvalidContent;
}
switch (field_type) {
case simdjson::ondemand::json_type::array: {
std::vector<std::string> result;
simdjson::ondemand::array raw_imports;
if (option_value->get_array().get(raw_imports)) {
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
return ParseResult::InvalidContent;
}
for (auto raw_import : raw_imports) {
std::string_view import;
if (raw_import.get_string(import)) {
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + std::string(import));
}
break;
}
case simdjson::ondemand::json_type::string: {
std::string result;
if (option_value->get_string(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + result);
break;
}
default:
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
return ParseResult::InvalidContent;
}
break;
}
case options_parser::OptionType::kString: {
std::string result;
if (option_value->get_string(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + result);
break;
}
case options_parser::OptionType::kInteger: {
int64_t result;
if (option_value->get_int64().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + std::to_string(result));
break;
}
case options_parser::OptionType::kHostPort:
case options_parser::OptionType::kUInteger: {
uint64_t result;
if (option_value->get_uint64().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", option_name.c_str());
return ParseResult::InvalidContent;
}
output->push_back(option_name + "=" + std::to_string(result));
break;
}
case options_parser::OptionType::kNoOp: {
FPrintF(stderr,
"No-op flag %s is currently not supported\n",
option_name.c_str());
return ParseResult::InvalidContent;
break;
}
case options_parser::OptionType::kV8Option: {
FPrintF(stderr,
"V8 flag %s is currently not supported\n",
option_name.c_str());
return ParseResult::InvalidContent;
}
default:
UNREACHABLE();
}
return ParseResult::Valid;
}
ParseResult ConfigReader::ParseOptions(
simdjson::ondemand::object* options_object,
std::unordered_set<std::string>* unique_options,
const std::string& namespace_name) {
// Determine which options map to use and output vector
std::unordered_map<std::string, options_parser::OptionType> options_map;
std::vector<std::string>* output_vector;
if (namespace_name == "nodeOptions") {
// Special case for backward compatibility: handle nodeOptions with env
// options map
options_map = options_parser::MapEnvOptionsFlagInputType();
output_vector = &node_options_;
} else {
// Handle other namespaces
options_map = options_parser::MapOptionsByNamespace(namespace_name);
output_vector = &namespace_options_;
if (!env_options_initialized_) {
env_options_map_ = options_parser::MapEnvOptionsFlagInputType();
env_options_initialized_ = true;
}
}
simdjson::ondemand::value option_value;
std::string_view option_key;
for (auto field : *options_object) {
if (field.unescaped_key().get(option_key) ||
field.value().get(option_value)) {
return ParseResult::InvalidContent;
}
// The key needs to match the CLI option
std::string prefix = "--";
auto it = env_options_map.find(prefix.append(key));
if (it != env_options_map.end()) {
switch (it->second) {
case options_parser::OptionType::kBoolean: {
bool result;
if (ondemand_value.get_bool().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
return ParseResult::InvalidContent;
}
if (result) {
// If the value is true, we need to set the flag
node_options_.push_back(it->first);
}
break;
}
// String array can allow both string and array types
case options_parser::OptionType::kStringList: {
simdjson::ondemand::json_type field_type;
if (ondemand_value.type().get(field_type)) {
return ParseResult::InvalidContent;
}
switch (field_type) {
case simdjson::ondemand::json_type::array: {
std::vector<std::string> result;
simdjson::ondemand::array raw_imports;
if (ondemand_value.get_array().get(raw_imports)) {
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
return ParseResult::InvalidContent;
}
for (auto raw_import : raw_imports) {
std::string_view import;
if (raw_import.get_string(import)) {
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
return ParseResult::InvalidContent;
}
node_options_.push_back(it->first + "=" + std::string(import));
}
break;
}
case simdjson::ondemand::json_type::string: {
std::string result;
if (ondemand_value.get_string(result)) {
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
return ParseResult::InvalidContent;
}
node_options_.push_back(it->first + "=" + result);
break;
}
default:
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
return ParseResult::InvalidContent;
}
break;
}
case options_parser::OptionType::kString: {
std::string result;
if (ondemand_value.get_string(result)) {
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
return ParseResult::InvalidContent;
}
node_options_.push_back(it->first + "=" + result);
break;
}
case options_parser::OptionType::kInteger: {
int64_t result;
if (ondemand_value.get_int64().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
return ParseResult::InvalidContent;
}
node_options_.push_back(it->first + "=" + std::to_string(result));
break;
}
case options_parser::OptionType::kHostPort:
case options_parser::OptionType::kUInteger: {
uint64_t result;
if (ondemand_value.get_uint64().get(result)) {
FPrintF(stderr, "Invalid value for %s\n", it->first.c_str());
return ParseResult::InvalidContent;
}
node_options_.push_back(it->first + "=" + std::to_string(result));
break;
}
case options_parser::OptionType::kNoOp: {
FPrintF(stderr,
"No-op flag %s is currently not supported\n",
it->first.c_str());
return ParseResult::InvalidContent;
break;
}
case options_parser::OptionType::kV8Option: {
FPrintF(stderr,
"V8 flag %s is currently not supported\n",
it->first.c_str());
return ParseResult::InvalidContent;
}
default:
UNREACHABLE();
auto option = options_map.find(prefix.append(option_key));
if (option != options_map.end()) {
// If the option has already been set, return an error
if (unique_options->contains(option->first)) {
FPrintF(
stderr, "Option %s is already defined\n", option->first.c_str());
return ParseResult::InvalidContent;
}
// Add the option to the unique set to prevent duplicates
// on future iterations
unique_options->insert(option->first);
// Process the option value based on its type
ParseResult result =
ProcessOptionValue(*option, &option_value, output_vector);
if (result != ParseResult::Valid) {
return result;
}
} else {
FPrintF(stderr, "Unknown or not allowed option %s\n", key.data());
FPrintF(stderr,
"Unknown or not allowed option %s for namespace %s\n",
option_key.data(),
namespace_name.c_str());
return ParseResult::InvalidContent;
}
}
@ -177,9 +227,10 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
return ParseResult::InvalidContent;
}
// Validate config is an object
simdjson::ondemand::object main_object;
// If document is not an object, throw an error.
if (auto root_error = document.get_object().get(main_object)) {
auto root_error = document.get_object().get(main_object);
if (root_error) {
if (root_error == simdjson::error_code::INCORRECT_TYPE) {
FPrintF(stderr,
"Root value unexpected not an object for %s\n\n",
@ -190,36 +241,67 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
return ParseResult::InvalidContent;
}
simdjson::ondemand::object node_options_object;
// If "nodeOptions" is an object, parse it
if (auto node_options_error =
main_object["nodeOptions"].get_object().get(node_options_object)) {
if (node_options_error != simdjson::error_code::NO_SUCH_FIELD) {
// Get all available namespaces for validation
std::vector<std::string> available_namespaces =
options_parser::MapAvailableNamespaces();
// Add "nodeOptions" as a special case for backward compatibility
available_namespaces.emplace_back("nodeOptions");
// Create a set for faster lookup of valid namespaces
std::unordered_set<std::string> valid_namespaces(available_namespaces.begin(),
available_namespaces.end());
// Create a set to track unique options
std::unordered_set<std::string> unique_options;
// Iterate through the main object to find all namespaces
for (auto field : main_object) {
std::string_view field_name;
if (field.unescaped_key().get(field_name)) {
return ParseResult::InvalidContent;
}
// Check if this field is a valid namespace
std::string namespace_name(field_name);
if (!valid_namespaces.contains(namespace_name)) {
// If not, skip it
continue;
}
// Get the namespace object
simdjson::ondemand::object namespace_object;
auto field_error = field.value().get_object().get(namespace_object);
// If namespace value is not an object
if (field_error) {
FPrintF(stderr,
"\"nodeOptions\" value unexpected for %s\n\n",
"\"%s\" value unexpected for %s (should be an object)\n",
namespace_name.c_str(),
config_path.data());
return ParseResult::InvalidContent;
}
} else {
return ParseNodeOptions(&node_options_object);
// Process options for this namespace using the unified method
ParseResult result =
ParseOptions(&namespace_object, &unique_options, namespace_name);
if (result != ParseResult::Valid) {
return result;
}
}
return ParseResult::Valid;
}
std::string ConfigReader::AssignNodeOptions() {
if (node_options_.empty()) {
return "";
} else {
DCHECK_GT(node_options_.size(), 0);
std::string acc;
acc.reserve(node_options_.size() * 2);
for (size_t i = 0; i < node_options_.size(); ++i) {
// The space is necessary at the beginning of the string
acc += " " + node_options_[i];
}
return acc;
std::string ConfigReader::GetNodeOptions() {
std::string acc = "";
const size_t total_options = node_options_.size();
acc.reserve(total_options * 2);
for (auto& opt : node_options_) {
acc += " " + opt;
}
return acc;
}
const std::vector<std::string>& ConfigReader::GetNamespaceFlags() const {
return namespace_options_;
}
size_t ConfigReader::GetFlagsSize() {

View File

@ -5,7 +5,10 @@
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <variant>
#include <vector>
#include "node_internals.h"
#include "simdjson.h"
#include "util-inl.h"
@ -29,14 +32,30 @@ class ConfigReader {
std::optional<std::string_view> GetDataFromArgs(
const std::vector<std::string>& args);
std::string AssignNodeOptions();
std::string GetNodeOptions();
const std::vector<std::string>& GetNamespaceFlags() const;
size_t GetFlagsSize();
private:
ParseResult ParseNodeOptions(simdjson::ondemand::object* node_options_object);
// Parse options for a specific namespace (including nodeOptions for backward
// compatibility)
ParseResult ParseOptions(simdjson::ondemand::object* options_object,
std::unordered_set<std::string>* unique_options,
const std::string& namespace_name);
// Process a single option value based on its type
ParseResult ProcessOptionValue(
const std::pair<std::string, options_parser::OptionType>& option_info,
simdjson::ondemand::value* option_value,
std::vector<std::string>* output);
std::vector<std::string> node_options_;
std::vector<std::string> namespace_options_;
// Cache for fast lookup of environment options
std::unordered_map<std::string, options_parser::OptionType> env_options_map_;
bool env_options_initialized_ = false;
};
} // namespace node

View File

@ -31,98 +31,128 @@ namespace options_parser {
template <typename Options>
void OptionsParser<Options>::AddOption(const char* name,
const char* help_text,
bool Options::* field,
bool Options::*field,
OptionEnvvarSettings env_setting,
bool default_is_true) {
bool default_is_true,
OptionNamespaces namespace_id) {
options_.emplace(name,
OptionInfo{kBoolean,
std::make_shared<SimpleOptionField<bool>>(field),
env_setting,
help_text,
default_is_true});
default_is_true,
NamespaceEnumToString(namespace_id)});
}
template <typename Options>
void OptionsParser<Options>::AddOption(const char* name,
const char* help_text,
uint64_t Options::* field,
OptionEnvvarSettings env_setting) {
uint64_t Options::*field,
OptionEnvvarSettings env_setting,
OptionNamespaces namespace_id) {
options_.emplace(
name,
OptionInfo{kUInteger,
std::make_shared<SimpleOptionField<uint64_t>>(field),
env_setting,
help_text});
help_text,
false,
NamespaceEnumToString(namespace_id)});
}
template <typename Options>
void OptionsParser<Options>::AddOption(const char* name,
const char* help_text,
int64_t Options::* field,
OptionEnvvarSettings env_setting) {
int64_t Options::*field,
OptionEnvvarSettings env_setting,
OptionNamespaces namespace_id) {
options_.emplace(
name,
OptionInfo{kInteger,
std::make_shared<SimpleOptionField<int64_t>>(field),
env_setting,
help_text});
help_text,
false,
NamespaceEnumToString(namespace_id)});
}
template <typename Options>
void OptionsParser<Options>::AddOption(const char* name,
const char* help_text,
std::string Options::* field,
OptionEnvvarSettings env_setting) {
std::string Options::*field,
OptionEnvvarSettings env_setting,
OptionNamespaces namespace_id) {
options_.emplace(
name,
OptionInfo{kString,
std::make_shared<SimpleOptionField<std::string>>(field),
env_setting,
help_text});
}
template <typename Options>
void OptionsParser<Options>::AddOption(
const char* name,
const char* help_text,
std::vector<std::string> Options::* field,
OptionEnvvarSettings env_setting) {
options_.emplace(name, OptionInfo {
kStringList,
std::make_shared<SimpleOptionField<std::vector<std::string>>>(field),
env_setting,
help_text
});
help_text,
false,
NamespaceEnumToString(namespace_id)});
}
template <typename Options>
void OptionsParser<Options>::AddOption(const char* name,
const char* help_text,
HostPort Options::* field,
OptionEnvvarSettings env_setting) {
std::vector<std::string> Options::*field,
OptionEnvvarSettings env_setting,
OptionNamespaces namespace_id) {
options_.emplace(
name,
OptionInfo{
kStringList,
std::make_shared<SimpleOptionField<std::vector<std::string>>>(field),
env_setting,
help_text,
false,
NamespaceEnumToString(namespace_id)});
}
template <typename Options>
void OptionsParser<Options>::AddOption(const char* name,
const char* help_text,
HostPort Options::*field,
OptionEnvvarSettings env_setting,
OptionNamespaces namespace_id) {
options_.emplace(
name,
OptionInfo{kHostPort,
std::make_shared<SimpleOptionField<HostPort>>(field),
env_setting,
help_text});
help_text,
false,
NamespaceEnumToString(namespace_id)});
}
template <typename Options>
void OptionsParser<Options>::AddOption(const char* name,
const char* help_text,
NoOp no_op_tag,
OptionEnvvarSettings env_setting) {
options_.emplace(name, OptionInfo{kNoOp, nullptr, env_setting, help_text});
OptionEnvvarSettings env_setting,
OptionNamespaces namespace_id) {
options_.emplace(name,
OptionInfo{kNoOp,
nullptr,
env_setting,
help_text,
false,
NamespaceEnumToString(namespace_id)});
}
template <typename Options>
void OptionsParser<Options>::AddOption(const char* name,
const char* help_text,
V8Option v8_option_tag,
OptionEnvvarSettings env_setting) {
OptionEnvvarSettings env_setting,
OptionNamespaces namespace_id) {
options_.emplace(name,
OptionInfo{kV8Option, nullptr, env_setting, help_text});
OptionInfo{kV8Option,
nullptr,
env_setting,
help_text,
false,
NamespaceEnumToString(namespace_id)});
}
template <typename Options>
@ -198,7 +228,8 @@ auto OptionsParser<Options>::Convert(
Convert(original.field, get_child),
original.env_setting,
original.help_text,
original.default_is_true};
original.default_is_true,
original.namespace_id};
}
template <typename Options>

View File

@ -17,6 +17,7 @@
#include <limits>
#include <sstream>
#include <string_view>
#include <vector>
using v8::Boolean;
using v8::Context;
@ -244,6 +245,64 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
namespace options_parser {
// Helper function to convert option types to their string representation
// and add them to a V8 Map
static bool AddOptionTypeToMap(Isolate* isolate,
Local<Context> context,
Local<Map> map,
const std::string& option_name,
const OptionType& option_type) {
std::string type;
switch (static_cast<int>(option_type)) {
case 0: // No-op
case 1: // V8 flags
break; // V8 and NoOp flags are not supported
case 2:
type = "boolean";
break;
case 3: // integer
case 4: // unsigned integer
case 6: // host port
type = "number";
break;
case 5: // string
type = "string";
break;
case 7: // string array
type = "array";
break;
default:
UNREACHABLE();
}
if (type.empty()) {
return true; // Skip this entry but continue processing
}
Local<String> option_key;
if (!String::NewFromUtf8(isolate,
option_name.data(),
v8::NewStringType::kNormal,
option_name.size())
.ToLocal(&option_key)) {
return true; // Skip this entry but continue processing
}
Local<String> type_value;
if (!String::NewFromUtf8(
isolate, type.data(), v8::NewStringType::kNormal, type.size())
.ToLocal(&type_value)) {
return true; // Skip this entry but continue processing
}
if (map->Set(context, option_key, type_value).IsEmpty()) {
return false; // Error occurred, stop processing
}
return true;
}
class DebugOptionsParser : public OptionsParser<DebugOptions> {
public:
DebugOptionsParser();
@ -697,82 +756,119 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::experimental_default_config_file);
AddOption("--test",
"launch test runner on startup",
&EnvironmentOptions::test_runner);
&EnvironmentOptions::test_runner,
kDisallowedInEnvvar);
AddOption("--test-concurrency",
"specify test runner concurrency",
&EnvironmentOptions::test_runner_concurrency);
&EnvironmentOptions::test_runner_concurrency,
kDisallowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-force-exit",
"force test runner to exit upon completion",
&EnvironmentOptions::test_runner_force_exit);
&EnvironmentOptions::test_runner_force_exit,
kDisallowedInEnvvar,
false,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-timeout",
"specify test runner timeout",
&EnvironmentOptions::test_runner_timeout);
&EnvironmentOptions::test_runner_timeout,
kDisallowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-update-snapshots",
"regenerate test snapshots",
&EnvironmentOptions::test_runner_update_snapshots);
&EnvironmentOptions::test_runner_update_snapshots,
kDisallowedInEnvvar,
false,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--experimental-test-coverage",
"enable code coverage in the test runner",
&EnvironmentOptions::test_runner_coverage);
&EnvironmentOptions::test_runner_coverage,
kDisallowedInEnvvar,
false,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-coverage-branches",
"the branch coverage minimum threshold",
&EnvironmentOptions::test_coverage_branches,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-coverage-functions",
"the function coverage minimum threshold",
&EnvironmentOptions::test_coverage_functions,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-coverage-lines",
"the line coverage minimum threshold",
&EnvironmentOptions::test_coverage_lines,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-isolation",
"configures the type of test isolation used in the test runner",
&EnvironmentOptions::test_isolation,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
// TODO(cjihrig): Remove this alias in a semver major.
AddAlias("--experimental-test-isolation", "--test-isolation");
AddOption("--experimental-test-module-mocks",
"enable module mocking in the test runner",
&EnvironmentOptions::test_runner_module_mocks);
AddOption("--experimental-test-snapshots", "", NoOp{});
&EnvironmentOptions::test_runner_module_mocks,
kDisallowedInEnvvar,
false,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--experimental-test-snapshots",
"",
NoOp{},
kDisallowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-name-pattern",
"run tests whose name matches this regular expression",
&EnvironmentOptions::test_name_pattern,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-reporter",
"report test output using the given reporter",
&EnvironmentOptions::test_reporter,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-reporter-destination",
"report given reporter to the given destination",
&EnvironmentOptions::test_reporter_destination,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-only",
"run tests with 'only' option set",
&EnvironmentOptions::test_only,
kAllowedInEnvvar);
kAllowedInEnvvar,
false,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-shard",
"run test at specific shard",
&EnvironmentOptions::test_shard,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-skip-pattern",
"run tests whose name do not match this regular expression",
&EnvironmentOptions::test_skip_pattern,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-coverage-include",
"include files in coverage report that match this glob pattern",
&EnvironmentOptions::coverage_include_pattern,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-coverage-exclude",
"exclude files from coverage report that match this glob pattern",
&EnvironmentOptions::coverage_exclude_pattern,
kAllowedInEnvvar);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-global-setup",
"specifies the path to the global setup file",
&EnvironmentOptions::test_global_setup_path,
kAllowedInEnvvar);
AddOption("--test-udp-no-try-send", "", // For testing only.
&EnvironmentOptions::test_udp_no_try_send);
kAllowedInEnvvar,
OptionNamespaces::kTestRunnerNamespace);
AddOption("--test-udp-no-try-send",
"", // For testing only.
&EnvironmentOptions::test_udp_no_try_send,
kDisallowedInEnvvar);
AddOption("--throw-deprecation",
"throw an exception on deprecations",
&EnvironmentOptions::throw_deprecation,
@ -1331,6 +1427,49 @@ MapEnvOptionsFlagInputType() {
return type_map;
}
std::vector<std::string> MapAvailableNamespaces() {
std::vector<std::string> namespaceNames;
auto availableNamespaces = AllNamespaces();
for (size_t i = 1; i < availableNamespaces.size(); i++) {
OptionNamespaces ns = availableNamespaces[i];
std::string ns_string = NamespaceEnumToString(ns);
if (!ns_string.empty()) {
namespaceNames.push_back(ns_string);
}
}
return namespaceNames;
}
std::unordered_map<std::string, options_parser::OptionType>
MapOptionsByNamespace(std::string namespace_name) {
std::unordered_map<std::string, options_parser::OptionType> type_map;
const auto& parser = _ppop_instance;
for (const auto& item : parser.options_) {
if (!item.first.empty() && !item.first.starts_with('[') &&
item.second.namespace_id == namespace_name) {
type_map[item.first] = item.second.type;
}
}
return type_map;
}
std::unordered_map<std::string,
std::unordered_map<std::string, options_parser::OptionType>>
MapNamespaceOptionsAssociations() {
std::vector<std::string> available_namespaces =
options_parser::MapAvailableNamespaces();
std::unordered_map<
std::string,
std::unordered_map<std::string, options_parser::OptionType>>
namespace_option_mapping;
for (const std::string& available_namespace : available_namespaces) {
namespace_option_mapping[available_namespace] =
options_parser::MapOptionsByNamespace(available_namespace);
}
return namespace_option_mapping;
}
struct IterateCLIOptionsScope {
explicit IterateCLIOptionsScope(Environment* env) {
// Temporarily act as if the current Environment's/IsolateData's options
@ -1598,51 +1737,8 @@ void GetEnvOptionsInputType(const FunctionCallbackInfo<Value>& args) {
for (const auto& item : _ppop_instance.options_) {
if (!item.first.empty() && !item.first.starts_with('[') &&
item.second.env_setting == kAllowedInEnvvar) {
std::string type;
switch (static_cast<int>(item.second.type)) {
case 0: // No-op
case 1: // V8 flags
break; // V8 and NoOp flags are not supported
case 2:
type = "boolean";
break;
case 3: // integer
case 4: // unsigned integer
case 6: // host port
type = "number";
break;
case 5: // string
type = "string";
break;
case 7: // string array
type = "array";
break;
default:
UNREACHABLE();
}
if (type.empty()) {
continue;
}
Local<String> value;
if (!String::NewFromUtf8(
isolate, type.data(), v8::NewStringType::kNormal, type.size())
.ToLocal(&value)) {
continue;
}
Local<String> field;
if (!String::NewFromUtf8(isolate,
item.first.data(),
v8::NewStringType::kNormal,
item.first.size())
.ToLocal(&field)) {
continue;
}
if (flags_map->Set(context, field, value).IsEmpty()) {
if (!AddOptionTypeToMap(
isolate, context, flags_map, item.first, item.second.type)) {
return;
}
}
@ -1650,6 +1746,64 @@ void GetEnvOptionsInputType(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(flags_map);
}
// This function returns a two-level nested map containing all the available
// options grouped by their namespaces along with their input types. This is
// used for config file JSON schema generation
void GetNamespaceOptionsInputType(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Context> context = isolate->GetCurrentContext();
Environment* env = Environment::GetCurrent(context);
if (!env->has_run_bootstrapping_code()) {
// No code because this is an assertion.
THROW_ERR_OPTIONS_BEFORE_BOOTSTRAPPING(
isolate, "Should not query options before bootstrapping is done");
}
Mutex::ScopedLock lock(per_process::cli_options_mutex);
Local<Map> namespaces_map = Map::New(isolate);
// Get the mapping of namespaces to their options and types
auto namespace_options = options_parser::MapNamespaceOptionsAssociations();
for (const auto& ns_entry : namespace_options) {
const std::string& namespace_name = ns_entry.first;
const auto& options_map = ns_entry.second;
Local<Map> options_type_map = Map::New(isolate);
for (const auto& opt_entry : options_map) {
const std::string& option_name = opt_entry.first;
const options_parser::OptionType& option_type = opt_entry.second;
if (!AddOptionTypeToMap(
isolate, context, options_type_map, option_name, option_type)) {
return;
}
}
// Only add namespaces that have options
if (options_type_map->Size() > 0) {
Local<String> namespace_key;
if (!String::NewFromUtf8(isolate,
namespace_name.data(),
v8::NewStringType::kNormal,
namespace_name.size())
.ToLocal(&namespace_key)) {
continue;
}
if (namespaces_map->Set(context, namespace_key, options_type_map)
.IsEmpty()) {
return;
}
}
}
args.GetReturnValue().Set(namespaces_map);
}
void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
@ -1664,6 +1818,10 @@ void Initialize(Local<Object> target,
context, target, "getEmbedderOptions", GetEmbedderOptions);
SetMethodNoSideEffect(
context, target, "getEnvOptionsInputType", GetEnvOptionsInputType);
SetMethodNoSideEffect(context,
target,
"getNamespaceOptionsInputType",
GetNamespaceOptionsInputType);
Local<Object> env_settings = Object::New(isolate);
NODE_DEFINE_CONSTANT(env_settings, kAllowedInEnvvar);
NODE_DEFINE_CONSTANT(env_settings, kDisallowedInEnvvar);
@ -1690,6 +1848,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetCLIOptionsInfo);
registry->Register(GetEmbedderOptions);
registry->Register(GetEnvOptionsInputType);
registry->Register(GetNamespaceOptionsInputType);
}
} // namespace options_parser

View File

@ -380,7 +380,7 @@ class PerProcessOptions : public Options {
namespace options_parser {
HostPort SplitHostPort(const std::string& arg,
std::vector<std::string>* errors);
std::vector<std::string>* errors);
void GetOptions(const v8::FunctionCallbackInfo<v8::Value>& args);
std::string GetBashCompletion();
@ -395,6 +395,43 @@ enum OptionType {
kStringList,
};
std::unordered_map<std::string, OptionType> MapEnvOptionsFlagInputType();
std::unordered_map<std::string, OptionType> MapOptionsByNamespace(
std::string namespace_name);
std::unordered_map<std::string,
std::unordered_map<std::string, options_parser::OptionType>>
MapNamespaceOptionsAssociations();
std::vector<std::string> MapAvailableNamespaces();
// Define all namespace entries
#define OPTION_NAMESPACE_LIST(V) \
V(kNoNamespace, "") \
V(kTestRunnerNamespace, "testRunner")
enum class OptionNamespaces {
#define V(name, _) name,
OPTION_NAMESPACE_LIST(V)
#undef V
};
inline const std::string NamespaceEnumToString(OptionNamespaces ns) {
switch (ns) {
#define V(name, string_value) \
case OptionNamespaces::name: \
return string_value;
OPTION_NAMESPACE_LIST(V)
#undef V
default:
return "";
}
}
inline constexpr auto AllNamespaces() {
return std::array{
#define V(name, _) OptionNamespaces::name,
OPTION_NAMESPACE_LIST(V)
#undef V
};
}
template <typename Options>
class OptionsParser {
@ -413,39 +450,55 @@ class OptionsParser {
// default_is_true is only a hint in printing help text, it does not
// affect the default value of the option. Set the default value in the
// Options struct instead.
void AddOption(const char* name,
const char* help_text,
bool Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
bool default_is_true = false);
void AddOption(const char* name,
const char* help_text,
uint64_t Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
void AddOption(const char* name,
const char* help_text,
int64_t Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
void AddOption(const char* name,
const char* help_text,
std::string Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
void AddOption(const char* name,
const char* help_text,
std::vector<std::string> Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
void AddOption(const char* name,
const char* help_text,
HostPort Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
void AddOption(const char* name,
const char* help_text,
NoOp no_op_tag,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
void AddOption(const char* name,
const char* help_text,
V8Option v8_option_tag,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar);
void AddOption(
const char* name,
const char* help_text,
bool Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
bool default_is_true = false,
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
void AddOption(
const char* name,
const char* help_text,
uint64_t Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
void AddOption(
const char* name,
const char* help_text,
int64_t Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
void AddOption(
const char* name,
const char* help_text,
std::string Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
void AddOption(
const char* name,
const char* help_text,
std::vector<std::string> Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
void AddOption(
const char* name,
const char* help_text,
HostPort Options::*field,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
void AddOption(
const char* name,
const char* help_text,
NoOp no_op_tag,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
void AddOption(
const char* name,
const char* help_text,
V8Option v8_option_tag,
OptionEnvvarSettings env_setting = kDisallowedInEnvvar,
OptionNamespaces namespace_id = OptionNamespaces::kNoNamespace);
// Adds aliases. An alias can be of the form "--option-a" -> "--option-b",
// or have a more complex group expansion, like
@ -535,12 +588,15 @@ class OptionsParser {
// - A type.
// - A way to store/access the property value.
// - The information of whether it may occur in an env var or not.
// - A default value (if applicable).
// - A namespace ID (optional) to allow for namespacing of options.
struct OptionInfo {
OptionType type;
std::shared_ptr<BaseOptionField> field;
OptionEnvvarSettings env_setting;
std::string help_text;
bool default_is_true = false;
std::string namespace_id;
};
// An implied option is composed of the information on where to store a
@ -581,6 +637,9 @@ class OptionsParser {
friend std::string GetBashCompletion();
friend std::unordered_map<std::string, OptionType>
MapEnvOptionsFlagInputType();
friend std::unordered_map<std::string, OptionType> MapOptionsByNamespace(
std::string namespace_name);
friend std::vector<std::string> MapAvailableNamespaces();
friend void GetEnvOptionsInputType(
const v8::FunctionCallbackInfo<v8::Value>& args);
};

View File

@ -0,0 +1,6 @@
{
"testRunner": {
"test-name-pattern": "first-pattern",
"test-name-pattern": "second-pattern"
}
}

View File

@ -0,0 +1,3 @@
{
"testRunner": {}
}

View File

@ -0,0 +1,5 @@
{
"testRunner": {
"test-coverage-exclude": ["config-pattern1", "config-pattern2"]
}
}

View File

@ -0,0 +1,6 @@
{
"testRunner": {
"test-concurrency": 1,
"experimental-test-coverage": true
}
}

View File

@ -0,0 +1,5 @@
{
"testRunner": {
"test-isolation": "none"
}
}

View File

@ -0,0 +1,8 @@
{
"testRunner": {
"test-isolation": "process"
},
"nodeOptions": {
"test-isolation": "none"
}
}

View File

@ -0,0 +1,8 @@
{
"nodeOptions": {
"test-isolation": "none"
},
"testRunner": {
"test-isolation": "process"
}
}

View File

@ -0,0 +1,5 @@
{
"testRunner": {
"unknown-flag": true
}
}

View File

@ -0,0 +1,5 @@
{
"an-invalid-namespace": {
"a-key": "a-value"
}
}

View File

@ -3,8 +3,8 @@
const { spawnPromisified, skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const fixtures = require('../common/fixtures');
const { match, strictEqual } = require('node:assert');
const { test } = require('node:test');
const { match, strictEqual, deepStrictEqual } = require('node:assert');
const { test, it, describe } = require('node:test');
const { chmodSync, constants } = require('node:fs');
const common = require('../common');
@ -55,18 +55,19 @@ test('should parse boolean flag', async () => {
strictEqual(result.code, 0);
});
test('should not override a flag declared twice', async () => {
test('should throw an error when a flag is declared twice', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/override-property.json'),
fixtures.path('typescript/ts/transformation/test-enum.ts'),
]);
strictEqual(result.stderr, '');
strictEqual(result.stdout, 'Hello, TypeScript!\n');
strictEqual(result.code, 0);
match(result.stderr, /Option --experimental-transform-types is already defined/);
strictEqual(result.stdout, '');
strictEqual(result.code, 9);
});
test('should override env-file', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
@ -97,7 +98,7 @@ test('should not override NODE_OPTIONS', async () => {
strictEqual(result.code, 1);
});
test('should not ovverride CLI flags', async () => {
test('should not override CLI flags', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--no-experimental-transform-types',
@ -375,3 +376,147 @@ test('should throw an error when the file is non readable', { skip: common.isWin
chmodSync(fixtures.path('rc/non-readable/node.config.json'),
constants.S_IRWXU | constants.S_IRWXG | constants.S_IRWXO);
});
describe('namespace-scoped options', () => {
it('should parse a namespace-scoped option correctly', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/namespaced/node.config.json'),
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
strictEqual(result.stderr, '');
strictEqual(result.stdout, 'none\n');
strictEqual(result.code, 0);
});
it('should throw an error when a namespace-scoped option is not recognised', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/unknown-flag-namespace.json'),
'-p', '"Hello, World!"',
]);
match(result.stderr, /Unknown or not allowed option unknown-flag/);
strictEqual(result.stdout, '');
strictEqual(result.code, 9);
});
it('should not throw an error when a namespace is not recognised', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/unknown-namespace.json'),
'-p', '"Hello, World!"',
]);
strictEqual(result.stderr, '');
strictEqual(result.stdout, 'Hello, World!\n');
strictEqual(result.code, 0);
});
it('should handle an empty namespace valid namespace', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/empty-valid-namespace.json'),
'-p', '"Hello, World!"',
]);
strictEqual(result.stderr, '');
strictEqual(result.stdout, 'Hello, World!\n');
strictEqual(result.code, 0);
});
it('should throw an error if a namespace-scoped option has already been set in node options', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/override-node-option-with-namespace.json'),
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
match(result.stderr, /Option --test-isolation is already defined/);
strictEqual(result.stdout, '');
strictEqual(result.code, 9);
});
it('should throw an error if a node option has already been set in a namespace-scoped option', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/override-namespace.json'),
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
match(result.stderr, /Option --test-isolation is already defined/);
strictEqual(result.stdout, '');
strictEqual(result.code, 9);
});
it('should prioritise CLI namespace-scoped options over config file options', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--test-isolation', 'process',
'--experimental-config-file',
fixtures.path('rc/namespaced/node.config.json'),
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
strictEqual(result.stderr, '');
strictEqual(result.stdout, 'process\n');
strictEqual(result.code, 0);
});
it('should append namespace-scoped config file options with CLI options in case of array', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--test-coverage-exclude', 'cli-pattern1',
'--test-coverage-exclude', 'cli-pattern2',
'--experimental-config-file',
fixtures.path('rc/namespace-with-array.json'),
'-p', 'JSON.stringify(require("internal/options").getOptionValue("--test-coverage-exclude"))',
]);
strictEqual(result.stderr, '');
const excludePatterns = JSON.parse(result.stdout);
const expected = [
'config-pattern1',
'config-pattern2',
'cli-pattern1',
'cli-pattern2',
];
deepStrictEqual(excludePatterns, expected);
strictEqual(result.code, 0);
});
it('should allow setting kDisallowedInEnvvar in the config file if part of a namespace', async () => {
// This test assumes that the --test-concurrency flag is configured as kDisallowedInEnvVar
// and that it is part of at least one namespace.
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/namespace-with-disallowed-envvar.json'),
'-p', 'require("internal/options").getOptionValue("--test-concurrency")',
]);
strictEqual(result.stderr, '');
strictEqual(result.stdout, '1\n');
strictEqual(result.code, 0);
});
it('should override namespace-scoped config file options with CLI options', async () => {
// This test assumes that the --test-concurrency flag is configured as kDisallowedInEnvVar
// and that it is part of at least one namespace.
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--test-concurrency', '2',
'--experimental-config-file',
fixtures.path('rc/namespace-with-disallowed-envvar.json'),
'-p', 'require("internal/options").getOptionValue("--test-concurrency")',
]);
strictEqual(result.stderr, '');
strictEqual(result.stdout, '2\n');
strictEqual(result.code, 0);
});
});