/
githubmirror
/
node
Обзор
Документация
Войти
/
githubmirror
/
node
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/node_config_file.cc
560 строк
18 KB
Marco Ippolito
src: support multiple versions in node.config.json
06 май 2026, 20:27
06 май 2026, 20:27
4002853
Код
Авторство
О чём код?
#include "node_config_file.h" #include "debug_utils-inl.h" #include "node_version.h" #include "simdjson.h" #include <cinttypes> 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"; constexpr std::string_view kSchemaField = "$schema"; 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( 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) { std::string& arg = (*args)[i]; 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 (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; } } return result; } bool ConfigReader::HasInvalidDefaultConfigFileArgument() const { return invalid_default_config_file_argument_; } ParseResult ConfigReader::ProcessOptionValue( const std::pair<std::string, options_parser::OptionMappingDetails>& option_details, simdjson::ondemand::value* option_value, std::vector<std::string>* output) { const std::string& option_name = option_details.first; const options_parser::OptionType option_type = option_details.second.type; switch (option_type) { case options_parser::OptionType::kBoolean: { bool result; if (option_value->get_bool().get(result)) { FPrintF(stderr, "Invalid value for %s\n", option_name); return ParseResult::InvalidContent; } if (result) { // If the value is true, we need to set the flag output->push_back(option_name); } else { // Ensure negation is made putting the "--no-" prefix output->push_back("--no-" + option_name.substr(2, option_name.size() - 2)); } break; } // String array can allow both string and array types case options_parser::OptionType::kStringList: { simdjson::ondemand::json_type field_type; if (option_value->type().get(field_type)) { return ParseResult::InvalidContent; } switch (field_type) { case simdjson::ondemand::json_type::array: { std::vector<std::string> result; simdjson::ondemand::array raw_imports; if (option_value->get_array().get(raw_imports)) { FPrintF(stderr, "Invalid value for %s\n", option_name); return ParseResult::InvalidContent; } for (auto raw_import : raw_imports) { std::string_view import; if (raw_import.get_string(import)) { FPrintF(stderr, "Invalid value for %s\n", option_name); return ParseResult::InvalidContent; } output->push_back(option_name + "=" + std::string(import)); } break; } case simdjson::ondemand::json_type::string: { std::string result; if (option_value->get_string(result)) { FPrintF(stderr, "Invalid value for %s\n", option_name); return ParseResult::InvalidContent; } output->push_back(option_name + "=" + result); break; } default: FPrintF(stderr, "Invalid value for %s\n", option_name); return ParseResult::InvalidContent; } break; } case options_parser::OptionType::kString: { std::string result; if (option_value->get_string(result)) { FPrintF(stderr, "Invalid value for %s\n", option_name); return ParseResult::InvalidContent; } output->push_back(option_name + "=" + result); break; } case options_parser::OptionType::kInteger: { int64_t result; if (option_value->get_int64().get(result)) { FPrintF(stderr, "Invalid value for %s\n", option_name); return ParseResult::InvalidContent; } output->push_back(option_name + "=" + std::to_string(result)); break; } case options_parser::OptionType::kHostPort: case options_parser::OptionType::kUInteger: { uint64_t result; if (option_value->get_uint64().get(result)) { FPrintF(stderr, "Invalid value for %s\n", option_name); return ParseResult::InvalidContent; } output->push_back(option_name + "=" + std::to_string(result)); break; } case options_parser::OptionType::kNoOp: { FPrintF( stderr, "No-op flag %s is currently not supported\n", option_name); return ParseResult::InvalidContent; } case options_parser::OptionType::kV8Option: { FPrintF(stderr, "V8 flag %s is currently not supported\n", option_name); return ParseResult::InvalidContent; } default: UNREACHABLE(); } return ParseResult::Valid; } ParseResult ConfigReader::ParseOptions( simdjson::ondemand::object* options_object, std::unordered_set<std::string>* unique_options, const std::string& namespace_name) { // Determine which options map to use and output vector std::unordered_map<std::string, options_parser::OptionMappingDetails> options_map; std::vector<std::string>* output_vector; if (namespace_name == "nodeOptions") { // Special case for backward compatibility: handle nodeOptions with env // options map options_map = options_parser::MapEnvOptionsFlagInputType(); output_vector = &node_options_; } else { // Handle other namespaces options_map = options_parser::MapOptionsByNamespace(namespace_name); output_vector = &namespace_options_; if (!env_options_initialized_) { env_options_map_ = options_parser::MapEnvOptionsFlagInputType(); env_options_initialized_ = true; } } simdjson::ondemand::value option_value; std::string_view option_key; for (auto field : *options_object) { if (field.unescaped_key().get(option_key) || field.value().get(option_value)) { return ParseResult::InvalidContent; } // The key needs to match the CLI option std::string prefix = "--"; auto option = options_map.find(prefix.append(option_key)); if (option != options_map.end()) { // If the option has already been set, return an error if (unique_options->contains(option->first)) { FPrintF(stderr, "Option %s is already defined\n", option->first); return ParseResult::InvalidContent; } // Add the option to the unique set to prevent duplicates // on future iterations unique_options->insert(option->first); // Process the option value based on its type ParseResult result = ProcessOptionValue(*option, &option_value, output_vector); if (result != ParseResult::Valid) { return result; } } else { FPrintF(stderr, "Unknown or not allowed option %s for namespace %s\n", option_key, namespace_name); return ParseResult::InvalidContent; } } return ParseResult::Valid; } ParseResult ConfigReader::ParseNodeVersion( simdjson::ondemand::value* version_value, const std::string_view& config_path) { int64_t version; if (version_value->get_int64().get(version)) { FPrintF(stderr, "\"nodeVersion\" value unexpected for %s " "(should be an integer)\n", config_path.data()); return ParseResult::InvalidContent; } if (version != NODE_MAJOR_VERSION) { FPrintF(stderr, "\"nodeVersion\" %" PRId64 " does not match current Node.js version %d " "for %s\n", version, NODE_MAJOR_VERSION, config_path.data()); return ParseResult::InvalidContent; } return ParseResult::Valid; } ParseResult ConfigReader::ParseConfigs(simdjson::ondemand::array* configs, const std::string_view& config_path) { size_t index = 0; for (auto raw_config : *configs) { simdjson::ondemand::object config_wrapper; if (raw_config.get_object().get(config_wrapper)) { FPrintF(stderr, "\"configs[%zu]\" value unexpected for %s " "(should be an object)\n", index, config_path.data()); return ParseResult::InvalidContent; } simdjson::ondemand::value version_value; auto version_error = config_wrapper.find_field_unordered("nodeVersion").get(version_value); if (version_error == simdjson::NO_SUCH_FIELD) { FPrintF(stderr, "\"configs[%zu].nodeVersion\" is required for %s\n", index, config_path.data()); return ParseResult::InvalidContent; } if (version_error) { return ParseResult::InvalidContent; } int64_t version; if (version_value.get_int64().get(version)) { FPrintF(stderr, "\"configs[%zu].nodeVersion\" value unexpected for %s " "(should be an integer)\n", index, config_path.data()); return ParseResult::InvalidContent; } if (version != NODE_MAJOR_VERSION) { index++; continue; } simdjson::ondemand::value config_value; auto config_error = config_wrapper.find_field_unordered("config").get(config_value); if (config_error == simdjson::NO_SUCH_FIELD) { FPrintF(stderr, "\"configs[%zu].config\" is required for %s\n", index, config_path.data()); return ParseResult::InvalidContent; } if (config_error) { return ParseResult::InvalidContent; } simdjson::ondemand::object selected_config; if (config_value.get_object().get(selected_config)) { FPrintF(stderr, "\"configs[%zu].config\" value unexpected for %s " "(should be an object)\n", index, config_path.data()); return ParseResult::InvalidContent; } return ParseConfigObject(&selected_config, config_path, false); } FPrintF(stderr, "No config found for current Node.js version %d in " "\"configs\" for %s\n", NODE_MAJOR_VERSION, config_path.data()); return ParseResult::InvalidContent; } ParseResult ConfigReader::ParseConfigObject( simdjson::ondemand::object* config_object, const std::string_view& config_path, bool allow_version_selection) { // Get all available namespaces for validation std::vector<std::string> available_namespaces = options_parser::MapAvailableNamespaces(); // Add "nodeOptions" as a special case for backward compatibility available_namespaces.emplace_back("nodeOptions"); // Create a set for faster lookup of valid namespaces std::unordered_set<std::string> valid_namespaces(available_namespaces.begin(), available_namespaces.end()); // Create a set to track unique options std::unordered_set<std::string> unique_options; // Namespaces in OPTION_NAMESPACE_LIST std::unordered_set<std::string> namespaces_with_implicit_flags; // Iterate through the main object to find all namespaces for (auto field : *config_object) { std::string_view field_name; if (field.unescaped_key().get(field_name)) { return ParseResult::InvalidContent; } std::string namespace_name(field_name); if (namespace_name == "$schema") { continue; } if (namespace_name == "nodeVersion") { simdjson::ondemand::value version_value; if (field.value().get(version_value)) { return ParseResult::InvalidContent; } ParseResult result = ParseNodeVersion(&version_value, config_path); if (result != ParseResult::Valid) { return result; } continue; } if (namespace_name == "configs") { if (!allow_version_selection) { FPrintF(stderr, "\"configs\" is not allowed inside a versioned config " "for %s\n", config_path.data()); return ParseResult::InvalidContent; } simdjson::ondemand::array configs; auto field_error = field.value().get_array().get(configs); if (field_error) { FPrintF(stderr, "\"configs\" value unexpected for %s " "(should be an array)\n", config_path.data()); return ParseResult::InvalidContent; } ParseResult result = ParseConfigs(&configs, config_path); if (result != ParseResult::Valid) { return result; } continue; } // TODO(@marco-ippolito): Remove warning for testRunner namespace if (namespace_name == "testRunner") { FPrintF(stderr, "the \"testRunner\" namespace has been removed. " "Use \"test\" instead.\n"); // Better to throw an error than to ignore it // Otherwise users might think their test suite is green // when it's not running return ParseResult::InvalidContent; } if (namespace_name == kSchemaField) { continue; } // Check if this field is a valid namespace if (!valid_namespaces.contains(namespace_name)) { FPrintF(stderr, "Unknown namespace %s\n", namespace_name); return ParseResult::InvalidContent; } // List of implicit namespace flags for (auto ns_enum : options_parser::AllNamespaces()) { std::string ns_str = options_parser::NamespaceEnumToString(ns_enum); if (!ns_str.empty() && namespace_name == ns_str) { namespaces_with_implicit_flags.insert(namespace_name); break; } } // Get the namespace object simdjson::ondemand::object namespace_object; auto field_error = field.value().get_object().get(namespace_object); // If namespace value is not an object if (field_error) { FPrintF(stderr, "\"%s\" value unexpected for %s (should be an object)\n", namespace_name, config_path.data()); return ParseResult::InvalidContent; } // Process options for this namespace using the unified method ParseResult result = ParseOptions(&namespace_object, &unique_options, namespace_name); if (result != ParseResult::Valid) { return result; } } // Add implicit flags for namespaces (--test, --permission, --watch) // These flags are automatically enabled when their namespace is present for (const auto& ns : namespaces_with_implicit_flags) { std::string flag = "--" + ns; std::string no_flag = "--no-" + ns; // We skip if the user has already set the flag or its negation if (!unique_options.contains(flag) && !unique_options.contains(no_flag)) { namespace_options_.push_back(flag); } } return ParseResult::Valid; } ParseResult ConfigReader::ParseConfig(const std::string_view& config_path) { std::string file_content; // Read the configuration file int r = ReadFileSync(&file_content, config_path.data()); 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.data()); return ParseResult::InvalidContent; } // Validate config is an object simdjson::ondemand::object main_object; auto root_error = document.get_object().get(main_object); if (root_error) { if (root_error == simdjson::error_code::INCORRECT_TYPE) { FPrintF(stderr, "Root value unexpected not an object for %s\n\n", config_path.data()); } else { FPrintF(stderr, "Can't parse %s\n", config_path.data()); } return ParseResult::InvalidContent; } bool has_configs = false; bool has_other_fields = false; for (auto field : main_object) { std::string_view field_name; if (field.unescaped_key().get(field_name)) { return ParseResult::InvalidContent; } if (field_name == "$schema") { continue; } if (field_name == "configs") { has_configs = true; } else { has_other_fields = true; } } if (has_configs && has_other_fields) { FPrintF(stderr, "\"configs\" cannot be mixed with other configuration fields " "for %s\n", config_path.data()); return ParseResult::InvalidContent; } simdjson::ondemand::parser config_parser; simdjson::ondemand::document config_document; if (config_parser.iterate(file_content).get(config_document)) { FPrintF(stderr, "Can't parse %s\n", config_path.data()); return ParseResult::InvalidContent; } simdjson::ondemand::object config_object; if (config_document.get_object().get(config_object)) { return ParseResult::InvalidContent; } return ParseConfigObject(&config_object, config_path, true); } std::string ConfigReader::GetNodeOptions() { std::string acc = ""; const size_t total_options = node_options_.size(); acc.reserve(total_options * 2); for (auto& opt : node_options_) { acc += " " + opt; } return acc; } const std::vector<std::string>& ConfigReader::GetNamespaceFlags() const { return namespace_options_; } size_t ConfigReader::GetFlagsSize() { return node_options_.size(); } } // namespace node