From 0bc90398e0402affd844041cd7b2b813ce5e9de4 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Fri, 21 Nov 2025 08:50:55 +0100 Subject: [PATCH 1/8] src: add permission flag to config file PR-URL: https://github.com/nodejs/node/pull/60798 Reviewed-By: Jacob Smith Reviewed-By: Rafael Gonzaga --- src/node_options.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/node_options.cc b/src/node_options.cc index 9ccad939f1ae..77027fd0c110 100644 --- a/src/node_options.cc +++ b/src/node_options.cc @@ -655,7 +655,8 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { "enable the permission system", &EnvironmentOptions::permission, kAllowedInEnvvar, - false); + false, + OptionNamespaces::kPermissionNamespace); AddOption("--permission-audit", "enable audit only for the permission system", &EnvironmentOptions::permission_audit, From 477bba671f42f0f06bbcb481ee75bdad283661ea Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Fri, 21 Nov 2025 10:02:54 +0100 Subject: [PATCH 2/8] src: implicitly enable namespace in config PR-URL: https://github.com/nodejs/node/pull/60798 Reviewed-By: Jacob Smith Reviewed-By: Rafael Gonzaga --- doc/api/cli.md | 35 ++++++++++++++++- doc/api/permissions.md | 5 ++- src/node_config_file.cc | 23 +++++++++++ .../options-as-flags/test-config.json | 1 + test/fixtures/rc/empty-valid-namespace.json | 2 +- test/fixtures/rc/permission-namespace.json | 5 +++ .../rc/test-namespace-explicit-false.json | 6 +++ test/fixtures/rc/watch-namespace.json | 5 +++ test/parallel/test-config-file.js | 38 +++++++++++++++++++ 9 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/rc/permission-namespace.json create mode 100644 test/fixtures/rc/test-namespace-explicit-false.json create mode 100644 test/fixtures/rc/watch-namespace.json diff --git a/doc/api/cli.md b/doc/api/cli.md index b4f389241f66..d39c90ca6b4c 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1039,7 +1039,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. diff --git a/doc/api/permissions.md b/doc/api/permissions.md index 2887d9624f61..2d01c09df911 100644 --- a/doc/api/permissions.md +++ b/doc/api/permissions.md @@ -280,10 +280,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/src/node_config_file.cc b/src/node_config_file.cc index 5afba0854beb..b2c87970b6eb 100644 --- a/src/node_config_file.cc +++ b/src/node_config_file.cc @@ -268,6 +268,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; @@ -298,6 +301,15 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { return ParseResult::InvalidContent; } + // 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); @@ -319,6 +331,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/test/fixtures/options-as-flags/test-config.json b/test/fixtures/options-as-flags/test-config.json index aa6355c37a16..c956eb6d2747 100644 --- a/test/fixtures/options-as-flags/test-config.json +++ b/test/fixtures/options-as-flags/test-config.json @@ -4,6 +4,7 @@ "max-http-header-size": 8192 }, "test": { + "test": false, "test-isolation": "none" } } diff --git a/test/fixtures/rc/empty-valid-namespace.json b/test/fixtures/rc/empty-valid-namespace.json index c267aa9bde29..9f6cb4b89f91 100644 --- a/test/fixtures/rc/empty-valid-namespace.json +++ b/test/fixtures/rc/empty-valid-namespace.json @@ -1,3 +1,3 @@ { - "test": {} + "permission": {} } diff --git a/test/fixtures/rc/permission-namespace.json b/test/fixtures/rc/permission-namespace.json new file mode 100644 index 000000000000..72167d77dd18 --- /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 000000000000..42e68aa6a43f --- /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/watch-namespace.json b/test/fixtures/rc/watch-namespace.json new file mode 100644 index 000000000000..11aad97fffd3 --- /dev/null +++ b/test/fixtures/rc/watch-namespace.json @@ -0,0 +1,5 @@ +{ + "watch": { + "watch-preserve-output": true + } +} diff --git a/test/parallel/test-config-file.js b/test/parallel/test-config-file.js index 4f5b57e0572c..1adc1e2fe017 100644 --- a/test/parallel/test-config-file.js +++ b/test/parallel/test-config-file.js @@ -498,6 +498,7 @@ describe('namespace-scoped options', () => { '--no-warnings', '--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, ''); @@ -580,6 +581,7 @@ describe('namespace-scoped options', () => { '--expose-internals', '--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, ''); @@ -594,6 +596,7 @@ describe('namespace-scoped options', () => { '--test-coverage-exclude', 'cli-pattern1', '--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, ''); @@ -615,6 +618,7 @@ describe('namespace-scoped options', () => { '--no-warnings', '--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, ''); @@ -630,6 +634,7 @@ describe('namespace-scoped options', () => { '--expose-internals', '--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, ''); @@ -637,4 +642,37 @@ describe('namespace-scoped options', () => { assert.strictEqual(result.code, 0); }); + 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); + }); }); From a4f62de4f2c95cd044ba42b7a32aa1e80e7e8a29 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Wed, 29 Apr 2026 11:59:55 +0200 Subject: [PATCH 3/8] src: support multiple versions in node.config.json Signed-off-by: Marco Ippolito PR-URL: https://github.com/nodejs/node/pull/63033 Reviewed-By: Pietro Marchini Reviewed-By: James M Snell --- doc/api/cli.md | 38 + doc/node-config-schema.json | 2052 +++++++++++++++-------------- lib/internal/options.js | 60 +- src/node_config_file.cc | 243 +++- src/node_config_file.h | 8 + test/parallel/test-config-file.js | 227 ++++ 6 files changed, 1593 insertions(+), 1035 deletions(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index d39c90ca6b4c..9640b15b3989 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1041,6 +1041,44 @@ The configuration file supports namespace-specific options: * Namespace fields like `test`, `watch`, and `permission` contain configuration specific to that subsystem. +The configuration file can target a specific Node.js major version with +`nodeVersion`: + +```json +{ + "nodeVersion": 25, + "nodeOptions": { + "watch-path": "src" + } +} +``` + +To keep multiple version-specific configurations in the same file, use the +`configs` array. Node.js will use the first entry whose `nodeVersion` matches +the current Node.js major version: + +```json +{ + "$schema": "https://nodejs.org/dist/latest-v26.x/docs/node-config-schema.json", + "configs": [ + { + "nodeVersion": 25, + "config": { + "$schema": "https://nodejs.org/dist/latest-v25.x/docs/node-config-schema.json", + "nodeOptions": { + "watch-path": "src" + } + } + } + ] +} +``` + +When `configs` is used, the top level may only contain `$schema` and +`configs`. Each `configs` item must define an integer `nodeVersion` and an +object `config`. A single top-level config does not require `nodeVersion`, but +if present it must match the current Node.js major version. + 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 diff --git a/doc/node-config-schema.json b/doc/node-config-schema.json index e0a104a11fe7..52581fbd1d54 100644 --- a/doc/node-config-schema.json +++ b/doc/node-config-schema.json @@ -1,1031 +1,1071 @@ { "$schema": "https://json-schema.org/draft/2020-12/schema", - "additionalProperties": false, - "required": [], - "properties": { - "$schema": { - "type": "string" + "oneOf": [ + { + "$ref": "#/$defs/config" }, - "nodeOptions": { - "additionalProperties": false, - "required": [], - "properties": { - "addons": { - "type": "boolean", - "description": "disable loading native addons" - }, - "allow-addons": { - "type": "boolean", - "description": "allow use of addons when any permissions are set" - }, - "allow-child-process": { - "type": "boolean", - "description": "allow use of child process when any permissions are set" - }, - "allow-fs-read": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "allow permissions to read the filesystem" - }, - "allow-fs-write": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "allow permissions to write in the filesystem" - }, - "allow-inspector": { - "type": "boolean", - "description": "allow use of inspector when any permissions are set" - }, - "allow-openssl-store": { - "type": "boolean", - "description": "allow use of OpenSSL STORE loaders when any permissions are set" - }, - "allow-wasi": { - "type": "boolean", - "description": "allow wasi when any permissions are set" - }, - "allow-worker": { - "type": "boolean", - "description": "allow worker threads when any permissions are set" - }, - "async-context-frame": { - "type": "boolean", - "description": "Improve AsyncLocalStorage performance with AsyncContextFrame" - }, - "conditions": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "additional user conditions for conditional exports and imports" - }, - "cpu-prof": { - "type": "boolean", - "description": "Start the V8 CPU profiler on start up, and write the CPU profile to disk before exit. If --cpu-prof-dir is not specified, write the profile to the current working directory." - }, - "cpu-prof-dir": { - "type": "string", - "description": "Directory where the V8 profiles generated by --cpu-prof will be placed. Does not affect --prof." - }, - "cpu-prof-interval": { - "type": "number", - "description": "specified sampling interval in microseconds for the V8 CPU profile generated with --cpu-prof. (default: 1000)" - }, - "cpu-prof-name": { - "type": "string", - "description": "specified file name of the V8 CPU profile generated with --cpu-prof" - }, - "debug-arraybuffer-allocations": { - "type": "boolean", - "description": "" - }, - "deprecation": { - "type": "boolean", - "description": "silence deprecation warnings" - }, - "diagnostic-dir": { - "type": "string", - "description": "set dir for all output files (default: current working directory)" - }, - "disable-proto": { - "type": "string", - "description": "disable Object.prototype.__proto__" - }, - "disable-sigusr1": { - "type": "boolean", - "description": "Disable inspector thread to be listening for SIGUSR1 signal" - }, - "disable-warning": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "silence specific process warnings" - }, - "disable-wasm-trap-handler": { - "type": "boolean", - "description": "Disable trap-handler-based WebAssembly bound checks. V8 will insert inline bound checks when compiling WebAssembly which may slow down performance." - }, - "dns-result-order": { - "type": "string", - "description": "set default value of verbatim in dns.lookup. Options are 'ipv4first' (IPv4 addresses are placed before IPv6 addresses) 'ipv6first' (IPv6 addresses are placed before IPv4 addresses) 'verbatim' (addresses are in the order the DNS resolver returned)" - }, - "enable-fips": { - "type": "boolean", - "description": "enable FIPS crypto at startup" - }, - "enable-source-maps": { - "type": "boolean", - "description": "Source Map V3 support for stack traces" - }, - "entry-url": { - "type": "boolean", - "description": "Treat the entrypoint as a URL" - }, - "experimental-addon-modules": { - "type": "boolean", - "description": "experimental import support for addons" - }, - "experimental-detect-module": { - "type": "boolean", - "description": "when ambiguous modules fail to evaluate because they contain ES module syntax, try again to evaluate them as ES modules" - }, - "experimental-eventsource": { - "type": "boolean", - "description": "experimental EventSource API" - }, - "experimental-global-navigator": { - "type": "boolean", - "description": "expose experimental Navigator API on the global scope" - }, - "experimental-import-meta-resolve": { - "type": "boolean", - "description": "experimental ES Module import.meta.resolve() parentURL support" - }, - "experimental-loader": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "use the specified module as a custom loader" - }, - "experimental-print-required-tla": { - "type": "boolean", - "description": "Print pending top-level await. If --require-module is true, evaluate asynchronous graphs loaded by `require()` but do not run the microtasks, in order to find and print top-level await in the graph" - }, - "experimental-repl-await": { - "type": "boolean", - "description": "experimental await keyword support in REPL" - }, - "experimental-require-module": { - "type": "boolean", - "description": "Legacy alias for --require-module" - }, - "experimental-shadow-realm": { - "type": "boolean", - "description": "" - }, - "experimental-sqlite": { - "type": "boolean", - "description": "experimental node:sqlite module" - }, - "experimental-transform-types": { - "type": "boolean", - "description": "enable transformation of TypeScript-onlysyntax into JavaScript code" - }, - "experimental-vm-modules": { - "type": "boolean", - "description": "experimental ES Module support in vm module" - }, - "experimental-websocket": { - "type": "boolean", - "description": "experimental WebSocket API" - }, - "experimental-webstorage": { - "type": "boolean", - "description": "experimental Web Storage API" - }, - "extra-info-on-fatal-exception": { - "type": "boolean", - "description": "hide extra information on fatal exception that causes exit" - }, - "force-async-hooks-checks": { - "type": "boolean", - "description": "disable checks for async_hooks" - }, - "force-context-aware": { - "type": "boolean", - "description": "disable loading non-context-aware addons" - }, - "force-fips": { - "type": "boolean", - "description": "force FIPS crypto (cannot be disabled)" - }, - "force-node-api-uncaught-exceptions-policy": { - "type": "boolean", - "description": "enforces 'uncaughtException' event on Node API asynchronous callbacks" - }, - "frozen-intrinsics": { - "type": "boolean", - "description": "experimental frozen intrinsics support" - }, - "global-search-paths": { - "type": "boolean", - "description": "disable global module search paths" - }, - "heap-prof": { - "type": "boolean", - "description": "Start the V8 heap profiler on start up, and write the heap profile to disk before exit. If --heap-prof-dir is not specified, write the profile to the current working directory." - }, - "heap-prof-dir": { - "type": "string", - "description": "Directory where the V8 heap profiles generated by --heap-prof will be placed." - }, - "heap-prof-interval": { - "type": "number", - "description": "specified sampling interval in bytes for the V8 heap profile generated with --heap-prof. (default: 512 * 1024)" - }, - "heap-prof-name": { - "type": "string", - "description": "specified file name of the V8 heap profile generated with --heap-prof" - }, - "heapsnapshot-near-heap-limit": { - "type": "number", - "description": "Generate heap snapshots whenever V8 is approaching the heap limit. No more than the specified number of heap snapshots will be generated." - }, - "heapsnapshot-signal": { - "type": "string", - "description": "Generate heap snapshot on specified signal" - }, - "icu-data-dir": { - "type": "string", - "description": "set ICU data load path to dir (overrides NODE_ICU_DATA) (note: linked-in ICU data is present)" - }, - "import": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "ES module to preload (option can be repeated)" - }, - "input-type": { - "type": "string", - "description": "set module type for string input" - }, - "insecure-http-parser": { - "type": "boolean", - "description": "use an insecure HTTP parser that accepts invalid HTTP headers" - }, - "inspect": { - "type": "boolean", - "description": "activate inspector on host:port (default: 127.0.0.1:9229)" - }, - "inspect-brk": { - "type": "boolean", - "description": "activate inspector on host:port and break at start of user script" - }, - "inspect-port": { - "type": "number", - "description": "set host:port for inspector" - }, - "inspect-publish-uid": { - "type": "string", - "description": "comma separated list of destinations for inspector uid(default: stderr,http)" - }, - "inspect-wait": { - "type": "boolean", - "description": "activate inspector on host:port and wait for debugger to be attached" - }, - "localstorage-file": { - "type": "string", - "description": "file used to persist localStorage data" - }, - "max-http-header-size": { - "type": "number", - "description": "set the maximum size of HTTP headers (default: 16384 (16KB))" - }, - "max-old-space-size-percentage": { - "type": "string", - "description": "set V8's max old space size as a percentage of available memory (e.g., '50%'). Takes precedence over --max-old-space-size." - }, - "network-family-autoselection": { - "type": "boolean", - "description": "Disable network address family autodetection algorithm" - }, - "network-family-autoselection-attempt-timeout": { - "type": "number", - "description": "Sets the default value for the network family autoselection attempt timeout." - }, - "node-snapshot": { - "type": "boolean", - "description": "" - }, - "openssl-config": { - "type": "string", - "description": "load OpenSSL configuration from the specified file (overrides OPENSSL_CONF)" - }, - "openssl-legacy-provider": { - "type": "boolean", - "description": "enable OpenSSL 3.0 legacy provider" - }, - "openssl-shared-config": { - "type": "boolean", - "description": "enable OpenSSL shared configuration" - }, - "pending-deprecation": { - "type": "boolean", - "description": "emit pending deprecation warnings" - }, - "permission": { - "type": "boolean", - "description": "enable the permission system" - }, - "preserve-symlinks": { - "type": "boolean", - "description": "preserve symbolic links when resolving" - }, - "preserve-symlinks-main": { - "type": "boolean", - "description": "preserve symbolic links when resolving the main module" - }, - "redirect-warnings": { - "type": "string", - "description": "write warnings to file instead of stderr" - }, - "report-compact": { - "type": "boolean", - "description": "output compact single-line JSON" - }, - "report-dir": { - "type": "string", - "description": "define custom report pathname. (default: current working directory)" - }, - "report-exclude-env": { - "type": "boolean", - "description": "Exclude environment variables when generating report (default: false)" - }, - "report-exclude-network": { - "type": "boolean", - "description": "exclude network interface diagnostics. (default: false)" - }, - "report-filename": { - "type": "string", - "description": "define custom report file name. (default: YYYYMMDD.HHMMSS.PID.SEQUENCE#.txt)" - }, - "report-on-fatalerror": { - "type": "boolean", - "description": "generate diagnostic report on fatal (internal) errors" - }, - "report-on-signal": { - "type": "boolean", - "description": "generate diagnostic report upon receiving signals" - }, - "report-signal": { - "type": "string", - "description": "causes diagnostic report to be produced on provided signal, unsupported in Windows. (default: SIGUSR2)" - }, - "report-uncaught-exception": { - "type": "boolean", - "description": "generate diagnostic report on uncaught exceptions" - }, - "require": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "CommonJS module to preload (option can be repeated)" - }, - "require-module": { - "type": "boolean", - "description": "Allow loading synchronous ES Modules in require()." - }, - "secure-heap": { - "type": "number", - "description": "total size of the OpenSSL secure heap" - }, - "secure-heap-min": { - "type": "number", - "description": "minimum allocation size from the OpenSSL secure heap" - }, - "snapshot-blob": { - "type": "string", - "description": "Path to the snapshot blob that's either the result of snapshotbuilding, or the blob that is used to restore the application state" - }, - "stack-trace-limit": { - "type": "number", - "description": "" - }, - "strip-types": { - "type": "boolean", - "description": "Type-stripping for TypeScript files." - }, - "test-coverage-branches": { - "type": "number", - "description": "the branch coverage minimum threshold" - }, - "test-coverage-exclude": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "exclude files from coverage report that match this glob pattern" - }, - "test-coverage-functions": { - "type": "number", - "description": "the function coverage minimum threshold" - }, - "test-coverage-include": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "include files in coverage report that match this glob pattern" - }, - "test-coverage-lines": { - "type": "number", - "description": "the line coverage minimum threshold" - }, - "test-global-setup": { - "type": "string", - "description": "specifies the path to the global setup file" - }, - "test-isolation": { - "type": "string", - "description": "configures the type of test isolation used in the test runner" - }, - "test-name-pattern": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "run tests whose name matches this regular expression" - }, - "test-only": { - "type": "boolean", - "description": "run tests with 'only' option set" - }, - "test-random-seed": { - "type": "number", - "description": "seed used to randomize test execution order" - }, - "test-randomize": { - "type": "boolean", - "description": "run tests in a random order" - }, - "test-reporter": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "report test output using the given reporter" - }, - "test-reporter-destination": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "report given reporter to the given destination" - }, - "test-rerun-failures": { - "type": "string", - "description": "specifies the path to the rerun state file" - }, - "test-shard": { - "type": "string", - "description": "run test at specific shard" - }, - "test-skip-pattern": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "run tests whose name do not match this regular expression" - }, - "throw-deprecation": { - "type": "boolean", - "description": "throw an exception on deprecations" - }, - "title": { - "type": "string", - "description": "the process title to use on startup" - }, - "tls-cipher-list": { - "type": "string", - "description": "use an alternative default TLS cipher list" - }, - "tls-keylog": { - "type": "string", - "description": "log TLS decryption keys to named file for traffic analysis" - }, - "tls-max-v1.2": { - "type": "boolean", - "description": "set default TLS maximum to TLSv1.2 (default: TLSv1.3)" - }, - "tls-max-v1.3": { - "type": "boolean", - "description": "set default TLS maximum to TLSv1.3 (default: TLSv1.3)" - }, - "tls-min-v1.0": { - "type": "boolean", - "description": "set default TLS minimum to TLSv1.0 (default: TLSv1.2)" - }, - "tls-min-v1.1": { - "type": "boolean", - "description": "set default TLS minimum to TLSv1.1 (default: TLSv1.2)" - }, - "tls-min-v1.2": { - "type": "boolean", - "description": "set default TLS minimum to TLSv1.2 (default: TLSv1.2)" - }, - "tls-min-v1.3": { - "type": "boolean", - "description": "set default TLS minimum to TLSv1.3 (default: TLSv1.2)" - }, - "trace-deprecation": { - "type": "boolean", - "description": "show stack traces on deprecations" - }, - "trace-env": { - "type": "boolean", - "description": "Print accesses to the environment variables" - }, - "trace-env-js-stack": { - "type": "boolean", - "description": "Print accesses to the environment variables and the JavaScript stack trace" - }, - "trace-env-native-stack": { - "type": "boolean", - "description": "Print accesses to the environment variables and the native stack trace" - }, - "trace-event-categories": { - "type": "string", - "description": "comma separated list of trace event categories to record" - }, - "trace-event-file-pattern": { - "type": "string", - "description": "Template string specifying the filepath for the trace-events data, it supports ${rotation} and ${pid}." - }, - "trace-exit": { - "type": "boolean", - "description": "show stack trace when an environment exits" - }, - "trace-promises": { - "type": "boolean", - "description": "show stack traces on promise initialization and resolution" - }, - "trace-require-module": { - "type": "string", - "description": "Print access to require(esm). Options are 'all' (print all usage) and 'no-node-modules' (excluding usage from the node_modules folder)" - }, - "trace-sigint": { - "type": "boolean", - "description": "enable printing JavaScript stacktrace on SIGINT" - }, - "trace-sync-io": { - "type": "boolean", - "description": "show stack trace when use of sync IO is detected after the first tick" - }, - "trace-tls": { - "type": "boolean", - "description": "prints TLS packet trace information to stderr" - }, - "trace-uncaught": { - "type": "boolean", - "description": "show stack traces for the `throw` behind uncaught exceptions" - }, - "trace-warnings": { - "type": "boolean", - "description": "show stack traces on process warnings" - }, - "track-heap-objects": { - "type": "boolean", - "description": "track heap object allocations for heap snapshots" - }, - "unhandled-rejections": { - "type": "string", - "description": "define unhandled rejections behavior. Options are 'strict' (always raise an error), 'throw' (raise an error unless 'unhandledRejection' hook is set), 'warn' (log a warning), 'none' (silence warnings), 'warn-with-error-code' (log a warning and set exit code 1 unless 'unhandledRejection' hook is set). (default: throw)" - }, - "use-bundled-ca": { - "type": "boolean", - "description": "use bundled CA store (default)" - }, - "use-env-proxy": { - "type": "boolean", - "description": "parse proxy settings from HTTP_PROXY/HTTPS_PROXY/NO_PROXYenvironment variables and apply the setting in global HTTP/HTTPS clients" - }, - "use-largepages": { - "type": "string", - "description": "Map the Node.js static code to large pages. Options are 'off' (the default value, meaning do not map), 'on' (map and ignore failure, reporting it to stderr), or 'silent' (map and silently ignore failure)" - }, - "use-openssl-ca": { - "type": "boolean", - "description": "use OpenSSL's default CA store" - }, - "use-system-ca": { - "type": "boolean", - "description": "use system's CA store" - }, - "v8-pool-size": { - "type": "number", - "description": "set V8's thread pool size" - }, - "verify-base-objects": { - "type": "boolean", - "description": "" - }, - "warnings": { - "type": "boolean", - "description": "silence all process warnings" - }, - "watch": { - "type": "boolean", - "description": "run in watch mode" - }, - "watch-kill-signal": { - "type": "string", - "description": "kill signal to send to the process on watch mode restarts(default: SIGTERM)" - }, - "watch-path": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "path to watch" - }, - "watch-preserve-output": { - "type": "boolean", - "description": "preserve outputs on watch mode restart" - }, - "zero-fill-buffers": { - "type": "boolean", - "description": "automatically zero-fill all newly allocated Buffer instances" - } - }, - "type": "object" - }, - "permission": { + { "type": "object", "additionalProperties": false, - "required": [], + "required": [ + "configs" + ], "properties": { - "allow-addons": { - "type": "boolean", - "description": "allow use of addons when any permissions are set" - }, - "allow-child-process": { - "type": "boolean", - "description": "allow use of child process when any permissions are set" - }, - "allow-fs-read": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" + "$schema": { + "type": "string" + }, + "configs": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "nodeVersion", + "config" + ], + "properties": { + "nodeVersion": { + "type": "integer" + }, + "config": { + "$ref": "#/$defs/config" } } - ], - "description": "allow permissions to read the filesystem" - }, - "allow-fs-write": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "allow permissions to write in the filesystem" - }, - "allow-inspector": { - "type": "boolean", - "description": "allow use of inspector when any permissions are set" - }, - "allow-openssl-store": { - "type": "boolean", - "description": "allow use of OpenSSL STORE loaders when any permissions are set" - }, - "allow-wasi": { - "type": "boolean", - "description": "allow wasi when any permissions are set" - }, - "allow-worker": { - "type": "boolean", - "description": "allow worker threads when any permissions are set" - }, - "permission": { - "type": "boolean", - "description": "enable the permission system" + } } } - }, - "test": { - "type": "object", + } + ], + "$defs": { + "config": { "additionalProperties": false, "required": [], "properties": { - "experimental-test-coverage": { - "type": "boolean", - "description": "enable code coverage in the test runner" - }, - "experimental-test-module-mocks": { - "type": "boolean", - "description": "enable module mocking in the test runner" - }, - "test": { - "type": "boolean", - "description": "launch test runner on startup" - }, - "test": { - "type": "boolean" - }, - "test-concurrency": { - "type": "number", - "description": "specify test runner concurrency" - }, - "test-coverage-branches": { - "type": "number", - "description": "the branch coverage minimum threshold" - }, - "test-coverage-exclude": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "exclude files from coverage report that match this glob pattern" - }, - "test-coverage-functions": { - "type": "number", - "description": "the function coverage minimum threshold" - }, - "test-coverage-include": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "include files in coverage report that match this glob pattern" - }, - "test-coverage-lines": { - "type": "number", - "description": "the line coverage minimum threshold" - }, - "test-force-exit": { - "type": "boolean", - "description": "force test runner to exit upon completion" - }, - "test-global-setup": { - "type": "string", - "description": "specifies the path to the global setup file" - }, - "test-isolation": { - "type": "string", - "description": "configures the type of test isolation used in the test runner" - }, - "test-name-pattern": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } + "$schema": { + "type": "string" + }, + "nodeOptions": { + "additionalProperties": false, + "required": [], + "properties": { + "addons": { + "type": "boolean", + "description": "disable loading native addons" + }, + "allow-addons": { + "type": "boolean", + "description": "allow use of addons when any permissions are set" + }, + "allow-child-process": { + "type": "boolean", + "description": "allow use of child process when any permissions are set" + }, + "allow-fs-read": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "allow permissions to read the filesystem" + }, + "allow-fs-write": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "allow permissions to write in the filesystem" + }, + "allow-inspector": { + "type": "boolean", + "description": "allow use of inspector when any permissions are set" + }, + "allow-openssl-store": { + "type": "boolean", + "description": "allow use of OpenSSL STORE loaders when any permissions are set" + }, + "allow-wasi": { + "type": "boolean", + "description": "allow wasi when any permissions are set" + }, + "allow-worker": { + "type": "boolean", + "description": "allow worker threads when any permissions are set" + }, + "async-context-frame": { + "type": "boolean", + "description": "Improve AsyncLocalStorage performance with AsyncContextFrame" + }, + "conditions": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "additional user conditions for conditional exports and imports" + }, + "cpu-prof": { + "type": "boolean", + "description": "Start the V8 CPU profiler on start up, and write the CPU profile to disk before exit. If --cpu-prof-dir is not specified, write the profile to the current working directory." + }, + "cpu-prof-dir": { + "type": "string", + "description": "Directory where the V8 profiles generated by --cpu-prof will be placed. Does not affect --prof." + }, + "cpu-prof-interval": { + "type": "number", + "description": "specified sampling interval in microseconds for the V8 CPU profile generated with --cpu-prof. (default: 1000)" + }, + "cpu-prof-name": { + "type": "string", + "description": "specified file name of the V8 CPU profile generated with --cpu-prof" + }, + "debug-arraybuffer-allocations": { + "type": "boolean", + "description": "" + }, + "deprecation": { + "type": "boolean", + "description": "silence deprecation warnings" + }, + "diagnostic-dir": { + "type": "string", + "description": "set dir for all output files (default: current working directory)" + }, + "disable-proto": { + "type": "string", + "description": "disable Object.prototype.__proto__" + }, + "disable-sigusr1": { + "type": "boolean", + "description": "Disable inspector thread to be listening for SIGUSR1 signal" + }, + "disable-warning": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "silence specific process warnings" + }, + "disable-wasm-trap-handler": { + "type": "boolean", + "description": "Disable trap-handler-based WebAssembly bound checks. V8 will insert inline bound checks when compiling WebAssembly which may slow down performance." + }, + "dns-result-order": { + "type": "string", + "description": "set default value of verbatim in dns.lookup. Options are 'ipv4first' (IPv4 addresses are placed before IPv6 addresses) 'ipv6first' (IPv6 addresses are placed before IPv4 addresses) 'verbatim' (addresses are in the order the DNS resolver returned)" + }, + "enable-fips": { + "type": "boolean", + "description": "enable FIPS crypto at startup" + }, + "enable-source-maps": { + "type": "boolean", + "description": "Source Map V3 support for stack traces" + }, + "entry-url": { + "type": "boolean", + "description": "Treat the entrypoint as a URL" + }, + "experimental-addon-modules": { + "type": "boolean", + "description": "experimental import support for addons" + }, + "experimental-detect-module": { + "type": "boolean", + "description": "when ambiguous modules fail to evaluate because they contain ES module syntax, try again to evaluate them as ES modules" + }, + "experimental-eventsource": { + "type": "boolean", + "description": "experimental EventSource API" + }, + "experimental-global-navigator": { + "type": "boolean", + "description": "expose experimental Navigator API on the global scope" + }, + "experimental-import-meta-resolve": { + "type": "boolean", + "description": "experimental ES Module import.meta.resolve() parentURL support" + }, + "experimental-loader": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "use the specified module as a custom loader" + }, + "experimental-print-required-tla": { + "type": "boolean", + "description": "Print pending top-level await. If --require-module is true, evaluate asynchronous graphs loaded by `require()` but do not run the microtasks, in order to find and print top-level await in the graph" + }, + "experimental-repl-await": { + "type": "boolean", + "description": "experimental await keyword support in REPL" + }, + "experimental-require-module": { + "type": "boolean", + "description": "Legacy alias for --require-module" + }, + "experimental-shadow-realm": { + "type": "boolean", + "description": "" + }, + "experimental-sqlite": { + "type": "boolean", + "description": "experimental node:sqlite module" + }, + "experimental-transform-types": { + "type": "boolean", + "description": "enable transformation of TypeScript-onlysyntax into JavaScript code" + }, + "experimental-vm-modules": { + "type": "boolean", + "description": "experimental ES Module support in vm module" + }, + "experimental-websocket": { + "type": "boolean", + "description": "experimental WebSocket API" + }, + "experimental-webstorage": { + "type": "boolean", + "description": "experimental Web Storage API" + }, + "extra-info-on-fatal-exception": { + "type": "boolean", + "description": "hide extra information on fatal exception that causes exit" + }, + "force-async-hooks-checks": { + "type": "boolean", + "description": "disable checks for async_hooks" + }, + "force-context-aware": { + "type": "boolean", + "description": "disable loading non-context-aware addons" + }, + "force-fips": { + "type": "boolean", + "description": "force FIPS crypto (cannot be disabled)" + }, + "force-node-api-uncaught-exceptions-policy": { + "type": "boolean", + "description": "enforces 'uncaughtException' event on Node API asynchronous callbacks" + }, + "frozen-intrinsics": { + "type": "boolean", + "description": "experimental frozen intrinsics support" + }, + "global-search-paths": { + "type": "boolean", + "description": "disable global module search paths" + }, + "heap-prof": { + "type": "boolean", + "description": "Start the V8 heap profiler on start up, and write the heap profile to disk before exit. If --heap-prof-dir is not specified, write the profile to the current working directory." + }, + "heap-prof-dir": { + "type": "string", + "description": "Directory where the V8 heap profiles generated by --heap-prof will be placed." + }, + "heap-prof-interval": { + "type": "number", + "description": "specified sampling interval in bytes for the V8 heap profile generated with --heap-prof. (default: 512 * 1024)" + }, + "heap-prof-name": { + "type": "string", + "description": "specified file name of the V8 heap profile generated with --heap-prof" + }, + "heapsnapshot-near-heap-limit": { + "type": "number", + "description": "Generate heap snapshots whenever V8 is approaching the heap limit. No more than the specified number of heap snapshots will be generated." + }, + "heapsnapshot-signal": { + "type": "string", + "description": "Generate heap snapshot on specified signal" + }, + "icu-data-dir": { + "type": "string", + "description": "set ICU data load path to dir (overrides NODE_ICU_DATA) (note: linked-in ICU data is present)" + }, + "import": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "ES module to preload (option can be repeated)" + }, + "input-type": { + "type": "string", + "description": "set module type for string input" + }, + "insecure-http-parser": { + "type": "boolean", + "description": "use an insecure HTTP parser that accepts invalid HTTP headers" + }, + "inspect": { + "type": "boolean", + "description": "activate inspector on host:port (default: 127.0.0.1:9229)" + }, + "inspect-brk": { + "type": "boolean", + "description": "activate inspector on host:port and break at start of user script" + }, + "inspect-port": { + "type": "number", + "description": "set host:port for inspector" + }, + "inspect-publish-uid": { + "type": "string", + "description": "comma separated list of destinations for inspector uid(default: stderr,http)" + }, + "inspect-wait": { + "type": "boolean", + "description": "activate inspector on host:port and wait for debugger to be attached" + }, + "localstorage-file": { + "type": "string", + "description": "file used to persist localStorage data" + }, + "max-http-header-size": { + "type": "number", + "description": "set the maximum size of HTTP headers (default: 16384 (16KB))" + }, + "max-old-space-size-percentage": { + "type": "string", + "description": "set V8's max old space size as a percentage of available memory (e.g., '50%'). Takes precedence over --max-old-space-size." + }, + "network-family-autoselection": { + "type": "boolean", + "description": "Disable network address family autodetection algorithm" + }, + "network-family-autoselection-attempt-timeout": { + "type": "number", + "description": "Sets the default value for the network family autoselection attempt timeout." + }, + "node-snapshot": { + "type": "boolean", + "description": "" + }, + "openssl-config": { + "type": "string", + "description": "load OpenSSL configuration from the specified file (overrides OPENSSL_CONF)" + }, + "openssl-legacy-provider": { + "type": "boolean", + "description": "enable OpenSSL 3.0 legacy provider" + }, + "openssl-shared-config": { + "type": "boolean", + "description": "enable OpenSSL shared configuration" + }, + "pending-deprecation": { + "type": "boolean", + "description": "emit pending deprecation warnings" + }, + "permission": { + "type": "boolean", + "description": "enable the permission system" + }, + "preserve-symlinks": { + "type": "boolean", + "description": "preserve symbolic links when resolving" + }, + "preserve-symlinks-main": { + "type": "boolean", + "description": "preserve symbolic links when resolving the main module" + }, + "redirect-warnings": { + "type": "string", + "description": "write warnings to file instead of stderr" + }, + "report-compact": { + "type": "boolean", + "description": "output compact single-line JSON" + }, + "report-dir": { + "type": "string", + "description": "define custom report pathname. (default: current working directory)" + }, + "report-exclude-env": { + "type": "boolean", + "description": "Exclude environment variables when generating report (default: false)" + }, + "report-exclude-network": { + "type": "boolean", + "description": "exclude network interface diagnostics. (default: false)" + }, + "report-filename": { + "type": "string", + "description": "define custom report file name. (default: YYYYMMDD.HHMMSS.PID.SEQUENCE#.txt)" + }, + "report-on-fatalerror": { + "type": "boolean", + "description": "generate diagnostic report on fatal (internal) errors" + }, + "report-on-signal": { + "type": "boolean", + "description": "generate diagnostic report upon receiving signals" + }, + "report-signal": { + "type": "string", + "description": "causes diagnostic report to be produced on provided signal, unsupported in Windows. (default: SIGUSR2)" + }, + "report-uncaught-exception": { + "type": "boolean", + "description": "generate diagnostic report on uncaught exceptions" + }, + "require": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "CommonJS module to preload (option can be repeated)" + }, + "require-module": { + "type": "boolean", + "description": "Allow loading synchronous ES Modules in require()." + }, + "secure-heap": { + "type": "number", + "description": "total size of the OpenSSL secure heap" + }, + "secure-heap-min": { + "type": "number", + "description": "minimum allocation size from the OpenSSL secure heap" + }, + "snapshot-blob": { + "type": "string", + "description": "Path to the snapshot blob that's either the result of snapshotbuilding, or the blob that is used to restore the application state" + }, + "stack-trace-limit": { + "type": "number", + "description": "" + }, + "strip-types": { + "type": "boolean", + "description": "Type-stripping for TypeScript files." + }, + "test-coverage-branches": { + "type": "number", + "description": "the branch coverage minimum threshold" + }, + "test-coverage-exclude": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "exclude files from coverage report that match this glob pattern" + }, + "test-coverage-functions": { + "type": "number", + "description": "the function coverage minimum threshold" + }, + "test-coverage-include": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "include files in coverage report that match this glob pattern" + }, + "test-coverage-lines": { + "type": "number", + "description": "the line coverage minimum threshold" + }, + "test-global-setup": { + "type": "string", + "description": "specifies the path to the global setup file" + }, + "test-isolation": { + "type": "string", + "description": "configures the type of test isolation used in the test runner" + }, + "test-name-pattern": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "run tests whose name matches this regular expression" + }, + "test-only": { + "type": "boolean", + "description": "run tests with 'only' option set" + }, + "test-random-seed": { + "type": "number", + "description": "seed used to randomize test execution order" + }, + "test-randomize": { + "type": "boolean", + "description": "run tests in a random order" + }, + "test-reporter": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "report test output using the given reporter" + }, + "test-reporter-destination": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "report given reporter to the given destination" + }, + "test-rerun-failures": { + "type": "string", + "description": "specifies the path to the rerun state file" + }, + "test-shard": { + "type": "string", + "description": "run test at specific shard" + }, + "test-skip-pattern": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "run tests whose name do not match this regular expression" + }, + "throw-deprecation": { + "type": "boolean", + "description": "throw an exception on deprecations" + }, + "title": { + "type": "string", + "description": "the process title to use on startup" + }, + "tls-cipher-list": { + "type": "string", + "description": "use an alternative default TLS cipher list" + }, + "tls-keylog": { + "type": "string", + "description": "log TLS decryption keys to named file for traffic analysis" + }, + "tls-max-v1.2": { + "type": "boolean", + "description": "set default TLS maximum to TLSv1.2 (default: TLSv1.3)" + }, + "tls-max-v1.3": { + "type": "boolean", + "description": "set default TLS maximum to TLSv1.3 (default: TLSv1.3)" + }, + "tls-min-v1.0": { + "type": "boolean", + "description": "set default TLS minimum to TLSv1.0 (default: TLSv1.2)" + }, + "tls-min-v1.1": { + "type": "boolean", + "description": "set default TLS minimum to TLSv1.1 (default: TLSv1.2)" + }, + "tls-min-v1.2": { + "type": "boolean", + "description": "set default TLS minimum to TLSv1.2 (default: TLSv1.2)" + }, + "tls-min-v1.3": { + "type": "boolean", + "description": "set default TLS minimum to TLSv1.3 (default: TLSv1.2)" + }, + "trace-deprecation": { + "type": "boolean", + "description": "show stack traces on deprecations" + }, + "trace-env": { + "type": "boolean", + "description": "Print accesses to the environment variables" + }, + "trace-env-js-stack": { + "type": "boolean", + "description": "Print accesses to the environment variables and the JavaScript stack trace" + }, + "trace-env-native-stack": { + "type": "boolean", + "description": "Print accesses to the environment variables and the native stack trace" + }, + "trace-event-categories": { + "type": "string", + "description": "comma separated list of trace event categories to record" + }, + "trace-event-file-pattern": { + "type": "string", + "description": "Template string specifying the filepath for the trace-events data, it supports ${rotation} and ${pid}." + }, + "trace-exit": { + "type": "boolean", + "description": "show stack trace when an environment exits" + }, + "trace-promises": { + "type": "boolean", + "description": "show stack traces on promise initialization and resolution" + }, + "trace-require-module": { + "type": "string", + "description": "Print access to require(esm). Options are 'all' (print all usage) and 'no-node-modules' (excluding usage from the node_modules folder)" + }, + "trace-sigint": { + "type": "boolean", + "description": "enable printing JavaScript stacktrace on SIGINT" + }, + "trace-sync-io": { + "type": "boolean", + "description": "show stack trace when use of sync IO is detected after the first tick" + }, + "trace-tls": { + "type": "boolean", + "description": "prints TLS packet trace information to stderr" + }, + "trace-uncaught": { + "type": "boolean", + "description": "show stack traces for the `throw` behind uncaught exceptions" + }, + "trace-warnings": { + "type": "boolean", + "description": "show stack traces on process warnings" + }, + "track-heap-objects": { + "type": "boolean", + "description": "track heap object allocations for heap snapshots" + }, + "unhandled-rejections": { + "type": "string", + "description": "define unhandled rejections behavior. Options are 'strict' (always raise an error), 'throw' (raise an error unless 'unhandledRejection' hook is set), 'warn' (log a warning), 'none' (silence warnings), 'warn-with-error-code' (log a warning and set exit code 1 unless 'unhandledRejection' hook is set). (default: throw)" + }, + "use-bundled-ca": { + "type": "boolean", + "description": "use bundled CA store (default)" + }, + "use-env-proxy": { + "type": "boolean", + "description": "parse proxy settings from HTTP_PROXY/HTTPS_PROXY/NO_PROXYenvironment variables and apply the setting in global HTTP/HTTPS clients" + }, + "use-largepages": { + "type": "string", + "description": "Map the Node.js static code to large pages. Options are 'off' (the default value, meaning do not map), 'on' (map and ignore failure, reporting it to stderr), or 'silent' (map and silently ignore failure)" + }, + "use-openssl-ca": { + "type": "boolean", + "description": "use OpenSSL's default CA store" + }, + "use-system-ca": { + "type": "boolean", + "description": "use system's CA store" + }, + "v8-pool-size": { + "type": "number", + "description": "set V8's thread pool size" + }, + "verify-base-objects": { + "type": "boolean", + "description": "" + }, + "warnings": { + "type": "boolean", + "description": "silence all process warnings" + }, + "watch": { + "type": "boolean", + "description": "run in watch mode" + }, + "watch-kill-signal": { + "type": "string", + "description": "kill signal to send to the process on watch mode restarts(default: SIGTERM)" + }, + "watch-path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "path to watch" + }, + "watch-preserve-output": { + "type": "boolean", + "description": "preserve outputs on watch mode restart" + }, + "zero-fill-buffers": { + "type": "boolean", + "description": "automatically zero-fill all newly allocated Buffer instances" } - ], - "description": "run tests whose name matches this regular expression" + }, + "type": "object" }, - "test-only": { - "type": "boolean", - "description": "run tests with 'only' option set" + "nodeVersion": { + "type": "integer" }, - "test-random-seed": { - "type": "number", - "description": "seed used to randomize test execution order" - }, - "test-randomize": { - "type": "boolean", - "description": "run tests in a random order" - }, - "test-reporter": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } - } - ], - "description": "report test output using the given reporter" - }, - "test-reporter-destination": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } + "permission": { + "type": "object", + "additionalProperties": false, + "required": [], + "properties": { + "allow-addons": { + "type": "boolean", + "description": "allow use of addons when any permissions are set" + }, + "allow-child-process": { + "type": "boolean", + "description": "allow use of child process when any permissions are set" + }, + "allow-fs-read": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "allow permissions to read the filesystem" + }, + "allow-fs-write": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "allow permissions to write in the filesystem" + }, + "allow-inspector": { + "type": "boolean", + "description": "allow use of inspector when any permissions are set" + }, + "allow-openssl-store": { + "type": "boolean", + "description": "allow use of OpenSSL STORE loaders when any permissions are set" + }, + "allow-wasi": { + "type": "boolean", + "description": "allow wasi when any permissions are set" + }, + "allow-worker": { + "type": "boolean", + "description": "allow worker threads when any permissions are set" + }, + "permission": { + "type": "boolean", + "description": "enable the permission system" } - ], - "description": "report given reporter to the given destination" - }, - "test-rerun-failures": { - "type": "string", - "description": "specifies the path to the rerun state file" - }, - "test-shard": { - "type": "string", - "description": "run test at specific shard" + } }, - "test-skip-pattern": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } + "test": { + "type": "object", + "additionalProperties": false, + "required": [], + "properties": { + "experimental-test-coverage": { + "type": "boolean", + "description": "enable code coverage in the test runner" + }, + "experimental-test-module-mocks": { + "type": "boolean", + "description": "enable module mocking in the test runner" + }, + "test": { + "type": "boolean" + }, + "test-concurrency": { + "type": "number", + "description": "specify test runner concurrency" + }, + "test-coverage-branches": { + "type": "number", + "description": "the branch coverage minimum threshold" + }, + "test-coverage-exclude": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "exclude files from coverage report that match this glob pattern" + }, + "test-coverage-functions": { + "type": "number", + "description": "the function coverage minimum threshold" + }, + "test-coverage-include": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "include files in coverage report that match this glob pattern" + }, + "test-coverage-lines": { + "type": "number", + "description": "the line coverage minimum threshold" + }, + "test-force-exit": { + "type": "boolean", + "description": "force test runner to exit upon completion" + }, + "test-global-setup": { + "type": "string", + "description": "specifies the path to the global setup file" + }, + "test-isolation": { + "type": "string", + "description": "configures the type of test isolation used in the test runner" + }, + "test-name-pattern": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "run tests whose name matches this regular expression" + }, + "test-only": { + "type": "boolean", + "description": "run tests with 'only' option set" + }, + "test-random-seed": { + "type": "number", + "description": "seed used to randomize test execution order" + }, + "test-randomize": { + "type": "boolean", + "description": "run tests in a random order" + }, + "test-reporter": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "report test output using the given reporter" + }, + "test-reporter-destination": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "report given reporter to the given destination" + }, + "test-rerun-failures": { + "type": "string", + "description": "specifies the path to the rerun state file" + }, + "test-shard": { + "type": "string", + "description": "run test at specific shard" + }, + "test-skip-pattern": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "run tests whose name do not match this regular expression" + }, + "test-timeout": { + "type": "number", + "description": "specify test runner timeout" + }, + "test-update-snapshots": { + "type": "boolean", + "description": "regenerate test snapshots" } - ], - "description": "run tests whose name do not match this regular expression" - }, - "test-timeout": { - "type": "number", - "description": "specify test runner timeout" + } }, - "test-update-snapshots": { - "type": "boolean", - "description": "regenerate test snapshots" - } - } - }, - "watch": { - "type": "object", - "additionalProperties": false, - "required": [], - "properties": { "watch": { - "type": "boolean", - "description": "run in watch mode" - }, - "watch-kill-signal": { - "type": "string", - "description": "kill signal to send to the process on watch mode restarts(default: SIGTERM)" - }, - "watch-path": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "minItems": 1, - "items": { - "type": "string" - } + "type": "object", + "additionalProperties": false, + "required": [], + "properties": { + "watch": { + "type": "boolean", + "description": "run in watch mode" + }, + "watch-kill-signal": { + "type": "string", + "description": "kill signal to send to the process on watch mode restarts(default: SIGTERM)" + }, + "watch-path": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + ], + "description": "path to watch" + }, + "watch-preserve-output": { + "type": "boolean", + "description": "preserve outputs on watch mode restart" } - ], - "description": "path to watch" - }, - "watch-preserve-output": { - "type": "boolean", - "description": "preserve outputs on watch mode restart" + } } - } + }, + "type": "object" } - }, - "type": "object" + } } diff --git a/lib/internal/options.js b/lib/internal/options.js index 92993d037fb6..3480f5794b76 100644 --- a/lib/internal/options.js +++ b/lib/internal/options.js @@ -65,9 +65,8 @@ function generateConfigJsonSchema() { return { __proto__: null, type, description }; } - const schema = { + const configSchema = { __proto__: null, - $schema: 'https://json-schema.org/draft/2020-12/schema', additionalProperties: false, required: [], properties: { @@ -82,13 +81,17 @@ function generateConfigJsonSchema() { properties: { __proto__: null }, type: 'object', }, + nodeVersion: { + __proto__: null, + type: 'integer', + }, __proto__: null, }, type: 'object', }; // Get the root properties object for adding namespaces - const rootProperties = schema.properties; + const rootProperties = configSchema.properties; const nodeOptions = rootProperties.nodeOptions.properties; // Add env options to nodeOptions (backward compatibility) @@ -130,7 +133,7 @@ function generateConfigJsonSchema() { ArrayPrototypeMap(sortedKeys, (key) => [key, nodeOptions[key]]), ); - schema.properties.nodeOptions.properties = sortedProperties; + configSchema.properties.nodeOptions.properties = sortedProperties; // Also sort the root level properties const sortedRootKeys = ArrayPrototypeSort(ObjectKeys(rootProperties)); @@ -138,7 +141,54 @@ function generateConfigJsonSchema() { ArrayPrototypeMap(sortedRootKeys, (key) => [key, rootProperties[key]]), ); - schema.properties = sortedRootProperties; + configSchema.properties = sortedRootProperties; + + const schema = { + __proto__: null, + $schema: 'https://json-schema.org/draft/2020-12/schema', + oneOf: [ + { __proto__: null, $ref: '#/$defs/config' }, + { + __proto__: null, + type: 'object', + additionalProperties: false, + required: ['configs'], + properties: { + __proto__: null, + $schema: { + __proto__: null, + type: 'string', + }, + configs: { + __proto__: null, + type: 'array', + minItems: 1, + items: { + __proto__: null, + type: 'object', + additionalProperties: false, + required: ['nodeVersion', 'config'], + properties: { + __proto__: null, + nodeVersion: { + __proto__: null, + type: 'integer', + }, + config: { + __proto__: null, + $ref: '#/$defs/config', + }, + }, + }, + }, + }, + }, + ], + $defs: { + __proto__: null, + config: configSchema, + }, + }; return schema; } diff --git a/src/node_config_file.cc b/src/node_config_file.cc index b2c87970b6eb..68c13ad881fc 100644 --- a/src/node_config_file.cc +++ b/src/node_config_file.cc @@ -1,7 +1,10 @@ #include "node_config_file.h" #include "debug_utils-inl.h" +#include "node_version.h" #include "simdjson.h" +#include + namespace node { constexpr std::string_view kConfigFileFlag = "--experimental-config-file"; @@ -224,39 +227,115 @@ ParseResult ConfigReader::ParseOptions( return ParseResult::Valid; } -ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { - std::string file_content; - // Read the configuration file - int r = ReadFileSync(&file_content, config_path.data()); - if (r != 0) { - const char* err = uv_strerror(r); - FPrintF( - stderr, "Cannot read configuration from %s: %s\n", config_path, err); - return ParseResult::FileError; +ParseResult ConfigReader::ParseNodeVersion( + simdjson::ondemand::value* version_value, + const std::string_view& config_path) { + int64_t version; + if (version_value->get_int64().get(version)) { + FPrintF(stderr, + "\"nodeVersion\" value unexpected for %s " + "(should be an integer)\n", + config_path.data()); + return ParseResult::InvalidContent; } - // Parse the configuration file - simdjson::ondemand::parser json_parser; - simdjson::ondemand::document document; - if (json_parser.iterate(file_content).get(document)) { - FPrintF(stderr, "Can't parse %s\n", config_path.data()); + if (version != NODE_MAJOR_VERSION) { + FPrintF(stderr, + "\"nodeVersion\" %" PRId64 + " does not match current Node.js version %d " + "for %s\n", + version, + NODE_MAJOR_VERSION, + config_path.data()); return ParseResult::InvalidContent; } - // Validate config is an object - simdjson::ondemand::object main_object; - auto root_error = document.get_object().get(main_object); - if (root_error) { - if (root_error == simdjson::error_code::INCORRECT_TYPE) { + return ParseResult::Valid; +} + +ParseResult ConfigReader::ParseConfigs(simdjson::ondemand::array* configs, + const std::string_view& config_path) { + size_t index = 0; + + for (auto raw_config : *configs) { + simdjson::ondemand::object config_wrapper; + if (raw_config.get_object().get(config_wrapper)) { FPrintF(stderr, - "Root value unexpected not an object for %s\n\n", + "\"configs[%zu]\" value unexpected for %s " + "(should be an object)\n", + index, config_path.data()); - } else { - FPrintF(stderr, "Can't parse %s\n", config_path.data()); + return ParseResult::InvalidContent; } - return ParseResult::InvalidContent; + + simdjson::ondemand::value version_value; + auto version_error = + config_wrapper.find_field_unordered("nodeVersion").get(version_value); + if (version_error == simdjson::NO_SUCH_FIELD) { + FPrintF(stderr, + "\"configs[%zu].nodeVersion\" is required for %s\n", + index, + config_path.data()); + return ParseResult::InvalidContent; + } + if (version_error) { + return ParseResult::InvalidContent; + } + + int64_t version; + if (version_value.get_int64().get(version)) { + FPrintF(stderr, + "\"configs[%zu].nodeVersion\" value unexpected for %s " + "(should be an integer)\n", + index, + config_path.data()); + return ParseResult::InvalidContent; + } + + if (version != NODE_MAJOR_VERSION) { + index++; + continue; + } + + simdjson::ondemand::value config_value; + auto config_error = + config_wrapper.find_field_unordered("config").get(config_value); + if (config_error == simdjson::NO_SUCH_FIELD) { + FPrintF(stderr, + "\"configs[%zu].config\" is required for %s\n", + index, + config_path.data()); + return ParseResult::InvalidContent; + } + if (config_error) { + return ParseResult::InvalidContent; + } + + simdjson::ondemand::object selected_config; + if (config_value.get_object().get(selected_config)) { + FPrintF(stderr, + "\"configs[%zu].config\" value unexpected for %s " + "(should be an object)\n", + index, + config_path.data()); + return ParseResult::InvalidContent; + } + + return ParseConfigObject(&selected_config, config_path, false); } + FPrintF(stderr, + "No config found for current Node.js version %d in " + "\"configs\" for %s\n", + NODE_MAJOR_VERSION, + config_path.data()); + return ParseResult::InvalidContent; +} + +ParseResult ConfigReader::ParseConfigObject( + simdjson::ondemand::object* config_object, + const std::string_view& config_path, + bool allow_version_selection) { // Get all available namespaces for validation std::vector available_namespaces = options_parser::MapAvailableNamespaces(); @@ -272,7 +351,7 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { std::unordered_set namespaces_with_implicit_flags; // Iterate through the main object to find all namespaces - for (auto field : main_object) { + for (auto field : *config_object) { std::string_view field_name; if (field.unescaped_key().get(field_name)) { return ParseResult::InvalidContent; @@ -280,6 +359,47 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { std::string namespace_name(field_name); + if (namespace_name == "$schema") { + continue; + } + + if (namespace_name == "nodeVersion") { + simdjson::ondemand::value version_value; + if (field.value().get(version_value)) { + return ParseResult::InvalidContent; + } + ParseResult result = ParseNodeVersion(&version_value, config_path); + if (result != ParseResult::Valid) { + return result; + } + continue; + } + + if (namespace_name == "configs") { + if (!allow_version_selection) { + FPrintF(stderr, + "\"configs\" is not allowed inside a versioned config " + "for %s\n", + config_path.data()); + return ParseResult::InvalidContent; + } + + simdjson::ondemand::array configs; + auto field_error = field.value().get_array().get(configs); + if (field_error) { + FPrintF(stderr, + "\"configs\" value unexpected for %s " + "(should be an array)\n", + config_path.data()); + return ParseResult::InvalidContent; + } + ParseResult result = ParseConfigs(&configs, config_path); + if (result != ParseResult::Valid) { + return result; + } + continue; + } + // TODO(@marco-ippolito): Remove warning for testRunner namespace if (namespace_name == "testRunner") { FPrintF(stderr, @@ -345,6 +465,81 @@ ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { return ParseResult::Valid; } +ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { + std::string file_content; + // Read the configuration file + int r = ReadFileSync(&file_content, config_path.data()); + if (r != 0) { + const char* err = uv_strerror(r); + FPrintF( + stderr, "Cannot read configuration from %s: %s\n", config_path, err); + return ParseResult::FileError; + } + + // Parse the configuration file + simdjson::ondemand::parser json_parser; + simdjson::ondemand::document document; + if (json_parser.iterate(file_content).get(document)) { + FPrintF(stderr, "Can't parse %s\n", config_path.data()); + return ParseResult::InvalidContent; + } + + // Validate config is an object + simdjson::ondemand::object 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", + config_path.data()); + } else { + FPrintF(stderr, "Can't parse %s\n", config_path.data()); + } + return ParseResult::InvalidContent; + } + + bool has_configs = false; + bool has_other_fields = false; + for (auto field : main_object) { + std::string_view field_name; + if (field.unescaped_key().get(field_name)) { + return ParseResult::InvalidContent; + } + + if (field_name == "$schema") { + continue; + } + + if (field_name == "configs") { + has_configs = true; + } else { + has_other_fields = true; + } + } + + if (has_configs && has_other_fields) { + FPrintF(stderr, + "\"configs\" cannot be mixed with other configuration fields " + "for %s\n", + config_path.data()); + return ParseResult::InvalidContent; + } + + simdjson::ondemand::parser config_parser; + simdjson::ondemand::document config_document; + if (config_parser.iterate(file_content).get(config_document)) { + FPrintF(stderr, "Can't parse %s\n", config_path.data()); + return ParseResult::InvalidContent; + } + + simdjson::ondemand::object config_object; + if (config_document.get_object().get(config_object)) { + return ParseResult::InvalidContent; + } + + return ParseConfigObject(&config_object, config_path, true); +} + std::string ConfigReader::GetNodeOptions() { std::string acc = ""; const size_t total_options = node_options_.size(); diff --git a/src/node_config_file.h b/src/node_config_file.h index afe0e84765b8..cc8d8e4c4c23 100644 --- a/src/node_config_file.h +++ b/src/node_config_file.h @@ -39,6 +39,14 @@ class ConfigReader { size_t GetFlagsSize(); private: + ParseResult ParseConfigObject(simdjson::ondemand::object* config_object, + const std::string_view& config_path, + bool allow_version_selection); + ParseResult ParseNodeVersion(simdjson::ondemand::value* version_value, + const std::string_view& config_path); + ParseResult ParseConfigs(simdjson::ondemand::array* configs, + const std::string_view& config_path); + // Parse options for a specific namespace (including nodeOptions for backward // compatibility) ParseResult ParseOptions(simdjson::ondemand::object* options_object, diff --git a/test/parallel/test-config-file.js b/test/parallel/test-config-file.js index 1adc1e2fe017..51cf2f16dab0 100644 --- a/test/parallel/test-config-file.js +++ b/test/parallel/test-config-file.js @@ -55,6 +55,233 @@ test('should handle empty object json', async () => { assert.strictEqual(result.code, 0); }); +describe('runtime version checks', () => { + const currentMajor = Number(process.versions.node.split('.')[0]); + + async function runConfig(config, filename = 'version-config.json') { + tmpdir.refresh(); + const configPath = join(tmpdir.path, filename); + writeFileSync(configPath, JSON.stringify(config)); + + return spawnPromisified(process.execPath, [ + '--no-warnings', + `--experimental-config-file=${configPath}`, + '-p', 'http.maxHeaderSize', + ]); + } + + it('should accept a top-level config without nodeVersion', async () => { + const result = await runConfig({ + nodeOptions: { 'max-http-header-size': 10 }, + }, 'top-level-without-version.json'); + assert.strictEqual(result.stderr, ''); + assert.strictEqual(result.stdout, '10\n'); + assert.strictEqual(result.code, 0); + }); + + it('should accept a config file matching the current Node.js version', async () => { + const result = await runConfig({ + nodeVersion: currentMajor, + nodeOptions: { 'max-http-header-size': 10 }, + }, 'matching-version.json'); + assert.strictEqual(result.stderr, ''); + assert.strictEqual(result.stdout, '10\n'); + assert.strictEqual(result.code, 0); + }); + + it('should reject a config file targeting another Node.js version', async () => { + const result = await runConfig({ + nodeVersion: currentMajor + 1, + nodeOptions: { 'max-http-header-size': 10 }, + }, 'mismatching-version.json'); + assert.match(result.stderr, /"nodeVersion" \d+ does not match current Node\.js version \d+/); + assert.strictEqual(result.stdout, ''); + assert.strictEqual(result.code, 9); + }); + + it('should select a matching config from configs', async () => { + const result = await runConfig({ + configs: [ + { + nodeVersion: currentMajor + 1, + config: { + nodeOptions: { 'max-http-header-size': 20 }, + }, + }, + { + nodeVersion: currentMajor, + config: { + nodeOptions: { 'max-http-header-size': 10 }, + }, + }, + ], + }, 'versioned-configs.json'); + assert.strictEqual(result.stderr, ''); + assert.strictEqual(result.stdout, '10\n'); + assert.strictEqual(result.code, 0); + }); + + it('should reject configs without an entry for the current version', async () => { + const result = await runConfig({ + configs: [ + { + nodeVersion: currentMajor + 1, + config: { + nodeOptions: { 'max-http-header-size': 10 }, + }, + }, + ], + }, 'missing-versioned-config.json'); + assert.match(result.stderr, /No config found for current Node\.js version \d+ in "configs"/); + assert.strictEqual(result.stdout, ''); + assert.strictEqual(result.code, 9); + }); + + it('should ignore invalid config payloads for non-matching versions', async () => { + const result = await runConfig({ + configs: [ + { + nodeVersion: currentMajor + 1, + config: false, + }, + { + nodeVersion: currentMajor, + config: { + nodeOptions: { 'max-http-header-size': 10 }, + }, + }, + ], + }, 'ignored-non-matching-config.json'); + assert.strictEqual(result.stderr, ''); + assert.strictEqual(result.stdout, '10\n'); + assert.strictEqual(result.code, 0); + }); + + it('should use the first matching config from configs', async () => { + const result = await runConfig({ + configs: [ + { + nodeVersion: currentMajor, + config: { + nodeOptions: { 'max-http-header-size': 10 }, + }, + }, + { + nodeVersion: currentMajor, + config: { + nodeOptions: { 'max-http-header-size': 20 }, + }, + }, + ], + }, 'first-matching-versioned-config.json'); + assert.strictEqual(result.stderr, ''); + assert.strictEqual(result.stdout, '10\n'); + assert.strictEqual(result.code, 0); + }); + + it('should allow $schema with configs', async () => { + const result = await runConfig({ + $schema: 'https://nodejs.org/dist/vX.Y.Z/docs/node-config-schema.json', + configs: [ + { + nodeVersion: currentMajor, + config: { + $schema: 'https://nodejs.org/dist/vX.Y.Z/docs/node-config-schema.json', + nodeOptions: { 'max-http-header-size': 10 }, + }, + }, + ], + }, 'schema-with-versioned-config.json'); + assert.strictEqual(result.stderr, ''); + assert.strictEqual(result.stdout, '10\n'); + assert.strictEqual(result.code, 0); + }); + + for (const { name, config, error } of [ + { + name: 'configs is empty', + config: { configs: [] }, + error: /No config found for current Node\.js version \d+ in "configs"/, + }, + { + name: 'configs is not an array', + config: { configs: {} }, + error: /"configs" value unexpected .* \(should be an array\)/, + }, + { + name: 'configs contains a non-object entry', + config: { configs: [false] }, + error: /"configs\[0\]" value unexpected .* \(should be an object\)/, + }, + { + name: 'configs entry is missing nodeVersion', + config: { configs: [{ config: {} }] }, + error: /"configs\[0\]\.nodeVersion" is required/, + }, + { + name: 'configs entry has a non-integer nodeVersion', + config: { configs: [{ nodeVersion: `${currentMajor}`, config: {} }] }, + error: /"configs\[0\]\.nodeVersion" value unexpected .* \(should be an integer\)/, + }, + { + name: 'matching configs entry is missing config', + config: { configs: [{ nodeVersion: currentMajor }] }, + error: /"configs\[0\]\.config" is required/, + }, + { + name: 'matching configs entry has a non-object config', + config: { configs: [{ nodeVersion: currentMajor, config: false }] }, + error: /"configs\[0\]\.config" value unexpected .* \(should be an object\)/, + }, + { + name: 'configs is mixed with preceding config fields', + config: { + nodeOptions: { 'max-http-header-size': 10 }, + configs: [{ nodeVersion: currentMajor, config: {} }], + }, + error: /"configs" cannot be mixed with other configuration fields/, + }, + { + name: 'configs is mixed with following config fields', + config: { + configs: [{ nodeVersion: currentMajor, config: {} }], + nodeOptions: { 'max-http-header-size': 10 }, + }, + error: /"configs" cannot be mixed with other configuration fields/, + }, + { + name: 'configs is nested inside a selected config', + config: { + configs: [{ + nodeVersion: currentMajor, + config: { configs: [] }, + }], + }, + error: /"configs" is not allowed inside a versioned config/, + }, + { + name: 'selected config targets another version', + config: { + configs: [{ + nodeVersion: currentMajor, + config: { + nodeVersion: currentMajor + 1, + nodeOptions: { 'max-http-header-size': 10 }, + }, + }], + }, + error: /"nodeVersion" \d+ does not match current Node\.js version \d+/, + }, + ]) { + it(`should reject when ${name}`, async () => { + const result = await runConfig(config); + assert.match(result.stderr, error); + assert.strictEqual(result.stdout, ''); + assert.strictEqual(result.code, 9); + }); + } +}); + test('should parse boolean flag', onlyWithAmaroAndNodeOptions, async () => { const result = await spawnPromisified(process.execPath, [ `--experimental-config-file=${fixtures.path('rc/transform-types.json')}`, From 7692237f287d3267b2f1db0c77b336f970704114 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Sun, 2 Aug 2026 10:53:34 +0200 Subject: [PATCH 4/8] doc: mark config-file as release candidate Signed-off-by: Marco Ippolito PR-URL: https://github.com/nodejs/node/pull/64516 Reviewed-By: Luigi Pinca --- doc/api/cli.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/api/cli.md b/doc/api/cli.md index 9640b15b3989..5d862934fa2d 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -1000,9 +1000,13 @@ Enable experimental import support for `.node` addons. -> Stability: 1.0 - Early development +> Stability: 1.2 - Release candidate If present, Node.js will look for a configuration file at the specified path. If the path is not specified, Node.js will look for a `node.config.json` file From f74dfe1bb2d2d9b09e81a80a979ce3b9f95cfb11 Mon Sep 17 00:00:00 2001 From: Marco Ippolito Date: Thu, 1 Oct 2026 11:47:11 +0200 Subject: [PATCH 5/8] src: mark config file as stable Signed-off-by: Marco Ippolito PR-URL: https://github.com/nodejs/node/pull/66431 Reviewed-By: Matteo Collina Reviewed-By: Colin Ihrig Reviewed-By: Jacob Smith Reviewed-By: Edy Silva Reviewed-By: James M Snell Reviewed-By: Pietro Marchini --- doc/api/cli.md | 322 +++++++++--------- doc/api/permissions.md | 9 +- doc/api/test.md | 5 +- doc/node.1 | 194 +++++++---- lib/internal/debugger/inspect_helpers.js | 3 +- lib/internal/main/watch_mode.js | 4 +- lib/internal/process/pre_execution.js | 8 - lib/internal/test_runner/runner.js | 4 +- src/node_config_file.cc | 24 +- src/node_config_file.h | 2 +- src/node_options.cc | 7 +- src/node_options.h | 2 +- ...fig-file.test.mjs => config-file.test.mjs} | 4 +- test/parallel/test-cli-node-options-docs.js | 1 - test/parallel/test-cli-options-as-flags.js | 4 +- test/parallel/test-config-file.js | 133 +++++--- test/parallel/test-debugger-no-inspect-brk.js | 8 + test/parallel/test-permission-config-file.mjs | 18 +- test/parallel/test-runner-cli.js | 4 +- test/parallel/test-runner-flag-propagation.js | 8 +- test/sequential/test-watch-mode.mjs | 6 +- 21 files changed, 421 insertions(+), 349 deletions(-) rename test/fixtures/test-runner/options-propagation/{experimental-config-file.test.mjs => config-file.test.mjs} (50%) diff --git a/doc/api/cli.md b/doc/api/cli.md index 5d862934fa2d..27348f238251 100644 --- a/doc/api/cli.md +++ b/doc/api/cli.md @@ -531,6 +531,161 @@ For example, to run a module with "development" resolutions: node -C development app.js ``` +### `--config-file=path`, `--config-file` + + + +If present, Node.js will look for a configuration file at the specified path. +If the path is not specified, Node.js will look for a `node.config.json` file +in the current working directory. +To specify a custom path, use the `--config-file=path` form. +The space-separated `--config-file path` form is not supported. +Node.js will read the configuration file and apply the settings. The +configuration file should be a JSON file with the following structure. `vX.Y.Z` +in the `$schema` must be replaced with the version of Node.js you are using or +`latest-vX.x` for the latest version of that major release line. + +```json +{ + "$schema": "https://nodejs.org/dist/vX.Y.Z/docs/node-config-schema.json", + "nodeOptions": { + "import": [ + "amaro/strip" + ], + "watch-path": "src", + "watch-preserve-output": true + }, + "test": { + "test-isolation": "process" + }, + "watch": { + "watch-preserve-output": true + } +} +``` + +The configuration file supports namespace-specific options: + +* The `nodeOptions` field contains CLI flags that are allowed in [`NODE_OPTIONS`][]. + +* Namespace fields like `test`, `watch`, and `permission` contain configuration specific to that subsystem. + +The configuration file can target a specific Node.js major version with +`nodeVersion`: + +```json +{ + "nodeVersion": 25, + "nodeOptions": { + "watch-path": "src" + } +} +``` + +To keep multiple version-specific configurations in the same file, use the +`configs` array. Node.js will use the first entry whose `nodeVersion` matches +the current Node.js major version: + +```json +{ + "$schema": "https://nodejs.org/dist/latest-v26.x/docs/node-config-schema.json", + "configs": [ + { + "nodeVersion": 25, + "config": { + "$schema": "https://nodejs.org/dist/latest-v25.x/docs/node-config-schema.json", + "nodeOptions": { + "watch-path": "src" + } + } + } + ] +} +``` + +When `configs` is used, the top level may only contain `$schema` and +`configs`. Each `configs` item must define an integer `nodeVersion` and an +object `config`. A single top-level config does not require `nodeVersion`, but +if present it must match the current Node.js major version. + +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. + +It is possible to use the [official JSON schema](../node-config-schema.json) +to validate the configuration file, which may vary depending on the Node.js version. +Each key in the configuration file corresponds to a flag that can be passed +as a command-line argument. The value of the key is the value that would be +passed to the flag. + +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 --test-isolation=process +``` + +The priority in configuration is as follows: + +1. NODE\_OPTIONS and command-line options +2. Dotenv NODE\_OPTIONS +3. Configuration file + +Values in the configuration file will not override the values in the environment +variables, command-line options, or the `NODE_OPTIONS` env file parsed by the +`--env-file` flag. + +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 be used in a namespace. + +Node.js will not sanitize or perform validation on the user-provided configuration, +so **NEVER** use untrusted configuration files. + ### `--cpu-prof` - -> Stability: 1.2 - Release candidate - -If present, Node.js will look for a configuration file at the specified path. -If the path is not specified, Node.js will look for a `node.config.json` file -in the current working directory. -To specify a custom path, use the `--experimental-config-file=path` form. -The space-separated `--experimental-config-file path` form is not supported. -The alias `--experimental-default-config-file` is equivalent to -`--experimental-config-file` without an argument. -Node.js will read the configuration file and apply the settings. The -configuration file should be a JSON file with the following structure. `vX.Y.Z` -in the `$schema` must be replaced with the version of Node.js you are using or -`latest-vX.x` for the latest version of that major release line. - -```json -{ - "$schema": "https://nodejs.org/dist/vX.Y.Z/docs/node-config-schema.json", - "nodeOptions": { - "import": [ - "amaro/strip" - ], - "watch-path": "src", - "watch-preserve-output": true - }, - "test": { - "test-isolation": "process" - }, - "watch": { - "watch-preserve-output": true - } -} -``` - -The configuration file supports namespace-specific options: - -* The `nodeOptions` field contains CLI flags that are allowed in [`NODE_OPTIONS`][]. - -* Namespace fields like `test`, `watch`, and `permission` contain configuration specific to that subsystem. - -The configuration file can target a specific Node.js major version with -`nodeVersion`: - -```json -{ - "nodeVersion": 25, - "nodeOptions": { - "watch-path": "src" - } -} -``` - -To keep multiple version-specific configurations in the same file, use the -`configs` array. Node.js will use the first entry whose `nodeVersion` matches -the current Node.js major version: - -```json -{ - "$schema": "https://nodejs.org/dist/latest-v26.x/docs/node-config-schema.json", - "configs": [ - { - "nodeVersion": 25, - "config": { - "$schema": "https://nodejs.org/dist/latest-v25.x/docs/node-config-schema.json", - "nodeOptions": { - "watch-path": "src" - } - } - } - ] -} -``` - -When `configs` is used, the top level may only contain `$schema` and -`configs`. Each `configs` item must define an integer `nodeVersion` and an -object `config`. A single top-level config does not require `nodeVersion`, but -if present it must match the current Node.js major version. - -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. - -It is possible to use the [official JSON schema](../node-config-schema.json) -to validate the configuration file, which may vary depending on the Node.js version. -Each key in the configuration file corresponds to a flag that can be passed -as a command-line argument. The value of the key is the value that would be -passed to the flag. - -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 --test-isolation=process -``` - -The priority in configuration is as follows: - -1. NODE\_OPTIONS and command-line options -2. Dotenv NODE\_OPTIONS -3. Configuration file - -Values in the configuration file will not override the values in the environment -variables, command-line options, or the `NODE_OPTIONS` env file parsed by the -`--env-file` flag. - -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 be used in a namespace. - -Node.js will not sanitize or perform validation on the user-provided configuration, -so **NEVER** use untrusted configuration files. - -### `--experimental-default-config-file` - - - -> Stability: 1.0 - Early development - -This flag is an alias for `--experimental-config-file` without an argument. -If present, Node.js will look for a -`node.config.json` file in the current working directory and load it as a -configuration file. - ### `--experimental-eventsource`