Skip to content

node emulation: keep the first positional in process.argv for node -e / -p - #38556

Closed
robobun wants to merge 1 commit into
mainfrom
farm/4a1a1d36/as-node-eval-argv
Closed

robobun wants to merge 1 commit into
mainfrom
farm/4a1a1d36/as-node-eval-argv

Conversation

@robobun

@robobun robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • When bun runs as node (the node symlink bun installs, bun --bun node ..., or anything that spawns bun with argv0 = node), -e / -p / -pe drop the first argument after the code from process.argv:
    $ bun --bun node -e 'console.log(JSON.stringify(process.argv.slice(1)))' a b
    ["b"]
    $ node -e 'console.log(JSON.stringify(process.argv.slice(1)))' a b        # node v26.3.0
    ["a","b"]
    $ bun -e 'console.log(JSON.stringify(process.argv.slice(1)))' a b         # plain bun is already right
    ["a","b"]
    
    With a single argument (node -e code a) it is lost entirely and process.argv.slice(1) is [].
  • Cause: Arguments::parse parses RunAsNodeCommand with stop_after_positional_at: 1 (src/runtime/cli/Arguments.rs:786, same as AutoCommand), so node -e code a b arrives as ctx.positionals = ["a"], ctx.passthrough = ["b"]. The -e branch of RunCommand::exec_as_if_node (src/runtime/cli/run_command.rs:2903) built the synthetic cwd/[eval] entry and called boot() directly, and boot() only hands ctx.passthrough to vm.argv (run_command.rs:966). positionals[0] was never copied over.
  • bun -e does not have the bug because Command::exec_auto_or_run routes it through RunCommand::exec_eval (run_command.rs:2859), which prepends positionals onto passthrough before booting. The bun -e form of this bug was bun --eval ignores first command line arg #12209 (closed); the exec_as_if_node branch was a copy of exec_eval minus that step (the pre-port Zig execAsIfNode had the same shape, so this is not a regression).

Fix

  • exec_as_if_node's -e/-p branch now calls Self::exec_eval(ctx) instead of re-implementing it; the fixing line is the return Self::exec_eval(ctx); hunk, the rest of the diff is the deleted copy.
  • Correct because -e/-p never take a script positional, so under node everything after the code is process.argv (output above); exec_eval is the one place that already encodes that for the bun -e path, and the two commands split argv identically (stop_after_positional_at: 1), so the node path needs the identical merge. The [eval] entry path and boot() call are unchanged, exec_eval builds the same ones.
  • No-argument node -e code is unaffected: exec_eval skips the merge when positionals is empty. node --interactive -e code is unaffected: it is routed to exec_node_repl before this branch, as before.
  • Verified: test/cli/run/as-node.test.ts gains a test.each covering -e with one, two, and flag-like arguments, -e with none, -p, and -pe. With the released bun (USE_SYSTEM_BUN=1) the five cases with arguments fail (each receives the list minus its first entry); with this build all 17 tests in the file pass.
  • Also run against this build: test/cli/run/run-eval.test.ts (37 pass, covers the shared exec_eval path for bun -e), the bun-as-node tests in test/js/bun/repl/repl.test.ts, and the argv0=node tests in test/cli/run/env.test.ts.

Background

  • Node emulation: Command::which (src/runtime/cli/mod.rs) dispatches to RunAsNodeCommand when argv[0] is node; RunCommand::exec_as_if_node is that command's body. It is how bun --bun makes child processes that exec node run under bun.
  • positionals vs passthrough: the CLI parser collects non-flag arguments into ctx.positionals until it has seen stop_after_positional_at of them; everything after that point, flags included, is left unparsed in ctx.passthrough. For the run-like commands the limit is 1 so that flags after the script name belong to the script. boot() installs ctx.passthrough as vm.argv, which becomes process.argv after the executable path.
  • [eval]: -e code has no file, so bun boots the VM with a synthetic <cwd>/[eval] entry path, and the module loader serves ctx.runtime_options.eval.script for that path.

Related, not changed here

Other node-CLI shapes in the same exec_as_if_node / argv area, each already owned by its own open issue or PR, so this PR stays one hunk:

… / -p

exec_as_if_node built the [eval] entry and booted directly, but
Arguments::parse stops at the first positional for RunAsNodeCommand, so
`node -e code a b` arrived as positionals=["a"], passthrough=["b"] and
boot() only hands passthrough to process.argv. Route the branch through
exec_eval, which already folds positionals into passthrough for
`bun -e`.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 60d2d037-5851-4d42-877e-d1175be300a4

📥 Commits

Reviewing files that changed from the base of the PR and between eabb96d and cea7aca.

📒 Files selected for processing (2)
  • src/runtime/cli/run_command.rs
  • test/cli/run/as-node.test.ts

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

@robobun

robobun commented Aug 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. The diff is green; CI is blocked only on macOS agent capacity.

  • Reproduced with the released bun (USE_SYSTEM_BUN=1 bun test test/cli/run/as-node.test.ts): the five new cases that pass arguments after -e / -p / -pe each receive the list minus its first entry, e.g. node -e '...' a b prints ["b"] where node prints ["a","b"]. With this branch built, all 17 tests in the file pass.
  • Build 96166 (cea7aca): 177 of 179 jobs passed. The only two that did not are the darwin 14 aarch64 - test-bun jobs, which expired after three automatic retries without ever being assigned an agent; every build queued alongside this one currently shows the same two jobs waiting, so this is a lane backlog, not something in this change. The test file ran and passed on the lanes that did execute (for example the debian 13 x64-asan shard). The remaining annotations are tests that passed on retry, none of them in this area.
  • Not re-running the whole build while that lane is backlogged; retrying just the two expired jobs once agents are available is enough.
  • Automated review had no findings; the description's "Related, not changed here" section lists the neighbouring node-CLI shapes that are tracked elsewhere.

@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.

LGTM — deduplicates the node-emulation -e/-p path onto exec_eval, which fixes the dropped first positional in process.argv.

What was reviewed:

  • Confirmed the deleted block in exec_as_if_node is byte-identical to the tail of exec_eval (run_command.rs:2870-2880); the only delta is the positionals→passthrough merge, which is the fix.
  • Checked that ctx.args.disable_default_env_files = true (set earlier in exec_as_if_node) still flows through since exec_eval receives the same ctx.
  • Test matrix covers -e/-p/-pe, single/multi/flag-like args, and the empty case (guards against regressing the no-positional path).
Extended reasoning...

Overview

This PR fixes a Node.js-compat bug where bun running as node (via argv0 emulation) drops the first positional argument from process.argv when using -e / -p / -pe. The fix replaces an 11-line duplicated block in RunCommand::exec_as_if_node with a call to the existing RunCommand::exec_eval, which already handles the positionals→passthrough merge correctly for the plain bun -e path. A 6-case test.each is added to test/cli/run/as-node.test.ts.

Security risks

None. This is CLI argument routing for how positional args after -e <code> land in process.argv. No untrusted input parsing, no file/network I/O, no auth or crypto surface.

Level of scrutiny

Low-to-medium. The change is a net -8 lines in one function, replacing a copy-pasted block with a call to the shared helper it was copied from. I verified the deleted block is byte-for-byte the tail of exec_eval (lines 2870-2880), so the only behavioral delta is the prepended merge of ctx.positionals into ctx.passthrough — exactly the fix. The node-specific state set earlier in exec_as_if_node (disable_default_env_files, the --interactive REPL routing) happens on ctx before the call and is preserved. This also satisfies the repo's "one implementation" rule — the previous state had two byte-identical eval-entry-path builders.

Other factors

The PR description documents USE_SYSTEM_BUN=1 failure (5 of 6 new cases fail on the released bun) and passing on the debug build, plus regression runs of run-eval.test.ts, the REPL tests, and env argv0 tests. The test uses test.each per harness convention, exercises the empty-positionals branch (guarding the if !ctx.positionals.is_empty() skip in exec_eval), and includes a flag-like arg case to confirm passthrough doesn't re-parse. No prior reviewer comments to address; timeline only has a CodeRabbit rate-limit notice.

@robobun

robobun commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favour of #40484, which carries the same change (route the -e/-p branch of exec_as_if_node through exec_eval) and links the issue (#40482). The test matrix from this PR (-pe, a single argument, a flag-like argument, no arguments) is folded into #40484.

@robobun robobun closed this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant