118 lines
4.5 KiB
TypeScript
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,
|
|
},
|
|
}));
|