src: implicitly enable namespace in config

PR-URL: https://github.com/nodejs/node/pull/60798
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
This commit is contained in:
Marco Ippolito 2025-11-21 10:02:54 +01:00 committed by Node.js GitHub Bot
parent 49e56bfc55
commit 1758b74829
9 changed files with 120 additions and 4 deletions

View File

@ -1029,7 +1029,40 @@ The configuration file supports namespace-specific options:
* The `nodeOptions` field contains CLI flags that are allowed in [`NODE_OPTIONS`][].
* Namespace fields like `test` contain configuration specific to that subsystem.
* Namespace fields like `test`, `watch`, and `permission` contain configuration specific to that subsystem.
When a namespace is present in the
configuration file, Node.js automatically enables the corresponding flag
(e.g., `--test`, `--watch`, `--permission`). This allows you to configure
subsystem-specific options without explicitly passing the flag on the command line.
For example:
```json
{
"test": {
"test-isolation": "process"
}
}
```
is equivalent to:
```bash
node --test --test-isolation=process
```
To disable the automatic flag while still using namespace options, you can
explicitly set the flag to `false` within the namespace:
```json
{
"test": {
"test": false,
"test-isolation": "process"
}
}
```
No-op flags are not supported.
Not all V8 flags are currently supported.

View File

@ -175,10 +175,11 @@ Example `node.config.json`:
}
```
Run with the configuration file:
When the `permission` namespace is present in the configuration file, Node.js
automatically enables the `--permission` flag. Run with:
```console
$ node --permission --experimental-default-config-file app.js
$ node --experimental-default-config-file app.js
```
#### Using the Permission Model with `npx`

View File

@ -255,6 +255,9 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
available_namespaces.end());
// Create a set to track unique options
std::unordered_set<std::string> unique_options;
// Namespaces in OPTION_NAMESPACE_LIST
std::unordered_set<std::string> namespaces_with_implicit_flags;
// Iterate through the main object to find all namespaces
for (auto field : main_object) {
std::string_view field_name;
@ -281,6 +284,15 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
continue;
}
// List of implicit namespace flags
for (auto ns_enum : options_parser::AllNamespaces()) {
std::string ns_str = options_parser::NamespaceEnumToString(ns_enum);
if (!ns_str.empty() && namespace_name == ns_str) {
namespaces_with_implicit_flags.insert(namespace_name);
break;
}
}
// Get the namespace object
simdjson::ondemand::object namespace_object;
auto field_error = field.value().get_object().get(namespace_object);
@ -302,6 +314,17 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) {
}
}
// Add implicit flags for namespaces (--test, --permission, --watch)
// These flags are automatically enabled when their namespace is present
for (const auto& ns : namespaces_with_implicit_flags) {
std::string flag = "--" + ns;
std::string no_flag = "--no-" + ns;
// We skip if the user has already set the flag or its negation
if (!unique_options.contains(flag) && !unique_options.contains(no_flag)) {
namespace_options_.push_back(flag);
}
}
return ParseResult::Valid;
}

View File

@ -4,6 +4,7 @@
"max-http-header-size": 8192
},
"test": {
"test": false,
"test-isolation": "none"
}
}

View File

@ -1,3 +1,3 @@
{
"test": {}
"permission": {}
}

View File

@ -0,0 +1,5 @@
{
"permission": {
"allow-fs-read": "*"
}
}

View File

@ -0,0 +1,6 @@
{
"test": {
"test": false,
"test-isolation": "none"
}
}

5
test/fixtures/rc/watch-namespace.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"watch": {
"watch-preserve-output": true
}
}

View File

@ -407,6 +407,7 @@ describe('namespace-scoped options', () => {
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/namespaced/node.config.json'),
'--no-test',
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
assert.strictEqual(result.stderr, '');
@ -483,6 +484,7 @@ describe('namespace-scoped options', () => {
'--test-isolation', 'process',
'--experimental-config-file',
fixtures.path('rc/namespaced/node.config.json'),
'--no-test',
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
assert.strictEqual(result.stderr, '');
@ -498,6 +500,7 @@ describe('namespace-scoped options', () => {
'--test-coverage-exclude', 'cli-pattern2',
'--experimental-config-file',
fixtures.path('rc/namespace-with-array.json'),
'--no-test',
'-p', 'JSON.stringify(require("internal/options").getOptionValue("--test-coverage-exclude"))',
]);
assert.strictEqual(result.stderr, '');
@ -520,6 +523,7 @@ describe('namespace-scoped options', () => {
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/namespace-with-disallowed-envvar.json'),
'--no-test',
'-p', 'require("internal/options").getOptionValue("--test-concurrency")',
]);
assert.strictEqual(result.stderr, '');
@ -536,6 +540,7 @@ describe('namespace-scoped options', () => {
'--test-concurrency', '2',
'--experimental-config-file',
fixtures.path('rc/namespace-with-disallowed-envvar.json'),
'--no-test',
'-p', 'require("internal/options").getOptionValue("--test-concurrency")',
]);
assert.strictEqual(result.stderr, '');
@ -554,4 +559,41 @@ describe('namespace-scoped options', () => {
assert.strictEqual(result.stdout, '');
assert.strictEqual(result.code, 9);
});
it('should automatically enable --test flag when test namespace is present', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/namespaced/node.config.json'),
fixtures.path('rc/test.js'),
]);
assert.strictEqual(result.code, 0);
assert.match(result.stdout, /tests 1/);
});
it('should automatically enable --permission flag when permission namespace is present', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/permission-namespace.json'),
'-p', 'require("internal/options").getOptionValue("--permission")',
]);
assert.strictEqual(result.stderr, '');
assert.strictEqual(result.stdout, 'true\n');
assert.strictEqual(result.code, 0);
});
it('should respect explicit test: false in test namespace', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/test-namespace-explicit-false.json'),
'-p', 'require("internal/options").getOptionValue("--test")',
]);
assert.strictEqual(result.stderr, '');
assert.strictEqual(result.stdout, 'false\n');
assert.strictEqual(result.code, 0);
});
});