Last active
September 4, 2025 16:00
-
-
Save samwightt/f78b0bffddca4f74b06519cae285da1c to your computer and use it in GitHub Desktop.
Runs a list of commands concurrently using bun. npm-run-all but in less than 100 lines of code.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| interface Runnable { | |
| name: string; | |
| command: string[]; | |
| } | |
| function createPrefixedWriter(prefix: string, output: Bun.BunFile) { | |
| let buffer = ""; | |
| // Tried implementing this using a TransformStream but Bun didn't like it for some reason. So we use a | |
| // WritableStream and Bun.write instead :( | |
| const textWriter = new WritableStream({ | |
| write(chunk) { | |
| buffer += chunk; | |
| const lines = buffer.split("\n"); | |
| buffer = lines.pop() || ""; // Keep the last incomplete line | |
| for (const line of lines) { | |
| Bun.write(output, `[${prefix}] ${line}\n`); | |
| } | |
| }, | |
| close() { | |
| // Handle any remaining buffer | |
| if (buffer) { | |
| Bun.write(output, `[${prefix}] ${buffer}\n`); | |
| } | |
| }, | |
| }); | |
| const decoderStream = new TextDecoderStream(); | |
| decoderStream.readable.pipeTo(textWriter); | |
| return decoderStream.writable; | |
| } | |
| // Takes list of commands and runs them. | |
| export async function runConcurrently(commands: Runnable[]) { | |
| const controller = new AbortController(); | |
| const { signal } = controller; | |
| const processes = commands.map((cmd) => { | |
| const proc = Bun.spawn(cmd.command, { | |
| stdout: "pipe", | |
| stderr: "pipe", | |
| signal, | |
| }); | |
| proc.stdout.pipeTo(createPrefixedWriter(cmd.name, Bun.stdout)); | |
| proc.stderr.pipeTo(createPrefixedWriter(cmd.name, Bun.stderr)); | |
| return proc; | |
| }); | |
| const waitForAllProcessesExited = () => | |
| Promise.all(processes.map((p) => p.exited)); | |
| let exiting = false; | |
| // Ask processes nicely to quit, wait 5 seconds, then nuke them if they haven't yet. | |
| async function killAllProcesses() { | |
| // Can receive sigint more than once for some reason. Also don't want to kill when we're already killing | |
| // and one of the other processes has exited. | |
| if (exiting) return; | |
| exiting = true; | |
| console.log("Gracefully shutting down..."); | |
| for (const item of processes) { | |
| item.kill("SIGTERM"); | |
| } | |
| const res = await Promise.race([ | |
| Bun.sleep(5000), | |
| waitForAllProcessesExited(), | |
| ]); | |
| if (res === undefined) { | |
| console.log("Processes not quitting, sending SIGKILL..."); | |
| // controller.abort sends a SIGKILL | |
| controller.abort(); | |
| } | |
| process.exit(0); | |
| } | |
| for (const item of processes) { | |
| item.exited.then(() => { | |
| if (!exiting) { | |
| console.log("Shutting down because process failed..."); | |
| killAllProcesses(); | |
| } | |
| }); | |
| } | |
| process.on("SIGINT", killAllProcesses); | |
| await waitForAllProcessesExited(); | |
| console.log("Finished killing processes."); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Run with bun run ./example.ts | |
| import { runConcurrently } from './bun-concurrent' | |
| // Silence TS. | |
| export {}; | |
| await runConcurrently([ | |
| { | |
| name: "dragonflydb", | |
| command: [ | |
| "docker", | |
| "run", | |
| "--name", | |
| "dragonfly", | |
| "--rm", | |
| "-p", | |
| "6379:6379", | |
| "--ulimit", | |
| "memlock=-1", | |
| "docker.dragonflydb.io/dragonflydb/dragonfly", | |
| ], | |
| }, | |
| { | |
| name: "server", | |
| command: ["bun", "run", "dev:server"], | |
| }, | |
| ]); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment