76 lines
2.4 KiB
JavaScript
76 lines
2.4 KiB
JavaScript
// 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.');
|
|
});
|