Files
Auditor-Portal-Revamp/serve.cjs

71 lines
2.5 KiB
JavaScript

// Static server for the built SPA (replaces `pm2 serve dist 8443 --spa`).
// Exists for ONE reason pm2-serve can't do: correct cache headers.
// - index.html (and any SPA-fallback response): Cache-Control: no-store
// -> every page load fetches fresh HTML, so deploys are visible on a
// normal reload. This ends the recurring "deployed but users still see
// the old UI" stale-tab problem (bit us three times on 2026-08-28).
// - /assets/* (Vite content-hashed filenames): immutable, cached 1 year
// -> repeat visits stay fast; a new build = new hash = new URL.
// Zero dependencies; run under PM2 as "audit-portal-fe".
const http = require("http");
const fs = require("fs");
const path = require("path");
const PORT = 8443;
const ROOT = path.join(__dirname, "dist");
const MIME = {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".json": "application/json",
".map": "application/json",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
".ico": "image/x-icon",
".woff": "font/woff",
".woff2": "font/woff2",
".webmanifest": "application/manifest+json",
};
const sendFile = (res, filePath, cacheControl) => {
const ext = path.extname(filePath).toLowerCase();
res.writeHead(200, {
"Content-Type": MIME[ext] || "application/octet-stream",
"Cache-Control": cacheControl,
});
fs.createReadStream(filePath).pipe(res);
};
http
.createServer((req, res) => {
// Resolve inside ROOT only - refuse path traversal.
const urlPath = decodeURIComponent((req.url || "/").split("?")[0]);
const resolved = path.normalize(path.join(ROOT, urlPath));
if (!resolved.startsWith(ROOT)) {
res.writeHead(403).end();
return;
}
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
// Hashed build assets are immutable; everything else served as a real
// file (favicon, images) gets a short cache.
const immutable = urlPath.startsWith("/assets/");
const cache = resolved.endsWith(".html")
? "no-store"
: immutable
? "public, max-age=31536000, immutable"
: "public, max-age=300";
sendFile(res, resolved, cache);
return;
}
// SPA fallback: every unknown path is the app (React Router owns routing).
sendFile(res, path.join(ROOT, "index.html"), "no-store");
})
.listen(PORT, "0.0.0.0", () => {
console.log(`audit-portal-fe static server on :${PORT} (root: ${ROOT})`);
});