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
108 changes: 108 additions & 0 deletions argparse.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
#include "src/node_options.h"
#include "src/util.h"

#include <iostream>
#include <ranges>
#include <string>
#include <vector>

extern "C" {

uint64_t uv_get_total_memory() {
return 0;
}

uint64_t uv_get_constrained_memory() {
return 0;
}
};

namespace node {

void Assert(const AssertionInfo& info) {
fprintf(stderr,
"\n"
" # %s at %s\n"
" # Assertion failed: %s\n\n",
info.function ? info.function : "(unknown function)",
info.file_line ? info.file_line : "(unknown source location)",
info.message);

fflush(stderr);

abort();
}

} // namespace node

int main(int argc, char** argv) {
std::vector<std::string> args(argv, argv + argc);
std::vector<std::string> exec_args;
std::vector<std::string> errors;

node::PerProcessOptions cli_options;

cli_options.cmdline = args;

if (const char* result = std::getenv("NODE_OPTIONS")) {
std::string node_options(result);

std::vector<std::string> env_argv =
node::ParseNodeOptionsEnvVar(node_options, &errors);
for (auto error : errors) {
std::cout << "error: " << error << std::endl;
}
if (!errors.empty()) return 1;

env_argv.insert(env_argv.begin(), args.at(0));

{
std::vector<std::string> v8_args;
node::options_parser::Parse(&env_argv,
nullptr,
&v8_args,
&cli_options,
node::OptionEnvvarSettings::kAllowedInEnvvar,
&errors);

for (auto arg : v8_args | std::views::drop(1)) {
std::cout << "v8_arg: " << arg << std::endl;
}
}
}

node::HandleEnvOptions(cli_options.per_isolate->per_env,
Copy link
Copy Markdown
Member

@joyeecheung joyeecheung Sep 1, 2025

Choose a reason for hiding this comment

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

How would this be maintained going forward? For example, this creates a dependence on the current shape of PerProcessOptions etc, which are very much just ad-hoc internal data structures that are very subject to change. Things like HandleEnvOptions or Parse are also subject to internal refactoring, like for example adding a different mechanism than HandleEnvOptions for creating env var/flag pairs like EnvImplies("NODE_PENDING_DEPRECATION", "--pending-deprecation") and then just create inline versions for different parsing code to reduce the use of lambdas then this would be ditched. We can also change the option categories like creating RealmOptions etc, or stop using std::strings but use std::string_view on a backing store.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i would not expect node to maintain any compatibility for this interface aside from somehow being able to compile it as a standalone file or set of files that doesn't need to link against uv/libnode/v8/etc.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

also if node decided at some point in the future that it was too annoying to maintain even that and removed the ifdefs or whatever it was, that would be understandable.

Copy link
Copy Markdown
Member

@joyeecheung joyeecheung Sep 1, 2025

Choose a reason for hiding this comment

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

If compatibility is not a concern, then I think it makes more sense to just do a CI elsewhere instead of checking the test into the Node.js source, because then the maintainability burden would be shifted to Node.js contributors, and may create barriers to new contributors if they don't know the context and somehow think that breaking or removing this test is bad (think how many tests that are created > 10 years ago tend to give us a hard time in the CI because nobody knows what they are doing anymore or whether they can be removed because they are just irrelevant now). We can keep the macros internally to reduce the merge conflicts, but keeping the test in the CI doesn't seem very helpful if it's supposed to be maintained by another project. (I think every time I try to touch the node.h I have a lot of doubts about whether I am breaking something important, even with some GitHub archeology I still am doubtful and as a result, I think a lot of public APIs there are just rotting without users because nobody is certain whether they can be touched)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm think it is not guaranteed that node_options.h/cc has no dependencies like uv/libnode/v8/etc.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yeah I feel that to address #46339 we might end up adding a v8 dependency to the option parser....

[](const char* name) {
const char* value = std::getenv(name);
if (value != nullptr) {
return std::string(value);
}
return std::string("");
});

{
std::vector<std::string> v8_args;
node::options_parser::Parse(&args,
&exec_args,
&v8_args,
&cli_options,
node::OptionEnvvarSettings::kDisallowedInEnvvar,
&errors);

for (auto arg : v8_args | std::views::drop(1)) {
std::cout << "v8_arg: " << arg << std::endl;
}
}

for (auto arg : args) {
std::cout << "arg: " << arg << std::endl;
}

for (auto arg : exec_args) {
std::cout << "exec_arg: " << arg << std::endl;
}

for (auto error : errors) {
std::cout << "error: " << error << std::endl;
}
}
127 changes: 69 additions & 58 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include <string_view>
#include <vector>

#ifndef NODE_OPTIONS_STANDALONE
using v8::Boolean;
using v8::Context;
using v8::FunctionCallbackInfo;
Expand All @@ -36,12 +37,16 @@ using v8::Object;
using v8::String;
using v8::Undefined;
using v8::Value;
#endif // NODE_OPTIONS_STANDALONE

