fix(web): stop shipping 7 MB of sound the console cannot play

`@unom/ui/button` reaches `sound/defaults.js`, which resolves two game-UI sprite
sheets with `new URL(…, import.meta.url)` at module scope — a 4.8 MB .wav and a
2.2 MB .mp3. Vite emitted both into the build, so they rode into the Windows
installer and the .deb. The console never mounts UnomProviders, so no player is
bundled and not one byte of it could ever be played. A build-time rewrite of
those two expressions takes the asset payload from 8.2 MB to 1.5 MB; the login
page and the button chunk are unchanged. Deleting the plugin is the whole revert
if the console ever wants click sounds.

Also:

- `bun run dev` forwards the management bearer, so developing against a real
  host stops 401ing into a /login bounce that dev has no gate to satisfy.
- `check-i18n` runs after `build`, not only inside `codegen`. It exists to stop a
  zero-message console shipping, and the CI job and the installer build both
  install with `--ignore-scripts`, so it had never once run where it mattered.
- Bun's idle timeout goes from its 10 s default to 120 s. The host sends SSE
  keep-alives every 15 s, so anything long-lived proxied through the console was
  cut by us first — which the event stream is about to depend on.
- The typecheck covers the Storybook config and preview.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 00:20:10 +02:00
co-authored by Claude Opus 5
parent e30d94573a
commit 55e01c1460
4 changed files with 61 additions and 3 deletions
+4 -1
View File
@@ -51,8 +51,11 @@ const tls =
const server = Bun.serve({
port: process.env.NITRO_PORT || process.env.PORT || 3000,
host: process.env.NITRO_HOST || process.env.HOST,
// Bun defaults this to 10 s, which is SHORTER than the host's 15 s SSE keep-alive comment — so a
// proxied `/api/v1/events` stream (or any other quiet long-lived response) gets cut by us and
// reconnects on a loop. 120 s is comfortably above any keep-alive we forward; still overridable.
idleTimeout:
Number.parseInt(process.env.NITRO_BUN_IDLE_TIMEOUT, 10) || undefined,
Number.parseInt(process.env.NITRO_BUN_IDLE_TIMEOUT, 10) || 120,
// `tls: undefined` ⇒ plain HTTP (dev); otherwise HTTPS over HTTP/1.1.
tls,
websocket: import.meta._websocket ? ws.websocket : undefined,
+1
View File
@@ -11,6 +11,7 @@
"dev": "vite dev --port 47992",
"prebuild": "orval --config orval.config.ts",
"build": "vite build",
"postbuild": "node tools/check-i18n.mjs",
"start": "bun run .output/server/index.mjs",
"api:gen": "orval --config orval.config.ts",
"lint": "tsc --noEmit",
+8 -1
View File
@@ -20,5 +20,12 @@
"@/*": ["./src/*"]
}
},
"include": ["src", "server", "vite.config.ts", "orval.config.ts"]
"include": [
"src",
"server",
".storybook",
"vite.config.ts",
"vite.storybook.config.ts",
"orval.config.ts"
]
}
+48 -1
View File
@@ -115,16 +115,63 @@ function pluginUiDevProxy(): Plugin {
};
}
/**
* Drop @unom/ui's game-UI sound sprites from the build.
*
* `@unom/ui/button` pulls in `sound/defaults.js`, which resolves two sprite sheets with
* `new URL(…, import.meta.url)` at module scope — a 4.8 MB .wav and a 2.2 MB .mp3. Vite therefore
* emits both into `.output/public/assets/`, where they were ~7 MB of an 8.2 MB asset payload, and
* they ride along into the Windows installer and the .deb.
*
* The console never mounts `UnomProviders`, so no sound player is bundled and not one byte of that
* can ever be played. Stub the two files to an empty URL instead of shipping them.
*
* If the console ever DOES want click sounds, delete this plugin — that is the whole revert.
*/
function dropUnomSoundSprites(): Plugin {
// The module that names them, and the `new URL(<sprite>, import.meta.url).href` expressions
// inside it. Rewriting the EXPRESSION is what works: Vite emits these assets from its own
// `new URL(…, import.meta.url)` transform, so intercepting the .wav/.mp3 module id never fires.
const DEFAULTS = /@unom[\\/]ui[\\/].*sound[\\/]defaults\.(?:js|mjs)$/;
const SPRITE_URL =
/new URL\(\s*(["'])[^"']*\.(?:wav|mp3)\1\s*,\s*import\.meta\.url\s*\)\.href/g;
return {
name: "punktfunk-drop-unom-sound-sprites",
enforce: "pre",
transform(code, id) {
if (!DEFAULTS.test(id)) return null;
const out = code.replace(SPRITE_URL, '""');
return out === code ? null : { code: out, map: null };
},
};
}
export default defineConfig({
server: {
proxy: {
// `secure: false`: the host serves its own self-signed identity cert on loopback.
"/api": { target: MGMT_URL, changeOrigin: true, secure: false },
"/api": {
target: MGMT_URL,
changeOrigin: true,
secure: false,
// Inject the management bearer, exactly as the deployed BFF does
// (server/routes/api/[...].ts). The host requires a token on every route now, so
// without this `bun run dev` 401s on every call and `apiFetch` bounces the developer
// to /login — where logging in doesn't help, because dev has no login gate at all.
configure(proxy) {
const token = process.env.PUNKTFUNK_MGMT_TOKEN;
if (!token) return;
proxy.on("proxyReq", (proxyReq) => {
proxyReq.setHeader("authorization", `Bearer ${token}`);
});
},
},
},
},
plugins: [
// First, so it intercepts /plugin-ui before the SSR catch-all in dev.
pluginUiDevProxy(),
dropUnomSoundSprites(),
viteTsConfigPaths({ projects: ["./tsconfig.json"] }),
tailwindcss(),
paraglideVitePlugin({