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" } },
},
});