import { existsSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { gzipSync } from "node:zlib"; import type { ServerResponse, IncomingMessage } from "node:http"; import { isPathInsideRoot } from "../utils/paths.ts"; const MIME: Record = { ".html": "text/html; charset=utf-8", ".js": "application/javascript; charset=utf-8", ".css": "text/css; charset=utf-8", ".png": "image/png", ".svg": "image/svg+xml", ".woff2": "font/woff2", ".woff": "font/woff", ".ico": "image/x-icon", ".webmanifest": "application/manifest+json", ".json": "application/json", }; const GZIPABLE = new Set([".html", ".js", ".css", ".json", ".webmanifest"]); // Vite build outputs hashed filenames like index-DeT2iZAf.js const HASHED_FILE_RE = /-[A-Za-z0-9_-]{6,}\.\w+$/; function getCacheControl(file: string): string | undefined { if (file === "/index.html") { return "no-cache"; } if (HASHED_FILE_RE.test(file) || file.endsWith(".woff2") || file.endsWith(".woff")) { return "public, max-age=31536000, immutable"; } return undefined; } export function serveStatic( publicDir: string, urlPath: string, req: IncomingMessage, res: ServerResponse, spaFallback = false, ): void { const file = urlPath === "/" ? "/index.html" : urlPath; const full = join(publicDir, file); const resolvedPublic = resolve(publicDir); const resolvedFull = resolve(full); if (!isPathInsideRoot(resolvedPublic, resolvedFull) && resolvedFull !== resolvedPublic) { res.writeHead(403); res.end("Forbidden"); return; } if (!existsSync(full)) { if (spaFallback && !urlPath.includes(".")) { serveStatic(publicDir, "/index.html", req, res, false); return; } res.writeHead(404); res.end("Not found"); return; } const ext = file.match(/\.\w+$/)?.[0] || ".html"; const mimeType = MIME[ext] || "application/octet-stream"; const content = readFileSync(full); const headers: Record = { "Content-Type": mimeType, }; const cacheControl = getCacheControl(file); if (cacheControl) { headers["Cache-Control"] = cacheControl; } const acceptEncoding = req.headers["accept-encoding"] || ""; const shouldGzip = GZIPABLE.has(ext) && acceptEncoding.includes("gzip"); if (shouldGzip) { const compressed = gzipSync(content); headers["Content-Encoding"] = "gzip"; res.writeHead(200, headers); res.end(compressed); return; } res.writeHead(200, headers); res.end(content); }