From dd300ac1543ca1015e7c32a99945deb11424e2c8 Mon Sep 17 00:00:00 2001 From: Jesse Date: Fri, 31 Jul 2026 23:34:18 -0400 Subject: [PATCH] Fix caching, Limits placement, and explain the board better --- scripts/build-web.ts | 47 ++++++++- src/engine/track.ts | 9 ++ src/sim/narrate.ts | 17 ++- src/sim/replay.ts | 7 +- src/sim/view.ts | 109 +++++++++++++++++-- src/web/game.ts | 33 +++++- src/web/index.html | 6 +- src/web/main.ts | 22 +++- test/mainline-cards.test.ts | 40 +++++++ test/web.test.ts | 202 +++++++++++++++++++++++++++++++++++- 10 files changed, 466 insertions(+), 26 deletions(-) diff --git a/scripts/build-web.ts b/scripts/build-web.ts index 376d924..23d0887 100644 --- a/scripts/build-web.ts +++ b/scripts/build-web.ts @@ -10,7 +10,7 @@ */ import { execFileSync } from 'node:child_process'; -import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -68,7 +68,48 @@ function buildStamp(): string { } const stamp = buildStamp(); -const page = readFileSync(join(root, 'src/web/index.html'), 'utf8').replaceAll('__BUILD__', stamp); + +/** + * Cache-bust every module. + * + * A returning visitor gets a fresh index.html and STALE JavaScript, because a static host caches + * .js and the filenames never change. That is not a cosmetic staleness: it mixes new HTML with old + * code, and the first mismatch is fatal. It happened on the first real deploy — index.html had + * dropped an element that the cached main.js still asked for, so the page threw + * `missing element: target` and the game never started. + * + * Appending the build tag to every relative import makes each deploy a new set of URLs, so a + * browser cannot serve half of one build and half of another. + */ +const tag = encodeURIComponent(stamp.split(' · ')[1] ?? stamp); + +function bustImports(dir: string): number { + let count = 0; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) { + count += bustImports(full); + continue; + } + if (!entry.name.endsWith('.js')) continue; + const src = readFileSync(full, 'utf8'); + const next = src.replace( + /^(\s*(?:import|export)[^'"\n]*?from\s+['"])(\.[^'"]+\.js)(['"])/gm, + (_m, head: string, spec: string, tail: string) => `${head}${spec}?v=${tag}${tail}`, + ); + if (next !== src) { + writeFileSync(full, next); + count++; + } + } + return count; +} + +const busted = bustImports(dist); + +const page = readFileSync(join(root, 'src/web/index.html'), 'utf8') + .replaceAll('__BUILD__', stamp) + .replace(/(