Skip to content

Commit

Permalink
src: add config file support
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-ippolito committed Feb 12, 2025
1 parent 9ce1fff commit 3f46529
Show file tree
Hide file tree
Showing 18 changed files with 423 additions and 0 deletions.
30 changes: 30 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,36 @@ added: v23.6.0
Enable experimental import support for `.node` addons.

### `--experimental-config-file`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.0 - Early development
Use this flag to specify a configuration file that will be loaded and parsed
before the application starts.
Node.js will read the configuration file and apply the settings as
[`NODE_OPTIONS`][].
The configuration file should be a JSON file
with the following structure:

```json
{
"version": 0,
"experimental_transform_types": true
}
```

Currently the only supported version is 0.
The configuration file cannot be used in conjuction with `--env-file`.
The configuration file cannot be used if [`NODE_OPTIONS`][] are set.
The configuration file will not override the flag passed in the command line.
If multiple keys are present in the configuration file, only the first one
will be considered and the followin will be ignored.
Unknown keys will be ignored.

### `--experimental-eventsource`

<!-- YAML
Expand Down
3 changes: 3 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ Interpret the entry point as a URL.
.It Fl -experimental-addon-modules
Enable experimental addon module support.
.
.It Fl -experimental-config-file
Enable support for experimental config file
.
.It Fl -experimental-import-meta-resolve
Enable experimental ES modules support for import.meta.resolve().
.
Expand Down
8 changes: 8 additions & 0 deletions lib/internal/process/pre_execution.js
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ function prepareExecution(options) {
initializeSourceMapsHandlers();
initializeDeprecations();

setupConfigFile();

require('internal/dns/utils').initializeDns();

if (isMainThread) {
Expand Down Expand Up @@ -312,6 +314,12 @@ function setupSQLite() {
BuiltinModule.allowRequireByUsers('sqlite');
}

function setupConfigFile() {
if (getOptionValue('--experimental-config-file')) {
emitExperimentalWarning('--experimental-config-file');
}
}

function setupQuic() {
if (!getOptionValue('--experimental-quic')) {
return;
Expand Down
2 changes: 2 additions & 0 deletions node.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@
'src/node_process_events.cc',
'src/node_process_methods.cc',
'src/node_process_object.cc',
'src/node_rc.cc',
'src/node_realm.cc',
'src/node_report.cc',
'src/node_report_module.cc',
Expand Down Expand Up @@ -262,6 +263,7 @@
'src/node_platform.h',
'src/node_process.h',
'src/node_process-inl.h',
'src/node_rc.h',
'src/node_realm.h',
'src/node_realm-inl.h',
'src/node_report.h',
Expand Down
30 changes: 30 additions & 0 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

#include "node.h"
#include "node_dotenv.h"
#include "node_rc.h"
#include "node_task_runner.h"

// ========== local headers ==========
Expand Down Expand Up @@ -150,6 +151,9 @@ namespace per_process {
// Instance is used to store environment variables including NODE_OPTIONS.
node::Dotenv dotenv_file = Dotenv();

// node_rc.h
node::ConfigReader config_reader = ConfigReader();

// node_revert.h
// Bit flag used to track security reverts.
unsigned int reverted_cve = 0;
Expand Down Expand Up @@ -884,6 +888,32 @@ static ExitCode InitializeNodeWithArgsInternal(
per_process::dotenv_file.AssignNodeOptionsIfAvailable(&node_options);
}

auto result = per_process::config_reader.GetDataFromArgs(*argv);
// Skip if env_files is not empty, as it has already been processed.
if (result.has_value() && !env_files.empty()) {
errors->push_back(
"--experimental-config-file cannot be used with .env files");
return ExitCode::kInvalidCommandLineArgument;
}
if (result.has_value() && env_files.empty()) {
switch (per_process::config_reader.ParseConfig(result.value())) {
case ConfigReader::ParseResult::Valid:
break;
case ConfigReader::ParseResult::InvalidContent:
errors->push_back(result.value() + ": invalid format");
break;
case ConfigReader::ParseResult::FileError:
errors->push_back(result.value() + ": not found");
break;
case ConfigReader::ParseResult::InvalidVersion:
errors->push_back(result.value() + ": invalid version");
break;
default:
UNREACHABLE();
}
per_process::config_reader.AssignNodeOptions(&node_options);
}

#if !defined(NODE_WITHOUT_NODE_OPTIONS)
if (!(flags & ProcessInitializationFlags::kDisableNodeOptionsEnv)) {
// NODE_OPTIONS environment variable is preferred over the file one.
Expand Down
3 changes: 3 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"set environment variables from supplied file",
&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);
AddOption("--test",
"launch test runner on startup",
&EnvironmentOptions::test_runner);
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ class EnvironmentOptions : public Options {

bool report_exclude_env = false;
bool report_exclude_network = false;
std::string experimental_config_file;

inline DebugOptions* get_debug_options() { return &debug_options_; }
inline const DebugOptions& debug_options() const { return debug_options_; }
Expand Down
133 changes: 133 additions & 0 deletions src/node_rc.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#include "node_rc.h"
#include "debug_utils-inl.h"
#include "env-inl.h"
#include "node_errors.h"
#include "node_file.h"
#include "node_internals.h"
#include "simdjson.h"

#include <functional>
#include <map>
#include <string>

namespace node {

std::optional<ConfigReader::ConfigV0> ConfigReader::ParseConfigV0(
simdjson::ondemand::object* main_object) {
ConfigReader::ConfigV0 config;
config.version = 0;

if (auto value = (*main_object)["experimental_transform_types"];
value.error() != simdjson::NO_SUCH_FIELD) {
if (value.get_bool().get(config.experimental_transform_types)) {
FPrintF(stderr, "Invalid value for experimental_transform_types\n");
return std::nullopt;
}
}

return config;
}

ConfigReader::ConfigReader() {
config_parsers_[0] = &ConfigReader::ParseConfigV0;
}

std::optional<std::string> ConfigReader::GetDataFromArgs(
const std::vector<std::string>& args) {
constexpr std::string_view flag = "--experimental-config-file";

for (auto it = args.begin(); it != args.end(); ++it) {
if (*it == flag) {
// Case: "--experimental-config-file foo"
if (auto next = std::next(it); next != args.end()) {
return *next;
}
} else if (it->starts_with(flag)) {
// Case: "--experimental-config-file=foo"
if (it->size() > flag.size() && (*it)[flag.size()] == '=') {
return it->substr(flag.size() + 1);
}
}
}

return std::nullopt;
}

ConfigReader::ParseResult ConfigReader::ParseConfig(
const std::string& config_path) {
std::string file_content;
// Read the configuration file
int r = ReadFileSync(&file_content, config_path.c_str());
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.c_str());
return ParseResult::InvalidContent;
}

simdjson::ondemand::object main_object;
// If document is not an object, throw an error.
if (auto root_error = document.get_object().get(main_object)) {
if (root_error == simdjson::error_code::INCORRECT_TYPE) {
FPrintF(stderr,
"Root value unexpected not an object for %s\n\n",
config_path.c_str());
} else {
FPrintF(stderr, "Can't parse %s\n", config_path.c_str());
}
return ParseResult::InvalidContent;
}

// If json object doesn't have "version" field, throw an error.
simdjson::ondemand::number version_field;
if (main_object["version"].get_number().get(version_field)) {
FPrintF(stderr,
"Can't find numeric \"version\" field in %s\n",
config_path.c_str());
return ParseResult::InvalidVersion;
}

// Check if version is an integer
if (!version_field.is_int64()) {
FPrintF(
stderr, "Version field is not an integer in %s\n", config_path.c_str());
return ParseResult::InvalidVersion;
}

uint64_t version = version_field.get_int64();
if (version < 0 || version >= config_parsers_.size()) {
FPrintF(stderr, "Version %" PRIu64 " does not exist\n", version);
return ParseResult::InvalidVersion;
}

// Get the config parser for the specific version
auto config_parser = config_parsers_.at(version_field.get_int64());
auto config = config_parser(&main_object);
if (!config.has_value()) {
return ParseResult::InvalidContent;
}

// save the config for later
config_ = config.value();
return ParseResult::Valid;
}

void ConfigReader::AssignNodeOptions(std::string* node_options) {
if (ConfigV0* config = std::get_if<ConfigReader::ConfigV0>(&config_)) {
std::string result = "";
if (config->experimental_transform_types) {
result += "--experimental-transform-types";
}
*node_options = result;
return;
}
}
} // namespace node
49 changes: 49 additions & 0 deletions src/node_rc.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#ifndef SRC_NODE_RC_H_
#define SRC_NODE_RC_H_

#if defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#include <map>
#include <string>
#include <variant>
#include "simdjson.h"
#include "util-inl.h"

namespace node {

class ConfigReader {
public:
enum ParseResult { Valid, FileError, InvalidContent, InvalidVersion };
struct ConfigV0 {
int64_t version;
bool experimental_transform_types;
};
using Config = std::variant<ConfigV0>;
using ConfigParser =
std::function<std::optional<Config>(simdjson::ondemand::object*)>;

ConfigReader();

ConfigReader::ParseResult ParseConfig(const std::string& config_path);

std::optional<std::string> GetDataFromArgs(
const std::vector<std::string>& args);

void AssignNodeOptions(std::string* node_options);

private:
simdjson::ondemand::parser json_parser_;

ConfigReader::Config config_;

static std::optional<ConfigReader::ConfigV0> ParseConfigV0(
simdjson::ondemand::object* main_object);

std::array<ConfigParser, 1> config_parsers_;
};

} // namespace node

#endif // defined(NODE_WANT_INTERNALS) && NODE_WANT_INTERNALS

#endif // SRC_NODE_RC_H_
4 changes: 4 additions & 0 deletions test/fixtures/rc/empty-object.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{

}

1 change: 1 addition & 0 deletions test/fixtures/rc/empty.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

3 changes: 3 additions & 0 deletions test/fixtures/rc/non-existing-version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": 9999
}
3 changes: 3 additions & 0 deletions test/fixtures/rc/non-numeric-version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": "foo"
}
5 changes: 5 additions & 0 deletions test/fixtures/rc/override-property.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"version": 0,
"experimental_transform_types": true,
"experimental_transform_types": false
}
4 changes: 4 additions & 0 deletions test/fixtures/rc/override-version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": 0,
"version": 9999
}
4 changes: 4 additions & 0 deletions test/fixtures/rc/transform-types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": 0,
"experimental_transform_types": true
}
3 changes: 3 additions & 0 deletions test/fixtures/rc/version-only.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"version": 0
}
Loading

0 comments on commit 3f46529

Please sign in to comment.