import { readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { defineConfig, type Plugin } from "vite"; /** * The office's copy of the page, emitted from the city's. * * `office.lumbridgecorp.com` and `tera.lumbridgecorp.com` are one bundle behind * two names, and the app works out which door it is at runtime by reading its * own hostname. A crawler cannot: it reads the HTML and nothing else. So a * single `index.html` means both doors unfurl with the same title, the same * sentence and the same picture — and the office door's whole reason to exist is * that it is a different place. * * The obvious fix, a second hand-written `office.html`, is the one the Caddy * config already argues against for the static root: "sharing the root rather * than copying it means a deploy cannot leave the two doors on different * builds". Two 900-line files each carrying the whole inline stylesheet would * drift on the first CSS change, and nothing would notice, because the drift is * invisible until somebody opens the other door. * * So there is one source and the build emits both shells. Everything outside the * `ogc:` markers in `index.html` is copied byte for byte — the stylesheet, the * DOM, the script tag Vite has already rewritten to the hashed bundle — and only * the block between them is replaced. A change to the interface reaches both * doors by construction, and the only thing that can differ is the thing that is * supposed to. * * It runs in `writeBundle` rather than `transformIndexHtml` because it needs the * *finished* document, after Vite has substituted the asset URLs. Transforming * earlier would emit a page pointing at unhashed source paths. */ function twoDoors(): Plugin { const MARKER_END = ""; const OFFICE = ` Spaces — an office you can walk around `; return { name: "lumbridge:two-doors", apply: "build", async writeBundle(options) { const dir = options.dir ?? "dist"; const html = await readFile(join(dir, "index.html"), "utf8"); const open = html.indexOf("ogc:start"); const end = html.indexOf(MARKER_END); // Loudly, rather than by silently shipping two identical shells. A card // that is quietly the wrong one is the failure this plugin exists to // prevent, so losing the markers must not be a no-op. if (open === -1 || end === -1) { this.error("index.html has no ogc:start/ogc:end markers — cannot emit office.html"); return; } // The opening marker sits inside a larger comment, so cut from the start // of that comment: otherwise the explanation ships on both doors while // being true of only one. const from = html.lastIndexOf("" + OFFICE + html.slice(end + MARKER_END.length); await writeFile(join(dir, "office.html"), office, "utf8"); }, }; } export default defineConfig({ // Mounted under tera.lumbridgecorp.com in production; the trailing // slash matters, since every asset URL is resolved against it. base: process.env.TERA_BASE ?? "/", plugins: [twoDoors()], build: { outDir: "dist", target: "es2022", // Two entries. `login.html` used to sit in `public/`, which Vite copies // verbatim — so `import.meta.env` was never substituted there and the page // could not be told which identity provider to sign in against. It is a real // entry now; the built URL (`/login.html`) is unchanged. // // `office.html` is deliberately not a third entry: it is the same document // as `index.html` with a different head, so making it one would give it its // own copy of the bundle graph. It is emitted after the build instead. rollupOptions: { input: { index: "index.html", login: "login.html" } }, }, });