/** * The `file://` build: one HTML file, everything inlined. * * Chrome gives a `file://` document an opaque origin, which costs it IndexedDB, * workers, `fetch` and module scripts. So this build is a classic IIFE with the * game data inlined rather than dynamically imported, and the worker aliased out * entirely. The app degrades behind `PersistenceAdapter` and `SolverHost` and * never sees the difference. */ import { readFileSync, writeFileSync, rmSync, readdirSync } from "node:fs"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { defineConfig, type Plugin } from "vite"; import react from "@vitejs/plugin-react"; const fromRoot = (relative: string): string => fileURLToPath(new URL(relative, import.meta.url)); const OUT_DIR = "dist/single"; const TARGET = "dist/nte.html"; /** Fold the emitted script and stylesheet into one document. */ function inlineEverything(): Plugin { return { name: "inline-everything", writeBundle() { const files = readdirSync(OUT_DIR, { recursive: true }) as string[]; const scripts = files.filter((name) => name.endsWith(".js")); const styles = files.filter((name) => name.endsWith(".css")); if (scripts.length !== 1) { // More than one chunk means something escaped `inlineDynamicImports`, // and a file:// page cannot fetch the second one. throw new Error(`expected one script, got ${scripts.length}: ${scripts.join(", ")}`); } const script = readFileSync(join(OUT_DIR, scripts[0]!), "utf8"); const css = styles.map((name) => readFileSync(join(OUT_DIR, name), "utf8")).join("\n"); const html = `