flybrain/apps/stage/vite.config.ts
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

118 lines
4.5 KiB
TypeScript

/**
* Vite config for the broadcast page.
*
* Two things here are not boilerplate:
*
* 1. The dataset artifacts in the repo's `data/fafb-v783` are served at `/data/fafb-v783` in dev
* by a small middleware and copied into `dist/` on build. They are not copied into
* `public/`: they are 11 MB of generated artifacts that belong to the repo root, and a second
* copy in git is exactly what the task forbids. They are `.binz` (gzip) files that the
* browser inflates itself with `DecompressionStream`, so the middleware must NOT set
* `content-encoding: gzip` — that would make the browser inflate them first and
* `loadCompressed` would then fail on already-inflated bytes.
* 2. `?res=720` scales the whole page down with a CSS transform, so nothing here changes per
* resolution. There is one authoring resolution (1920x1080, the native broadcast frame) and one
* build.
*/
import { execFileSync } from 'node:child_process';
import { createReadStream } from 'node:fs';
import { cp, stat } from 'node:fs/promises';
import { dirname, join, normalize, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react';
import { defineConfig, type Plugin } from 'vite';
const here = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(here, '../..');
const datasetDir = resolve(repoRoot, 'data/fafb-v783');
const datasetRoute = '/data/fafb-v783';
/**
* The version chip's string (`src/panels/TitleStrip.tsx`, `data-testid="stage-version"`).
*
* `git describe --tags --always --dirty` from the repo root: a tagged, clean checkout gives
* `v0.1.0`; an untagged one falls back to the short sha on its own (`--always`); a dirty tree
* appends `-dirty`. `apps/stage/README.md` names the one thing this depends on that Vite cannot
* enforce — the build has to run from the tagged checkout for the string to be right.
*
* The dev server never shells out for it: HMR does not represent a build, so `npm run dev` always
* reads `dev` rather than a sha that would go stale on the next commit without a restart.
*/
function stageVersion(command: 'build' | 'serve'): string {
if (command === 'serve') return 'dev';
try {
return execFileSync('git', ['describe', '--tags', '--always', '--dirty'], { cwd: repoRoot })
.toString()
.trim();
} catch {
return 'dev';
}
}
/** Serve `data/fafb-v783` at `/data/fafb-v783` in dev, and copy it into `dist/` on build. */
function datasetArtifacts(): Plugin {
return {
name: 'flystage-dataset-artifacts',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
const url = req.url ?? '';
if (!url.startsWith(`${datasetRoute}/`)) return next();
const relative = normalize(decodeURIComponent(url.slice(datasetRoute.length + 1)).split('?')[0] ?? '');
if (!relative || relative.startsWith('..')) {
res.statusCode = 403;
res.end('forbidden');
return;
}
const file = join(datasetDir, relative);
try {
const info = await stat(file);
if (!info.isFile()) throw new Error('not a file');
res.setHeader('content-type', file.endsWith('.json') ? 'application/json' : 'application/octet-stream');
res.setHeader('content-length', String(info.size));
res.setHeader('cache-control', 'no-cache');
createReadStream(file).pipe(res);
} catch {
res.statusCode = 404;
res.end('not found');
}
});
},
async closeBundle() {
// Build-time copy. `dist/data/fafb-v783` mirrors the dev route exactly.
await cp(datasetDir, resolve(here, 'dist/data/fafb-v783'), { recursive: true });
},
};
}
export default defineConfig(({ command }) => ({
plugins: [react(), tailwindcss(), datasetArtifacts()],
define: {
__STAGE_VERSION__: JSON.stringify(stageVersion(command)),
},
resolve: {
alias: { '@': resolve(here, 'src') },
},
server: {
port: 5273,
strictPort: true,
// The dataset middleware reads from the repo root, which is outside the app root.
fs: { allow: [repoRoot] },
},
preview: {
port: 4300,
strictPort: true,
},
worker: {
format: 'es',
},
build: {
target: 'es2023',
// One long-lived page in one pinned Chromium: inlining nothing keeps the waterfall honest,
// and a broadcast page has no cold-start budget worth optimising.
assetsInlineLimit: 0,
sourcemap: true,
},
}));