Skip to content

feat(bun,deno): add Bun and Deno runtime support - #988

Merged
KuSh merged 28 commits into
rtk-ai:developfrom
geofflittle:feat/bun-deno-support
Sep 3, 2026
Merged

KuSh merged 28 commits into
rtk-ai:developfrom
geofflittle:feat/bun-deno-support

Conversation

@geofflittle

@geofflittle geofflittle commented Apr 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Token-optimized filtering for Bun and Deno CLI commands.

  • Bun: install/add/remove (strip the version header and resolution lines, ~70% measured), test (failures only, ~90%), pm ls (compact dependency list, ~70%), run, build and pm <non-ls> (passthrough), bunx (routes tsc and eslint to their filters, everything else through the npm line filter)
  • Deno: test (failures only, ~90%), lint (~40%) and check (~50%) (strip ANSI, download and blank lines + tee recovery), run/task/install/compile (passthrough)

Based on #548 by @bz00qa and carried forward by @geofflittle. Supersedes #548 and #1374.

Why some subcommands are passthrough

bun build writes its bundle to stdout when no output flag is given, and its emitted-file summary is the point of the run when one is. deno compile is the same: the artifact path and, on failure, the type-check diagnostics are the payload. An errors-only filter kept neither, so both run unfiltered and are declared 0% in the discover rules rather than claiming savings they cannot deliver.

Watch mode (--watch) is passthrough on every filtered entry point: the filtered runners buffer the whole stream until the child exits, and a watched child never does — so a filtered watch run printed nothing at all and lost the buffer on Ctrl-C. A deno reporter the user named (--reporter=junit, --junit-path) is likewise left alone, since it writes a machine-readable report to stdout.

Savings figures

Every percentage above is measured against a captured fixture of real bun 1.2.20 / deno 2.9.6 output, and asserted by that module's own test — not against a fixture shaped to the filter. deno lint and check keep every diagnostic; their reduction is ANSI and blank lines, which is why the numbers are lower than a summarizing filter's.

Known limitation

bunx <tool> forwards the caller's stdin only when it is a pipe, and rtk npx does not forward it at all. That is #2431, which covers every filtered entry point rather than this one; the bunx half arrives with this PR and should be fixed there.

Test plan

  • cargo fmt --all --check clean
  • cargo clippy --all-targets — 0 warnings
  • cargo test --all — 3058 tests pass
  • Savings measured against captured fixtures, asserted per filter
  • Exercised against real bun 1.2.20 and deno 2.9.6: pass/fail/timeout/module-load-error runs, type-check failures, leak failures, watch mode, pm ls nested trees, piped stdin, exit-code propagation

Maintainer takeover

@geofflittle has stepped back from this PR. I have taken ownership: rebased onto develop and pushed fixes for five rounds of review findings on top of his work, which is preserved in the history.

This needs review by another maintainer — I authored the remediation commits, so I should not be the one approving them.

@aeppling aeppling added the enhancement New feature or request label Apr 3, 2026
@CLAassistant

CLAassistant commented Apr 4, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@geofflittle
geofflittle force-pushed the feat/bun-deno-support branch 2 times, most recently from 3dbf9d8 to 6e7822e Compare April 9, 2026 00:28
@geofflittle

Copy link
Copy Markdown
Contributor Author

I had to remove the co-authored-by @bz00qa trailers to get the CLA check to pass (they still have credit in the PR descr)

@geofflittle

Copy link
Copy Markdown
Contributor Author

Hi @aeppling — picking up #548 since it went quiet. Rebased, fixed 4 routing bugs (e.g. bun remove ran bun install), aligned with conventions, added token savings tests. Mind taking a look?

@pszymkowiak

Copy link
Copy Markdown
Collaborator

Thanks for picking up #548 and doing the cleanup — this is a solid PR. RTK has no bun/deno support at all today, so it fills a real gap.

