Files
festipod/scripts/link-polyfill.ts
T
Sylvain Duchesne ac55dc96a4 A refreshed data-layer package never reaches a running dev server
`link:polyfill` overlays the package into node_modules and keeps it current.
That is all it does. A `bun run dev` already running goes on serving what it
loaded at startup, however many times the overlay is rewritten underneath it:
node_modules is excluded by file watchers, so the change lands in the one place
nothing is looking.

The script promised the opposite -- that `bun --hot` would reload the copied
file live. That promise is now removed, and replaced by the instruction to
restart.

It cost about an hour. A defect had been fixed upstream, the overlay refreshed,
and a probe on a freshly launched server confirmed the fix 3 runs out of 3. The
same sequence by hand reproduced the defect at once. The two observations looked
irreconcilable and the hunt went to the wallet, to prior state, to timing.

The dev server had been up for six days. It predated the package rename and the
whole migration; the browser was running a different application from the one
under test. Nothing warned: a stale server looks exactly like a current one, and
the script's own header ruled out the true cause for anyone who trusted it.

The reflex is written down with the leaf: when a fix seems not to take, or when
a hand-run and an automated run disagree, check how long the server has been up
before anything else. One command, cheapest hypothesis first.
2026-08-16 22:06:52 +02:00

115 lines
5.3 KiB
TypeScript

#!/usr/bin/env bun
/**
* link-polyfill.ts — Reactive local link for the @ng-eventually/polyfill polyfill.
*
* WHY a copy-overlay and not a symlink:
* The committed prod dependency installs @ng-eventually/polyfill from Gitea (git+https)
* into pnpm's store WITHOUT its own node_modules/@ng-org → @ng-org/web resolves up to
* Festipod → ONE @ng-org instance (one verifier). The local polyfill CHECKOUT, however,
* carries its own node_modules/@ng-org/* (symlinks into the ng-eventually-js monorepo
* store). Symlinking node_modules/@ng-eventually/polyfill to that checkout puts the
* checkout's @ng-org in the resolution path → a SECOND @ng-org instance → broken SDK
* (two verifiers). So we overlay a real directory that contains ONLY the polyfill's
* source (no node_modules) and keep it in sync by copying — @ng-org still resolves to
* Festipod, single instance preserved.
*
* WHAT IT DOES:
* 1. Replaces node_modules/@ng-eventually/polyfill (the pnpm store symlink) with a real
* directory holding the local polyfill's package.json + src (NO node_modules).
* 2. Asserts the single-instance invariant (same @ng-org/web realpath from Festipod and
* from the overlay) — aborts if it would break.
* 3. Watches the local polyfill src and copies each change into the overlay.
*
* ⚠️ RESTART `bun run dev` AFTER THIS SCRIPT WRITES. The overlay lives inside
* node_modules, which file watchers exclude by convention — so a running dev
* server keeps serving the polyfill it loaded at startup, however many times
* this script rewrites the files underneath it. VERIFIED the hard way: a dev
* server six days old served pre-fix code while the overlay on disk was current,
* and an hour went into hunting a defect that had already been fixed. Watching
* copies the files; it does not make anything reload them.
*
* USAGE (reactive dev):
* Terminal 1: pnpm run link:polyfill # overlays local source, then watches
* Terminal 2: bun run dev # portless festipod bun --hot src/index.ts
* Edit files under packages/polyfill/src → they land in node_modules → RESTART dev to pick them up.
*
* pnpm run link:polyfill --once # overlay + verify, no watch (CI / one-shot)
* Return to the committed git-installed dependency: pnpm install
*
* Override the local checkout path with NG_EVENTUALLY_LOCAL=/path/to/packages/polyfill.
*/
import { existsSync, lstatSync, mkdirSync, rmSync, cpSync, copyFileSync, realpathSync } from "node:fs";
import { watch } from "node:fs";
import { join, dirname } from "node:path";
const FESTIPOD = realpathSync(join(import.meta.dir, ".."));
const LOCAL =
process.env.NG_EVENTUALLY_LOCAL ??
"/home/sylvain/projects/nextgraph/ng-eventually-js/packages/polyfill";
const TARGET = join(FESTIPOD, "node_modules", "@ng-eventually", "polyfill");
const SRC_LOCAL = join(LOCAL, "src");
const SRC_TARGET = join(TARGET, "src");
const ONCE = process.argv.includes("--once");
function fail(msg: string): never {
console.error(`✖ link:polyfill — ${msg}`);
process.exit(1);
}
if (!existsSync(join(LOCAL, "package.json"))) {
fail(`local polyfill not found at ${LOCAL} (set NG_EVENTUALLY_LOCAL to override)`);
}
// 1. Replace the pnpm store symlink with a real overlay dir (metadata + src, NO node_modules).
console.log(`→ overlaying local polyfill: ${LOCAL}`);
if (existsSync(TARGET) || lstatSync(TARGET, { throwIfNoEntry: false })) {
rmSync(TARGET, { recursive: true, force: true });
}
mkdirSync(TARGET, { recursive: true });
for (const meta of ["package.json", "tsconfig.json", "README.md"]) {
const from = join(LOCAL, meta);
if (existsSync(from)) copyFileSync(from, join(TARGET, meta));
}
// Copy src fresh (NEVER a node_modules dir — that is what guarantees single @ng-org instance).
cpSync(SRC_LOCAL, SRC_TARGET, { recursive: true });
// 2. Assert the single-instance invariant.
const fromFestipod = realpathSync(Bun.resolveSync("@ng-org/web", FESTIPOD));
const overlayReal = realpathSync(TARGET);
const fromPolyfill = realpathSync(Bun.resolveSync("@ng-org/web", overlayReal));
console.log(` @ng-org/web (Festipod): ${fromFestipod}`);
console.log(` @ng-org/web (overlay) : ${fromPolyfill}`);
if (fromFestipod !== fromPolyfill) {
fail(
"single-instance invariant BROKEN — @ng-org/web resolves to two different realpaths.\n" +
" The overlay must not contain its own node_modules/@ng-org. Aborting.",
);
}
console.log("✓ single @ng-org/web instance preserved");
if (ONCE) {
console.log("✓ overlay ready (--once, not watching)");
process.exit(0);
}
// 3. Watch and copy on change. This keeps the overlay CURRENT; it does not make a
// running dev server notice (node_modules is outside the watcher) — restart it.
console.log(`👀 watching ${SRC_LOCAL}${SRC_TARGET} (Ctrl-C to stop)`);
watch(SRC_LOCAL, { recursive: true }, (_event, filename) => {
if (!filename) return;
const from = join(SRC_LOCAL, filename);
const to = join(SRC_TARGET, filename);
try {
if (existsSync(from)) {
mkdirSync(dirname(to), { recursive: true });
copyFileSync(from, to);
console.log(` ↻ ${filename}`);
} else if (existsSync(to)) {
rmSync(to, { force: true });
console.log(` ✗ ${filename} (removed)`);
}
} catch (err) {
console.error(` ! failed to sync ${filename}:`, err);
}
});