Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 17 additions & 0 deletions src/runtime/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,12 @@ pub use bun_install::PRETEND_TO_BE_NODE;
/// This is set `true` during `Command.which()` if argv0 is "bunx"
static IS_BUNX_EXE: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);

/// This is set `true` during `Command.which()` if argv[1] is "node"
/// (i.e. `bun node <file>`), so `exec_as_if_node` knows to skip the
/// "node" positional. argv0=node (symlink) keeps this false.
pub(crate) static IS_NODE_ARG: core::sync::atomic::AtomicBool =
core::sync::atomic::AtomicBool::new(false);

bun_core::declare_scope!(CLI, hidden);

pub(crate) type LoaderColonList =
Expand Down Expand Up @@ -963,6 +969,17 @@ pub mod command {
}
}

if first_arg_name == b"node" {
// `bun node <file>`: emulate node even though argv0 is "bun".
// Node-mode must not warn on flags Bun doesn't know.
bun_clap::streaming::WARN_ON_UNRECOGNIZED_FLAG
.store(false, core::sync::atomic::Ordering::Relaxed);
// SAFETY: single-threaded startup
PRETEND_TO_BE_NODE.store(true, core::sync::atomic::Ordering::Relaxed);
IS_NODE_ARG.store(true, core::sync::atomic::Ordering::Relaxed);
return Tag::RunAsNodeCommand;
Comment on lines +972 to +980

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Parse the first positional before enabling Node emulation.

The leading-flag loop does not consume option operands. Therefore, bun --cwd node app.js treats the --cwd value as the node command and selects RunAsNodeCommand.

Make the prescan consume value-taking options, or defer this decision until argument parsing identifies the actual first positional. As per coding guidelines, deliberately distinguish input forms at the CLI boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/cli/mod.rs` around lines 972 - 980, Update the first-argument
prescan in the CLI dispatch flow so it skips operands for value-taking options
such as --cwd before comparing first_arg_name with b"node". Ensure
RunAsNodeCommand is selected only when node is the actual first positional
argument, while preserving distinct handling for flag and operand forms.

Source: Coding guidelines

}

type RootCommandMatcher = strings::ExactSizeMatcher<12>;
let x = RootCommandMatcher::r#match(first_arg_name);
// PERF: `if x == const` is a chain of compares rather than a jump
Expand Down
50 changes: 50 additions & 0 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2987,6 +2987,56 @@ impl RunCommand {
// values. Explicit `--env-file` is still honored. #6338
ctx.args.disable_default_env_files = true;

// `bun node <file>`: argv[1]=="node" made which() take the node
// emulation path, but the literal "node" positional is still in
// ctx.positionals[0] — clap's stop_after_positional_at=1 parked
// everything after it in passthrough, so node flags like `-e` were
// never parsed either. Remove the "node" placeholder and re-parse the
// passthrough head as node flags. argv0=node (symlink) keeps
// IS_NODE_ARG false and never takes this path.
if crate::cli::IS_NODE_ARG.load(::core::sync::atomic::Ordering::Relaxed) {
if ctx.positionals.first().is_some_and(|p| p.as_ref() == b"node") {
ctx.positionals.remove(0);
// Re-parse node flags parked in passthrough (they were never
// seen by clap): -e/--eval <code>, -p/--print <code>,
// --version, --revision, --help.
while let Some(first) = ctx.passthrough.first().cloned() {
let first: &[u8] = &first;
if first == b"--" {
ctx.passthrough.remove(0);
break;
}
if first == b"-e" || first == b"--eval" {
ctx.passthrough.remove(0);
if !ctx.passthrough.is_empty() {
ctx.runtime_options.eval.script = ctx.passthrough.remove(0);
}
} else if first == b"-p" || first == b"--print" {
ctx.passthrough.remove(0);
if !ctx.passthrough.is_empty() {
ctx.runtime_options.eval.script = ctx.passthrough.remove(0);
ctx.runtime_options.eval.eval_and_print = true;
}
Comment on lines +3009 to +3019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Distinguish an empty eval string from a missing eval argument.

bun node -e "" and bun node -p "" store an empty script. The later !ctx.runtime_options.eval.script.is_empty() check then skips evaluation and reports a missing script. A missing value after -e or -p follows the same incorrect path.

Track eval-option presence separately from script bytes. Return an explicit option-value error only when the value is absent. As per coding guidelines, distinguish empty input from unset input.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/cli/run_command.rs` around lines 3009 - 3019, Update the
eval-option handling around the -e/--eval and -p/--print branches to track
whether an argument was supplied separately from the script’s byte contents.
Preserve an explicitly supplied empty script for evaluation, and return the
explicit option-value error only when no argument follows the option; ensure the
later missing-script check uses presence state rather than script emptiness.

Source: Coding guidelines

} else if first == b"--version" {
crate::cli::print_version_and_exit();
} else if first == b"--revision" {
crate::cli::print_revision_and_exit();
} else if first == b"--help" || first == b"-h" {
crate::cli::command::tag_print_help(CommandTag::RunAsNodeCommand, true);
Output::flush();
bun_core::Global::exit(0);
} else {
break;
}
}
if ctx.positionals.is_empty() && !ctx.passthrough.is_empty() {
// The real target file (or remaining arg) parked in
// passthrough; promote it back.
ctx.positionals.push(ctx.passthrough.remove(0));
Comment on lines +3003 to +3035

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not promote an unparsed Node option as the script.

For bun node --inspect app.js, the loop stops at --inspect, and Line 3035 promotes --inspect into ctx.positionals. The command then attempts to run the option as the script.

Use Node option parsing that preserves options and their operands in ctx.passthrough. Promote only the actual script positional. This must also support attached forms such as --eval=<code>. As per coding guidelines, use a real parser for user input and enumerate input forms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/cli/run_command.rs` around lines 3003 - 3035, Update the
run-command argument parsing loop around ctx.passthrough so unrecognized Node
options such as --inspect are parsed and retained with their operands rather
than promoted as ctx.positionals. Use the established real parser and explicitly
handle supported option forms, including attached --eval=<code>, then promote
only the actual script positional while preserving remaining arguments.

Source: Coding guidelines

}
}
}

// `node --interactive [-e code]`: same gate as AutoCommand — a script
// positional wins, and `-p` currently bypasses the REPL (see mod.rs).
if ctx.runtime_options.interactive
Expand Down