71 lines
2.5 KiB
JavaScript
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})`);
|
|
});
|