It's been open a while and develop has moved on quite a bit. Could you rebase onto develop? Two things to handle along with it:

  1. BunCommands and DenoCommands need #[derive(Debug, Subcommand)]. develop's Commands enum now requires Debug on every sub-enum — without it the build fails with error[E0277]: BunCommands doesn't implement Debug. (Same regression feat(sbt): add SBT (Scala Build Tool) support #752 hit.)

  2. Please drop the CHANGELOG.md change — release-please manages the changelog automatically; manual entries cause merge conflicts. (The README.md Runtimes section is great, keep that.)

Once rebased with CI green, this looks ready to merge. Thanks again!

@gandazgul

Copy link
Copy Markdown

@geofflittle I would love this, I use deno a lot and was looking to do this exact same thing. Let me know if I can help.

@geofflittle
geofflittle force-pushed the feat/bun-deno-support branch from 6e7822e to 5001771 Compare June 15, 2026 20:57
@geofflittle

Copy link
Copy Markdown
Contributor Author

@pszymkowiak
Ready!

@pszymkowiak

Copy link
Copy Markdown
Collaborator

@geofflittle — first off, apologies for leaving this hanging. You addressed both points I raised (the Debug derives on BunCommands/DenoCommands and dropping the CHANGELOG.md change) and rebased back in June, and I never circled back to move it forward. That's on me. 🙏

The PR is in good shape and I'd like to get it in. Two small things:

  1. develop has moved again since your last rebase, so GitHub now shows conflicts — could you do one more rebase onto develop?
  2. One gap against Handle supported commands through the bunx / bun x / pnpx / pnpm dlx / npx #197: the space form bun x <tool> (e.g. bun x tsc) isn't routed to the tool filter the way bunx tsc is — the Bun rule pattern excludes x, so it falls through to a plain passthrough. If it's a quick add, great; otherwise it can be a follow-up.

Once it's rebased I'll review and get it merged. Thanks for your patience, and sorry again for the silence.

@geofflittle
geofflittle force-pushed the feat/bun-deno-support branch from 5001771 to a121629 Compare July 6, 2026 21:07
@geofflittle

Copy link
Copy Markdown
Contributor Author

@pszymkowiak ready x2!

@pszymkowiak

Copy link
Copy Markdown
Collaborator

Tested this extensively with real bun/deno binaries (installed both to validate end-to-end, not just unit tests). Found a real bug in the bun test path worth fixing before merge.

is_bun branch of extract_test_summary (core/runner.rs) loses failure detail and duplicates source context on multi-failure runs

Ran bun test on a real 10-test suite (4 failing). The SUMMARY: section fills with duplicated source-code context (2 | test("t1 passes", ...), 3 | test("t2 fails", ...), repeated and growing before each subsequent failure), while the actual diagnostic info — error: expect(received).toBe(expected), Expected: X, Received: Y — is dropped entirely. Measured with a real BPE tokenizer (tiktoken): only ~38% savings on this case, vs 77%+ for the equivalent deno test case, and the output is less useful to an agent, not more — the one thing needed to fix a failing test (expected vs received) never survives the filter.

Root cause:

if is_bun {
    let trimmed = line.trim_start();
    if line.contains(" pass") || line.contains(" fail") || trimmed.starts_with("Ran ") {
        result.push(line.to_string());
    }
    if line.contains('✗') || line.contains("(fail)") {
        failures.push(line.to_string());
    }
}

line.contains(" fail")/" pass" is an unanchored substring match — it also matches bun's own echoed source lines whenever a test's name contains "passes"/"fails" (e.g. test("t2 fails", ...)), which is exactly what bun prints as growing context before each subsequent failure. That's why SUMMARY: fills with duplicate snippets as failure count grows.

Separately, is_bun has no equivalent to is_cargo's/is_deno's in_failure && line.starts_with(" ") state machine that captures the indented detail lines after a failure marker — so Expected:/Received: never make it into failures/failure_lines.

is_deno doesn't share this bug — verified clean output on the equivalent 5-test/2-failure case (exact failure list, correct 3 passed | 2 failed, no duplication), because it reuses the same robust state-machine approach as is_cargo.

Suggested fix: give is_bun the same failure-marker + indented-detail capture used by is_cargo/is_deno, anchored on bun's actual output shape ((fail) <name> as the marker, then capture the error:/Expected:/Received: lines that follow) instead of the loose substring check.

Everything else I tested (install/add/remove, pm ls with JSON+text fallback, build success/failure, run passthrough, bunx/bun x routing to tsc/eslint, deno lint/check/test/run/compile/install) worked correctly against real binaries — this is the one thing I'd fix before merge, since test-failure output is the most common thing an agent needs to act on correctly.

@geofflittle

Copy link
Copy Markdown
Contributor Author

Fixed in 92aa5dd. I used the same methodology as your review (real bun/deno binaries, real failing suites, tiktoken measurements) for everything below.

The bun test fix. Both parts of the root cause are addressed: summary lines are now anchored on bun's exact count shape ( 4 fail), so echoed source context can't match even when a test name contains "fails", and there's now a state machine that captures the error: / Expected: / Received: block per failure. One correction from fixturing: bun prints the diagnostic before the (fail) marker, not after it, so the capture anchors on error: and closes at (fail). Reproduced your 10-test/4-fail case with real bun 1.3.6 output (committed as a fixture, with a byte-exact golden test): tiktoken savings went from ~38% to 74.9%, and Expected/Received now survives for every failure.

Running the same end-to-end approach over the rest of the branch found four more real issues. All fixed before merge:

  1. rtk bun add "lodash@^4.17.21" failed before bun even ran. An rtk-side character whitelist rejected ^, ~, :, #, so semver ranges, github:/npm: specifiers, and workspace:* were all broken. Since args go to bun as an argv vector (never through a shell), the validation had no security value, so it's gone. A test now locks verbatim spec passthrough.

  2. deno test had the same bug class as bun test. The parser matched deno's old cargo-shaped output (test result:, failures:), which deno 2.x no longer emits, so the summary fell back to a raw tail and the assertion diffs were dropped entirely. Rewritten against real deno 2.9.1 output (FAILURES section, FAILED | 9 passed | 4 failed footer), with the legacy anchors kept for older denos. Measured on a 13-test/4-fail suite: 90.2% tiktoken savings, with the [Diff] Actual / Expected blocks intact. While in there I replaced the per-ecosystem substring flags with a TestEcosystem enum picked once at the boundary ("cargo test" contains "go test" as a substring, so the flags could co-fire).

  3. bun pm ls --json is silently ignored by bun 1.x, so the JSON path never fired and every real run hit a 500-char text truncation. There's now a tree parser for what bun actually prints, and the filter picks a parser based on the output shape rather than the flag we passed.

  4. deno lint/deno check bypassed the never_worse guard by hand-rolling their print path. They now go through the shared core runner like the other filters.

Everything is backed by committed real-output fixtures and regression tests, and CI is green at 2209a04. Three smaller things I left for a follow-up PR, per your earlier note: bunx/npx tool-dispatch parity (bunx currently routes only tsc/eslint to dedicated filters), compacting deno lint's verbose per-diagnostic frames, and deno install being classified as filtered in discover when it's actually passthrough.

@pszymkowiak pszymkowiak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks, tested this again after your fix — all good. bun test/deno test failures now show the actual error details, no more duplication, exit codes and savings all check out on my end too. LGTM 👍

@pszymkowiak
pszymkowiak requested a review from aeppling July 8, 2026 21:54

@pszymkowiak pszymkowiak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-confirming my approval after a full end-to-end verification on the current head (2209a04).

Verified locally:

  • cargo build ✅, cargo test → 43 tests pass (32 bun + 11 deno), all against real command-output fixtures.
  • Live e2e on real runtimes (installed bun 1.3.14 and deno 2.9.3, each with a one-pass/one-fail suite):
    • rtk bun test: surfaced the failing test + assertion detail (Expected: 5 / Received: 4), correct 1 pass / 1 fail summary, propagated exit code 1. ~62% token savings.
    • rtk deno test: surfaced the failing test + diff (Actual 4 / Expected 5), correct summary, exit code 1, tee log for full output. ~76% token savings.

Covers both Bun and Deno, real fixtures, exit-code propagation is correct, and it shows assertion detail (nice for agent debugging). LGTM.

@TaKO8Ki could you give this a maintainer pass when you have a moment? It supersedes the narrower Bun-only #1374 and #548. Thanks!

@pszymkowiak
pszymkowiak requested a review from TaKO8Ki July 18, 2026 14:06
@pszymkowiak

Copy link
Copy Markdown
Collaborator

Thanks for this — the bun/deno filters look solid and I verified them end-to-end on macOS with real bun/deno (test summaries + failure detail, exit codes propagated, bun pm ls compression, argv-vector safety). The blocker right now is that the branch has drifted: it's ~108 commits behind develop and currently CONFLICTING.

I reproduced the rebase locally to de-risk it — it's small, only 2 things to resolve:

  1. src/main.rs — two purely additive conflicts. develop added enum SbtCommands right where this PR adds enum BunCommands; keep both (each with its own Other(...) variant), and keep the PR's BunPmCommands / DenoCommands / run_bunx_tool block.

  2. src/discover/rules.rs — the non-obvious one: develop added a pipeline_final_safe: bool field to RtkRule (via fix(rewrite): only rewrite safe final pipeline stages #3128 pipe-rewrite-safety). The three new rules need it:

    pipeline_final_safe: false,   // matches cargo/npm/pnpm/jest/vitest/go

    The compiler flags this as E0063 missing field — it's what a mechanical rebase misses.

Everything else (README, registry.rs, core/runner.rs, cmds/rust/runner.rs) auto-merges cleanly. After those two fixes: cargo build + clippy --all-targets clean, full suite green, and cat x | bun test correctly falls through to raw (the pipeline_final_safe: false integrates with the new pipe-safety).

A git merge --squash origin/develop-style resolution is much easier here than replaying all 19 commits. Could you rebase on develop when you get a chance? Happy to help if anything's unclear.

@geofflittle
geofflittle force-pushed the feat/bun-deno-support branch from 2209a04 to 05db4d7 Compare July 24, 2026 19:08
@geofflittle
geofflittle requested a review from pszymkowiak July 24, 2026 19:14
@zacgoodwin

Copy link
Copy Markdown

Looking forward to this!

@pszymkowiak

Copy link
Copy Markdown
Collaborator

This is approved on my end, but develop has moved and GitHub now shows conflicts — could you rebase/resync when you get a chance so we can merge? Thanks for your patience.

@geofflittle
geofflittle force-pushed the feat/bun-deno-support branch from 05db4d7 to f1e819f Compare August 25, 2026 18:58
@geofflittle

Copy link
Copy Markdown
Contributor Author

@pszymkowiak ready x3!

@KuSh KuSh left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Requesting changes. Thanks for the rework — everything from the previous round is addressed (Debug derives, the bun x <tool> space form, argv passthrough so ^/~/npm:/workspace: specs survive, the is_bun_count_line state machine, current-deno FAILURES / FAILED | parsing, the bun pm ls tree parser, deno lint/check routed through never_worse, and pipeline_final_safe via ..RtkRule::DEFAULT).

Is this still needed? Yes. develop has no bun/deno support whatsoever — no bun_cmd.rs/deno_cmd.rs, no bun/bunx/deno tokens in src/main.rs, no discover rules. This supersedes the narrower #548 and #1374, which I'm closing in favour of it.

Two things left before merge:

1. Rebase onto develop — the PR is currently CONFLICTING. It's mechanical: only src/main.rs conflicts, and only inside the mod tests block (develop's new grep-routing tests landed on the same lines as your bun/deno parse tests). Both sides are purely additive — keep both. I reproduced the resolution locally: cargo build clean, cargo test --all → 2918 passed, 0 failed.

2. Eight inline findings below. The first three are the ones that matter — they all hit bun test / deno test failure output, which is the highest-value path in this PR. The rest are small.

Unrelated heads-up so you don't chase it: develop itself doesn't compile right now (the phpt filter from #1503 uses lazy_static! without the crate, and src/discover/rules.rs is missing a pipeline_final_safe). CI will be red on any rebase until that lands separately — it's being fixed.

Comment thread src/core/runner.rs Outdated
Comment thread src/core/runner.rs Outdated
Comment thread src/core/runner.rs Outdated
Comment thread src/cmds/js/bun_cmd.rs
Comment thread src/cmds/js/bun_cmd.rs Outdated
Comment thread src/cmds/js/bun_cmd.rs Outdated
Comment thread src/discover/rules.rs Outdated
Comment thread src/discover/rules.rs
@KuSh KuSh mentioned this pull request Aug 31, 2026
5 tasks
- core/runner.rs: bun error blocks are kept only with proof bun wrote them
- core/runner.rs: emit diagnostics when no failure marker was printed
- core/runner.rs: deno section boundaries close an open failure block
- tests: real bun 1.3.6 and deno 2.9.6 fixtures for all three cases
- bun_cmd.rs: run_pkg and run_pm_ls use run_filtered, which interleaves the
  streams and tracks what was actually shown
- bun_cmd.rs: pm ls JSON requires a version, so a grouped shape falls through
  to the tree parser instead of reporting group names as packages
- bun_cmd.rs: keep original indentation on kept install lines
- rules.rs: anchor the deno pattern so it cannot match subcommand prefixes
- rules.rs: mark deno install and bun pm (non-ls) as passthrough
- registry.rs: normalize whitespace in a captured subcommand
- bun_cmd.rs: bun build passes through unless an output flag sends the bundle
  to disk, and bunx uses the light npm filter instead of errors-only
- core/runner.rs: the err and test runners record the tool that actually ran,
  with a per-tool tee slug
- utils.rs, tsc_cmd.rs, main.rs: resolve tools via the project package manager,
  and keep the tool name when routing eslint
- tracking.rs, README.md: categorize bun and deno as js, and describe what the
  deno check and bun build filters really do
- core/runner.rs: bun and deno share a buffered engine and differ only in an
  open, prove, close policy table, so a guard cannot land on one runtime alone
- core/runner.rs: deno gains the proof rule it never had, so a passing test
  that logs its own error line no longer reports as a failed suite
- core/runner.rs: a block closes when the next one opens, so consecutive
  stack-free failures stay separate
- tests: every scenario is asserted on both runtimes from real fixtures
- utils.rs: tool_exec prefers an explicitly named package runner over lockfile
  detection, which applies only when nothing was named
- tsc_cmd.rs, lint_cmd.rs, main.rs: thread that runner from the invocation
- utils.rs: detection can be asked about a directory, so its test needs no chdir
- bun_cmd.rs, rules.rs: strip ANSI on the pm ls text fallback, and stop bun
  build claiming savings it does not deliver
- utils.rs: MissingTool says whether the npm arm may fetch, instead of every
  newly routed caller inheriting --no-install by accident
- tsc_cmd.rs: tsc fetches as it always did, lint and the rest still do not
@KuSh

KuSh commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Resolving the remaining review threads. Each was verified against the pushed tree and, where the behaviour is observable, against real bun 1.2.20 / deno 2.9.6 rather than only by reading the diff.

Thread Fix
rules.rs:687deno install credited savings classify_command derives savings from status, so a passthrough subcommand is 0% and the two fields cannot drift
rules.rs:676bun build credited 75% Same derivation, plus ("build", Passthrough); bun build now runs unfiltered
bun_cmd.rs:244bun test --watch hangs silently is_watch_mode guard on every filtered entry point
runner.rs:610 — frameless failures lose their reason A (fail)/ marker vouches for the block it closes, so a timeout keeps timed out after 200ms
bun_cmd.rs:56 — progress branch matched yarn's shape Replaced with the lines bun actually prints when piped; claim now 70%, measured against a captured fixture
rules.rs:708deno lint claimed 80% Measured: lint 40%, check 50%, each backed by a real capture and asserted by the module's test
main.rs:2410rtk lint bunx eslint ignored the runner The stripped prefix is recovered and wins over lockfile detection
bun_cmd.rs:184--json injected into bun pm ls Removed; the tree parser selects on output shape
utils.rs:345 — stale doc and default test Both now cover bun
runner.rs:638DENO_POLICY.proves was dead The field is gone; vouching moved into closes, trust into section

Four further rounds of review ran after these threads were opened; those findings are fixed in the same two commits. The ones deliberately left alone, with reasons, are in the PR description and in #2431 (bunx/npx stdin), #3738 and #3844 (rtk err dropping diagnostics — not reachable from this PR, since bun build and deno compile are passthrough here).

As noted in the description: I authored the remediation, so this still needs a second maintainer's review.

KuSh and others added 2 commits September 3, 2026 02:16
Failure detection, so a green run is never reported red:

- a failure marker vouches for the block it closes, so a frameless failure
  such as a timeout keeps the line that says why it failed
- bun's marker and run footer are matched with the duration and test count
  they carry, since a test is free to log a line that starts the same way
- deno's FAILURES entries are matched by shape, because a bare " => " also
  occurs in arrow functions and assertion messages
- deno fences output it did not write, and names the fence for when it was
  printed, so the rule is matched by shape and gates both the failures list
  and the block engine

Diagnostics that were reaching nobody:

- deno's type-check errors are kept with the frame under them: they open no
  block and carry no section, so a type error left the caller with a bare
  "Type checking failed."
- bun's frames are kept, since two failing tests that share a name, and a
  module error that has no marker at all, cannot otherwise be located

Runs whose output rtk must not touch:

- watch mode on every filtered entry point goes through unfiltered, since the
  runners buffer the whole stream until a child that never exits
- a deno reporter the user named is left alone, because --reporter=junit and
  --junit-path write the report to stdout
- bun build and deno compile run unfiltered: without an output flag the bundle
  is stdout, with one the emitted-file summary is the point of the run, and on
  failure the diagnostics are, none of which an errors-only filter keeps

Tool resolution:

- a package runner the user named wins over lockfile detection, and over a
  tool that merely happens to be on PATH
- lockfile detection never resolves through bunx, which always fetches a
  missing tool and cannot be told not to
- a tool rtk may fetch resolves through npx, the only runner that can

Input and output:

- bunx forwards the caller's stdin when it is a pipe, since the filtered path
  buffers output and an inherited terminal would let a prompt wait invisibly
- strip the resolution lines bun actually prints when piped, count the tree
  levels drawn with a leading pipe, and stop injecting --json into a command
  the user typed
- the lint label records the arguments once, and the shell runners' placeholder
  tool name stays out of their verbose line

Fixtures are real bun 1.2.20 and deno 2.9.6 captures for each case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A subcommand that runs unfiltered cannot save anything, so classify_command
zeroes its estimate rather than letting it inherit the rule's headline. The
status and the percentage were separate fields and only the percentage reaches
the projection, so marking a subcommand passthrough left the claim standing.

The bun and deno percentages are measured against real output rather than
against fixtures shaped to the filter: bun install 70, deno lint 40, deno
check 50. Each is backed by a captured fixture the module's own test measures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@KuSh
KuSh force-pushed the feat/bun-deno-support branch from d549f9d to e78427b Compare September 3, 2026 00:16
@KuSh
KuSh dismissed their stale review September 3, 2026 00:19

Took over the PR, will require another maintainer review

@KuSh

KuSh commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Validated by @pszymkowiak, merging

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants