Skip to content

feat(runtime): --runtime axis (node --run / bun --bun run / deno task) - #96

Merged
kjanat merged 9 commits into
masterfrom
feat/runtime-override
Jul 23, 2026
Merged

feat(runtime): --runtime axis (node --run / bun --bun run / deno task)#96
kjanat merged 9 commits into
masterfrom
feat/runtime-override

Conversation

@kjanat

@kjanat kjanat commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Closes #94.

--pm bun meant two things at once: bun installs here, and bun runs scripts
here. It never meant "put the script's process tree on bun", so there was no
way to ask for bun --bun run. This adds a runtime axis that is that ask.

The axis

--runtime <node|bun|deno>, RUNNER_RUNTIME, [runtime].js. Each runtime uses
its own script runner and its own package-exec primitive. No package manager is
consulted on the runtime path.

--runtime package.json script local file exec fallback
node node --run <task> node <file> npx
bun bun --bun run <task> bun <file> bunx --bun
deno deno task <task> deno run <file> deno x

node --run (Node 22+) is the piece that makes it symmetric; there was no way
to run a package.json script on plain Node's own runner before.

An explicit runtime outranks a local file's #! line — overriding the
shebang is the whole point, so run --runtime bun ./cli.js runs a
node-shebanged file on bun. It reaches dependency bins, the exec fallback, and
the bun-test fallback too. In a turborepo a runtime override biases selection
toward package.json (the source that can honour it). A source that cannot
honour a runtime (make, just, cargo, …) warns, with an --explain line,
rather than dropping the request silently.

Behaviour notes that ship in the docs

  • node --run does not run pre/post lifecycle scripts (npm run, bun run
    and deno task do). Runner warns when the task has one.
  • Argument forwarding differs per runtime and is handled per runtime: node
    requires an injected -- before user args, deno must not receive one, bun
    needs neither.
  • RUNNER_RUNTIME is exported to nested runner/run children only for CLI
    and env origins, never for a config-file [runtime].js — a repo's runtime
    choice does not leak into a different project reached through a nested call.
  • Local files run through Deno get six granular permissions
    (--allow-read/write/net/env/run/sys), not -A. -A also grants
    --allow-import, which would let a local file fetch and execute code from any
    host past Deno's default allowlist; node and bun refuse remote imports, so the
    parity argument for a blanket grant does not hold.
  • --explain, runner why, and runner doctor --json all report the
    runtime-correct command.

History

The first push of this branch applied the runtime on only two dispatch paths
and dropped it on the rest, so the advertised node-shebang case did not work and
--runtime node hard-errored in Deno projects. Commit 8cf3ffa reworks it onto
a single chokepoint and fixes the dependent defects. The diff-from-master is the
final design; the intermediate commits are kept rather than squashed.

Verification

dprint check, cargo lint, cargo test --all-features all clean; 1106 tests.
tests/runtime_override.rs drives real node/bun/deno binaries for each
behaviour above and skips when a runtime is not installed.

Residual, disclosed

  • node --run needs Node 22+; on older Node the spawn fails naming node, with
    no pre-flight version check.
  • The two dispatch-time warnings (runtime-not-applied, node-skips-lifecycle) are
    not surfaced in doctor --json, which does not dispatch a task.

kjanat added 2 commits July 22, 2026 23:42
`--pm bun` conflated "bun installs and invokes scripts here" with "use bun's
runtime", so forcing a script's process tree onto bun had nowhere to live.
`bun run build` starts under bun but a dependency bin with a
`#!/usr/bin/env node` shebang still resolves to system Node; `bun --bun run`
is what moves it, and nothing could ask for that.

--runtime <node|bun|deno>, RUNNER_RUNTIME, [runtime].js, same three-layer
precedence as every other override. bun forces its runtime, deno dispatches
`deno task` (which reads package.json scripts), node forces nothing and falls
through to the resolved node PM. Applies whichever PM wrote the lockfile,
selects the local-file runtime too, propagates to nested runners, and is
reported by --explain and doctor --json.

Bundles the CLI override strings into CliOverrides, mirroring DiagnosticFlags,
so the constructors stay under clippy's argument threshold.

Closes #94.
`deno run <file>` is deny-all and ignores the file's shebang, so a file
that works under node and bun dies under deno on the first env/fs/net
access. Pre-dates --runtime; --pm deno <file> took the same path.
@kjanat kjanat added enhancement New feature or request area: cli Argument parsing, commands, and CLI UX labels Jul 22, 2026
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one) (1)
  • cr:review
