Address architecture + stability review (todo §0): wire buildContext, graceful shutdown, prod template caching
This commit is contained in:
@@ -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);
|
||||
|
||||
11
src/app.ts
11
src/app.ts
@@ -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");
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user