Some checks failed
CI / build-check-test (push) Has been cancelled
Restructure local extensions into per-feature directories, split WebUI into backend modules with slash commands and systemd support, and track prompts/skills under .pi/agent for portable Gitea deployment. Co-authored-by: Cursor <cursoragent@cursor.com>
84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { gzipSync } from "node:zlib";
|
|
import type { ServerResponse, IncomingMessage } from "node:http";
|
|
|
|
const MIME: Record<string, string> = {
|
|
".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);
|
|
if (!full.startsWith(publicDir)) {
|
|
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<string, string> = {
|
|
"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);
|
|
}
|