Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1014,7 +1014,7 @@ added:

Enable experimental import support for `.node` addons.

### `--experimental-config-file=config`
### `--experimental-config-file=path`, `--experimental-config-file`

<!-- YAML
added:
Expand All @@ -1025,6 +1025,10 @@ 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.
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.
Expand Down Expand Up @@ -1131,9 +1135,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`

Expand Down
2 changes: 1 addition & 1 deletion doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -4131,7 +4131,7 @@ Can be used to abort test subtasks when the test has been aborted.
[`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
[running tests from the command line]: #running-tests-from-the-command-line
Expand Down
9 changes: 6 additions & 3 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -602,8 +602,10 @@ 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.
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.
Expand Down Expand Up @@ -689,9 +691,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.
Expand Down
3 changes: 2 additions & 1 deletion lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,8 @@ function setupSQLite() {

function initializeConfigFileSupport() {
if (getOptionValue('--experimental-default-config-file') ||
getOptionValue('--experimental-config-file')) {
getOptionValue('--experimental-config-file') ||
getOptionValue('--experimental-config-file=')) {
emitExperimentalWarning('--experimental-config-file');
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -913,7 +913,7 @@ static ExitCode InitializeNodeWithArgsInternal(
}

std::string node_options_from_config;
if (auto path = per_process::config_reader.GetDataFromArgs(*argv)) {
if (auto path = per_process::config_reader.GetDataFromArgs(argv)) {
switch (per_process::config_reader.ParseConfig(*path)) {
case ParseResult::Valid:
break;
Expand Down
68 changes: 42 additions & 26 deletions src/node_config_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,55 @@
#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";

// Check if "--experimental-config-file=" has path arg
inline bool HasEqualsPrefix(std::string_view arg, std::string_view flag) {
return arg.size() > flag.size() + 1 && 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";

bool has_default_config_file = false;

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;
}
} 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);
std::vector<std::string>* args) {
std::optional<std::string_view> result;

for (size_t i = 0; i < args->size(); ++i) {
std::string& arg = (*args)[i];

if (arg == kConfigFileFlag) {
// Check if next arg is a path
const bool has_next_arg = i + 1 < args->size();
const bool next_is_path =
has_next_arg && !(*args)[i + 1].starts_with("-");

if (next_is_path) {
// --experimental-config-file path
arg = std::string(kConfigFileFlag) + "=" + (*args)[i + 1];
args->erase(args->begin() + static_cast<ptrdiff_t>(i + 1));
result = std::string_view(arg).substr(kConfigFileFlag.size() + 1);
} else {
// --experimental-config-file
arg = std::string(kConfigFileFlag) + "=" +
std::string(kDefaultConfigFileName);
result = kDefaultConfigFileName;
}
} else if (*it == default_file || it->starts_with(default_file)) {
has_default_config_file = true;
} else if (HasEqualsPrefix(arg, kConfigFileFlag)) {
// --experimental-config-file=path
result = std::string_view(arg).substr(kConfigFileFlag.size() + 1);
} else if (arg == kDefaultConfigFileFlag) {
// --experimental-default-config-file
arg = std::string(kConfigFileFlag) + "=" +
std::string(kDefaultConfigFileName);
result = kDefaultConfigFileName;
}
}

if (has_default_config_file) {
return "node.config.json";
}

return std::nullopt;
return result;
}

ParseResult ConfigReader::ProcessOptionValue(
Expand Down
2 changes: 1 addition & 1 deletion src/node_config_file.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ 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);

std::string GetNodeOptions();
const std::vector<std::string>& GetNamespaceFlags() const;
Expand Down
10 changes: 5 additions & 5 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -869,11 +869,11 @@ 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-config-file=", "--experimental-config-file");
AddAlias("--experimental-default-config-file", "--experimental-config-file");
AddOption("--test",
"launch test runner on startup",
&EnvironmentOptions::test_runner,
Expand Down
1 change: 0 additions & 1 deletion src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,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_; }
Expand Down
1 change: 1 addition & 0 deletions test/parallel/test-cli-node-options-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]}`);
71 changes: 70 additions & 1 deletion test/parallel/test-config-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,34 @@ 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 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 override node.config.json when specified', onlyIfNodeOptionsSupport, async () => {
const result = await spawnPromisified(process.execPath, [
'--no-warnings',
'--experimental-default-config-file',
Expand All @@ -385,6 +412,48 @@ 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', {
Expand Down
2 changes: 0 additions & 2 deletions test/parallel/test-runner-flag-propagation.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ 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],
Comment on lines -18 to -19
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we removing those entries in the test?

['--env-file', '.env', '.env'],
['--env-file-if-exists', '.env', '.env'],
['--test-concurrency', '2', '2'],
Expand Down
Loading