Re-render and reload the browser on change with --watch - #32
Merged
Merged
Conversation
ManuelRauber
force-pushed
the
feature/watch-option
branch
from
August 21, 2026 15:33
576b3e8 to
3d62331
Compare
ManuelRauber
enabled auto-merge
August 21, 2026 15:33
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #31
mat README.md --watch(short-w) keeps rendering: every change to a rendered file re-renders itand reloads the open tab. This implements the plan recorded on the issue, commit by commit in the
order it lists.
How it works
src/cli/reload-server.tsstarts oneBun.serveper session on127.0.0.1, on an ephemeral port,with a random 128-bit token as its path. It upgrades only on that exact path and answers 404
otherwise. The page itself stays a plain
file://document.src/html/reload-client.tsemits a classic inline<script>, injected only when a render is givena reload channel, so
--outputand every plain render structurally cannot carry it. It reconnectsevery second, at most 25 times, with the counter reset on
open, so a tab left open goes quietabout 25 seconds after mat exits.
src/cli/file-watcher.tswatches the parent directories withfs.watchand filters by file name,with one shared 200 ms debounce. Directory watches rather than file watches, because an atomic
save replaces the inode and leaves a file watch pointing at the one that is gone.
src/cli/watch.tsholds the session loop. It is driven by anAbortSignal, so tests never needprocess signals, and it queues exactly one follow-up for whatever lands during a render.
src/cli.tsgained a reusablerenderDocuments()step, split out ofrunRenderin abehaviour-neutral first commit, plus the
mainwiring for--watch.Deviations from the plan
(
sendPings, against its 120 secondidleTimeout), so the planned 30 second'ping'interval wasredundant. The server carries no timer of its own.
directory under the name that vanished: an atomic save arrives as
index.md.tmp, never asindex.md. The watcher therefore keeps an inode, size and mtime baseline per watched file andtreats a changed baseline as a hit.
re-renderedrather than naming the document, which kept theinterface between
runWatchand its render step down to what it actually needs.watchconfiguration key exists after all. The plan ruled one out; Manu asked for it, so{"watch": true}now makes--watchthe default. LikefollowLinks, the flag became tristate so--watch=falsecan override the file, and the configured default is dropped rather than rejectedwhere it cannot apply: with
--output(no tab to reload) and with-(no path to watch). Worthknowing before enabling it: every plain
matcall then becomes a long-running foreground process,including calls from scripts.
feat:, the building blocks arechore:, sorelease-please describes the feature in two changelog lines rather than seven.
Verification
bunx tsc --noEmit,bunx biome check .andbun testare clean (399 pass, 7 skip, 0 fail; 347before). A compiled binary was built and driven by hand: render, save, re-render, Ctrl+C, exit 0.
Every new test was mutation-checked against the production line it claims to cover.
Beyond the plan's test list,
tests/watch.test.tsdrivesrunWatchwith a render step the testcontrols, which is the only way to reach the mid-render change queue: without it, deleting that
queue left every other watch test green.
Known limitations, deliberately not fixed here
rm -rf docs && mkdir docs, or a branch switch thatremoves and recreates it) leaves the
fs.watchhandle on the destroyed inode, and the sessionthen silently stops noticing changes. Both candidate fixes are unsatisfying: the inode is handed
straight back to the replacement on this filesystem, and
birthtimeequalsctimethere, so anidentity check would also re-arm whenever a sibling file appears; the robust alternative is
periodic re-arming, which the plan deliberately avoided. Worth its own issue.
fix, comparing mtime against a
Date.now()taken before the render, is not reliable: mtime andthe wall clock run on different sources here, with mtime lagging by a fraction of a millisecond,
so the comparison silently drops the change. Doing it properly means threading the mtime the
render actually read out through the follow queue.
--follow-links, a link target that could not be rendered (missing, binary, too large)is not watched, so creating or repairing it is only picked up on the next change to a file that
is watched.
local process that squats the ephemeral port after mat exits could drive a still-reconnecting tab
into repeated reloads. Closing that means carrying a second secret in the reload message.