Skip to content

fix(cli): support 'bun node <file>' — node emulation via argv[1] - #37277

Open
springmin wants to merge 3 commits into
oven-sh:mainfrom
springmin:claude/fix-bun-node-emulation
Open

fix(cli): support 'bun node <file>' — node emulation via argv[1]#37277
springmin wants to merge 3 commits into
oven-sh:mainfrom
springmin:claude/fix-bun-node-emulation

Conversation

@springmin

Copy link
Copy Markdown

What does this PR do?

PRETEND_TO_BE_NODE only triggered on argv0=node (via symlink). Running bun node file.js fell through to AutoCommand and errored with Script not found "node"test/cli/run/as-node.test.ts was 0/11 passing.

Changes

  1. src/runtime/cli/mod.rswhich(): after the flag-skip loop, treat first_arg_name == "node" exactly like argv0=node: disable unknown-flag warnings, set PRETEND_TO_BE_NODE, and return Tag::RunAsNodeCommand. A new IS_NODE_ARG static marks the argv[1] form (the symlink form keeps it false, so its positional layout is untouched).

  2. src/runtime/cli/run_command.rsexec_as_if_node: when IS_NODE_ARG is set, drop the literal "node" positional — clap's stop_after_positional_at=1 parked everything after it in passthrough, so node flags like -e were never parsed. Re-parse the passthrough head (-e/--eval/-p/--print/--version/--revision/--help) before the eval/REPL/positional dispatch, then promote the real target file back into positionals.

Why does it fix the bug?

With the "node" placeholder removed and node flags re-parsed, bun node <file> and bun node -e <code> reach the exact same code paths as the argv0=node symlink form, which already worked correctly.

How did you verify your code works?

  • bun test test/cli/run/as-node.test.ts11/11 pass (was 0/11)
  • Manual: bun node file.js, bun --bun node file.js, bun node -e "code", bun node --eval "code", bun node -p "1+1", bun node --version all work
  • No regression in multi-run or install lifecycle script tests

This is a pure CLI-logic change with no platform-specific code.

PRETEND_TO_BE_NODE only triggered on argv0=node (symlink); 'bun node
file.js' fell through to AutoCommand and errored with 'Script not found
"node"' (as-node.test.ts 0/11 pass).

- which(): after the flag-skip loop, treat first_arg_name == "node"
  like argv0=node: disable unknown-flag warnings, set
  PRETEND_TO_BE_NODE, return RunAsNodeCommand. New IS_NODE_ARG static
  marks the argv[1] form (symlink keeps it false).
- exec_as_if_node: with IS_NODE_ARG, drop the literal "node"
  positional — clap's stop_after_positional_at=1 parked everything
  after it in passthrough, so node flags were never parsed. Re-parse
  the passthrough head (-e/--eval/-p/--print/--version/--revision/
  --help) before the eval/REPL/positional dispatch, then promote the
  real file back into positionals.

Verified: as-node.test.ts 11/11 pass (was 0/11); manual 'bun node
<file>', '--bun node <file>', -e/--eval, -p, --version all work.

@claude claude Bot left a comment

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.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 93b3460c-4c05-4d1c-b611-ca444db5d78d

📥 Commits

Reviewing files that changed from the base of the PR and between 7df1fca and 82aef4c.

📒 Files selected for processing (1)
  • src/runtime/cli/run_command.rs

Walkthrough

Changes

The CLI now identifies bun node invocations separately from Node shims. The Node execution path reparses Node flags and restores the target script argument.

Node CLI parsing

Layer / File(s) Summary
Detect bun node invocations
src/runtime/cli/mod.rs
Command::which() recognizes a first positional node, enables Node emulation, records IS_NODE_ARG, and dispatches RunAsNodeCommand.
Reparse Node arguments
src/runtime/cli/run_command.rs
exec_as_if_node reparses passthrough flags, handles eval, print, version, revision, help, and --, then restores the target positional.

Possibly related PRs

Suggested reviewers: robobun, jarred-sumner, cirospaciari

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description check ✅ Passed The description explains the bug, implementation, rationale, and verification results, including all required template sections.
Title check ✅ Passed The title clearly and concisely describes the CLI fix for supporting bun node file invocations through Node emulation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🤖 Prompt for all review comments with 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.

Inline comments:
In `@src/runtime/cli/mod.rs`:
- Around line 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.

In `@src/runtime/cli/run_command.rs`:
- Around line 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.
- Around line 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.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c5d8e0f9-fa39-4d7e-a1bf-6794f6cc45ec

📥 Commits

Reviewing files that changed from the base of the PR and between d2f48c6 and 7df1fca.

📒 Files selected for processing (2)
  • src/runtime/cli/mod.rs
  • src/runtime/cli/run_command.rs

Comment thread src/runtime/cli/mod.rs
Comment on lines +972 to +980
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;

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

Comment on lines +3003 to +3035
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;
}
} 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));

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

Comment on lines +3009 to +3019
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;
}

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant