Address architecture + stability review (todo §0): wire buildContext, graceful shutdown, prod template caching

This commit is contained in:
2026-06-15 08:42:16 +02:00
parent 0bc7998cfe
commit 17f4411518
5 changed files with 37 additions and 4 deletions

View File

@@ -34,6 +34,22 @@ test("serves static CSS", async () => {
assert.match(res.headers.get("content-type") ?? "", /text\/css/);
});
// Production caches compiled templates; rendering must stay correct across repeated requests.
test("renders correctly with template caching enabled", async () => {
const app = createApp({ cache: true });
try {
await new Promise<void>((resolve) => app.listen(0, resolve));
const url = `http://localhost:${(app.address() as AddressInfo).port}/`;
for (let i = 0; i < 2; i++) {
const res = await fetch(url);
assert.equal(res.status, 200);
assert.match(await res.text(), /Plainpages/);
}
} finally {
app.close();
}
});
test("returns the 404 HTML page for unknown routes", async () => {
const res = await fetch(base + "/missing");
assert.equal(res.status, 404);

View File

@@ -2,21 +2,26 @@ import { createServer, type Server, type ServerResponse } from "node:http";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as ejs from "ejs";
import { buildContext } from "./context.ts";
import { serveStatic } from "./static.ts";
const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..");
export interface AppOptions {
// Cache compiled templates (compile once vs. re-read+recompile per request).
// Defaults to on in production, off in dev so source edits show up live.
cache?: boolean;
publicDir?: string;
viewsDir?: string;
}
export function createApp(options: AppOptions = {}): Server {
const cache = options.cache ?? process.env["NODE_ENV"] === "production";
const publicDir = options.publicDir ?? join(rootDir, "public");
const viewsDir = options.viewsDir ?? join(rootDir, "views");
const render = (view: string, data: Record<string, unknown>): Promise<string> =>
ejs.renderFile(join(viewsDir, `${view}.ejs`), data);
ejs.renderFile(join(viewsDir, `${view}.ejs`), data, { cache });
const sendHtml = (res: ServerResponse, status: number, html: string): void => {
res.writeHead(status, { "content-type": "text/html; charset=utf-8" });
@@ -30,7 +35,9 @@ export function createApp(options: AppOptions = {}): Server {
return;
}
const { pathname } = new URL(req.url ?? "/", "http://localhost");
// The single request shape handlers receive (§2/§4 router passes it on); routing
// reads its parsed URL instead of building a throwaway one.
const { pathname } = buildContext(req, res).url;
if (pathname.startsWith("/public/")) {
await serveStatic(publicDir, pathname.slice("/public/".length), res, req.method === "HEAD");

View File

@@ -3,6 +3,11 @@ import { loadConfig } from "./config.ts";
const { port } = loadConfig(); // validates the env (incl. prod secrets) — fails loud at boot
createApp().listen(port, () => {
const server = createApp().listen(port, () => {
console.log(`Listening on http://localhost:${port}`);
});
// Drain in-flight requests on container stop instead of cutting them mid-response.
for (const signal of ["SIGINT", "SIGTERM"] as const) {
process.on(signal, () => server.close(() => process.exit(0)));
}