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 5d7ff50
Show file tree
Hide file tree
Showing 18 changed files with 386 additions and 0 deletions.
28 changes: 28 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -911,6 +911,34 @@ 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`.
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 5d7ff50

Please sign in to comment.