fix(auth): reload the preview on every save, not just the first

bun --watch watches each imported file, and many editors save by
writing a new file and renaming it over the old one. The watch does not
follow the rename, so the first such save reloaded the preview and every
later one was silently ignored.

The preview now runs under a small supervisor that watches the source
directories and restarts the server, which sees a save however it was
made. It also stops the server when it is stopped itself, so the port
is not left held.
This commit is contained in:
Wanjohi
2026-10-02 05:48:10 +03:00
parent 86394dd769
commit 5353b9a1db
3 changed files with 53 additions and 4 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"scripts": {
"dev": "wrangler dev",
"serve": "bun run src/server.ts",
"preview": "bun --watch script/preview.ts",
"preview": "bun script/preview-watch.ts",
"deploy": "wrangler deploy"
},
"dependencies": {
+49
View File
@@ -0,0 +1,49 @@
/**
* Runs `preview.ts`, and runs it again whenever the sign-in UI's source changes.
*
* Not `bun --watch`, which watches each imported file. Many editors save by
* writing a new file and renaming it over the old one, and a watch on a file
* does not follow the rename: the first such save reloads, and every one after
* it is silently ignored. A watch on the directory sees every save, however it
* was made.
*/
import { watch } from 'node:fs';
import { join } from 'node:path';
const ROOT = join(import.meta.dir, '..', '..', '..');
const DIRS = [join(ROOT, 'packages/auth/src'), join(ROOT, 'apps/auth/src'), import.meta.dir];
const SERVER = join(import.meta.dir, 'preview.ts');
let child = start();
let timer: ReturnType<typeof setTimeout> | undefined;
function start() {
return Bun.spawn(['bun', SERVER], { stdio: ['inherit', 'inherit', 'inherit'] });
}
async function restart(file: string) {
child.kill();
// The port is free only once the old server has exited.
await child.exited;
console.log(`changed: ${file}`);
child = start();
}
for (const dir of DIRS) {
watch(dir, { recursive: true }, (_event, file) => {
if (!file || !/\.(ts|tsx)$/.test(file)) return;
// One save can be several events: a write, then a rename.
clearTimeout(timer);
timer = setTimeout(() => restart(file), 60);
});
}
// Ctrl-C reaches the whole process group, but a plain `kill` reaches only
// this process, and would leave the server holding the port.
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
process.on(signal, () => {
child.kill();
process.exit(0);
});
}
+3 -3
View File
@@ -8,9 +8,9 @@
* screens from `ui/issuer.ts` — drawn by the real renderer with the real
* theme. What differs from production is only how the page was reached.
*
* `bun --watch` restarts this process when anything it imports changes, which
* includes `css.ts`, `render.tsx` and `base.tsx`. Each page polls for the
* restart and reloads itself, so saving a file is the whole loop.
* `preview-watch.ts` restarts this process when the UI's source changes —
* the renderer, the stylesheet, the copy, the issuer's screens. Each page polls
* for the restart and reloads itself, so saving a file is the whole loop.
*
* A state that is missing here is a state nobody will look at before it
* ships. Adding a screen to the flow means adding it to `STATES`.