serveSite
uses node:path
Answer a GET from a static site's directory: / and directories by their index.html, /a by a.html (before a directory a), and a path with
no file by the nearest 404.html above it (a static site's own, under its path, renders the app's other paths).
Each file is sent at most once to a browser that keeps it: its ETag is its content (a browser asks again, and is
told it hasn't changed), and a file named by its content (style.<digest>.css, the start of its SHA-256 in hex)
is kept for good. Text is compressed (brotli or gzip, as the browser accepts). A directory asked without its slash
goes to it with one, so its page's relative links are its own. With moved (a path of the site its pages moved
under), a path with no file that the site has under moved, or that proxied says an app beside it serves there,
goes there (a 301), so links from before the move work.
import { SITE_TYPES, siteEncoded } from "#codebase/tcmts/cli/sync"; import { acceptedEncodings } from "#codebase/tcmts/cli/web"; import { createHash } from "node:crypto"; import fs from "node:fs"; import type http from "node:http"; import path_1 from "node:path"; export function serveSite( root: string, req: http.IncomingMessage, res: http.ServerResponse, moved?: string, proxied?: (path: string) => boolean, ) { const base = path_1.resolve(root); let url: URL; let pathname: string; try { url = new URL(req.url ?? "/", "http://site"); pathname = decodeURIComponent(url.pathname); } catch { res.writeHead(400).end(); return; } // the file a path is (whether it's there or not), or undefined outside the site const fileAt = (p: string): string | undefined => { const f = path_1.resolve(base, `.${p}`); if (f !== base && !f.startsWith(base + path_1.sep)) return undefined; // (`/apps` is apps.html, before a directory of that name: a static site's page beside what it holds) if (fs.existsSync(f) && !fs.statSync(f).isDirectory()) return f; if (f !== base && fs.existsSync(`${f}.html`)) return `${f}.html`; return fs.existsSync(f) ? path_1.join(f, "index.html") : f; }; const redirect = (location: string) => { res.writeHead(301, { location, "cache-control": "no-cache" }).end(); }; const file = fileAt(pathname); // (nothing outside it) if (file === undefined) { res.writeHead(404).end(); return; } // (a directory's page, asked without its slash) if ( !pathname.endsWith("/") && file === path_1.join(path_1.resolve(base, `.${pathname}`), "index.html") && fs.existsSync(file) ) return redirect(`${url.pathname}/${url.search}`); if (!fs.existsSync(file)) { if (moved && pathname !== moved && !pathname.startsWith(`${moved}/`)) { const there = fileAt(`${moved}${pathname}`); if ((there && fs.existsSync(there)) || proxied?.(`${moved}${pathname}`)) return redirect(`${moved}${url.pathname}${url.search}`); } // the nearest 404.html: an app's own, in its directory (a static build's renders any of its paths), or the site's let dir = path_1.dirname(file); while ( dir !== base && dir.startsWith(base + path_1.sep) && !fs.existsSync(path_1.join(dir, "404.html")) ) dir = path_1.dirname(dir); const missing = path_1.join(dir.startsWith(base) ? dir : base, "404.html"); res.writeHead(404, { "content-type": SITE_TYPES[".html"]! }); res.end(fs.existsSync(missing) ? fs.readFileSync(missing) : "not found"); return; } const data = fs.readFileSync(file); const digest = createHash("sha256").update(data).digest("hex"); const named = /\.([0-9a-f]{12,64})\.[^./]+$/.exec(path_1.basename(file))?.[1]; const headers: Record<string, string> = { "content-type": SITE_TYPES[path_1.extname(file)] ?? "application/octet-stream", etag: `"${digest}"`, "cache-control": named && digest.startsWith(named) ? "public, max-age=31536000, immutable" : "no-cache", vary: "Accept-Encoding", }; if ( String(req.headers["if-none-match"] ?? "") .split(",") .some((t) => t.trim().replace(/^W\//, "") === `"${digest}"`) ) { res.writeHead(304, headers).end(); return; } const accepts = acceptedEncodings(req); const encoding = (["br", "gzip"] as const).find((e) => accepts.has(e)); const packed = encoding && siteEncoded(path_1.extname(file), data, digest, encoding); if (packed) headers["content-encoding"] = encoding; const body = packed || data; headers["content-length"] = String(body.length); res.writeHead(200, headers); res.end(req.method === "HEAD" ? undefined : body); }