1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/vite.config.ts
T
karti effa077cc7 The shot list catches up with the building it is photographing
The two office stills were captioned for a building that no longer exists:
"thirty-four metres by eighteen, one storey" against a plate that is now
forty-eight by eighteen over two storeys, and a night note promising the
office "keeps its own light after the sun has gone" — which was
aspirational when it was written and is only now true.

Two new frames. `office-commons` is the double-height wing, which is the
one view that shows why the storeys are held fourteen metres apart.
`office-hangar` is Frontier Valley, and it needed a new capability: the
harness presses the office picker before choosing a chapter, because
switching buildings disposes the scene and rebuilds it, so a chapter click
issued first would land against a legend that no longer exists. Selected by
visible label and asserted, for the same reason `chapter()` asserts its
`shortLabel` — a reordered `OFFICES` table would otherwise photograph one
building and caption it with the other's name.

The office share card said "Lumbridge HQ, one floor: the rooms, the desks
and who is at them". It is the first thing a stranger sees and it was wrong
in three ways.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 04:34:39 -07:00

201 lines
9.4 KiB
TypeScript

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 = "<!-- ogc:end -->";
const OFFICE = `
<title>Spaces — an office you can walk around</title>
<meta
name="description"
content="Two buildings you can walk around: a tower floor above San Francisco and a hangar at Alameda Point. Real sun, real hours, the lights on at night. Rendered in the browser from a data file anybody can copy. Apache-2.0, self-hostable."
/>
<link rel="canonical" href="https://office.lumbridgecorp.com/" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Lumbridge Spaces" />
<meta property="og:title" content="Spaces — an office you can walk around" />
<meta
property="og:description"
content="The rooms, the desks and who is at them — floor plans rendered from a data file anybody can copy, under the sun of the day you are reading this."
/>
<meta property="og:url" content="https://office.lumbridgecorp.com/" />
<meta property="og:image" content="https://office.lumbridgecorp.com/og-office.png" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta
property="og:image:alt"
content="An office seen from above as a cutaway model: two storeys held apart around a double-height commons, desks and meeting rooms visible, people at their seats."
/>
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Spaces — an office you can walk around" />
<meta
name="twitter:description"
content="The rooms, the desks and who is at them — floor plans rendered from a data file anybody can copy, under the sun of the day you are reading this."
/>
<meta name="twitter:image" content="https://office.lumbridgecorp.com/og-office.png" />`;
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("<!--", open);
const office =
html.slice(0, from) +
"<!-- The office door. Generated from index.html by `twoDoors` in vite.config.ts. -->" +
OFFICE +
html.slice(end + MARKER_END.length);
await writeFile(join(dir, "office.html"), office, "utf8");
},
};
}
/**
* Cut `satellite.js`'s WASM runtime out of the bundle.
*
* `satellite.js`'s entry point ends with `export * from './wasm/index.js'`, and
* that subtree is an Emscripten build: a megabyte of generated glue that reaches
* for `node:module` and `node:worker_threads`, wraps itself in a top-level
* `await`, and cannot be tree-shaken away because a star re-export of a module
* with side effects is not something a bundler may drop on its own.
*
* Measured, before this existed: **308 kB** of minified WASM loader shipped to
* every visitor, in a chunk nothing ever called. `engine/satellites.ts` uses the
* pure-JS `propagate` and says in its own header why it does not want the
* `BulkPropagator` — a binary loaded at runtime plus a fallback path for when
* that fails, to buy back two milliseconds a frame that are already budgeted.
* Paying 308 kB to *not* use it was the worst of both.
*
* The stub is empty because nothing imports a name from it. If a future version
* of this repo does want the bulk propagator, the fix is to delete this plugin
* and pay the kilobytes deliberately — not to widen the stub.
*
* Matching is on the **importer** as well as the specifier, so this cannot
* silently swallow some other package's `./wasm/index.js`.
*/
function noWasmPropagator(): Plugin {
const STUB = "\0lumbridge:satellite-wasm-stub";
return {
name: "lumbridge:no-wasm-propagator",
enforce: "pre",
resolveId(source, importer) {
if (source !== "./wasm/index.js") return null;
if (importer === undefined || !importer.includes("satellite.js")) return null;
return STUB;
},
load(id) {
return id === STUB ? "export {};" : null;
},
};
}
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: [noWasmPropagator(), twoDoors()],
/**
* The heightfield worker is constructed as `{ type: "module" }` in
* `world.ts`, and this is the build setting that agrees with it.
*
* Vite's default worker format is `iife`, which was survivable while nothing
* in the graph needed anything an IIFE cannot express — and stopped being
* survivable the moment it did. `satellite.js` reaches a WASM runtime with a
* top-level `await` in it, and rollup's answer to a top-level await in an
* `iife` chunk is to fail the whole build, with an error naming a file no
* worker imports.
*
* So this is agreement rather than a workaround: the module the browser is
* told to load as an ES module is now built as one.
*/
worker: { format: "es" },
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" },
output: {
/**
* three.js and the SGP4 propagator get a chunk of their own.
*
* Not to make the download smaller — it is the same bytes either way —
* but to stop them being *re-downloaded*. They were inside the entry
* chunk, so every deploy that changed a line of app code invalidated
* three quarters of a megabyte of dependency that had not changed since
* the last release. Split out, a returning visitor pays for the app and
* keeps the vendor chunk it already has.
*
* They are one chunk rather than two because they are always wanted
* together: `engine/satellites.ts` imports both, and it is reached from
* the entry on every board.
*
* Measured: the entry chunk goes from 758 kB to 208 kB and the vendor
* chunk is 550 kB. Rollup's 500 kB warning therefore still fires — and
* it should. It now points at three.js, where it is a true statement
* about a dependency nobody here can shrink, instead of at our own code,
* where it was pointing at three.js all along and reading as if it were
* about us. Raising `chunkSizeWarningLimit` would have hidden both.
*/
manualChunks: {
vendor: ["three", "satellite.js"],
},
},
},
},
});