🚫 Excluded labels (none allowed) (2)
  • wip
  • cr:skip

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: fcd1f0db-ff4f-41dd-9c81-81b416c19c86

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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

@kjanat kjanat changed the title feat(runtime): --runtime axis, separate from --pm feat(runtime): --runtime axis, separate from --pm Jul 22, 2026
@kjanat kjanat self-assigned this Jul 22, 2026
The first cut read the runtime override in two places and silently dropped
it everywhere else, so `run --runtime bun ./cli.js` on a node-shebanged file
ran on node, the exact case the feature exists for. `--runtime node` also
hard-errored in a Deno-resolved project, contradicting its own docs.

Rework:
- Each runtime uses its own script runner: node --run, bun --bun run,
  deno task. No package manager is consulted, so --runtime node stops
  resolving a PM and stops bailing.
- The override is read at one chokepoint and honoured on every path, or the
  user is warned it did not apply: shebang and exec-bit local files, dep
  bins, exec fallback, bun-test fallback, and source selection (a turborepo
  now biases to package.json). Non-JS sources warn instead of dropping it.
- Per-runtime arg forwarding: node needs an injected `--`, deno must not get
  one, bun neither; no trailing bare `--` on empty args.
- node --run skips pre/post lifecycle scripts; runner warns when the task
  has one.
- RUNNER_RUNTIME is exported to children only for CLI/env origins, not
  config, so a repo's [runtime].js no longer leaks into a nested project.
- deno local files get six granular permissions, not -A; -A widened remote
  imports past deno's allowlist on a false parity claim.
- why and doctor --json preview the runtime-correct command; the deno
  dispatch label matches its real argv.
@kjanat kjanat changed the title feat(runtime): --runtime axis, separate from --pm feat(runtime): --runtime axis (node --run / bun --bun run / deno task) Jul 22, 2026
kjanat added 6 commits July 23, 2026 02:02
The bun integration tests assert on stdout and discard the child's stderr,
so a CI-only failure reports only empty stdout with no cause. Route them
through a helper that checks the exit status and prints both streams.
…d run

Running the suite through `runner` under GitHub Actions inherits
RUNNER_GROUP_ACTIVE=1; a spawned runner then suppresses its own grouping
(GHA groups don't nest) and the grouping assertions fail. The tests spawned
the binary with the ambient environment intact. Route every spawn through a
helper that strips RUNNER_*.
CI installs bun/deno as mise shims. A shim resolves its version by walking
up from cwd for a config; runner spawns the runtime with cwd set to a
throwaway project outside the repo, where none exists, so bun died with
"No version is set for shim" and empty stdout. node passed only because it
ships on the image as a real binary. Prepend the real mise bin dirs
(`mise bin-paths`, resolved at the crate root where the config lives) to the
child PATH so a shimmed runtime resolves independent of cwd; empty and inert
when mise is absent.
- why --json: drop `runtime` from `required` (it is emitted only when a
  runtime override is set), so a payload without it still validates against
  schema v2; no version bump.
- Constrain the runtime label to node|bun|deno in both the why schema and
  `[runtime].js`, so a bad value fails validation in-editor, not at dispatch.
- Source selection: check a forced runtime before a forced PM. With both set,
  `--pm cargo --runtime bun` biased by the PM (which owns no JS source) and
  dropped the runtime; the runtime is the more specific intent and now wins.
- arrow_only test helper scrubs RUNNER_* like the other spawns, so the four
  fallback tests asserting on exact stderr are not exposed to ambient config.
- Cover the forced/unforced bunx exec branch and the runtime source-selection
  branch (including the pm+runtime combination) with unit tests.
- README: fix the garbled "applies whatever wrote the lockfile" sentence.
- schema.rs: raw-string literal for the runtime FIELD_TEMPLATE value, matching
  every other entry.
--runtime node dispatches `node --run`, which only exists in Node 22+. On an
older node the dispatch failed with a cryptic `bad option: --run`. Probe the
node on PATH and, when it is older than 22, bail before spawning with a
diagnostic naming the version and the alternatives (bun/deno, or drop
--runtime). node absent or unparseable is not blocked; the spawn surfaces its
own error. Node 22+ and local-file dispatch (`node <file>`) are unaffected.
The if-let/else in the previous commit trips clippy's option-map-or lint
(-D), reddening CI. No behaviour change.
@kjanat
kjanat merged commit d677a5d into master Jul 23, 2026
18 checks passed
@kjanat
kjanat deleted the feat/runtime-override branch July 23, 2026 01:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli Argument parsing, commands, and CLI UX enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Force Bun's runtime for scripts (bun --bun run), and a general runtime override

1 participant