1
0
Fork 0
tsgames/build/server.ts

111 lines
4.4 KiB
TypeScript

import Bun, { type ServerWebSocket } from 'bun';
import path from 'path';
import fs from 'fs';
import browser from 'browser-detect';
import { buildHTML, buildCSS } from './html';
import { isGame } from './isGame';
// ── CSS Hot Reload via WebSocket ────────────────────────────────────
const srcDir = path.resolve(import.meta.dir, '..', 'src');
interface ClientData {
game: string;
}
// Map of game name → set of WebSocket connections
const wsClients = new Map<string, Set<ServerWebSocket<ClientData>>>();
// Debounced watcher: on any source change, rebuild CSS only for affected games
const watcher = fs.watch(srcDir, { recursive: true });
let debounce: ReturnType<typeof setTimeout> | null = null;
watcher.on('change', async (_, filename) => {
if (!filename) return;
const filenameStr = filename.toString();
const rel = filenameStr.replace(/\\/g, '/');
const gameMatch = rel.match(/^games\/([^/]+)\//);
const changedGame = gameMatch ? gameMatch[1] : null;
if (debounce) clearTimeout(debounce);
debounce = setTimeout(async () => {
if (changedGame && !wsClients.has(changedGame)) return;
const affectedGames = changedGame
? [[changedGame, wsClients.get(changedGame)!] as const]
: [...wsClients.entries()]; // common/ → all games
for (const [game, clients] of affectedGames) {
const css = await buildCSS(game);
if (css === null) continue;
const message = JSON.stringify({ type: 'css', css });
for (const ws of clients) {
ws.send(message);
}
}
}, 50);
});
// ─────────────────────────────────────────────────────────────────────
Bun.serve<ClientData>({
websocket: {
open(ws) {
const game = ws.data.game;
if (!wsClients.has(game)) wsClients.set(game, new Set());
wsClients.get(game)!.add(ws);
console.log(`[css-hot] ${game} connected (${wsClients.get(game)!.size} clients)`);
},
close(ws) {
const game = ws.data.game;
const clients = wsClients.get(game);
if (clients) {
clients.delete(ws);
if (clients.size === 0) wsClients.delete(game);
}
console.log(`[css-hot] ${game} disconnected (${wsClients.get(game)?.size ?? 0} clients)`);
},
message() { },
},
async fetch(req, server) {
const url = new URL(req.url);
const pathname = path.basename(url.pathname);
const gameParam = url.searchParams.get('game');
const game = (gameParam && await isGame(gameParam)) ? gameParam : 'index';
switch (pathname) {
case 'css-ws': {
const upgraded = server.upgrade(req, {
data: { game } satisfies ClientData,
});
if (upgraded) return undefined;
return new Response('Upgrade failed', { status: 500 });
}
case '':
case '/':
case 'index.html':
try {
const detectedBrowser = browser(req.headers.get('user-agent') ?? '');
const html = await buildHTML(
game,
{
production: url.searchParams.get('production') === 'true',
mobile: detectedBrowser.mobile || url.searchParams.get('mobile') === 'true',
local: true,
}
);
if (html) {
return new Response(html, {
headers: {
'Content-Type': 'text/html;charset=utf-8'
}
});
}
} catch (e) {
console.error(e);
return new Response(`Error building HTML: ${e}`, { status: 500 });
}
return new Response(`Error building HTML`, { status: 500 });
default:
console.log(`[Dev Server] requested unknown pathname: ${pathname}`);
return new Response(null, { status: 404 });
}
}
});