Runs your bundled server during rolldown --watch, with zero-downtime restarts: each rebuild boots on a fresh port behind a tiny TCP proxy, and traffic only switches over once the new process actually accepts connections. The old process keeps serving until then — a rebuild never drops in-flight requests or leaves a dead window.
An alternative to rollup-plugin-run, built for Rolldown.
Your entry must be a server that listens on
process.env.PORT.
The plugin decides a build is "ready" by probing that port for a TCP listener. Anything speaking TCP works — HTTP, WebSockets, gRPC — but a script that never listens (a CLI, a one-shot job) will never be considered ready and will not be switched to. For arbitrary scripts, use a plain restart-style runner instead.
pnpm add -D @joylunow/rolldown-plugin-runRequires Node >= 20.6.
// rolldown.config.ts
import { defineConfig } from "rolldown";
import run from "@joylunow/rolldown-plugin-run";
export default defineConfig({
input: "src/server.ts",
platform: "node",
output: { dir: "dist" },
plugins: [run()],
});// src/server.ts
import { createServer } from "node:http";
createServer((req, res) => res.end("hello")).listen(process.env.PORT);rolldown --watch
# → serving on http://127.0.0.1:3000The plugin does nothing outside watch mode, so the same config is safe for production builds.
- On the first
writeBundlein watch mode, a raw TCP proxy binds the public port ($PORTor3000). This is the only address you and your tools ever talk to. - Each build is forked from the entry chunk with
PORTset to a random free port. - The plugin probes that port until the child accepts a connection, then atomically points the proxy at it and SIGTERMs the previous process (SIGKILL after 5s if it lingers).
- Rebuilds that produce byte-identical output are detected by hashing the chunks and skipped entirely — no pointless restart.
- A build that crashes (or never listens) is never switched to; the last good build keeps serving.
- Children are forked over an IPC channel and exit themselves if the watcher dies, so no orphan processes survive a killed terminal.
Because the proxy pipes raw TCP, WebSockets and streaming responses pass through untouched.
If the public port is already held by another live process (say, a second watch session), the plugin falls back to a random port and logs the address it actually bound.
run({
// extra flags for the forked child, e.g. ["--inspect"]
execArgv: ["--inspect"],
});The public port is taken from process.env.PORT (default 3000).
- Exactly one entry chunk per build (the plugin errors otherwise).
- The child inherits the watcher's environment plus its own
PORT. - The proxy holds early connections for up to 10s while the first build boots, then drops them.