orbit/dev/server.mjs

77 lines
2.7 KiB
JavaScript

/**
* dev/server.mjs — Orbit's dev static server.
*
* node dev/server.mjs [port] (default 8080)
*
* Why not `python3 -m http.server`? That server sends `Last-Modified`
* with no `Cache-Control`, so browsers apply HEURISTIC caching to the
* ES modules. During iterative development the browser then keeps
* running an OLD js/*.js module graph (stale GameScene.js, stale
* BuildWindow.js, …) long after the files on disk changed — while
* re-fetched JSON data is fresh. New data + old JS is exactly how you
* get "the build console behaves like an older version" no matter how
* often you clear the JSON cache or hard-reset the game.
*
* This server sends `Cache-Control: no-store` on every response, so a
* normal reload always runs what's on disk. Zero dependencies (node).
*/
import http from 'node:http';
import { promises as fs } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const PORT = Number(process.argv[2] ?? 8080);
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.mjs': 'text/javascript; charset=utf-8',
'.json': 'application/json; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.otf': 'font/otf',
'.ttf': 'font/ttf',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.md': 'text/markdown; charset=utf-8',
'.txt': 'text/plain; charset=utf-8',
};
const server = http.createServer(async (req, res) => {
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
let p = decodeURIComponent(url.pathname);
if (p.endsWith('/')) p += 'index.html';
const file = path.normalize(path.join(ROOT, p));
if (!file.startsWith(ROOT + path.sep) && file !== ROOT) {
res.writeHead(403).end('forbidden');
return;
}
try {
const data = await fs.readFile(file);
res.writeHead(200, {
'Content-Type': MIME[path.extname(file).toLowerCase()] ?? 'application/octet-stream',
'Content-Length': data.length,
// The whole point: browsers must never cache these.
'Cache-Control': 'no-store, no-cache, must-revalidate',
Pragma: 'no-cache',
Expires: '0',
});
res.end(data);
} catch {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'no-store' });
res.end(`404 — not found: ${url.pathname}\n(serving ${ROOT})`);
}
});
server.listen(PORT, '127.0.0.1', () => {
console.log(`orbit dev server → http://127.0.0.1:${PORT} (no-store caching; root ${ROOT})`);
});