Commit 019e869ad3a for nodejs
commit 019e869ad3a3941cd83d6b77fb1d4b3eaad2d64c
Author: Marco Ippolito <marcoippolito54@gmail.com>
Date: Thu Oct 1 12:58:27 2026 +0200
src: fix config file handling
Signed-off-by: Marco Ippolito <marcoippolito54@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/66431
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Jacob Smith <jacob@frende.me>
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Pietro Marchini <pietro.marchini94@gmail.com>
diff --git a/src/node_config_file.cc b/src/node_config_file.cc
index 0a44da48413..b4d23325e9f 100644
--- a/src/node_config_file.cc
+++ b/src/node_config_file.cc
@@ -3,6 +3,7 @@
#include "node_version.h"
#include "simdjson.h"
+#include <algorithm>
#include <cinttypes>
namespace node {
@@ -21,20 +22,67 @@ inline bool HasEqualsPrefix(std::string_view arg, std::string_view flag) {
arg[flag.size()] == '=';
}
+inline bool IsBareConfigFileFlag(std::string_view arg) {
+ return arg == kConfigFileFlag || arg == kExperimentalConfigFileFlag ||
+ arg == kDefaultConfigFileFlag;
+}
+
+inline bool IsConfigFileFlag(std::string_view arg) {
+ return IsBareConfigFileFlag(arg) || HasEqualsPrefix(arg, kConfigFileFlag) ||
+ HasEqualsPrefix(arg, kExperimentalConfigFileFlag) ||
+ HasEqualsPrefix(arg, kDefaultConfigFileFlag);
+}
+
+inline std::string DefaultConfigFileArg() {
+ return std::string(kConfigFileFlag) + "=" +
+ std::string(kDefaultConfigFileName);
+}
+
+// Returns the index of the first entry in `args` that is neither the program
+// name nor an option for Node.js itself. Everything from there on is the
+// script and its arguments, or follows `--`, so it must not be read as a
+// config file flag.
+size_t GetNodeOptionsEnd(const std::vector<std::string>& args) {
+ // The bare flags take no value. Spell them out the way GetDataFromArgs()
+ // does before the real parse, otherwise the parser would consume the next
+ // argument as the path.
+ std::vector<std::string> remaining;
+ remaining.reserve(args.size());
+ for (const std::string& arg : args) {
+ remaining.push_back(IsBareConfigFileFlag(arg) ? DefaultConfigFileArg()
+ : arg);
+ }
+
+ // Parsing into throwaway options stops exactly where the real parse will.
+ // Any errors are reported by the real parse later on.
+ PerProcessOptions options;
+ std::vector<std::string> v8_args;
+ std::vector<std::string> errors;
+ options_parser::Parse(
+ &remaining, nullptr, &v8_args, &options, kDisallowedInEnvvar, &errors);
+
+ // `remaining` is left with the program name and the unparsed arguments.
+ return args.size() - remaining.size() + 1;
+}
+
std::optional<std::string_view> ConfigReader::GetDataFromArgs(
std::vector<std::string>* args) {
std::optional<std::string_view> result;
invalid_default_config_file_argument_ = false;
- for (size_t i = 0; i < args->size(); ++i) {
+ // Only pay for the extra parse when a config file flag might be present.
+ if (args->empty() || std::ranges::none_of(*args, IsConfigFileFlag)) {
+ return result;
+ }
+
+ const size_t node_options_end = GetNodeOptionsEnd(*args);
+ for (size_t i = 1; i < node_options_end; ++i) {
std::string& arg = (*args)[i];
- if (arg == kConfigFileFlag || arg == kExperimentalConfigFileFlag ||
- arg == kDefaultConfigFileFlag) {
+ if (IsBareConfigFileFlag(arg)) {
// --config-file, --experimental-config-file or
// --experimental-default-config-file
- arg = std::string(kConfigFileFlag) + "=" +
- std::string(kDefaultConfigFileName);
+ arg = DefaultConfigFileArg();
result = kDefaultConfigFileName;
} else if (HasEqualsPrefix(arg, kConfigFileFlag) ||
HasEqualsPrefix(arg, kExperimentalConfigFileFlag)) {
diff --git a/test/parallel/test-config-file.js b/test/parallel/test-config-file.js
index d81b1703549..127e862eecc 100644
--- a/test/parallel/test-config-file.js
+++ b/test/parallel/test-config-file.js
@@ -617,6 +617,58 @@ test('should treat a space-separated config file path as the script',
assert.strictEqual(result.code, 1);
});
+describe('config file flags that are not Node.js options', () => {
+ const argsVariants = [
+ ['--config-file'],
+ ['--config-file', 'tool.config.mjs'],
+ ['--config-file=i-do-not-exist.json'],
+ ['--experimental-config-file'],
+ ['--experimental-config-file=i-do-not-exist.json'],
+ ['--experimental-default-config-file'],
+ ['--experimental-default-config-file=i-do-not-exist.json'],
+ ];
+
+ for (const args of argsVariants) {
+ it(`should not read ${args.join(' ')} after the script`, async () => {
+ const result = await spawnPromisified(process.execPath, [
+ fixtures.path('printA.js'),
+ ...args,
+ ], {
+ cwd: fixtures.path('rc'),
+ });
+ assert.strictEqual(result.stderr, '');
+ assert.strictEqual(result.stdout, 'A\n');
+ assert.strictEqual(result.code, 0);
+ });
+
+ it(`should not read ${args.join(' ')} after --`, async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '-p', 'process.argv.slice(1).join(" ")',
+ '--',
+ ...args,
+ ], {
+ cwd: fixtures.path('rc'),
+ });
+ assert.strictEqual(result.stderr, '');
+ assert.strictEqual(result.stdout, `${args.join(' ')}\n`);
+ assert.strictEqual(result.code, 0);
+ });
+ }
+});
+
+test('should read the config file after an option that takes a separate value',
+ onlyIfNodeOptionsSupport, async () => {
+ const result = await spawnPromisified(process.execPath, [
+ '--no-warnings',
+ '--title', 'config-file-test',
+ `--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 error when --config-file= has empty argument',
onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [