Skip to content

bun run dev: run the servers with bun run --parallel - #48

Merged
firsttris merged 3 commits into
mainfrom
dev-parallel
Nov 4, 2026
Merged

firsttris merged 3 commits into
mainfrom
dev-parallel

Conversation

@firsttris

Copy link
Copy Markdown
Owner

Replaces the hand-written process supervisor scripts/dev.ts (#47: process groups, own signal handlers, SIGSTOP/SIGCONT) with Bun's built-in bun run --parallel.

"dev": "bun scripts/dev-prepare.ts && bun run --parallel dev:server dev:ui",
"dev:server": "cargo run -p haul",
"dev:ui": "bun run --cwd ui dev",
  • The terminal sends Ctrl+C, Ctrl+Z and hangup to the whole foreground process group by itself. bun run --parallel runs both scripts with Foreman-style prefixed output and stops the other script when one fails.
  • scripts/dev-prepare.ts (12 lines, no process handling) does the rest of what dev.ts did: it creates .env from .env.example on the first run, builds the plugins (and aborts before any server starts if that fails), and prints the hint with the UI URL and login.
  • HAUL_DEV_UI gets no default in the script. .env.example has had it since the variable was added (4dec48b), and both Bun and the server (dotenvy) read .env. A ${HAUL_DEV_UI-…} default would also break under Bun's own shell (--shell=bun, the default on Windows), which passes it through literally.
  • scripts/dev.test.ts tested the supervisor and is removed along with it. scripts/dev-prepare.test.ts covers the prepare step: it creates .env, keeps an existing one, and passes the plugin build's failure on. The root test script and docs/development.md are updated.

Checked in a real pty (interactive bash), Bun 1.4.4

These runs used stand-ins for cargo (on PATH) and the UI script. Each stand-in starts a child of its own.

  • Both scripts run, and the server gets HAUL_DEV_UI=http://localhost:5173 from the freshly created .env.
  • Ctrl+C: nothing is left (exit code 130).
  • Ctrl+Z: all 7 processes are T, and the shell takes input again. fg resumes all of them, and a following Ctrl+C leaves nothing.
  • Closing the terminal: nothing is left.
  • The UI script exits with code 3: the server script gets SIGINT and stops. The only thing left is the server stand-in's own sleep child, the accepted case. bun run dev then reports exit code 130, not 3.
  • The plugin build fails with code 2: no server starts, and the exit code is 2.
  • Real cargo run -p haul plus Vite, once (on other ports through HAUL_LISTEN): both came up, and Ctrl+C left nothing.

bun run test and bun run typecheck pass.


Generated by Claude Code

scripts/dev.ts supervised the server and the UI by hand: process groups of
their own, signal handlers, SIGSTOP/SIGCONT for Ctrl+Z. The terminal already
sends Ctrl+C, Ctrl+Z and hangup to the whole foreground process group, and
`bun run --parallel` runs scripts side by side with prefixed output and stops
the others when one fails. So `dev` is now
`bun scripts/dev-prepare.ts && bun run --parallel dev:server dev:ui`.

scripts/dev-prepare.ts keeps the rest of what dev.ts did: .env from
.env.example on the first run, the plugin build (aborting before any server
starts), the hint with the UI URL and login. HAUL_DEV_UI needs no default in
the script any more: .env.example has carried it since the variable exists,
and both Bun and the server read .env.

dev.test.ts tested the supervisor and goes with it; dev-prepare.test.ts
covers the prepare step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z
--parallel pipes each script's output (for the name prefix) and gives it
/dev/null as input, so neither server sees a terminal: cargo and Vite
(picocolors checks stdout.isTTY or FORCE_COLOR) dropped the colours that
scripts/dev.ts kept by passing the terminal on. In a terminal `dev` now
sets FORCE_COLOR and CARGO_TERM_COLOR; with its output in a pipe nothing is
forced. scripts/dev-run.test.ts runs the `dev` script in a pty (Bun.spawn
terminal) and in a pipe with server stand-ins. Vite's keyboard shortcuts
need a terminal on stdin and stay with `bun run dev:ui`; the docs say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z
scripts/dev.ts forwarded SIGTERM, SIGINT and SIGHUP to both servers' process
groups. `bun run --parallel` does not: SIGTERM or SIGHUP to the `bun run dev`
pid (`kill <pid>`, an IDE's stop outside a terminal) ends it and orphans
cargo/haul and Vite with their children, SIGINT is ignored. Signals from the
terminal reach the whole foreground group and are fine. No process
supervisor by decision, so the docs say how to stop them instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y3rdfbWb1xDUdPRng7TX9Z

Copy link
Copy Markdown
Owner Author

TDD review

Two regressions compared with the old scripts/dev.ts, each proven with a failing test. Fixes are pushed on top (no process supervisor, as decided).

1. cargo and Vite lose their colours (and Vite its keyboard shortcuts)

File: package.json, the dev script (bun run --parallel dev:server dev:ui)
Test: scripts/dev-run.test.ts, "in a terminal, cargo and Vite are told to keep their colours". It runs the real dev script in a pty (Bun.spawn({ terminal })) with stand-ins for cargo, the UI and the plugin build.

Red on 7932b3a:

error: expect(received).toEqual(expected)
    {
-     "CARGO_TERM_COLOR": "always",
-     "FORCE_COLOR": "1",
+     "CARGO_TERM_COLOR": null,
+     "FORCE_COLOR": null,
    },  (same for ui)
(fail) in a terminal, cargo and Vite are told to keep their colours

A pty probe confirmed it: both stand-ins report tty=false stdin=false. --parallel pipes each script's output for the name prefix and gives the scripts /dev/null as stdin. As a result cargo and Vite drop their colours (picocolors needs isTTY or FORCE_COLOR), and Vite's h/r/o/q shortcuts are gone. dev.ts handed the terminal on.
Fix: 15c7a5c. In a terminal, dev now sets FORCE_COLOR=1 CARGO_TERM_COLOR=always (if [ -t 1 ]; then export …; fi); when the output goes into a pipe, nothing is forced (second test). The shortcuts can't come back under --parallel, so the docs point to bun run dev:ui for them.

2. A signal to the bun run dev pid alone leaves both servers running

File: package.json dev / docs/development.md (the old text promised "an IDE's stop (SIGTERM) stops both"; the new text no longer says so, but doesn't name the limit either)
Test (probe, not part of the suite, because there's no fix without a supervisor):

const dev = Bun.spawn(['bun', 'run', 'dev'], { cwd: dir, detached: true, stdio: ['ignore', 'ignore', 'ignore'] });
// … wait for server.pids and ui.pids …
dev.kill('SIGTERM'); await Bun.sleep(2000);
expect(['haul', "haul's child", 'vite', "vite's child"].filter((_, i) => alive(pids[i]))).toEqual([]);

Red:

- []
+ [ "haul", "haul's child", "vite", "vite's child" ]
(fail) SIGTERM to bun run dev stops both servers and what they started [2105.37ms]

The process tree shows that bun run dev passes SIGTERM/SIGHUP on to bun run --parallel, but --parallel exits without passing it to its scripts. The servers end up re-parented to pid 1. SIGINT to the pid alone is ignored. Signals from the terminal go to the whole foreground group, so they're fine. (bun run --help has no option for this.)
Fix: 30c82d9, docs only: it names the limit and says how to stop the servers (send the signal to the process group, or run the IDE configuration in a terminal). Following your rule, there's no signal code.

Checked and dropped

  • Old .env without HAUL_DEV_UI: the line came into .env.example with 5df9200, 4 minutes after .env itself (e0c9ab8, both 2026-09-28). Every .env created by dev already has it. Bun loads .env into the scripts' environment, and the server reads it with dotenvy, so the redirect works (the probe saw HAUL_DEV_UI=http://localhost:5173 in the server stand-in).
  • Calling it from a subfolder: bun run dev from crates/sub runs at the repo root; .env is created there and the server's cwd is the root.
  • Exit code of a failing plugin build: && keeps it (bun run dev → exit 3, no server started).
  • References to scripts/dev.ts: none left in docs, CI, README or the screenshots config; the root test script is updated.

Checks: bun run test (5 bun tests, 221 Vitest), bun run typecheck green. Rust not built (stand-in cargo).


Generated by Claude Code

Copy link
Copy Markdown
Owner Author

TDD-Review abgeschlossen: Farben unter --parallel gefixt (15c7a5c, mit Pty-Test). Signal nur an bun run dev (IDE-Stop außerhalb des Terminals) ist ein bewusst akzeptierter Kompromiss ohne eigenen Supervisor, dokumentiert in 30c82d9. CI grün. Merge per Squash.


Generated by Claude Code

@firsttris
firsttris merged commit b41ece2 into main Nov 4, 2026
3 checks passed
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