#!/usr/bin/env bun /** * link-polyfill.ts — Reactive local overlay of a data-layer PROVIDER's checkout into * node_modules. One script, one provider per run; only the paths differ between them. * * PROVIDERS (first non-flag argument; defaults to `polyfill`): * polyfill → node_modules/@ng-eventually/polyfill override: NG_EVENTUALLY_LOCAL * indexing → node_modules/@ng-helpers/indexing override: NG_HELPERS_LOCAL * * WHY a copy-overlay and not a symlink (identical for every provider): * A committed prod dependency installs from Gitea (git+https) into pnpm's store WITHOUT * its own node_modules/@ng-org, so @ng-org/web resolves UP to Festipod → ONE @ng-org * instance (one verifier). A local CHECKOUT, however, carries its own node_modules/* * (links into that provider's own dev tree). Symlinking node_modules/ to the * checkout would put the checkout's copies in the resolution path → a SECOND @ng-org * (and, for `indexing`, a second @ng-eventually/polyfill) → broken SDK, two verifiers. * So we overlay a real directory containing ONLY the provider's source (no node_modules): * shared packages still resolve up to Festipod, single instance preserved. * * WHAT IT DOES: * 1. Replaces node_modules/ (the pnpm store symlink) with a real directory * holding the local checkout's package.json + src (NO node_modules). * 2. Asserts the single-instance invariant — every package this provider SHARES with * Festipod must resolve to the same realpath from Festipod and from the overlay — * and aborts if it would break. * 3. Watches the local checkout's src and copies each change into the overlay. * * ⚠️ RESTART `bun run dev` AFTER THIS SCRIPT WRITES — a rebuild is NOT a substitute. * A running dev server NEVER picks up a package refreshed inside node_modules, not even * across a genuine rebuild: VERIFIED in a controlled test, an application-source edit * produced a new bundle hash and the rebuilt bundle STILL carried the stale dependency. * The server's resolution of that import is pinned at process start and a rebuild does not * re-resolve it. Only restarting serves the fresh copy, and nothing warns you — a stale * server looks exactly like a current one. See * .project/concepts/tech-stack/caveat_polyfill-overlay-needs-a-dev-restart.md, which cost * an hour to learn. Watching copies the files; it does not make anything reload them. * * USAGE (reactive dev): * Terminal 1: pnpm run link:polyfill # or: pnpm run link:indexing * Terminal 2: bun run dev # portless festipod bun --hot src/index.ts * Edit the checkout's src → it lands in node_modules → RESTART dev to pick it up. * * pnpm run link:indexing --once # overlay + verify, no watch (CI / one-shot) * Return to the committed git-installed dependencies: pnpm install */ import { existsSync, lstatSync, mkdirSync, rmSync, cpSync, copyFileSync, realpathSync } from "node:fs"; import { watch } from "node:fs"; import { join, dirname } from "node:path"; interface Provider { /** Package as installed, e.g. "@ng-eventually/polyfill" — also its node_modules path. */ readonly packageName: string; /** Local checkout used when the env override is unset. */ readonly defaultLocal: string; /** Env var overriding the local checkout path. */ readonly envOverride: string; /** * Packages this provider SHARES with Festipod and that must stay single-instance. * Each is resolved from Festipod and from the overlay; the realpaths must match. */ readonly singletons: readonly string[]; } const PROVIDERS: Record = { polyfill: { packageName: "@ng-eventually/polyfill", defaultLocal: "/home/sylvain/projects/nextgraph/ng-eventually-js/packages/polyfill", envOverride: "NG_EVENTUALLY_LOCAL", singletons: ["@ng-org/web"], }, indexing: { packageName: "@ng-helpers/indexing", defaultLocal: "/home/sylvain/projects/nextgraph/ng-helpers", envOverride: "NG_HELPERS_LOCAL", // Consumes the polyfill, so BOTH it and the verifier underneath must stay single. singletons: ["@ng-eventually/polyfill", "@ng-org/web"], }, }; const FESTIPOD = realpathSync(join(import.meta.dir, "..")); const args = process.argv.slice(2); const ONCE = args.includes("--once"); const KEY = args.find((a) => !a.startsWith("-")) ?? "polyfill"; function fail(msg: string): never { console.error(`✖ link:${KEY} — ${msg}`); process.exit(1); } const provider = PROVIDERS[KEY]; if (!provider) { fail(`unknown provider "${KEY}" — expected one of: ${Object.keys(PROVIDERS).join(", ")}`); } const LOCAL = process.env[provider.envOverride] ?? provider.defaultLocal; const TARGET = join(FESTIPOD, "node_modules", ...provider.packageName.split("/")); const SRC_LOCAL = join(LOCAL, "src"); const SRC_TARGET = join(TARGET, "src"); if (!existsSync(join(LOCAL, "package.json"))) { fail(`local checkout not found at ${LOCAL} (set ${provider.envOverride} to override)`); } if (!existsSync(SRC_LOCAL)) { fail(`local checkout has no src/ at ${SRC_LOCAL}`); } // 1. Replace the pnpm store symlink with a real overlay dir (metadata + src, NO node_modules). console.log(`→ overlaying local ${provider.packageName}: ${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 instances). cpSync(SRC_LOCAL, SRC_TARGET, { recursive: true }); // 2. Assert the single-instance invariant for every package shared with Festipod. const overlayReal = realpathSync(TARGET); for (const spec of provider.singletons) { const fromFestipod = realpathSync(Bun.resolveSync(spec, FESTIPOD)); const fromOverlay = realpathSync(Bun.resolveSync(spec, overlayReal)); console.log(` ${spec} (Festipod): ${fromFestipod}`); console.log(` ${spec} (overlay) : ${fromOverlay}`); if (fromFestipod !== fromOverlay) { fail( `single-instance invariant BROKEN — ${spec} resolves to two different realpaths.\n` + " The overlay must not contain its own node_modules. Aborting.", ); } } console.log(`✓ single instance preserved for: ${provider.singletons.join(", ")}`); 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 — not even across a rebuild (see the header). 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); } });