Skip to content

cli: add --check syntax mode and Node's CLI error contract - #35375

Closed
cirospaciari wants to merge 2 commits into
claude/callback-throw-uncaughtfrom
claude/node-v26-cli-flags
Closed

cli: add --check syntax mode and Node's CLI error contract#35375
cirospaciari wants to merge 2 commits into
claude/callback-throw-uncaughtfrom
claude/node-v26-cli-flags

Conversation

@cirospaciari

@cirospaciari cirospaciari commented Jul 24, 2026

Copy link
Copy Markdown
Member

Stacked on #34660 — review that first. This branch is based on claude/callback-throw-uncaught, not main.

What this does

Adds the Node.js CLI surface the test-cli-* files in Node's v26.3.0 test suite exercise, and vendors the ones that now pass:

  • --check / -c syntax-check mode. Parses the entry point (or piped stdin) without executing it. CommonJS sources are checked inside the module wrapper, so a top-level return is accepted exactly where require() would accept it — including a wrapper replaced through require('module').wrapper by a --require preload, which still runs first. .mjs and --input-type=module are checked as ES modules. Syntax errors print JSC's parser message prefixed by the file path (or [stdin]) and exit 1; a missing target prints Error: Cannot find module '<path>'; --check together with --eval exits 9.
  • --input-type is recognized and currently honored by --check.
  • Node-style missing-argument errors. -e, --eval, -p, --print, --inspect-port, --debug-port without a value now print <execPath>: <flag> requires an argument and exit 9, the way node does.
  • --inspect-port / --debug-port. They set the default [host:]port used when the debugger is activated by --inspect/--inspect-wait/--inspect-brk without its own target. They do not activate the debugger on their own, matching Node.
  • Node permission-model flags are rejected instead of ignored. --allow-fs-read / --allow-fs-write without --permission error with --permission is required (exit 1); --permission itself is rejected as unsupported. Bun does not implement the permission model, and silently running without a requested sandbox is worse than failing.
  • -e / -p follow Node's value-binding rules. -p turns on print mode and may also carry the script, so bun -pe 42, bun -p -e 42, bun -p 42 and bare bun -p (prints undefined) all work. A separate following argument starting with - is never taken as the value (bun -e -p exits 9), a leading backslash escapes that (bun -p "\-42" prints -42), and --eval= reports itself verbatim. Empty and absent are now distinct, so bun -e "" runs an empty program instead of printing help — that replaces an argv-shape shim in Command::start.
  • --no-<x> where <x> is a known option that takes a value is rejected with Node's wording and exit code, instead of being silently ignored.
  • --no-warnings is a real flag now: it suppresses process warnings, the same switch NODE_NO_WARNINGS=1 already flipped.
  • NODE_OPTIONS rejects the values Node rejects. --version, -v, --help, -h, --eval, -e, --print, -p, -pe, --check, -c, --interactive, -i, --v8-options, --test, --, and --expose-internals (either spelling) print <execPath>: <opt> is not allowed in NODE_OPTIONS and exit 9. Bun still does not apply the remaining NODE_OPTIONS entries — this covers only the error contract.

The -c shorthand

-c now means --check for bun and bun run. --config keeps its long form everywhere, and every other subcommand (bun install, bun outdated, …) keeps -c for --config.

--config is declared <PATH>? (optional value), and clap's optional-value path discards an attached value and never consumes the next token, so on main today neither spelling passes a path on the runtime commands:

  • bun -c=bunfig.toml run x parses as bare -c with the path dropped
  • bun -c bunfig.toml run x treats bunfig.toml as the entry point ("Bun cannot run toml files directly")

What bare -c did do is request the default ./bunfig.toml, which bun run does not load on its own. That capability is unchanged, it is just spelled --config now. The two repo tests that passed a path through -c= are updated: bun-run-bunfig.test.ts to --config=bunfig.toml (the file is always present there, so it now really loads the config it names), and the tsconfig-extends case in bun-run.test.ts to a bare --config, which is what it was actually relying on.

Params-table shape

