Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
src: implicitly enable namespace in config
  • Loading branch information
marco-ippolito committed Nov 21, 2025
commit 95f4f389ef90f090c5c2fea3dde11366d4d6bf57
35 changes: 34 additions & 1 deletion doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 3 additions & 2 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
23 changes: 23 additions & 0 deletions src/node_config_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand All @@ -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;
}

Expand Down
1 change: 1 addition & 0 deletions test/fixtures/options-as-flags/test-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"max-http-header-size": 8192
},
"test": {
"test": false,
"test-isolation": "none"
}
}
2 changes: 1 addition & 1 deletion test/fixtures/rc/empty-valid-namespace.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"test": {}
"permission": {}
}
5 changes: 5 additions & 0 deletions test/fixtures/rc/permission-namespace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"permission": {
"allow-fs-read": "*"
}
}
6 changes: 6 additions & 0 deletions test/fixtures/rc/test-namespace-explicit-false.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"test": {
"test": false,
"test-isolation": "none"
}
}
5 changes: 5 additions & 0 deletions test/fixtures/rc/watch-namespace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"watch": {
"watch-preserve-output": true
}
}
42 changes: 42 additions & 0 deletions test/parallel/test-config-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, '');
Expand Down Expand Up @@ -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, '');
Expand All @@ -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, '');
Expand All @@ -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, '');
Expand All @@ -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, '');
Expand All @@ -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);
});
});
Loading