namespace node {

#ifndef NODE_OPTIONS_STANDALONE
namespace per_process {
Mutex cli_options_mutex;
std::shared_ptr<PerProcessOptions> cli_options{new PerProcessOptions()};
} // namespace per_process
#endif // NODE_OPTIONS_STANDALONE

void DebugOptions::CheckOptions(std::vector<std::string>* errors,
std::vector<std::string>* argv) {
Expand Down Expand Up @@ -293,64 +298,6 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,

namespace options_parser {

// Helper function to convert option types to their string representation
// and add them to a V8 Map
static bool AddOptionTypeToMap(Isolate* isolate,
Local<Context> context,
Local<Map> map,
const std::string& option_name,
const OptionType& option_type) {
std::string type;
switch (static_cast<int>(option_type)) {
case 0: // No-op
case 1: // V8 flags
break; // V8 and NoOp flags are not supported

case 2:
type = "boolean";
break;
case 3: // integer
case 4: // unsigned integer
case 6: // host port
type = "number";
break;
case 5: // string
type = "string";
break;
case 7: // string array
type = "array";
break;
default:
UNREACHABLE();
}

if (type.empty()) {
return true; // Skip this entry but continue processing
}

Local<String> option_key;
if (!String::NewFromUtf8(isolate,
option_name.data(),
v8::NewStringType::kNormal,
option_name.size())
.ToLocal(&option_key)) {
return true; // Skip this entry but continue processing
}

Local<String> type_value;
if (!String::NewFromUtf8(
isolate, type.data(), v8::NewStringType::kNormal, type.size())
.ToLocal(&type_value)) {
return true; // Skip this entry but continue processing
}

if (map->Set(context, option_key, type_value).IsEmpty()) {
return false; // Error occurred, stop processing
}

return true;
}

class DebugOptionsParser : public OptionsParser<DebugOptions> {
public:
DebugOptionsParser();
Expand Down Expand Up @@ -1445,6 +1392,65 @@ HostPort SplitHostPort(const std::string& arg,
ParseAndValidatePort(arg.substr(colon + 1), errors) };
}

#ifndef NODE_OPTIONS_STANDALONE
// Helper function to convert option types to their string representation
// and add them to a V8 Map
static bool AddOptionTypeToMap(Isolate* isolate,
Local<Context> context,
Local<Map> map,
const std::string& option_name,
const OptionType& option_type) {
std::string type;
switch (static_cast<int>(option_type)) {
case 0: // No-op
case 1: // V8 flags
break; // V8 and NoOp flags are not supported

case 2:
type = "boolean";
break;
case 3: // integer
case 4: // unsigned integer
case 6: // host port
type = "number";
break;
case 5: // string
type = "string";
break;
case 7: // string array
type = "array";
break;
default:
UNREACHABLE();
}

if (type.empty()) {
return true; // Skip this entry but continue processing
}

Local<String> option_key;
if (!String::NewFromUtf8(isolate,
option_name.data(),
v8::NewStringType::kNormal,
option_name.size())
.ToLocal(&option_key)) {
return true; // Skip this entry but continue processing
}

Local<String> type_value;
if (!String::NewFromUtf8(
isolate, type.data(), v8::NewStringType::kNormal, type.size())
.ToLocal(&type_value)) {
return true; // Skip this entry but continue processing
}

if (map->Set(context, option_key, type_value).IsEmpty()) {
return false; // Error occurred, stop processing
}

return true;
}

std::string GetBashCompletion() {
Mutex::ScopedLock lock(per_process::cli_options_mutex);
const auto& parser = _ppop_instance;
Expand Down Expand Up @@ -2033,14 +2039,17 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(GetEnvOptionsInputType);
registry->Register(GetNamespaceOptionsInputType);
}
#endif // NODE_OPTIONS_STANDALONE
} // namespace options_parser

#ifndef NODE_OPTIONS_STANDALONE
void HandleEnvOptions(std::shared_ptr<EnvironmentOptions> env_options) {
HandleEnvOptions(env_options, [](const char* name) {
std::string text;
return credentials::SafeGetenv(name, &text) ? text : "";
});
}
#endif // NODE_OPTIONS_STANDALONE

void HandleEnvOptions(std::shared_ptr<EnvironmentOptions> env_options,
std::function<std::string(const char*)> opt_getter) {
Expand Down Expand Up @@ -2100,6 +2109,8 @@ std::vector<std::string> ParseNodeOptionsEnvVar(
}
} // namespace node

#ifndef NODE_OPTIONS_STANDALONE
NODE_BINDING_CONTEXT_AWARE_INTERNAL(options, node::options_parser::Initialize)
NODE_BINDING_EXTERNAL_REFERENCE(
options, node::options_parser::RegisterExternalReferences)
#endif // NODE_OPTIONS_STANDALONE
5 changes: 4 additions & 1 deletion src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,6 @@ namespace options_parser {

HostPort SplitHostPort(const std::string& arg,
std::vector<std::string>* errors);
void GetOptions(const v8::FunctionCallbackInfo<v8::Value>& args);
std::string GetBashCompletion();

enum OptionType {
Expand Down Expand Up @@ -638,6 +637,7 @@ class OptionsParser {
template <typename OtherOptions>
friend class OptionsParser;

#ifndef NODE_OPTIONS_STANDALONE
friend void GetCLIOptionsValues(
const v8::FunctionCallbackInfo<v8::Value>& args);
friend void GetCLIOptionsInfo(
Expand All @@ -652,6 +652,7 @@ class OptionsParser {
const v8::FunctionCallbackInfo<v8::Value>& args);
friend void GetOptionsAsFlags(
const v8::FunctionCallbackInfo<v8::Value>& args);
#endif // NODE_OPTIONS_STANDALONE
};

using StringVector = std::vector<std::string>;
Expand All @@ -663,6 +664,7 @@ void Parse(

} // namespace options_parser

#ifndef NODE_OPTIONS_STANDALONE
namespace per_process {

extern Mutex cli_options_mutex;
Expand All @@ -671,6 +673,7 @@ extern NODE_EXTERN_PRIVATE std::shared_ptr<PerProcessOptions> cli_options;
} // namespace per_process

void HandleEnvOptions(std::shared_ptr<EnvironmentOptions> env_options);
#endif // NODE_OPTIONS_STANDALONE
void HandleEnvOptions(std::shared_ptr<EnvironmentOptions> env_options,
std::function<std::string(const char*)> opt_getter);

Expand Down
Loading