BASE_PARAMS_ is split into BASE_HEAD_PARAMS / BASE_TAIL_PARAMS so the runtime commands can get a --config without the -c alias without duplicating the other base params. This is the only reason for the split. It does not touch RUNTIME_PARAMS_, where new flags normally get added.

Tests

Vendors 9 upstream files from Node v26.3.0, byte-identical, all passing one process per file against a debug build, invoked the way scripts/runner.node.mjs invokes them:

  • parallel/test-cli-bad-options.js
  • parallel/test-cli-node-options-disallowed.js
  • parallel/test-cli-syntax-eval.js
  • parallel/test-cli-syntax-piped-bad.js
  • parallel/test-cli-syntax-piped-good.js
  • sequential/test-cli-syntax-bad.js
  • sequential/test-cli-syntax-file-not-found.js
  • sequential/test-cli-syntax-good.js
  • sequential/test-cli-syntax-require.js

test/cli/run/run-eval.test.ts gains Bun-side coverage for --check (bad syntax, no execution, wrapper-only return, stdin, --input-type=module, missing file, --check + --eval), the missing-argument exit-9 contract, the permission-flag rejections, and the NODE_OPTIONS deny list. No changes to test/expectations.txt.

Why unknown --no-<x> is still ignored

Node rejects --no-i-dont-exist with bad option:. Bun does not, and this PR deliberately keeps it that way: Bun ignores unrecognized flags on purpose so the many Node options it does not implement stay harmless. An earlier revision of this branch did reject them, and it immediately broke --no-warnings in two of the vendored tests; --no-extra-info-on-fatal-exception and --no-global-search-paths would have gone the same way. Only the unambiguous case is rejected — negating an option Bun knows takes a value — which cannot false-positive on a flag Bun has never heard of. There is a regression test for the tolerated set.

Overlap with #32622

#32622 ("Node v26 CLI compatibility", open since June) covers much of the same ground and should be triaged against this PR rather than merged alongside it. It is based on a June main, so its commits no longer apply cleanly; this PR is a fresh implementation against the campaign base, using #32622's design as the reference for --check, the NODE_OPTIONS deny list, the node-style argument errors, and the -p/-e value-binding rules.

What #32622 has that this PR does not:

  • a different --print design that prints promises the way Node does (Promise { 42 }), which also removes the entry_point_result capture in Run::start
  • an src/js/internal/eval_print.ts helper and the associated ZigGlobalObject / JSCommonJSModule changes

What this PR has that #32622 does not:

  • the --no-<flag> negation diagnostics
  • sequential/test-cli-syntax-bad.js vendored
  • everything rebased onto the current parser, including --config's -c split against today's table layout

Pick one; do not land both.

Still failing, and why

Files from the same upstream group that this PR does not land:

  • test-cli-eval.js — the -p/-e binding rules it exercises are all implemented here, but one assertion compares console.log output of an array of strings against Node's spelling ([ '--use-strict', … ]). Bun's inspect quotes strings with double quotes. That is a repo-wide formatting choice, not a CLI bug, so the file stays out.
  • test-cli-node-options.js — needs NODE_OPTIONS to actually be applied (-r preloads, --stack-trace-limit, the V8 option set) and inherited by workers.
  • test-cli-options-as-flags.js — needs internal/options.getOptionsAsFlagsFromBinding and --experimental-config-file.
  • test-cli-options-negation.js — the negation diagnostics it asserts are implemented, but its first assertion needs the DEP0005 new Buffer() deprecation warning, which Bun does not emit. Emitting it faithfully also needs Node's isInsideNodeModules suppression, or every dependency still calling new Buffer() starts printing warnings. The file also wants --no-warnings --warnings to re-enable warnings, which needs order-sensitive boolean negation the parser does not track.
  • test-cli-print-promise.mjs — Bun's --print awaits a promise and prints the settled value; Node prints the promise itself (Promise { 42 }). Deliberate divergence, not changed here.
  • test-cli-node-cli-manpage-env-vars.mjs, test-cli-node-cli-manpage-options.mjs, test-cli-node-options-docs.js — these read doc/api/cli.md, doc/node.1, and src/node_options.cc from the Node repository root. They check Node's own documentation for internal consistency and cannot apply to Bun.

Adds the Node.js CLI surface that Node v26.3.0's test-cli-* files exercise.

--check / -c parses the entry point (or piped stdin) without executing it.
CommonJS sources are checked inside the module wrapper, so a top-level return
is accepted exactly where require() would accept it, including a wrapper
replaced through require('module').wrapper by a --require preload (preloads
still run first). .mjs files and --input-type=module are checked as ES
modules. Syntax errors print JSC's parser message prefixed by the file path
(or [stdin]) and exit 1; a missing target prints "Error: Cannot find module
'<path>'"; --check together with --eval exits 9.

--input-type is recognized and currently honored by --check.

-e, --eval, -p, --print, --inspect-port and --debug-port without a value now
print "<execPath>: <flag> requires an argument" and exit 9, matching Node.
--inspect-port / --debug-port set the default [host:]port used when the
debugger is activated by --inspect/--inspect-wait/--inspect-brk without its
own target; they do not activate the debugger on their own.

The Node permission-model flags are rejected rather than ignored:
--allow-fs-read / --allow-fs-write without --permission error with
"--permission is required", and --permission itself is rejected as
unsupported. Bun does not implement the permission model, and running without
a requested sandbox is worse than failing.

NODE_OPTIONS now rejects the values Node rejects (--version, -v, --help, -h,
--eval, -e, --print, -p, -pe, --check, -c, --interactive, -i, --v8-options,
--test, --, --expose-internals in either spelling) with Node's message and
exit code 9. The remaining NODE_OPTIONS entries are still not applied.

-c means --check for `bun` and `bun run`; --config keeps its long form
everywhere and every other subcommand keeps -c for --config.

--config is declared with an optional value, and clap's optional-value path
drops an attached value and never consumes the next token, so on the runtime
commands neither `bun -c=bunfig.toml run x` nor `bun -c bunfig.toml run x`
ever loaded the named config: the first was bare -c with the path dropped,
the second treated bunfig.toml as the entry point. What bare -c did do is ask
for the default ./bunfig.toml, which `bun run` does not load on its own;
spell that --config now. The two repo tests that passed a path through -c=
are updated accordingly.

Vendors the Node v26.3.0 tests that now pass: test-cli-bad-options.js,
test-cli-node-options-disallowed.js, test-cli-syntax-eval.js,
test-cli-syntax-piped-bad.js, test-cli-syntax-piped-good.js, and sequential
test-cli-syntax-{bad,file-not-found,good,require}.js. test/cli/run/run-eval.test.ts
gains Bun-side coverage for all of the above.
@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator
Updated 9:56 PM PT - Jul 23rd, 2026

@cirospaciari, your commit 3c39e30 is still building in Build #79301, but has 3 failures so far (All Failures):

@github-actions

Copy link
Copy Markdown
Contributor

Found 2 issues this PR may fix:

  1. Bug: -c flag does not set config file for bun run should work like --config #21431 - PR reassigns -c from --config to --check for bun/bun run, resolving the reported bug where -c=bunfig.toml silently discarded the path
  2. File not found when running bun --config bunfig.toml #6300 - PR removes -c as a --config alias on runtime commands, eliminating the confusion where -c bunfig.toml treated bunfig.toml as the entry point instead of the config path

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #21431
Fixes #6300

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Node v26 CLI compatibility: make node:cli tests pass #32622 - Implements nearly all the same features: --check/-c syntax-check, --input-type, missing-argument errors with exit code 9, and -c reassignment from --config to --check
  2. cli: add --check flag to syntax-check a file without executing #30981 - Implements --check syntax-check mode (parse without executing), directly duplicating the --check feature
  3. cli: parse NODE_OPTIONS for preload flags and validate against Node's allowlist #34101 - Implements NODE_OPTIONS parsing and validation against Node's allowlist, directly duplicating the NODE_OPTIONS validation feature
  4. cli: declare Node's value-taking flags so their value is not parsed as the entrypoint #34100 - Declares Node's value-taking flags (--inspect-port, --debug-port, permission flags) in the CLI parser, overlapping with the inspect-port and permission flag features

🤖 Generated with Claude Code

-p turns on print mode and may also carry the script, matching Node's
registration of --print as a boolean plus a `--print <arg>` alias for
`--print --eval <arg>`. `bun -pe 42`, `bun -p -e 42`, `bun -p 42` and bare
`bun -p` (prints undefined) all work now.

Bun's parser has no alias expansion, so -e/-p opt into Node's value-binding
rules through two new Values variants implemented once in node_style_value,
used by both the long and short option paths. Mirroring node_options-inl.h:

- a separate following argument starting with '-' is never the value, it is
  a missing value instead (`bun -e -p` exits 9)
- a separate following argument may escape that with a leading backslash,
  which is stripped (`bun -p "\-42"` prints -42); the '=' form does not
  unescape, so `--eval=\-42` keeps the backslash
- an empty '=' value is a missing argument for --eval but no value at all
  for --print, which is a boolean upstream
- an empty following argument is not consumed as --print's script

Empty and absent are now distinct: `provided` records that -e/-p was passed
at all, so `bun -e ""` runs an empty program instead of printing help. That
replaces the argv-shape shim in Command::start, and makes `bun --check -e ""`
report the --check/--eval conflict the way Node does. Missing-value errors
echo the argument as written, so `--eval=` reports `--eval= requires an
argument`.

`--no-<x>` where `<x>` is an option that takes a value is now rejected with
Node's wording and exit code instead of being ignored. An unknown `--no-<x>`
is still ignored: Bun tolerates unrecognized flags on purpose so the Node
options it does not implement stay harmless, and rejecting them broke
--no-warnings, --no-extra-info-on-fatal-exception and friends.

--no-warnings is a declared flag now and actually suppresses process
warnings, flipping the same switch NODE_NO_WARNINGS=1 already did.
@cirospaciari

Copy link
Copy Markdown
Member Author

Superseded by #35391, which combines this branch with the other five stacked on claude/callback-throw-uncaught so there is one review surface instead of six. Every source change and every vendored test from this branch is carried over unchanged; #35391's description lists the merge conflicts that needed a decision and links back here for the rationale.

The branch claude/node-v26-cli-flags is not deleted.

@cirospaciari

Copy link
Copy Markdown
Member Author

🤖 From the combined PR #35391: two files trace to the CLI error contract on your branch.

1. test/bundler/transpiler/transpiler.test.js — 'running a file with deeply nested unary operators does not crash the process'. The spawned child now exits 9 where the test accepts [0, 1]:

error: expect(received).toContain(expected)
Expected to contain: 9   Received: [ 0, 1 ]
  at test/bundler/transpiler/transpiler.test.js:4778:18

Exit 9 is your Node-compatible CLI error code. Confirmed member-caused: 181 pass / 1 fail on claude/node-v26-cli-flags @ 3c39e303c8 alone, same single failure. Red on 8 lanes.

2. parallel/test-process-execve-permission-granted.js — an already-vendored upstream test on the base, red on all 9 lanes:

NOTE: The test started as a child_process using these flags:
  [ '--permission', '--allow-fs-read=*', '--allow-child-process' ]
error: --permission is not supported by Bun (the Node.js permission model is not implemented)

The test re-spawns itself with --permission, so your rejection makes it exit 1. This is the same class as test-compile-cache-permission-allowed.js, which I dropped from #35391 as a false pass — but this one is different: it was already vendored and passing on the base, so your change regresses it rather than exposing it.

Worth deciding deliberately, since a permission-model branch is now in flight: does --permission keep failing closed (and this file comes out until the model exists), or does the rejection defer to whatever that branch lands? Either way it is your call, not something I want to resolve in the merge.

Everything else from your branch is green on #35391.

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.

2 participants