// Zero-dependency static file server for the pinball game. // Usage: `npm start` (or `PORT=8080 npm start`), then open the printed URL. import http from 'node:http'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; const PORT = Number(process.env.PORT) || 3000; const HOST = process.env.HOST || '127.0.0.1'; const PUBLIC_DIR = fileURLToPath(new URL('./public', import.meta.url)); // Module scripts must be served with a JavaScript MIME type or the browser refuses to run them. const MIME_TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.png': 'image/png', '.ico': 'image/x-icon', '.json': 'application/json; charset=utf-8', }; function sendText(res, status, message) { res.writeHead(status, { 'Content-Type': 'text/plain; charset=utf-8' }); res.end(message); } const server = http.createServer(async (req, res) => { if (req.method !== 'GET' && req.method !== 'HEAD') { res.setHeader('Allow', 'GET, HEAD'); sendText(res, 405, 'Method not allowed'); return; } let pathname; try { pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname); } catch { sendText(res, 400, 'Bad request'); return; } if (pathname.endsWith('/')) pathname += 'index.html'; // Resolve inside PUBLIC_DIR and refuse anything that escapes it. const filePath = path.join(PUBLIC_DIR, pathname); if (!filePath.startsWith(PUBLIC_DIR + path.sep)) { sendText(res, 403, 'Forbidden'); return; } try { const body = await readFile(filePath); const type = MIME_TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream'; res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-cache' }); res.end(req.method === 'HEAD' ? undefined : body); } catch (err) { if (err.code === 'ENOENT' || err.code === 'EISDIR') sendText(res, 404, 'Not found'); else sendText(res, 500, 'Server error'); } }); server.on('error', (err) => { if (err.code === 'EADDRINUSE') { console.error(`Port ${PORT} is already in use. Try: PORT=${PORT + 1} npm start`); } else { console.error(err); } process.exit(1); }); server.listen(PORT, HOST, () => { console.log(`Pinball is running at http://localhost:${PORT}`); console.log('Press Ctrl+C to stop.'); });