From 24edeb9d52ef625e87bd09946f180462ce57903a Mon Sep 17 00:00:00 2001 From: Quim T <26749475+espetro@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:46:28 +0000 Subject: [PATCH] docs: annotate handlers with (c: CtxCore), the scriptc-compilable form scriptc refuses unannotated handler params ('any') and the Ctx

generic interface (SC2009); CtxCore is the runtime class Handler is typed on. Fixes the @chttp/auth README quickstart (cutils#29) and the same handler idiom across the docs site and package READMEs. --- README.md | 22 +++++++++++----------- docs/deploy.md | 4 ++-- docs/html.md | 3 ++- docs/index.md | 4 ++-- docs/middleware.md | 26 +++++++++++++------------- docs/multipart.md | 4 ++-- docs/quickstart.md | 12 ++++++------ docs/routing.md | 6 ++++-- docs/static.md | 4 ++-- docs/streaming.md | 6 +++--- docs/unplugin.md | 6 +++--- packages/auth/README.md | 5 +++-- packages/router/README.md | 12 ++++++------ packages/stream/README.md | 4 ++-- 14 files changed, 61 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 302bbc7..4c940d3 100644 --- a/README.md +++ b/README.md @@ -22,14 +22,14 @@ npm i @chttp/router Import: ```ts -import { createRouter } from "@chttp/router"; +import { CtxCore, createRouter } from "@chttp/router"; const app = createRouter(); -app.get("/", (c) => c.text("hi")); -app.get("/user/:id", (c) => c.json({ id: c.req.param("id") })); -app.post("/echo", (c) => c.text(c.req.body())); -app.notFound((c) => c.status(404).text("not found")); +app.get("/", (c: CtxCore) => c.text("hi")); +app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") })); +app.post("/echo", (c: CtxCore) => c.text(c.req.body())); +app.notFound((c: CtxCore) => c.status(404).text("not found")); app.serve({ port: 3000 }); ``` @@ -39,8 +39,8 @@ app.serve({ port: 3000 }); Static segments, `:param`, and a tail `*` wildcard: ```ts -app.get("/user/:id", (c) => c.req.param("id")); // 'id' is literal-typed -app.get("/wild/*", (c) => c.req.param("*")); +app.get("/user/:id", (c: CtxCore) => c.req.param("id")); // 'id' is literal-typed +app.get("/wild/*", (c: CtxCore) => c.req.param("*")); app.all("/any-method", handler); app.on("m-search", "/custom", handler); ``` @@ -88,13 +88,13 @@ c.redirect("/elsewhere", 302); registered after it (snapshot semantics): ```ts -app.use((c, next) => { +app.use((c: CtxCore, next) => { c.header("x-start", Date.now().toString()); next(); }); // path-scoped: `*` matches every path, `/api/*` is a prefix -app.use("/api/*", async (c, next) => { +app.use("/api/*", async (c: CtxCore, next) => { await auth(c.req); await next(); }); @@ -116,7 +116,7 @@ at 100% static). ```ts const app = createRouter(); -app.get("/user/:id", (c) => c.json({ id: c.req.param("id") })); +app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") })); // Cloudflare Workers / Deno Deploy / Bun / Service Worker export default { async fetch(req, env, ctx) { return app.fetch(req, env, ctx); } }; @@ -201,7 +201,7 @@ same band as [valibot](https://github.com/open-circle/valibot). import * as v from "@chttp/val"; const Order = v.strictObject({ id: v.pipe(v.string(), v.uuid()) }); -app.post("/order", (c) => { +app.post("/order", (c: CtxCore) => { const r = v.safeParse(Order, JSON.parse(c.req.body())); return r.success ? c.json({ ok: true }) diff --git a/docs/deploy.md b/docs/deploy.md index 7f1aa54..5bfdfee 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -47,10 +47,10 @@ Use this when you need npm packages outside the scriptc subset. See [Run targets The canonical entry is `app.fetch(req: Request) → Promise`. The same `createRouter()` symbol works on Cloudflare Workers, Deno Deploy, Bun, Vercel Edge, Netlify Edge, Supabase, and as a Service Worker. `app.handler()` is a thin Node adapter around `fetch()` for `node:http` users. ```ts -import { createRouter } from "@chttp/router"; +import { CtxCore, createRouter } from "@chttp/router"; const app = createRouter(); -app.get("/user/:id", (c) => c.json({ id: c.req.param("id") })); +app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") })); // Cloudflare Workers / Deno Deploy / Bun / Vercel Edge / Service Worker export default { diff --git a/docs/html.md b/docs/html.md index 586663a..f0d4de6 100644 --- a/docs/html.md +++ b/docs/html.md @@ -16,13 +16,14 @@ | `isVoidElement`, `isValidTag`, `isValidAttrName` | `(s: string) => boolean` | Exposed helpers | ```ts +import { CtxCore } from "@chttp/router"; import { BARE, escapeHtml, h, html } from "@chttp/html"; function Todo(props: { text: string; done: boolean }): string { return h("li", ["class", props.done ? "done" : "open"], [escapeHtml(props.text)]); } -app.get("/todos", (c) => +app.get("/todos", (c: CtxCore) => c.html( h("ul", [], [Todo({ text: "ship", done: false })]) + h("input", ["disabled", BARE, "value", "x"], []), diff --git a/docs/index.md b/docs/index.md index e1b9aa4..cf7eea3 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,10 +3,10 @@ chttp is a minimal async-first HTTP router for `node:http`, authored inside the [scriptc](https://github.com/vercel-labs/scriptc) compilable subset so the same source also compiles to a single native ELF. ```ts -import { createRouter } from "@chttp/router"; +import { CtxCore, createRouter } from "@chttp/router"; const app = createRouter(); -app.get("/user/:id", async (c) => c.json(await fetchUser(c.req.param("id")))); +app.get("/user/:id", async (c: CtxCore) => c.json(await fetchUser(c.req.param("id")))); app.serve({ port: 3000 }); ``` diff --git a/docs/middleware.md b/docs/middleware.md index 63264c1..18bb14a 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -3,7 +3,7 @@ Middleware is `(c, next) => void | Promise` — sync or async. `next()` returns the tail's result, so `await next()` in an async middleware waits for the rest of the chain to settle. ```ts -app.use((c, next) => { +app.use((c: CtxCore, next) => { c.header("x-start", Date.now().toString()); next(); }); @@ -26,16 +26,16 @@ Every snippet below is written in the scriptc subset (indexed `for` loops, `| nu Two pieces, because of how dispatch orders things: an `OPTIONS` request to a path that only has `GET`/`POST` registered fails with 405 *before any middleware runs*, so a middleware alone cannot answer preflight. The working shape is a `use()` middleware for the response headers plus one explicit `app.options()` route per path (or `app.all()` with an in-handler method check) that answers the preflight with 204. ```ts -import type { Ctx } from "@chttp/router"; +import type { CtxCore } from "@chttp/router"; // 1. Response headers on every actual request (apply to routes after this use()). -app.use((c: Ctx, next: () => unknown): unknown => { +app.use((c: CtxCore, next: () => unknown): unknown => { c.header("access-control-allow-origin", "*"); return next(); }); // 2. Explicit preflight answer: 204, no body. -function preflight(c: Ctx): void { +function preflight(c: CtxCore): void { c.status(204) .header("access-control-allow-origin", "*") .header("access-control-allow-methods", "GET,POST,PUT,DELETE,OPTIONS") @@ -48,11 +48,11 @@ function preflight(c: Ctx): void { c.sent = true; // tells dispatch this middleware answered } -app.options("/users", (c: Ctx) => { +app.options("/users", (c: CtxCore) => { preflight(c); }); -app.get("/users", (c: Ctx) => c.json({ users: 1 })); -app.post("/users", (c: Ctx) => c.json({ ok: true }), { body: true }); +app.get("/users", (c: CtxCore) => c.json({ users: 1 })); +app.post("/users", (c: CtxCore) => c.json({ ok: true }), { body: true }); ``` With credentials instead of `*`: echo the request's `origin` header and add `access-control-allow-credentials: true`. For many paths, a shared `app.all("/*", ...)` fallback also answers preflight (a splat route matches paths with no other registration), but paths that have any method registered need their own `app.options()` entry or the 405 wins. @@ -71,7 +71,7 @@ function fnv1a(s: string): string { return h.toString(16); } -app.get("/users", (c: Ctx) => { +app.get("/users", (c: CtxCore) => { const body: string = JSON.stringify({ users: [1, 2] }); const etag: string = '"' + fnv1a(body) + '"'; const inm: string | null = c.req.header("if-none-match"); @@ -101,7 +101,7 @@ The comparison is exact equality against a single etag. Browsers may send a comm Async middleware: `await next()` waits until the chain settles, so `Date.now()` after it includes the handler's async work. Duration goes to stdout, not a response header, because the response may already be flushed (the flush rule above). ```ts -app.use(async (c: Ctx, next: () => unknown): Promise => { +app.use(async (c: CtxCore, next: () => unknown): Promise => { const t0: number = Date.now(); await next(); console.log(c.req.method + " " + c.req.path + " " + String(Date.now() - t0) + "ms"); @@ -115,7 +115,7 @@ Register it before the routes you want logged; routes registered earlier carry n Sync middleware that sets headers before `next()`; they land on every response from routes registered after it. ```ts -app.use((c: Ctx, next: () => unknown): unknown => { +app.use((c: CtxCore, next: () => unknown): unknown => { c.header("x-content-type-options", "nosniff"); c.header("x-frame-options", "DENY"); c.header("referrer-policy", "no-referrer"); @@ -141,7 +141,7 @@ const WINDOW_MS: number = 60000; // window length const buckets: { [k: string]: Bucket } = {}; -function rateLimit(c: Ctx, next: () => unknown): unknown { +function rateLimit(c: CtxCore, next: () => unknown): unknown { const key: string = c.req.header("x-api-key") === null ? "anon" : String(c.req.header("x-api-key")); const now: number = Date.now(); const cur: Bucket | null = buckets[key] === undefined ? null : buckets[key]; @@ -177,13 +177,13 @@ A true `server-timing: app;dur=N` header needs the duration *before* the body is ```ts // start stamp: header lands (set before the handler can flush) -app.use((c: Ctx, next: () => unknown): unknown => { +app.use((c: CtxCore, next: () => unknown): unknown => { c.header("x-request-start", String(Date.now())); return next(); }); // duration: log it, the response is already flushed by the time you know it -app.use(async (c: Ctx, next: () => unknown): Promise => { +app.use(async (c: CtxCore, next: () => unknown): Promise => { const t0: number = Date.now(); await next(); console.log(c.req.path + " " + String(Date.now() - t0) + "ms"); diff --git a/docs/multipart.md b/docs/multipart.md index c5c8b68..801fa52 100644 --- a/docs/multipart.md +++ b/docs/multipart.md @@ -3,13 +3,13 @@ `multipart/form-data` uploads are opt-in per route — the same seam as `{ body: true }`. Declare `multipart` in the route options and dispatch parses the stream before the handler chain runs, so `c.req` stays synchronous. ```ts -import { createRouter, diskStorage, memoryStorage } from "@chttp/router"; +import { CtxCore, createRouter, diskStorage, memoryStorage } from "@chttp/router"; const app = createRouter(); app.post( "/api/entries/:entryId/media", - (c) => { + (c: CtxCore) => { const file = c.req.file("file"); // UploadedFile | null const title = c.req.field("title"); // string | null db.run([file!.path, file!.mimeType, file!.size, title]); diff --git a/docs/quickstart.md b/docs/quickstart.md index cba2b86..d54aada 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -5,7 +5,7 @@ pnpm add @chttp/router ``` ```ts -import { createRouter } from "@chttp/router"; +import { CtxCore, createRouter } from "@chttp/router"; const app = createRouter(); @@ -16,18 +16,18 @@ function sleep(ms: number): Promise { } // Sync handler: returns immediately, no microtask cost. -app.get("/", (c) => c.text("hi")); -app.get("/user/:id", (c) => c.json({ id: c.req.param("id") })); +app.get("/", (c: CtxCore) => c.text("hi")); +app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") })); // Async handler: await anything, the router waits for it to settle. -app.get("/slow", async (c) => { +app.get("/slow", async (c: CtxCore) => { await sleep(100); c.text("done"); }); // Declare { body: true } or c.req.body() returns "". -app.post("/echo", (c) => c.text(c.req.body()), { body: true }); -app.notFound((c) => c.status(404).text("not found")); +app.post("/echo", (c: CtxCore) => c.text(c.req.body()), { body: true }); +app.notFound((c: CtxCore) => c.status(404).text("not found")); app.serve({ port: 3000 }); ``` diff --git a/docs/routing.md b/docs/routing.md index 2b1cbaf..213ab05 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -5,8 +5,10 @@ Routes are registered per method: `app.get | post | put | delete | patch | head Paths support static segments, `:param`, and a tail `*` wildcard. ```ts -app.get("/user/:id", (c) => c.req.param("id")); // literal-typed: 'id' -app.get("/wild/*", (c) => c.req.param("*")); +import { CtxCore } from "@chttp/router"; + +app.get("/user/:id", (c: CtxCore) => c.req.param("id")); // literal-typed: 'id' +app.get("/wild/*", (c: CtxCore) => c.req.param("*")); ``` ## Semantics diff --git a/docs/static.md b/docs/static.md index db87110..a6d9070 100644 --- a/docs/static.md +++ b/docs/static.md @@ -3,11 +3,11 @@ `@chttp/static` serves files and single page apps from a directory or from bytes baked into the binary. It depends on `@chttp/router` only and is authored in the scriptc subset, so it compiles into the same native ELF as the rest of your app. ```ts -import { createRouter } from "@chttp/router"; +import { CtxCore, createRouter } from "@chttp/router"; import { DiskAssets, serveStatic } from "@chttp/static"; const app = createRouter(); -app.get("/api/ping", (c) => c.json({ ok: true })); +app.get("/api/ping", (c: CtxCore) => c.json({ ok: true })); serveStatic(app, "/", new DiskAssets("./web/dist"), { spa: true }); app.serve({ port: 3000 }); ``` diff --git a/docs/streaming.md b/docs/streaming.md index b997527..fe3cebc 100644 --- a/docs/streaming.md +++ b/docs/streaming.md @@ -3,12 +3,12 @@ `@chttp/stream` adds Server-Sent Events and chunked text responses to `@chttp/router`. It is authored in the scriptc subset, so it compiles into the same native binary as your app. ```ts -import { createRouter } from "@chttp/router"; +import { CtxCore, createRouter } from "@chttp/router"; import { streamSSE } from "@chttp/stream"; const app = createRouter(); -app.get("/clock", (c) => +app.get("/clock", (c: CtxCore) => streamSSE(c, (s) => { const t = setInterval(() => s.send(new Date().toISOString(), "tick"), 1000); s.onAbort(() => clearInterval(t)); @@ -43,7 +43,7 @@ Multi-line data is split on LF, CRLF or CR into one `data:` line each, as the SS `streamText(c, fn)` is the same shape for `text/plain` chunked output. The stream has `write(s)`, `writeln(s)`, `close()` and `onAbort(fn)`. ```ts -app.get("/log", (c) => +app.get("/log", (c: CtxCore) => streamText(c, (s) => { s.writeln("starting"); s.writeln("done"); diff --git a/docs/unplugin.md b/docs/unplugin.md index c6d115b..946edf6 100644 --- a/docs/unplugin.md +++ b/docs/unplugin.md @@ -58,12 +58,12 @@ Mark a routes module: ```ts // src/routes.chttp.ts -import { createRouter } from '@chttp/router' +import { CtxCore, createRouter } from '@chttp/router' export const app = createRouter() -app.get('/user/:id', (c) => c.json({ id: c.req.param('id') })) -app.post('/echo', (c) => c.text(c.req.body()), { body: true }) +app.get('/user/:id', (c: CtxCore) => c.json({ id: c.req.param('id') })) +app.post('/echo', (c: CtxCore) => c.text(c.req.body()), { body: true }) ``` Wire the generated dispatch: diff --git a/packages/auth/README.md b/packages/auth/README.md index b7da27c..6633d91 100644 --- a/packages/auth/README.md +++ b/packages/auth/README.md @@ -7,6 +7,7 @@ Every function is string-in/string-out or boolean-out; none of them touches a re ## Quickstart ```ts +import { CtxCore } from "@chttp/router"; import { MemorySessionStore, buildSessionCookie, @@ -18,14 +19,14 @@ import { const store = new MemorySessionStore(); // dev/test only — see below -app.post("/login", async (c) => { +app.post("/login", async (c: CtxCore) => { const token = generateSessionToken(); await createSession(store, token, "user-1", { role: "admin" }); c.header("set-cookie", buildSessionCookie(token, { secure: false })); return c.status(201).text("ok"); }); -app.get("/me", async (c) => { +app.get("/me", async (c: CtxCore) => { const tok = readCookie(c.req.header("cookie"), "session"); const rec = tok === null ? null : await validateSessionToken(store, tok); if (rec === null) return c.status(401).text("unauthorized"); diff --git a/packages/router/README.md b/packages/router/README.md index c629f6b..4c41980 100644 --- a/packages/router/README.md +++ b/packages/router/README.md @@ -3,20 +3,20 @@ Minimal async-first HTTP router for `node:http`, zero dependencies, authored inside the [scriptc](https://github.com/vercel-labs/scriptc) compilable subset. The same source lowers to a native ELF directly, no plugin required. ```ts -import { createRouter } from "@chttp/router"; +import { CtxCore, createRouter } from "@chttp/router"; const app = createRouter(); -app.get("/", (c) => c.text("hi")); -app.get("/user/:id", (c) => c.json({ id: c.req.param("id") })); -app.post("/echo", (c) => c.text(c.req.body())); +app.get("/", (c: CtxCore) => c.text("hi")); +app.get("/user/:id", (c: CtxCore) => c.json({ id: c.req.param("id") })); +app.post("/echo", (c: CtxCore) => c.text(c.req.body())); -app.get("/slow", async (c) => { +app.get("/slow", async (c: CtxCore) => { const user = await fetchUser(c.req.param("id")); return c.json(user); }); -app.notFound((c) => c.status(404).text("not found")); +app.notFound((c: CtxCore) => c.status(404).text("not found")); app.serve({ port: 3000 }); ``` diff --git a/packages/stream/README.md b/packages/stream/README.md index 94b6991..9227bc9 100644 --- a/packages/stream/README.md +++ b/packages/stream/README.md @@ -3,12 +3,12 @@ Server-Sent Events and chunked text streaming for [@chttp/router](https://github.com/sigilco/chttp), authored inside the scriptc compilable subset so it compiles into the same native binary. ```ts -import { createRouter } from "@chttp/router"; +import { CtxCore, createRouter } from "@chttp/router"; import { streamSSE } from "@chttp/stream"; const app = createRouter(); -app.get("/clock", (c) => +app.get("/clock", (c: CtxCore) => streamSSE(c, (s) => { const t = setInterval(() => s.send(new Date().toISOString(), "tick"), 1000); s.onAbort(() => clearInterval(t));