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

@@ -346,6 +346,9 @@ docker compose -f compose.yml up --build -d # base config only, no source moun
_(Production compose grows to include the Ory services and Postgres — planned.)_
The server drains in-flight requests on `SIGTERM`/`SIGINT` rather than cutting them
mid-response, so container restarts are clean.
## Layout
```
@@ -365,6 +368,8 @@ html-css-foundation/ Raw HTML/CSS design reference — the source for the
building-block partials; not served.
```
Comments and docs cite roadmap phases as `§N` — the sections in `todo.md`.
## Extending the core
- **New page in a plugin:** add a route + handler to the plugin manifest and a

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)));
}

View File

@@ -17,7 +17,7 @@ everything via Docker.
- [x] Request context type threaded to handlers: `{ req, res, url, params, query, user|null, roles }`. → `src/context.ts` (`RequestContext` + `buildContext`); `roles` mirror `user.roles`, the §2 router/§4 JWT middleware supply `params`/`user`.
- [x] Error templates: add 403 + 500 (404 exists). → `views/403.ejs` + `views/500.ejs`; 500 wired into `app.ts` error handler (HTML, plain-text fallback).
- [x] Config/env loader: Ory endpoints, cookie/CSRF secret, JWKS location, ports. → `src/config.ts` (`loadConfig`); validated at boot, dev defaults for clean-clone, prod requires real secrets; wired into `server.ts`.
- [ ] Run the architecture _and_ the stability reviewer agents on the _whole_ project, not just the latest changes, and address their issues.
- [x] Run the architecture _and_ the stability reviewer agents on the _whole_ project, not just the latest changes, and address their issues. → Both: no bugs/security issues. Addressed: wired `buildContext` into `app.ts`; graceful SIGTERM/SIGINT shutdown; EJS template caching in prod. Deferred `core/`/`shell/` split (premature for an 8-file scaffold; revisit at §2/§4).
- [ ] Go over all comments in the code and the README and try to make it shorter and more information dense. Remove not strictly needed stuff.
- [ ] Go over all tests and combine/unify ones that cover the same stuff or are very related and could be combined in a good way. Remove tests that aren't helping, we only want tests that are actually helpful to us.