diff --git a/doc/api/cli.md b/doc/api/cli.md index 79fe3094ba2301..aeb901973c2af6 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1016,7 +1016,7 @@ in the `$schema` must be replaced with the version of Node.js you are using. "watch-path": "src", "watch-preserve-output": true }, - "testRunner": { + "test": { "test-isolation": "process" }, "watch": { @@ -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 `testRunner` 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. diff --git a/doc/api/permissions.md b/doc/api/permissions.md index dedb314f43e7eb..d36590ec3ae9cd 100644 --- a/doc/api/permissions.md +++ b/doc/api/permissions.md @@ -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` diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index 916bc1c4f62002..22f52952af54d4 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -186,6 +186,9 @@ "experimental-websocket": { "type": "boolean" }, + "experimental-webstorage": { + "type": "boolean" + }, "extra-info-on-fatal-exception": { "type": "boolean" }, @@ -594,9 +597,6 @@ "watch-preserve-output": { "type": "boolean" }, - "webstorage": { - "type": "boolean" - }, "zero-fill-buffers": { "type": "boolean" } @@ -652,10 +652,13 @@ }, "allow-worker": { "type": "boolean" + }, + "permission": { + "type": "boolean" } } }, - "testRunner": { + "test": { "type": "object", "additionalProperties": false, "properties": { @@ -665,6 +668,9 @@ "experimental-test-module-mocks": { "type": "boolean" }, + "test": { + "type": "boolean" + }, "test-concurrency": { "type": "number" }, diff --git a/src/node_config_file.cc b/src/node_config_file.cc index e6c049b4d14f10..39ea8f3a0634fd 100644 --- a/src/node_config_file.cc +++ b/src/node_config_file.cc @@ -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 unique_options; + // Namespaces in OPTION_NAMESPACE_LIST + std::unordered_set namespaces_with_implicit_flags; + // Iterate through the main object to find all namespaces for (auto field : main_object) { std::string_view field_name; @@ -262,13 +265,34 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { return ParseResult::InvalidContent; } - // Check if this field is a valid namespace std::string namespace_name(field_name); + + // TODO(@marco-ippolito): Remove warning for testRunner namespace + if (namespace_name == "testRunner") { + FPrintF(stderr, + "the \"testRunner\" namespace has been removed. " + "Use \"test\" instead.\n"); + // Better to throw an error than to ignore it + // Otherwise users might think their test suite is green + // when it's not running + return ParseResult::InvalidContent; + } + + // Check if this field is a valid namespace if (!valid_namespaces.contains(namespace_name)) { // If not, skip it 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); @@ -290,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; } diff --git a/src/node_options.cc b/src/node_options.cc index 959a5df163b609..baa3936075cd05 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -599,7 +599,8 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "enable the permission system", &EnvironmentOptions::permission, kAllowedInEnvvar, - false); + false, + OptionNamespaces::kPermissionNamespace); AddOption("--allow-fs-read", "allow permissions to read the filesystem", &EnvironmentOptions::allow_fs_read, @@ -839,7 +840,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { AddOption("--test", "launch test runner on startup", &EnvironmentOptions::test_runner, - kDisallowedInEnvvar); + kDisallowedInEnvvar, + false, + OptionNamespaces::kTestRunnerNamespace); AddOption("--test-concurrency", "specify test runner concurrency", &EnvironmentOptions::test_runner_concurrency, diff --git a/src/node_options.h b/src/node_options.h index 9afdeb983d6b26..dd782b460aef79 100644 --- a/src/node_options.h +++ b/src/node_options.h @@ -415,7 +415,7 @@ std::vector MapAvailableNamespaces(); // Define all namespace entries #define OPTION_NAMESPACE_LIST(V) \ V(kNoNamespace, "") \ - V(kTestRunnerNamespace, "testRunner") \ + V(kTestRunnerNamespace, "test") \ V(kWatchNamespace, "watch") \ V(kPermissionNamespace, "permission") diff --git a/test/fixtures/options-as-flags/test-config.json b/test/fixtures/options-as-flags/test-config.json index c80ffa4069fbf5..c956eb6d274785 100644 --- a/test/fixtures/options-as-flags/test-config.json +++ b/test/fixtures/options-as-flags/test-config.json @@ -3,7 +3,8 @@ "experimental-transform-types": true, "max-http-header-size": 8192 }, - "testRunner": { + "test": { + "test": false, "test-isolation": "none" } } diff --git a/test/fixtures/rc/deprecated-testrunner-namespace.json b/test/fixtures/rc/deprecated-testrunner-namespace.json new file mode 100644 index 00000000000000..d8a01949418cf7 --- /dev/null +++ b/test/fixtures/rc/deprecated-testrunner-namespace.json @@ -0,0 +1,5 @@ +{ + "testRunner": { + "test-isolation": "none" + } +} diff --git a/test/fixtures/rc/duplicate-namespace-option/node.config.json b/test/fixtures/rc/duplicate-namespace-option/node.config.json index 4d948fbd33961d..948dc4e6c5570b 100644 --- a/test/fixtures/rc/duplicate-namespace-option/node.config.json +++ b/test/fixtures/rc/duplicate-namespace-option/node.config.json @@ -1,5 +1,5 @@ { - "testRunner": { + "test": { "test-name-pattern": "first-pattern", "test-name-pattern": "second-pattern" } diff --git a/test/fixtures/rc/empty-valid-namespace.json b/test/fixtures/rc/empty-valid-namespace.json index dbeb33d7aa8b59..9f6cb4b89f91be 100644 --- a/test/fixtures/rc/empty-valid-namespace.json +++ b/test/fixtures/rc/empty-valid-namespace.json @@ -1,3 +1,3 @@ { - "testRunner": {} + "permission": {} } diff --git a/test/fixtures/rc/namespace-with-array.json b/test/fixtures/rc/namespace-with-array.json index 056a4291e9b666..1cddcdfd6c4ccd 100644 --- a/test/fixtures/rc/namespace-with-array.json +++ b/test/fixtures/rc/namespace-with-array.json @@ -1,5 +1,5 @@ { - "testRunner": { + "test": { "test-coverage-exclude": ["config-pattern1", "config-pattern2"] } } diff --git a/test/fixtures/rc/namespace-with-disallowed-envvar.json b/test/fixtures/rc/namespace-with-disallowed-envvar.json index 6152684e0583f4..f545f0ff62781f 100644 --- a/test/fixtures/rc/namespace-with-disallowed-envvar.json +++ b/test/fixtures/rc/namespace-with-disallowed-envvar.json @@ -1,5 +1,5 @@ { - "testRunner": { + "test": { "test-concurrency": 1, "experimental-test-coverage": true } diff --git a/test/fixtures/rc/namespaced/node.config.json b/test/fixtures/rc/namespaced/node.config.json index df929d25c10b52..0870c4c86033b2 100644 --- a/test/fixtures/rc/namespaced/node.config.json +++ b/test/fixtures/rc/namespaced/node.config.json @@ -1,5 +1,5 @@ { - "testRunner": { + "test": { "test-isolation": "none" } } diff --git a/test/fixtures/rc/override-namespace.json b/test/fixtures/rc/override-namespace.json index acb37b2eec485c..be79cf5d4415b5 100644 --- a/test/fixtures/rc/override-namespace.json +++ b/test/fixtures/rc/override-namespace.json @@ -1,5 +1,5 @@ { - "testRunner": { + "test": { "test-isolation": "process" }, "nodeOptions": { diff --git a/test/fixtures/rc/override-node-option-with-namespace.json b/test/fixtures/rc/override-node-option-with-namespace.json index 2db9e1a47f07ea..e5912da99590b4 100644 --- a/test/fixtures/rc/override-node-option-with-namespace.json +++ b/test/fixtures/rc/override-node-option-with-namespace.json @@ -2,7 +2,7 @@ "nodeOptions": { "test-isolation": "none" }, - "testRunner": { + "test": { "test-isolation": "process" } } diff --git a/test/fixtures/rc/permission-namespace.json b/test/fixtures/rc/permission-namespace.json new file mode 100644 index 00000000000000..72167d77dd18ec --- /dev/null +++ b/test/fixtures/rc/permission-namespace.json @@ -0,0 +1,5 @@ +{ + "permission": { + "allow-fs-read": "*" + } +} diff --git a/test/fixtures/rc/test-namespace-explicit-false.json b/test/fixtures/rc/test-namespace-explicit-false.json new file mode 100644 index 00000000000000..42e68aa6a43f59 --- /dev/null +++ b/test/fixtures/rc/test-namespace-explicit-false.json @@ -0,0 +1,6 @@ +{ + "test": { + "test": false, + "test-isolation": "none" + } +} diff --git a/test/fixtures/rc/unknown-flag-namespace.json b/test/fixtures/rc/unknown-flag-namespace.json index b5d87ad8dd3acd..01dd62824cf2b9 100644 --- a/test/fixtures/rc/unknown-flag-namespace.json +++ b/test/fixtures/rc/unknown-flag-namespace.json @@ -1,5 +1,5 @@ { - "testRunner": { + "test": { "unknown-flag": true } } diff --git a/test/fixtures/rc/watch-namespace.json b/test/fixtures/rc/watch-namespace.json new file mode 100644 index 00000000000000..11aad97fffd315 --- /dev/null +++ b/test/fixtures/rc/watch-namespace.json @@ -0,0 +1,5 @@ +{ + "watch": { + "watch-preserve-output": true + } +} diff --git a/test/fixtures/test-runner/options-propagation/node.config.json b/test/fixtures/test-runner/options-propagation/node.config.json index 98a35b7d3eebd2..7bb3d5c80fe0e5 100644 --- a/test/fixtures/test-runner/options-propagation/node.config.json +++ b/test/fixtures/test-runner/options-propagation/node.config.json @@ -1,5 +1,5 @@ { - "testRunner": { + "test": { "experimental-test-coverage": true } } diff --git a/test/parallel/test-config-file.js b/test/parallel/test-config-file.js index 4f4e6703f88702..438a138f6ba227 100644 --- a/test/parallel/test-config-file.js +++ b/test/parallel/test-config-file.js @@ -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, ''); @@ -421,7 +422,7 @@ describe('namespace-scoped options', () => { fixtures.path('rc/unknown-flag-namespace.json'), '-p', '"Hello, World!"', ]); - assert.match(result.stderr, /Unknown or not allowed option unknown-flag for namespace testRunner/); + assert.match(result.stderr, /Unknown or not allowed option unknown-flag for namespace test/); assert.strictEqual(result.stdout, ''); assert.strictEqual(result.code, 9); }); @@ -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,10 +540,60 @@ 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, ''); assert.strictEqual(result.stdout, '2\n'); assert.strictEqual(result.code, 0); }); + + it('should throw an error for removed "testRunner" namespace', async () => { + const result = await spawnPromisified(process.execPath, [ + '--no-warnings', + '--experimental-config-file', + fixtures.path('rc/deprecated-testrunner-namespace.json'), + '-p', '"Hello, World!"', + ]); + assert.match(result.stderr, /the "testRunner" namespace has been removed\. Use "test" instead\./); + 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); + }); }); diff --git a/test/parallel/test-runner-flag-propagation.js b/test/parallel/test-runner-flag-propagation.js index d5190251ef8db7..3248b91bcef24d 100644 --- a/test/parallel/test-runner-flag-propagation.js +++ b/test/parallel/test-runner-flag-propagation.js @@ -68,19 +68,19 @@ describe('test runner flag propagation', () => { describe('via config file', () => { const configFilePropagationTests = [ - ['--test-concurrency', 2, 2, 'testRunner'], - ['--test-timeout', 5000, 5000, 'testRunner'], - ['--test-coverage-branches', 100, 100, 'testRunner'], - ['--test-coverage-functions', 100, 100, 'testRunner'], - ['--test-coverage-lines', 100, 100, 'testRunner'], - ['--experimental-test-coverage', true, false, 'testRunner'], - ['--test-coverage-exclude', 'test/**', 'test/**', 'testRunner'], - ['--test-coverage-include', 'src/**', 'src/**', 'testRunner'], - ['--test-update-snapshots', true, true, 'testRunner'], - ['--test-concurrency', 3, 3, 'testRunner'], - ['--test-timeout', 2500, 2500, 'testRunner'], - ['--test-coverage-branches', 90, 90, 'testRunner'], - ['--test-coverage-functions', 85, 85, 'testRunner'], + ['--test-concurrency', 2, 2, 'test'], + ['--test-timeout', 5000, 5000, 'test'], + ['--test-coverage-branches', 100, 100, 'test'], + ['--test-coverage-functions', 100, 100, 'test'], + ['--test-coverage-lines', 100, 100, 'test'], + ['--experimental-test-coverage', true, false, 'test'], + ['--test-coverage-exclude', 'test/**', 'test/**', 'test'], + ['--test-coverage-include', 'src/**', 'src/**', 'test'], + ['--test-update-snapshots', true, true, 'test'], + ['--test-concurrency', 3, 3, 'test'], + ['--test-timeout', 2500, 2500, 'test'], + ['--test-coverage-branches', 90, 90, 'test'], + ['--test-coverage-functions', 85, 85, 'test'], ]; for (const [flagName, configValue, expectedValue, namespace] of configFilePropagationTests) {