src: allow empty --experimental-config-file

PR-URL: https://github.com/nodejs/node/pull/61610
Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
This commit is contained in:
Marco Ippolito authored and GitHub committed 2026-04-16 06:31:35 +00:00
1 parent 0dceddde2c
commit 2071c44630
18 files changed
+285 -182

No files matched your search

+10 -3
View File
@@ -1045,7 +1045,7 @@ added:
Enable experimental import support for `.node` addons.
### `--experimental-config-file=config`
### `--experimental-config-file=path`, `--experimental-config-file`
<!-- YAML
added:
@@ -1056,6 +1056,12 @@ added:
> Stability: 1.0 - Early development
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.
@@ -1162,9 +1168,10 @@ added:
> Stability: 1.0 - Early development
If the `--experimental-default-config-file` flag is present, Node.js will look for a
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
as configuration file.
configuration file.
### `--experimental-eventsource`
+1 -1
View File
@@ -4535,7 +4535,7 @@ test.describe('my suite', (suite) => {
[`suite()`]: #suitename-options-fn
[`test()`]: #testname-options-fn
[code coverage]: #collecting-code-coverage
[configuration files]: cli.md#--experimental-config-fileconfig
[configuration files]: cli.md#--experimental-config-filepath---experimental-config-file
[describe options]: #describename-options-fn
[it options]: #testname-options-fn
[module customization hooks]: module.md#customization-hooks
+8 -3
View File
@@ -623,8 +623,12 @@ It is possible to run code containing inline types unless the
.It Fl -experimental-addon-modules
Enable experimental import support for \fB.node\fR addons.
.
.It Fl -experimental-config-file Ns = Ns Ar config
.It Fl -experimental-config-file= Ns Ar config , Fl -experimental-config-file , Fl -experimental-default-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 \fBnode.config.json\fR file
in the current working directory.
To specify a custom path, use the \fB--experimental-config-file=\fR\fBpath\fR form.
The space-separated \fB--experimental-config-file path\fR 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. \fBvX.Y.Z\fR
in the \fB$schema\fR must be replaced with the version of Node.js you are using.
@@ -710,9 +714,10 @@ Node.js will not sanitize or perform validation on the user-provided configurati
so \fBNEVER\fR use untrusted configuration files.
.
.It Fl -experimental-default-config-file
If the \fB--experimental-default-config-file\fR flag is present, Node.js will look for a
This flag is an alias for \fB--experimental-config-file\fR without an argument.
If present, Node.js will look for a
\fBnode.config.json\fR file in the current working directory and load it as a
as configuration file.
configuration file.
.
.It Fl -experimental-eventsource
Enable exposition of EventSource Web API on the global scope.
+2 -6
View File
@@ -1,7 +1,6 @@
'use strict';
const {
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeJoin,
ArrayPrototypeMap,
ArrayPrototypePush,
@@ -67,11 +66,8 @@ for (let i = 0; i < process.execArgv.length; i++) {
}
continue;
}
if (StringPrototypeStartsWith(arg, '--experimental-config-file')) {
if (!ArrayPrototypeIncludes(arg, '=')) {
// Skip the flag and the next argument (the config file path)
i++;
}
if (arg === '--experimental-config-file' ||
StringPrototypeStartsWith(arg, '--experimental-config-file=')) {
continue;
}
if (arg === '--experimental-default-config-file') {
+1 -2
View File
@@ -397,8 +397,7 @@ function setupSQLite() {
}
function initializeConfigFileSupport() {
if (getOptionValue('--experimental-default-config-file') ||
getOptionValue('--experimental-config-file')) {
if (getOptionValue('--experimental-config-file')) {
emitExperimentalWarning('--experimental-config-file');
}
}
+5 -1
View File
@@ -161,7 +161,11 @@ function createTestFileList(patterns, cwd) {
function filterExecArgv(arg, i, arr) {
return !ArrayPrototypeIncludes(kFilterArgs, arg) &&
!ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`));
!ArrayPrototypeSome(kFilterArgValues, (p) => {
return arg === p ||
StringPrototypeStartsWith(arg, `${p}=`) ||
(p !== '--experimental-config-file' && i > 0 && arr[i - 1] === p);
});
}
function getRunArgs(path, { forceExit,
+10 -4
View File
@@ -900,15 +900,21 @@ static ExitCode InitializeNodeWithArgsInternal(
}
std::string node_options_from_config;
if (auto path = per_process::config_reader.GetDataFromArgs(*argv)) {
switch (per_process::config_reader.ParseConfig(*path)) {
auto config_path = per_process::config_reader.GetDataFromArgs(argv);
if (per_process::config_reader.HasInvalidDefaultConfigFileArgument()) {
errors->push_back("--experimental-default-config-file does not take an "
"argument");
return ExitCode::kInvalidCommandLineArgument;
}
if (config_path) {
switch (per_process::config_reader.ParseConfig(*config_path)) {
case ParseResult::Valid:
break;
case ParseResult::InvalidContent:
errors->push_back(std::string(*path) + ": invalid content");
errors->push_back(std::string(*config_path) + ": invalid content");
break;
case ParseResult::FileError:
errors->push_back(std::string(*path) + ": not found");
errors->push_back(std::string(*config_path) + ": not found");
break;
default:
UNREACHABLE();
+37 -23
View File
@@ -2,39 +2,53 @@
#include "debug_utils-inl.h"
#include "simdjson.h"
#include <string>
namespace node {
constexpr std::string_view kConfigFileFlag = "--experimental-config-file";
constexpr std::string_view kDefaultConfigFileFlag =
"--experimental-default-config-file";
constexpr std::string_view kDefaultConfigFileName = "node.config.json";
inline bool HasEqualsPrefix(std::string_view arg, std::string_view flag) {
return arg.size() > flag.size() && arg.starts_with(flag) &&
arg[flag.size()] == '=';
}
std::optional<std::string_view> ConfigReader::GetDataFromArgs(
const std::vector<std::string>& args) {
constexpr std::string_view flag_path = "--experimental-config-file";
constexpr std::string_view default_file =
"--experimental-default-config-file";
std::vector<std::string>* args) {
std::optional<std::string_view> result;
invalid_default_config_file_argument_ = false;
bool has_default_config_file = false;
for (size_t i = 0; i < args->size(); ++i) {
std::string& arg = (*args)[i];
for (auto it = args.begin(); it != args.end(); ++it) {
if (*it == flag_path) {
// Case: "--experimental-config-file foo"
if (auto next = std::next(it); next != args.end()) {
return *next;
if (arg == kConfigFileFlag) {
// --experimental-config-file
arg = std::string(kConfigFileFlag) + "=" +
std::string(kDefaultConfigFileName);
result = kDefaultConfigFileName;
} else if (HasEqualsPrefix(arg, kConfigFileFlag)) {
// --experimental-config-file=path
std::string_view path =
std::string_view(arg).substr(kConfigFileFlag.size() + 1);
if (!path.empty()) {
result = path;
}
} else if (it->starts_with(flag_path)) {
// Case: "--experimental-config-file=foo"
if (it->size() > flag_path.size() && (*it)[flag_path.size()] == '=') {
return std::string_view(*it).substr(flag_path.size() + 1);
}
} else if (*it == default_file || it->starts_with(default_file)) {
has_default_config_file = true;
} else if (arg == kDefaultConfigFileFlag) {
// --experimental-default-config-file
arg = std::string(kConfigFileFlag) + "=" +
std::string(kDefaultConfigFileName);
result = kDefaultConfigFileName;
} else if (HasEqualsPrefix(arg, kDefaultConfigFileFlag)) {
invalid_default_config_file_argument_ = true;
}
}
if (has_default_config_file) {
return "node.config.json";
}
return result;
}
return std::nullopt;
bool ConfigReader::HasInvalidDefaultConfigFileArgument() const {
return invalid_default_config_file_argument_;
}
ParseResult ConfigReader::ProcessOptionValue(
+3 -1
View File
@@ -30,7 +30,8 @@ class ConfigReader {
ParseResult ParseConfig(const std::string_view& config_path);
std::optional<std::string_view> GetDataFromArgs(
const std::vector<std::string>& args);
std::vector<std::string>* args);
bool HasInvalidDefaultConfigFileArgument() const;
std::string GetNodeOptions();
const std::vector<std::string>& GetNamespaceFlags() const;
@@ -53,6 +54,7 @@ class ConfigReader {
std::vector<std::string> node_options_;
std::vector<std::string> namespace_options_;
bool invalid_default_config_file_argument_ = false;
// Cache for fast lookup of environment options
std::unordered_map<std::string, options_parser::OptionMappingDetails>
+4 -5
View File
@@ -900,11 +900,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
&EnvironmentOptions::optional_env_file);
Implies("--env-file-if-exists", "[has_env_file_string]");
AddOption("--experimental-config-file",
"set config file from supplied file",
&EnvironmentOptions::experimental_config_file_path);
AddOption("--experimental-default-config-file",
"set config file from default config file",
&EnvironmentOptions::experimental_default_config_file);
"set config file path",
&EnvironmentOptions::experimental_config_file_path,
kDisallowedInEnvvar);
AddAlias("--experimental-default-config-file", "--experimental-config-file");
AddOption("--test",
"launch test runner on startup",
&EnvironmentOptions::test_runner,
-1
View File
@@ -273,7 +273,6 @@ class EnvironmentOptions : public Options {
bool report_exclude_env = false;
bool report_exclude_network = false;
std::string experimental_config_file_path;
bool experimental_default_config_file = false;
inline DebugOptions* get_debug_options() { return &debug_options_; }
inline const DebugOptions& debug_options() const { return debug_options_; }
@@ -122,5 +122,6 @@ for (const [, envVar, config] of nodeOptionsCC.matchAll(addOptionRE)) {
// add alias handling
manPagesOptions.delete('-trace-events-enabled');
manPagesOptions.delete('-experimental-default-config-file');
assert.strictEqual(manPagesOptions.size, 0, `Man page options not documented: ${[...manPagesOptions]}`);
+2 -4
View File
@@ -55,8 +55,7 @@ describe('getOptionsAsFlagsFromBinding', () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
configFile,
`--experimental-config-file=${configFile}`,
fixtureFile,
]);
@@ -75,8 +74,7 @@ describe('getOptionsAsFlagsFromBinding', () => {
'--no-warnings',
'--expose-internals',
'--stack-trace-limit=512',
'--experimental-config-file',
configFile,
`--experimental-config-file=${configFile}`,
fixtureFile,
]);
+155 -85
View File
@@ -24,8 +24,7 @@ const onlyWithInspectorAndNodeOptions = {
test('should handle non existing json', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-config-file',
'i-do-not-exist.json',
'--experimental-config-file=i-do-not-exist.json',
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Cannot read configuration from i-do-not-exist\.json: no such file or directory/);
@@ -36,8 +35,7 @@ test('should handle non existing json', async () => {
test('should handle empty json', async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-config-file',
fixtures.path('rc/empty.json'),
`--experimental-config-file=${fixtures.path('rc/empty.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Can't parse/);
@@ -49,8 +47,7 @@ test('should handle empty json', async () => {
test('should handle empty object json', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/empty-object.json'),
`--experimental-config-file=${fixtures.path('rc/empty-object.json')}`,
'-p', '"Hello, World!"',
]);
assert.strictEqual(result.stderr, '');
@@ -60,8 +57,7 @@ test('should handle empty object json', async () => {
test('should parse boolean flag', onlyWithAmaroAndNodeOptions, async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-config-file',
fixtures.path('rc/strip-types.json'),
`--experimental-config-file=${fixtures.path('rc/strip-types.json')}`,
fixtures.path('typescript/ts/test-typescript.ts'),
]);
assert.match(result.stderr, /--experimental-config-file is an experimental feature and might change at any time/);
@@ -71,8 +67,7 @@ test('should parse boolean flag', onlyWithAmaroAndNodeOptions, async () => {
test('should parse boolean flag defaulted to true', onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--experimental-config-file',
fixtures.path('rc/warnings-false.json'),
`--experimental-config-file=${fixtures.path('rc/warnings-false.json')}`,
'-p', 'process.emitWarning("A warning")',
]);
assert.strictEqual(result.stderr, '');
@@ -83,8 +78,7 @@ test('should parse boolean flag defaulted to true', onlyIfNodeOptionsSupport, as
test('should throw an error when a flag is declared twice', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/override-property.json'),
`--experimental-config-file=${fixtures.path('rc/override-property.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Option --strip-types is already defined/);
@@ -95,8 +89,7 @@ test('should throw an error when a flag is declared twice', async () => {
test('should override env-file', onlyWithAmaroAndNodeOptions, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/strip-types.json'),
`--experimental-config-file=${fixtures.path('rc/strip-types.json')}`,
'--env-file', fixtures.path('dotenv/node-options-no-tranform.env'),
fixtures.path('typescript/ts/test-typescript.ts'),
]);
@@ -108,8 +101,7 @@ test('should override env-file', onlyWithAmaroAndNodeOptions, async () => {
test('should not override NODE_OPTIONS', onlyWithAmaro, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/strip-types.json'),
`--experimental-config-file=${fixtures.path('rc/strip-types.json')}`,
fixtures.path('typescript/ts/test-typescript.ts'),
], {
env: {
@@ -126,8 +118,7 @@ test('should not override CLI flags', onlyWithAmaro, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--no-strip-types',
'--experimental-config-file',
fixtures.path('rc/strip-types.json'),
`--experimental-config-file=${fixtures.path('rc/strip-types.json')}`,
fixtures.path('typescript/ts/test-typescript.ts'),
]);
assert.match(result.stderr, /SyntaxError/);
@@ -138,8 +129,7 @@ test('should not override CLI flags', onlyWithAmaro, async () => {
test('should parse array flag correctly', onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/import.json'),
`--experimental-config-file=${fixtures.path('rc/import.json')}`,
'--eval', 'setTimeout(() => console.log("D"),99)',
]);
assert.strictEqual(result.stderr, '');
@@ -150,8 +140,7 @@ test('should parse array flag correctly', onlyIfNodeOptionsSupport, async () =>
test('should validate invalid array flag', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/invalid-import.json'),
`--experimental-config-file=${fixtures.path('rc/invalid-import.json')}`,
'--eval', 'setTimeout(() => console.log("D"),99)',
]);
assert.match(result.stderr, /invalid-import\.json: invalid content/);
@@ -162,8 +151,7 @@ test('should validate invalid array flag', async () => {
test('should validate array flag as string', onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/import-as-string.json'),
`--experimental-config-file=${fixtures.path('rc/import-as-string.json')}`,
'--eval', 'setTimeout(() => console.log("B"),99)',
]);
assert.strictEqual(result.stderr, '');
@@ -174,8 +162,7 @@ test('should validate array flag as string', onlyIfNodeOptionsSupport, async ()
test('should throw at unknown flag', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/unknown-flag.json'),
`--experimental-config-file=${fixtures.path('rc/unknown-flag.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Unknown or not allowed option some-unknown-flag for namespace nodeOptions/);
@@ -186,8 +173,7 @@ test('should throw at unknown flag', async () => {
test('should throw at flag not available in NODE_OPTIONS', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/not-node-options-flag.json'),
`--experimental-config-file=${fixtures.path('rc/not-node-options-flag.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Unknown or not allowed option test for namespace nodeOptions/);
@@ -198,8 +184,7 @@ test('should throw at flag not available in NODE_OPTIONS', async () => {
test('unsigned flag should be parsed correctly', onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/numeric.json'),
`--experimental-config-file=${fixtures.path('rc/numeric.json')}`,
'-p', 'http.maxHeaderSize',
]);
assert.strictEqual(result.stderr, '');
@@ -210,8 +195,7 @@ test('unsigned flag should be parsed correctly', onlyIfNodeOptionsSupport, async
test('numeric flag should not allow negative values', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/negative-numeric.json'),
`--experimental-config-file=${fixtures.path('rc/negative-numeric.json')}`,
'-p', 'http.maxHeaderSize',
]);
assert.match(result.stderr, /Invalid value for --max-http-header-size/);
@@ -223,8 +207,7 @@ test('numeric flag should not allow negative values', async () => {
test('v8 flag should not be allowed in config file', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/v8-flag.json'),
`--experimental-config-file=${fixtures.path('rc/v8-flag.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /V8 flag --abort-on-uncaught-exception is currently not supported/);
@@ -236,8 +219,7 @@ test('string flag should be parsed correctly', onlyIfNodeOptionsSupport, async (
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--test',
'--experimental-config-file',
fixtures.path('rc/string.json'),
`--experimental-config-file=${fixtures.path('rc/string.json')}`,
fixtures.path('rc/test.js'),
]);
assert.strictEqual(result.stderr, '');
@@ -249,8 +231,7 @@ test('host port flag should be parsed correctly', onlyWithInspectorAndNodeOption
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/host-port.json'),
`--experimental-config-file=${fixtures.path('rc/host-port.json')}`,
'-p', 'require("internal/options").getOptionValue("--inspect-port").port',
]);
assert.strictEqual(result.stderr, '');
@@ -261,8 +242,7 @@ test('host port flag should be parsed correctly', onlyWithInspectorAndNodeOption
test('--inspect=true should be parsed correctly', onlyWithInspectorAndNodeOptions, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/inspect-true.json'),
`--experimental-config-file=${fixtures.path('rc/inspect-true.json')}`,
'--inspect-port', '0',
'-p', 'require("node:inspector").url()',
]);
@@ -274,8 +254,7 @@ test('--inspect=true should be parsed correctly', onlyWithInspectorAndNodeOption
test('--inspect=false should be parsed correctly', { skip: !process.features.inspector }, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/inspect-false.json'),
`--experimental-config-file=${fixtures.path('rc/inspect-false.json')}`,
'-p', 'require("node:inspector").url()',
]);
assert.strictEqual(result.stderr, '');
@@ -286,8 +265,7 @@ test('--inspect=false should be parsed correctly', { skip: !process.features.ins
test('no op flag should throw', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/no-op.json'),
`--experimental-config-file=${fixtures.path('rc/no-op.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /No-op flag --http-parser is currently not supported/);
@@ -299,8 +277,7 @@ test('no op flag should throw', async () => {
test('should not allow users to sneak in a flag', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/sneaky-flag.json'),
`--experimental-config-file=${fixtures.path('rc/sneaky-flag.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /The number of NODE_OPTIONS doesn't match the number of flags in the config file/);
@@ -311,8 +288,7 @@ test('should not allow users to sneak in a flag', async () => {
test('non object root', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/non-object-root.json'),
`--experimental-config-file=${fixtures.path('rc/non-object-root.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Root value unexpected not an object for/);
@@ -323,8 +299,7 @@ test('non object root', async () => {
test('non object node options', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/non-object-node-options.json'),
`--experimental-config-file=${fixtures.path('rc/non-object-node-options.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /"nodeOptions" value unexpected for/);
@@ -335,8 +310,7 @@ test('non object node options', async () => {
test('should throw correct error when a json is broken', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/broken.json'),
`--experimental-config-file=${fixtures.path('rc/broken.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Can't parse/);
@@ -348,8 +322,7 @@ test('should throw correct error when a json is broken', async () => {
test('broken value in node_options', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/broken-node-options.json'),
`--experimental-config-file=${fixtures.path('rc/broken-node-options.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Can't parse/);
@@ -371,12 +344,83 @@ test('should use node.config.json as default', onlyIfNodeOptionsSupport, async (
assert.strictEqual(result.code, 0);
});
test('should override node.config.json when specificied', onlyIfNodeOptionsSupport, async () => {
test('should use node.config.json when --experimental-config-file has no argument',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
'-p', 'http.maxHeaderSize',
], {
cwd: fixtures.path('rc/default'),
});
assert.strictEqual(result.stderr, '');
assert.strictEqual(result.stdout, '10\n');
assert.strictEqual(result.code, 0);
});
test('should not treat the script path as a config file argument',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('printA.js'),
], {
cwd: fixtures.path('rc/default'),
});
assert.strictEqual(result.stderr, '');
assert.strictEqual(result.stdout, 'A\n');
assert.strictEqual(result.code, 0);
});
test('should treat a space-separated config file path as the script',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/empty.json'),
fixtures.path('printA.js'),
], {
cwd: fixtures.path('rc/default'),
});
assert.strictEqual(result.stdout, '');
assert.match(result.stderr, /SyntaxError/);
assert.match(result.stderr, /Unexpected end of JSON input/);
assert.doesNotMatch(result.stderr, /Can't parse/);
assert.doesNotMatch(result.stderr, /requires an argument/);
assert.strictEqual(result.code, 1);
});
test('should error when --experimental-config-file= has empty argument',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file=',
'-p', 'http.maxHeaderSize',
], {
cwd: fixtures.path('rc/default'),
});
assert.match(result.stderr, /--experimental-config-file= requires an argument/);
assert.strictEqual(result.code, 9);
});
test('should error when --experimental-default-config-file has an explicit argument',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-default-config-file=node.config.json',
'-p', 'http.maxHeaderSize',
], {
cwd: fixtures.path('rc/default'),
});
assert.match(result.stderr, /--experimental-default-config-file does not take an argument/);
assert.strictEqual(result.code, 9);
});
test('should override node.config.json when specified', onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-default-config-file',
'--experimental-config-file',
fixtures.path('rc/default/override.json'),
`--experimental-config-file=${fixtures.path('rc/default/override.json')}`,
'-p', 'http.maxHeaderSize',
], {
cwd: fixtures.path('rc/default'),
@@ -385,6 +429,46 @@ test('should override node.config.json when specificied', onlyIfNodeOptionsSuppo
assert.strictEqual(result.stdout, '20\n');
assert.strictEqual(result.code, 0);
});
test('should work with --experimental-config-file=path',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
`--experimental-config-file=${fixtures.path('rc/default/node.config.json')}`,
'-p', 'http.maxHeaderSize',
]);
assert.strictEqual(result.stderr, '');
assert.strictEqual(result.stdout, '10\n');
assert.strictEqual(result.code, 0);
});
test('should use last config file when multiple are specified',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
`--experimental-config-file=${fixtures.path('rc/default/node.config.json')}`,
`--experimental-config-file=${fixtures.path('rc/default/override.json')}`,
'-p', 'http.maxHeaderSize',
]);
assert.strictEqual(result.stderr, '');
assert.strictEqual(result.stdout, '20\n');
assert.strictEqual(result.code, 0);
});
test('should use default when next argument starts with dash',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
'-p', 'http.maxHeaderSize',
], {
cwd: fixtures.path('rc/default'),
});
assert.strictEqual(result.stderr, '');
assert.strictEqual(result.stdout, '10\n');
assert.strictEqual(result.code, 0);
});
// Skip on windows because it doesn't support chmod changing read permissions
// Also skip if user is root because it would have read permissions anyway
test('should throw an error when the file is non readable', {
@@ -413,8 +497,7 @@ describe('namespace-scoped options', () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/namespaced/node.config.json'),
`--experimental-config-file=${fixtures.path('rc/namespaced/node.config.json')}`,
'--no-test',
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
@@ -426,8 +509,7 @@ describe('namespace-scoped options', () => {
it('should throw an error when a namespace-scoped option is not recognised', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/unknown-flag-namespace.json'),
`--experimental-config-file=${fixtures.path('rc/unknown-flag-namespace.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /Unknown or not allowed option unknown-flag for namespace test/);
@@ -438,8 +520,7 @@ describe('namespace-scoped options', () => {
it('should not throw an error when a namespace is not recognised', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/unknown-namespace.json'),
`--experimental-config-file=${fixtures.path('rc/unknown-namespace.json')}`,
'-p', '"Hello, World!"',
]);
assert.strictEqual(result.stderr, '');
@@ -450,8 +531,7 @@ describe('namespace-scoped options', () => {
it('should handle an empty namespace valid namespace', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/empty-valid-namespace.json'),
`--experimental-config-file=${fixtures.path('rc/empty-valid-namespace.json')}`,
'-p', '"Hello, World!"',
]);
assert.strictEqual(result.stderr, '');
@@ -463,8 +543,7 @@ describe('namespace-scoped options', () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/override-node-option-with-namespace.json'),
`--experimental-config-file=${fixtures.path('rc/override-node-option-with-namespace.json')}`,
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
assert.match(result.stderr, /Option --test-isolation is already defined/);
@@ -476,8 +555,7 @@ describe('namespace-scoped options', () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/override-namespace.json'),
`--experimental-config-file=${fixtures.path('rc/override-namespace.json')}`,
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
assert.match(result.stderr, /Option --test-isolation is already defined/);
@@ -490,8 +568,7 @@ describe('namespace-scoped options', () => {
'--no-warnings',
'--expose-internals',
'--test-isolation', 'process',
'--experimental-config-file',
fixtures.path('rc/namespaced/node.config.json'),
`--experimental-config-file=${fixtures.path('rc/namespaced/node.config.json')}`,
'--no-test',
'-p', 'require("internal/options").getOptionValue("--test-isolation")',
]);
@@ -506,8 +583,7 @@ describe('namespace-scoped options', () => {
'--expose-internals',
'--test-coverage-exclude', 'cli-pattern1',
'--test-coverage-exclude', 'cli-pattern2',
'--experimental-config-file',
fixtures.path('rc/namespace-with-array.json'),
`--experimental-config-file=${fixtures.path('rc/namespace-with-array.json')}`,
'--no-test',
'-p', 'JSON.stringify(require("internal/options").getOptionValue("--test-coverage-exclude"))',
]);
@@ -529,8 +605,7 @@ describe('namespace-scoped options', () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/namespace-with-disallowed-envvar.json'),
`--experimental-config-file=${fixtures.path('rc/namespace-with-disallowed-envvar.json')}`,
'--no-test',
'-p', 'require("internal/options").getOptionValue("--test-concurrency")',
]);
@@ -546,8 +621,7 @@ describe('namespace-scoped options', () => {
'--no-warnings',
'--expose-internals',
'--test-concurrency', '2',
'--experimental-config-file',
fixtures.path('rc/namespace-with-disallowed-envvar.json'),
`--experimental-config-file=${fixtures.path('rc/namespace-with-disallowed-envvar.json')}`,
'--no-test',
'-p', 'require("internal/options").getOptionValue("--test-concurrency")',
]);
@@ -559,8 +633,7 @@ describe('namespace-scoped options', () => {
it('should throw an error for removed "testRunner" namespace', async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-config-file',
fixtures.path('rc/deprecated-testrunner-namespace.json'),
`--experimental-config-file=${fixtures.path('rc/deprecated-testrunner-namespace.json')}`,
'-p', '"Hello, World!"',
]);
assert.match(result.stderr, /the "testRunner" namespace has been removed\. Use "test" instead\./);
@@ -571,8 +644,7 @@ describe('namespace-scoped options', () => {
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'),
`--experimental-config-file=${fixtures.path('rc/namespaced/node.config.json')}`,
fixtures.path('rc/test.js'),
]);
assert.strictEqual(result.code, 0);
@@ -583,8 +655,7 @@ describe('namespace-scoped options', () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/permission-namespace.json'),
`--experimental-config-file=${fixtures.path('rc/permission-namespace.json')}`,
'-p', 'require("internal/options").getOptionValue("--permission")',
]);
assert.strictEqual(result.stderr, '');
@@ -596,8 +667,7 @@ describe('namespace-scoped options', () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--expose-internals',
'--experimental-config-file',
fixtures.path('rc/test-namespace-explicit-false.json'),
`--experimental-config-file=${fixtures.path('rc/test-namespace-explicit-false.json')}`,
'-p', 'require("internal/options").getOptionValue("--test")',
]);
assert.strictEqual(result.stderr, '');
+9 -18
View File
@@ -13,8 +13,7 @@ describe('Permission model config file support', () => {
{
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
readOnlyConfigPath,
`--experimental-config-file=${readOnlyConfigPath}`,
readTestPath,
]);
assert.strictEqual(result.code, 0);
@@ -23,8 +22,7 @@ describe('Permission model config file support', () => {
{
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
readWriteConfigPath,
`--experimental-config-file=${readWriteConfigPath}`,
writeTestPath,
]);
assert.strictEqual(result.code, 0);
@@ -33,8 +31,7 @@ describe('Permission model config file support', () => {
{
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
readOnlyConfigPath,
`--experimental-config-file=${readOnlyConfigPath}`,
writeTestPath,
]);
assert.strictEqual(result.code, 1);
@@ -50,8 +47,7 @@ describe('Permission model config file support', () => {
{
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
configPath,
`--experimental-config-file=${configPath}`,
childTestPath,
]);
assert.strictEqual(result.code, 0);
@@ -60,8 +56,7 @@ describe('Permission model config file support', () => {
{
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
readOnlyConfigPath,
`--experimental-config-file=${readOnlyConfigPath}`,
childTestPath,
]);
assert.strictEqual(result.code, 1, result.stderr);
@@ -76,8 +71,7 @@ describe('Permission model config file support', () => {
{
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
configPath,
`--experimental-config-file=${configPath}`,
'-p',
'process.permission.has("net") && process.permission.has("inspector")',
]);
@@ -88,8 +82,7 @@ describe('Permission model config file support', () => {
{
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
readOnlyConfigPath,
`--experimental-config-file=${readOnlyConfigPath}`,
'-p',
'process.permission.has("net") + process.permission.has("inspector")',
]);
@@ -103,8 +96,7 @@ describe('Permission model config file support', () => {
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
configPath,
`--experimental-config-file=${configPath}`,
'--allow-fs-read=*',
'-p',
'process.permission.has("addon") && process.permission.has("wasi")',
@@ -118,8 +110,7 @@ describe('Permission model config file support', () => {
const result = await spawnPromisified(process.execPath, [
'--permission',
'--experimental-config-file',
configPath,
`--experimental-config-file=${configPath}`,
'--allow-child-process',
'--allow-fs-write=*',
'-p',
+25 -18
View File
@@ -431,30 +431,37 @@ for (const isolation of ['none', 'process']) {
}
{
// Should not propagate --experimental-config-file option to sub test in isolation process
// Should not propagate config file options to sub tests in isolation process.
const fixturePath = join(testFixtures, 'options-propagation');
const args = [
'--test-reporter=tap',
'--no-warnings',
`--experimental-config-file=node.config.json`,
'--expose-internals',
'--test',
const configFlagVariants = [
['--experimental-config-file=node.config.json'],
['--experimental-config-file'],
['--experimental-default-config-file'],
];
const child = spawnSync(process.execPath, args, { cwd: fixturePath });
assert.strictEqual(child.stderr.toString(), '');
const stdout = child.stdout.toString();
for (const configArgs of configFlagVariants) {
const args = [
'--test-reporter=tap',
'--no-warnings',
...configArgs,
'--expose-internals',
'--test',
];
const child = spawnSync(process.execPath, args, { cwd: fixturePath });
assert.match(stdout, /tests 1/);
assert.match(stdout, /suites 0/);
assert.match(stdout, /pass 1/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /cancelled 0/);
assert.match(stdout, /skipped 0/);
assert.match(stdout, /todo 0/);
assert.strictEqual(child.stderr.toString(), '');
const stdout = child.stdout.toString();
assert.match(stdout, /tests 1/);
assert.match(stdout, /suites 0/);
assert.match(stdout, /pass 1/);
assert.match(stdout, /fail 0/);
assert.match(stdout, /cancelled 0/);
assert.match(stdout, /skipped 0/);
assert.match(stdout, /todo 0/);
assert.strictEqual(child.status, 0);
assert.strictEqual(child.status, 0);
}
}
{
+10 -5
View File
@@ -15,8 +15,10 @@ const runner = path.join(fixtureDir, 'runner.mjs');
describe('test runner flag propagation', () => {
describe('via command line', () => {
const flagPropagationTests = [
['--experimental-config-file', 'node.config.json', ''],
['--experimental-default-config-file', '', false],
['--experimental-config-file=node.config.json', '', '', '--experimental-config-file',
'should not propagate --experimental-config-file to child tests'],
['--experimental-default-config-file', '', '', '--experimental-config-file',
'should not propagate --experimental-default-config-file to child tests'],
['--env-file', '.env', '.env'],
['--env-file-if-exists', '.env', '.env'],
['--test-concurrency', '2', '2'],
@@ -32,8 +34,11 @@ describe('test runner flag propagation', () => {
['--require', './index.js', './index.js'],
];
for (const [flagName, testValue, expectedValue] of flagPropagationTests) {
const testDescription = `should propagate ${flagName} to child tests as expected`;
for (const [flagName,
testValue,
expectedValue,
propagatedFlag = flagName,
testDescription = `should propagate ${flagName} to child tests as expected`] of flagPropagationTests) {
it(testDescription, () => {
const args = [
@@ -45,7 +50,7 @@ describe('test runner flag propagation', () => {
// Use the runner fixture
runner,
// Pass parameters to the fixture
`--flag=${flagName}`,
`--flag=${propagatedFlag}`,
`--expected=${expectedValue}`,
`--description="${testDescription}"`,
].filter(Boolean);
+2 -2
View File
@@ -826,7 +826,7 @@ process.on('message', (message) => {
const file = createTmpFile();
const configFile = createTmpFile(JSON.stringify({ watch: { 'watch': true } }), '.json');
const { stderr, stdout } = await runWriteSucceed({
file, watchedFile: file, args: ['--experimental-config-file', configFile, file], options: {
file, watchedFile: file, args: [`--experimental-config-file=${configFile}`, file], options: {
timeout: 10000,
},
});
@@ -850,7 +850,7 @@ process.on('message', (message) => {
const watchedFile = createTmpFile('', '.js', dir);
const configFile = createTmpFile(JSON.stringify({ watch: { 'watch-path': [dir] } }), '.json', dir);
const args = ['--experimental-config-file', configFile, file];
const args = [`--experimental-config-file=${configFile}`, file];
const { stderr, stdout } = await runWriteSucceed({ file, watchedFile, args });
assert.strictEqual(stderr, '');