@nifrajs/node 2.2.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @nifrajs/node
2
2
 
3
- Run a [nifra](../../README.md) app on Node's `http` server — nifra's ergonomics, deployed
3
+ Run a [nifra](../../README.md) app on Node's `http` server - nifra's ergonomics, deployed
4
4
  on Node instead of Bun.
5
5
 
6
6
  ```sh
@@ -19,7 +19,7 @@ const node = await serve(app, { port: 3000 })
19
19
  ```
20
20
 
21
21
  `serve(app, { port })` bridges Node's stream-based `(req, res)` to/from the Web
22
- `Request`/`Response` that `app.fetch` speaks — that's the only Bun-specific seam, since
22
+ `Request`/`Response` that `app.fetch` speaks - that's the only Bun-specific seam, since
23
23
  nifra's lifecycle is already Web-standard. It also gives you a graceful `stop()`, the
24
24
  Node equivalent of Bun's `app.listen()`.
25
25
 
@@ -45,7 +45,7 @@ await serve(app, {
45
45
  ## Graceful shutdown on signals
46
46
 
47
47
  Opt in to SIGTERM/SIGINT handling so a `docker stop` / Ctrl-C drains in-flight requests
48
- before exit (mirrors Bun's `listen({ gracefulSignals })` — off by default, since owning
48
+ before exit (mirrors Bun's `listen({ gracefulSignals })` - off by default, since owning
49
49
  process signals should be a deliberate choice):
50
50
 
51
51
  ```ts
@@ -55,7 +55,7 @@ await serve(app, { port: 3000, signals: true })
55
55
  ## What you get for free
56
56
 
57
57
  The request timeout (`server({ requestTimeoutMs })` → `503 request_timeout`) and body-size
58
- cap live **inside `app.fetch`**, not Bun's `listen()` — so they apply through this adapter
58
+ cap live **inside `app.fetch`**, not Bun's `listen()` - so they apply through this adapter
59
59
  with no extra wiring. Slow-client / slow-loris protection is Node's built-in
60
60
  `requestTimeout` (300s) and `headersTimeout` (60s) defaults.
61
61
 
@@ -63,7 +63,7 @@ Works with any `{ fetch(req): Promise<Response> }` handler, not just nifra. ESM-
63
63
 
64
64
  ## For AI agents
65
65
 
66
- Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
66
+ Start with [`LLM.md`](./LLM.md) - this package's contract card (the exports you call + its footguns),
67
67
  one cheap read instead of the whole corpus. For the wider framework: the repo's
68
68
  [`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
69
69
  [`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import { type IncomingMessage } from "node:http";
2
- /** The runtime platform a nifra app accepts as `fetch`'s 2nd arg — here, the observed socket peer. */
2
+ /** The runtime platform a nifra app accepts as `fetch`'s 2nd arg - here, the observed socket peer. */
3
3
  interface NodePlatform {
4
4
  readonly clientIp?: string;
5
5
  }
6
- /** Anything exposing a Web `fetch` handler — a nifra `app`, for instance. */
6
+ /** Anything exposing a Web `fetch` handler - a nifra `app`, for instance. */
7
7
  export interface FetchHandler {
8
8
  fetch(request: Request, platform?: NodePlatform): Response | Promise<Response>;
9
9
  /** Nifra apps also expose this WS-upgrade seam; present → this adapter serves `app.ws()` routes via
@@ -23,20 +23,20 @@ interface NifraWs {
23
23
  data: unknown;
24
24
  readonly raw: unknown;
25
25
  }
26
- /** Mirror of core's `TopicRegistry` surface — the app's pub/sub the adapter wires `ws.subscribe` to. */
26
+ /** Mirror of core's `TopicRegistry` surface - the app's pub/sub the adapter wires `ws.subscribe` to. */
27
27
  interface WsPubSub {
28
28
  subscribe(topic: string, ws: NifraWs): void;
29
29
  unsubscribe(topic: string, ws: NifraWs): void;
30
30
  unsubscribeAll(ws: NifraWs): void;
31
31
  }
32
- /** A nifra WS route's lifecycle (mirror of core's `WebSocketHandler` — the post-upgrade callbacks). */
32
+ /** A nifra WS route's lifecycle (mirror of core's `WebSocketHandler` - the post-upgrade callbacks). */
33
33
  interface NifraWsHandler {
34
34
  open?(ws: NifraWs): void | Promise<void>;
35
35
  message?(ws: NifraWs, data: NifraWsData): void | Promise<void>;
36
36
  close?(ws: NifraWs, code: number, reason: string): void | Promise<void>;
37
37
  error?(ws: NifraWs, error: unknown): void | Promise<void>;
38
38
  }
39
- /** Mirror of core's `WebSocketUpgradeOutcome` — what `resolveWebSocketUpgrade` returns. */
39
+ /** Mirror of core's `WebSocketUpgradeOutcome` - what `resolveWebSocketUpgrade` returns. */
40
40
  type WsUpgradeOutcome = {
41
41
  readonly kind: "pass";
42
42
  } | {
@@ -51,16 +51,16 @@ type WsUpgradeOutcome = {
51
51
  export type RequestProtocol = "http" | "https";
52
52
  export type RequestProtocolOption = RequestProtocol | ((request: IncomingMessage) => RequestProtocol);
53
53
  /**
54
- * Serve static files from a directory (e.g. the client build) under a URL prefix — so a self-hosted
54
+ * Serve static files from a directory (e.g. the client build) under a URL prefix - so a self-hosted
55
55
  * Node deploy doesn't need a CDN or a hand-rolled `/assets/*` handler. (On Cloudflare/Vercel the
56
56
  * platform serves assets; this is for `node server.js`.)
57
57
  */
58
58
  export interface ServeStaticOptions {
59
- /** Directory to read files from — an absolute path or a `file://` URL (`new URL("./assets/", import.meta.url)`). */
59
+ /** Directory to read files from - an absolute path or a `file://` URL (`new URL("./assets/", import.meta.url)`). */
60
60
  readonly dir: string | URL;
61
61
  /** URL prefix these files are served under. Default `"/assets"`. Use `"/"` to serve the whole dir. */
62
62
  readonly prefix?: string;
63
- /** Emit `cache-control: public, max-age=31536000, immutable` — correct for content-hashed files. Default `true`. */
63
+ /** Emit `cache-control: public, max-age=31536000, immutable` - correct for content-hashed files. Default `true`. */
64
64
  readonly immutable?: boolean;
65
65
  /** Extra headers merged onto every served file. */
66
66
  readonly headers?: Readonly<Record<string, string>>;
@@ -78,17 +78,17 @@ export interface ServeOptions {
78
78
  readonly protocol?: RequestProtocolOption;
79
79
  /**
80
80
  * Install SIGTERM/SIGINT handlers that call `stop()` for a graceful drain on
81
- * `docker stop` / Ctrl-C. Off by default — taking over process signals is opt-in,
81
+ * `docker stop` / Ctrl-C. Off by default - taking over process signals is opt-in,
82
82
  * mirroring nifra's Bun `listen({ gracefulSignals })`.
83
83
  *
84
84
  * The app-level request timeout (`server({ requestTimeoutMs })` → 503) and body cap
85
- * are *not* set here — they live inside `app.fetch`, so they already apply through
85
+ * are *not* set here - they live inside `app.fetch`, so they already apply through
86
86
  * this adapter. Slow-client protection is Node's built-in `requestTimeout` (300s) /
87
87
  * `headersTimeout` (60s) defaults.
88
88
  */
89
89
  readonly signals?: boolean;
90
90
  /**
91
- * Serve static files from disk for matching GET/HEAD requests *before* the app runs — non-matching
91
+ * Serve static files from disk for matching GET/HEAD requests *before* the app runs - non-matching
92
92
  * requests fall through to `app.fetch` with the node-direct fast path intact (no perf regression on
93
93
  * SSR/API routes). Replaces the hand-rolled `/assets/*` reader in self-hosted entries.
94
94
  */
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
14
14
  * const app = server().get("/", () => ({ ok: true }))
15
15
  * serve(app, { port: 3000 })
16
16
  *
17
- * nifra's lifecycle is `app.fetch(Request): Response | Promise<Response>` — pure Web Standards —
17
+ * nifra's lifecycle is `app.fetch(Request): Response | Promise<Response>` - pure Web Standards -
18
18
  * so this adapter just bridges Node's stream-based `(req, res)` to/from Web
19
19
  * `Request`/`Response`, plus a Bun-`listen()`-style graceful `stop()`.
20
20
  */
@@ -24,7 +24,7 @@ import { extname, resolve, sep } from "node:path";
24
24
  import { Readable } from "node:stream";
25
25
  import { fileURLToPath } from "node:url";
26
26
  /**
27
- * The `Content-Type` the host runtime's `Response.json` emits — Node's undici uses `application/json`,
27
+ * The `Content-Type` the host runtime's `Response.json` emits - Node's undici uses `application/json`,
28
28
  * Bun uses `application/json;charset=utf-8`. Probed once at module load (zero per-request cost) so the
29
29
  * fast path is byte-for-byte identical to the `Response`-building path on whatever runtime hosts us.
30
30
  */
@@ -140,7 +140,7 @@ function staticStateOf(options) {
140
140
  return { root, prefix, immutable: options.immutable !== false, headers: options.headers };
141
141
  }
142
142
  /**
143
- * Resolve a request URL to a file under the served root — **synchronously**, so non-matching requests
143
+ * Resolve a request URL to a file under the served root - **synchronously**, so non-matching requests
144
144
  * stay on the app's sync fast path. Returns `"pass"` (let the app handle it), a rejection `Response`
145
145
  * (malformed encoding / NUL / `..` traversal out of root), or a confined absolute file path to read.
146
146
  */
@@ -163,7 +163,7 @@ function staticMatch(state, rawUrl) {
163
163
  if (rel.includes("\0"))
164
164
  return { reject: new Response("Bad Request", { status: 400 }) };
165
165
  const file = resolve(state.root, rel);
166
- // Confine to the served directory — block `..` from escaping root.
166
+ // Confine to the served directory - block `..` from escaping root.
167
167
  if (file !== state.root && !file.startsWith(state.root + sep)) {
168
168
  return { reject: new Response("Forbidden", { status: 403 }) };
169
169
  }
@@ -211,7 +211,7 @@ async function readStatic(file, state, method) {
211
211
  }
212
212
  }
213
213
  /**
214
- * Serve a Web-`fetch` app on a Node `http` server. Resolves once bound — Node binds
214
+ * Serve a Web-`fetch` app on a Node `http` server. Resolves once bound - Node binds
215
215
  * the port asynchronously, so awaiting gives you the real port (matters for `port: 0`).
216
216
  */
217
217
  // The node-direct fast path renders plain data instead of building + draining a `Response`. Serving on
@@ -256,7 +256,7 @@ export function serve(app, options) {
256
256
  }
257
257
  });
258
258
  // WebSocket upgrades (a nifra app exposing the seam): handled on the http server's `upgrade` event via
259
- // the optional `ws` package — lazy-imported (and the server lazily built) on the FIRST real WS
259
+ // the optional `ws` package - lazy-imported (and the server lazily built) on the FIRST real WS
260
260
  // upgrade, so a non-WS Node app never loads `ws`.
261
261
  const resolveWs = app.resolveWebSocketUpgrade?.bind(app);
262
262
  if (resolveWs !== undefined) {
@@ -326,7 +326,7 @@ function handle(app, nodeReq, nodeRes, getProtocol, staticState) {
326
326
  }
327
327
  }
328
328
  // Fast path: a nifra app exposes `resolveNode`, which renders a plain-data result as primitives we
329
- // write straight to the socket — skipping the undici `Response` build + body drain (the bulk of the
329
+ // write straight to the socket - skipping the undici `Response` build + body drain (the bulk of the
330
330
  // Web-bridge cost on Node). A handler-returned `Response`/redirect, 404/405, error, timeout, or any
331
331
  // `onResponse` hook comes back as `{ kind: "response" }` and takes the same Web path as before, so
332
332
  // behavior is identical. A plain `{ fetch }` handler (no `resolveNode`) uses the Web path too.
@@ -380,13 +380,13 @@ function writeNodeOutcome(outcome, nodeRes) {
380
380
  }
381
381
  return writeNodeResponse(outcome.response, nodeRes);
382
382
  }
383
- /** A flat 500 with no leaked detail — the adapter's last-resort guard if a handler throws. */
383
+ /** A flat 500 with no leaked detail - the adapter's last-resort guard if a handler throws. */
384
384
  function writeInternalError(nodeRes) {
385
385
  nodeRes.writeHead(500, { "content-type": "application/json" });
386
386
  nodeRes.end(INTERNAL_ERROR_BODY);
387
387
  }
388
388
  /**
389
- * Serialize a node-direct JSON outcome straight to the socket — no undici `Response`, no stream drain.
389
+ * Serialize a node-direct JSON outcome straight to the socket - no undici `Response`, no stream drain.
390
390
  * Mirrors `Response.json(data, { status, headers })` byte-for-byte: user headers are lowercased to
391
391
  * match undici's `Headers` normalization, the JSON `Content-Type` matches the host runtime's, and each
392
392
  * queued cookie is emitted as its own `Set-Cookie` line (never comma-joined).
@@ -397,7 +397,7 @@ function writeJsonOutcome(outcome, nodeRes) {
397
397
  for (const [key, value] of Object.entries(outcome.headers))
398
398
  headers[key.toLowerCase()] = value;
399
399
  }
400
- // A `null` body is a 204/no-content render — `new Response(null)` carries no Content-Type, so we add
400
+ // A `null` body is a 204/no-content render - `new Response(null)` carries no Content-Type, so we add
401
401
  // none either; a non-null body is JSON, matching `Response.json`'s Content-Type.
402
402
  if (outcome.body !== null)
403
403
  headers["content-type"] = JSON_CONTENT_TYPE;
@@ -661,7 +661,7 @@ function writeNodeResponse(response, nodeRes) {
661
661
  response.headers.forEach((value, key) => {
662
662
  headers[key] = value;
663
663
  });
664
- // `Headers.forEach` comma-joins repeated `Set-Cookie` into one value — wrong, since a cookie's
664
+ // `Headers.forEach` comma-joins repeated `Set-Cookie` into one value - wrong, since a cookie's
665
665
  // `Expires` attribute itself contains a comma (so a client can't safely split it back). Emit one
666
666
  // header line per cookie via the un-joined `getSetCookie()` array (a response can set several:
667
667
  // e.g. a session cookie + a CSRF cookie).
@@ -724,7 +724,7 @@ function nodeResponseBody(response) {
724
724
  const body = response[NODE_RESPONSE_BODY];
725
725
  return typeof body === "string" || body instanceof Uint8Array ? body : undefined;
726
726
  }
727
- /** A non-literal specifier so TS treats `import(...)` as `any` — `ws` is an optional peer with no
727
+ /** A non-literal specifier so TS treats `import(...)` as `any` - `ws` is an optional peer with no
728
728
  * `@types/ws` dependency here (the surface is structurally typed via {@link WsServer}/{@link WsSocket}). */
729
729
  const WS_MODULE_SPECIFIER = "ws";
730
730
  /** Lazily build a noServer `ws` `WebSocketServer`, or `undefined` if `ws` isn't installed. */
@@ -735,7 +735,7 @@ async function loadWsServer() {
735
735
  return Ctor === undefined ? undefined : new Ctor({ noServer: true });
736
736
  }
737
737
  catch {
738
- return undefined; // `ws` not installed — caller responds 501
738
+ return undefined; // `ws` not installed - caller responds 501
739
739
  }
740
740
  }
741
741
  const WS_STATUS_TEXT = {
@@ -775,7 +775,7 @@ async function handleUpgrade(resolveWs, getProtocol, nodeReq, socket, head, getW
775
775
  wss.handleUpgrade(nodeReq, socket, head, (ws) => attachNodeWebSocket(ws, handler, data, pubsub));
776
776
  }
777
777
  /** Wire a `ws` socket (already open in `handleUpgrade`'s callback) to a nifra WS handler. The Node copy
778
- * of core's `attachWebSocket` — binary frames normalize to `Uint8Array`; a thrown/rejected callback
778
+ * of core's `attachWebSocket` - binary frames normalize to `Uint8Array`; a thrown/rejected callback
779
779
  * routes to `error()` and never crashes the connection. */
780
780
  function attachNodeWebSocket(ws, handler, data, pubsub) {
781
781
  const nifra = {
@@ -798,7 +798,7 @@ function attachNodeWebSocket(ws, handler, data, pubsub) {
798
798
  r.catch(() => { });
799
799
  }
800
800
  catch {
801
- /* the error handler itself failed — last resort, swallow */
801
+ /* the error handler itself failed - last resort, swallow */
802
802
  }
803
803
  };
804
804
  const safe = (call) => {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nifrajs/node",
3
- "version": "2.2.0",
4
- "description": "Run a nifra app on Node's http server — a Request<->stream adapter with a graceful stop().",
3
+ "version": "2.4.0",
4
+ "description": "Run a nifra app on Node's http server - a Request<->stream adapter with a graceful stop().",
5
5
  "keywords": [
6
6
  "bun",
7
7
  "typescript",
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * const app = server().get("/", () => ({ ok: true }))
7
7
  * serve(app, { port: 3000 })
8
8
  *
9
- * nifra's lifecycle is `app.fetch(Request): Response | Promise<Response>` — pure Web Standards —
9
+ * nifra's lifecycle is `app.fetch(Request): Response | Promise<Response>` - pure Web Standards -
10
10
  * so this adapter just bridges Node's stream-based `(req, res)` to/from Web
11
11
  * `Request`/`Response`, plus a Bun-`listen()`-style graceful `stop()`.
12
12
  */
@@ -21,12 +21,12 @@ import { extname, resolve, sep } from "node:path"
21
21
  import { type Duplex, Readable } from "node:stream"
22
22
  import { fileURLToPath } from "node:url"
23
23
 
24
- /** The runtime platform a nifra app accepts as `fetch`'s 2nd arg — here, the observed socket peer. */
24
+ /** The runtime platform a nifra app accepts as `fetch`'s 2nd arg - here, the observed socket peer. */
25
25
  interface NodePlatform {
26
26
  readonly clientIp?: string
27
27
  }
28
28
 
29
- /** Anything exposing a Web `fetch` handler — a nifra `app`, for instance. */
29
+ /** Anything exposing a Web `fetch` handler - a nifra `app`, for instance. */
30
30
  export interface FetchHandler {
31
31
  fetch(request: Request, platform?: NodePlatform): Response | Promise<Response>
32
32
  /** Nifra apps also expose this WS-upgrade seam; present → this adapter serves `app.ws()` routes via
@@ -36,7 +36,7 @@ export interface FetchHandler {
36
36
  }
37
37
 
38
38
  // --- WebSocket types: structurally mirrored from @nifrajs/core (this adapter has no @nifrajs/core
39
- // dependency — see NodeServeOutcome above). Kept in lockstep by the WS integration test. ---
39
+ // dependency - see NodeServeOutcome above). Kept in lockstep by the WS integration test. ---
40
40
 
41
41
  /** A received frame, normalized: text → `string`, binary → `Uint8Array`. */
42
42
  type NifraWsData = string | Uint8Array
@@ -52,14 +52,14 @@ interface NifraWs {
52
52
  readonly raw: unknown
53
53
  }
54
54
 
55
- /** Mirror of core's `TopicRegistry` surface — the app's pub/sub the adapter wires `ws.subscribe` to. */
55
+ /** Mirror of core's `TopicRegistry` surface - the app's pub/sub the adapter wires `ws.subscribe` to. */
56
56
  interface WsPubSub {
57
57
  subscribe(topic: string, ws: NifraWs): void
58
58
  unsubscribe(topic: string, ws: NifraWs): void
59
59
  unsubscribeAll(ws: NifraWs): void
60
60
  }
61
61
 
62
- /** A nifra WS route's lifecycle (mirror of core's `WebSocketHandler` — the post-upgrade callbacks). */
62
+ /** A nifra WS route's lifecycle (mirror of core's `WebSocketHandler` - the post-upgrade callbacks). */
63
63
  interface NifraWsHandler {
64
64
  open?(ws: NifraWs): void | Promise<void>
65
65
  message?(ws: NifraWs, data: NifraWsData): void | Promise<void>
@@ -67,7 +67,7 @@ interface NifraWsHandler {
67
67
  error?(ws: NifraWs, error: unknown): void | Promise<void>
68
68
  }
69
69
 
70
- /** Mirror of core's `WebSocketUpgradeOutcome` — what `resolveWebSocketUpgrade` returns. */
70
+ /** Mirror of core's `WebSocketUpgradeOutcome` - what `resolveWebSocketUpgrade` returns. */
71
71
  type WsUpgradeOutcome =
72
72
  | { readonly kind: "pass" }
73
73
  | { readonly kind: "reject"; readonly response: Response }
@@ -78,7 +78,7 @@ type WsUpgradeOutcome =
78
78
  readonly pubsub: WsPubSub
79
79
  }
80
80
 
81
- /** Structural view of the `ws` package's `WebSocket` (no `@types/ws` dependency — see `loadWsServer`). */
81
+ /** Structural view of the `ws` package's `WebSocket` (no `@types/ws` dependency - see `loadWsServer`). */
82
82
  interface WsSocket {
83
83
  send(data: string | ArrayBufferView | ArrayBuffer): void
84
84
  close(code?: number, reason?: string): void
@@ -105,7 +105,7 @@ export type RequestProtocolOption =
105
105
  type RequestProtocolResolver = (request: IncomingMessage) => RequestProtocol
106
106
 
107
107
  /**
108
- * The node-direct render returned by a nifra app's `resolveNode` — structurally mirrored here so this
108
+ * The node-direct render returned by a nifra app's `resolveNode` - structurally mirrored here so this
109
109
  * adapter stays decoupled from `@nifrajs/core` (it bridges *any* handler exposing this seam, and has no
110
110
  * runtime dependency on nifra). A `kind: "json"` outcome is a plain-data result we serialize straight to
111
111
  * the socket; a `kind: "body"` outcome is a marked buffered response body (notably @nifrajs/web's
@@ -153,7 +153,7 @@ interface NodeOutcomeRuntime {
153
153
  }
154
154
 
155
155
  /** A `FetchHandler` that *also* exposes the node-direct fast path (every nifra app does). May resolve
156
- * **synchronously** (a bare route + sync handler allocates no promise) — we `await` it regardless. */
156
+ * **synchronously** (a bare route + sync handler allocates no promise) - we `await` it regardless. */
157
157
  interface NodeFastHandler extends FetchHandler {
158
158
  resolveNode(
159
159
  request: Request,
@@ -167,7 +167,7 @@ interface NodeFastHandler extends FetchHandler {
167
167
  }
168
168
 
169
169
  /**
170
- * The `Content-Type` the host runtime's `Response.json` emits — Node's undici uses `application/json`,
170
+ * The `Content-Type` the host runtime's `Response.json` emits - Node's undici uses `application/json`,
171
171
  * Bun uses `application/json;charset=utf-8`. Probed once at module load (zero per-request cost) so the
172
172
  * fast path is byte-for-byte identical to the `Response`-building path on whatever runtime hosts us.
173
173
  */
@@ -280,16 +280,16 @@ function appendCookiesToNodeHeaders(
280
280
  }
281
281
 
282
282
  /**
283
- * Serve static files from a directory (e.g. the client build) under a URL prefix — so a self-hosted
283
+ * Serve static files from a directory (e.g. the client build) under a URL prefix - so a self-hosted
284
284
  * Node deploy doesn't need a CDN or a hand-rolled `/assets/*` handler. (On Cloudflare/Vercel the
285
285
  * platform serves assets; this is for `node server.js`.)
286
286
  */
287
287
  export interface ServeStaticOptions {
288
- /** Directory to read files from — an absolute path or a `file://` URL (`new URL("./assets/", import.meta.url)`). */
288
+ /** Directory to read files from - an absolute path or a `file://` URL (`new URL("./assets/", import.meta.url)`). */
289
289
  readonly dir: string | URL
290
290
  /** URL prefix these files are served under. Default `"/assets"`. Use `"/"` to serve the whole dir. */
291
291
  readonly prefix?: string
292
- /** Emit `cache-control: public, max-age=31536000, immutable` — correct for content-hashed files. Default `true`. */
292
+ /** Emit `cache-control: public, max-age=31536000, immutable` - correct for content-hashed files. Default `true`. */
293
293
  readonly immutable?: boolean
294
294
  /** Extra headers merged onto every served file. */
295
295
  readonly headers?: Readonly<Record<string, string>>
@@ -308,17 +308,17 @@ export interface ServeOptions {
308
308
  readonly protocol?: RequestProtocolOption
309
309
  /**
310
310
  * Install SIGTERM/SIGINT handlers that call `stop()` for a graceful drain on
311
- * `docker stop` / Ctrl-C. Off by default — taking over process signals is opt-in,
311
+ * `docker stop` / Ctrl-C. Off by default - taking over process signals is opt-in,
312
312
  * mirroring nifra's Bun `listen({ gracefulSignals })`.
313
313
  *
314
314
  * The app-level request timeout (`server({ requestTimeoutMs })` → 503) and body cap
315
- * are *not* set here — they live inside `app.fetch`, so they already apply through
315
+ * are *not* set here - they live inside `app.fetch`, so they already apply through
316
316
  * this adapter. Slow-client protection is Node's built-in `requestTimeout` (300s) /
317
317
  * `headersTimeout` (60s) defaults.
318
318
  */
319
319
  readonly signals?: boolean
320
320
  /**
321
- * Serve static files from disk for matching GET/HEAD requests *before* the app runs — non-matching
321
+ * Serve static files from disk for matching GET/HEAD requests *before* the app runs - non-matching
322
322
  * requests fall through to `app.fetch` with the node-direct fast path intact (no perf regression on
323
323
  * SSR/API routes). Replaces the hand-rolled `/assets/*` reader in self-hosted entries.
324
324
  */
@@ -377,7 +377,7 @@ function staticStateOf(options: ServeStaticOptions): StaticState {
377
377
  }
378
378
 
379
379
  /**
380
- * Resolve a request URL to a file under the served root — **synchronously**, so non-matching requests
380
+ * Resolve a request URL to a file under the served root - **synchronously**, so non-matching requests
381
381
  * stay on the app's sync fast path. Returns `"pass"` (let the app handle it), a rejection `Response`
382
382
  * (malformed encoding / NUL / `..` traversal out of root), or a confined absolute file path to read.
383
383
  */
@@ -400,7 +400,7 @@ function staticMatch(
400
400
  if (rel === "" || rel.endsWith("/")) return "pass" // a directory request → the app decides
401
401
  if (rel.includes("\0")) return { reject: new Response("Bad Request", { status: 400 }) }
402
402
  const file = resolve(state.root, rel)
403
- // Confine to the served directory — block `..` from escaping root.
403
+ // Confine to the served directory - block `..` from escaping root.
404
404
  if (file !== state.root && !file.startsWith(state.root + sep)) {
405
405
  return { reject: new Response("Forbidden", { status: 403 }) }
406
406
  }
@@ -453,7 +453,7 @@ async function readStatic(
453
453
  }
454
454
 
455
455
  /**
456
- * Serve a Web-`fetch` app on a Node `http` server. Resolves once bound — Node binds
456
+ * Serve a Web-`fetch` app on a Node `http` server. Resolves once bound - Node binds
457
457
  * the port asynchronously, so awaiting gives you the real port (matters for `port: 0`).
458
458
  */
459
459
  // The node-direct fast path renders plain data instead of building + draining a `Response`. Serving on
@@ -496,7 +496,7 @@ export function serve(app: FetchHandler, options: ServeOptions): Promise<NodeSer
496
496
  })
497
497
 
498
498
  // WebSocket upgrades (a nifra app exposing the seam): handled on the http server's `upgrade` event via
499
- // the optional `ws` package — lazy-imported (and the server lazily built) on the FIRST real WS
499
+ // the optional `ws` package - lazy-imported (and the server lazily built) on the FIRST real WS
500
500
  // upgrade, so a non-WS Node app never loads `ws`.
501
501
  const resolveWs = app.resolveWebSocketUpgrade?.bind(app)
502
502
  if (resolveWs !== undefined) {
@@ -580,7 +580,7 @@ function handle(
580
580
  }
581
581
 
582
582
  // Fast path: a nifra app exposes `resolveNode`, which renders a plain-data result as primitives we
583
- // write straight to the socket — skipping the undici `Response` build + body drain (the bulk of the
583
+ // write straight to the socket - skipping the undici `Response` build + body drain (the bulk of the
584
584
  // Web-bridge cost on Node). A handler-returned `Response`/redirect, 404/405, error, timeout, or any
585
585
  // `onResponse` hook comes back as `{ kind: "response" }` and takes the same Web path as before, so
586
586
  // behavior is identical. A plain `{ fetch }` handler (no `resolveNode`) uses the Web path too.
@@ -652,14 +652,14 @@ function writeNodeOutcome(
652
652
  return writeNodeResponse(outcome.response, nodeRes)
653
653
  }
654
654
 
655
- /** A flat 500 with no leaked detail — the adapter's last-resort guard if a handler throws. */
655
+ /** A flat 500 with no leaked detail - the adapter's last-resort guard if a handler throws. */
656
656
  function writeInternalError(nodeRes: ServerResponse): void {
657
657
  nodeRes.writeHead(500, { "content-type": "application/json" })
658
658
  nodeRes.end(INTERNAL_ERROR_BODY)
659
659
  }
660
660
 
661
661
  /**
662
- * Serialize a node-direct JSON outcome straight to the socket — no undici `Response`, no stream drain.
662
+ * Serialize a node-direct JSON outcome straight to the socket - no undici `Response`, no stream drain.
663
663
  * Mirrors `Response.json(data, { status, headers })` byte-for-byte: user headers are lowercased to
664
664
  * match undici's `Headers` normalization, the JSON `Content-Type` matches the host runtime's, and each
665
665
  * queued cookie is emitted as its own `Set-Cookie` line (never comma-joined).
@@ -672,7 +672,7 @@ function writeJsonOutcome(
672
672
  if (outcome.headers !== undefined) {
673
673
  for (const [key, value] of Object.entries(outcome.headers)) headers[key.toLowerCase()] = value
674
674
  }
675
- // A `null` body is a 204/no-content render — `new Response(null)` carries no Content-Type, so we add
675
+ // A `null` body is a 204/no-content render - `new Response(null)` carries no Content-Type, so we add
676
676
  // none either; a non-null body is JSON, matching `Response.json`'s Content-Type.
677
677
  if (outcome.body !== null) headers["content-type"] = JSON_CONTENT_TYPE
678
678
  if (outcome.cookies !== undefined && outcome.cookies.length > 0) {
@@ -970,7 +970,7 @@ function writeNodeResponse(response: Response, nodeRes: ServerResponse): void |
970
970
  response.headers.forEach((value, key) => {
971
971
  headers[key] = value
972
972
  })
973
- // `Headers.forEach` comma-joins repeated `Set-Cookie` into one value — wrong, since a cookie's
973
+ // `Headers.forEach` comma-joins repeated `Set-Cookie` into one value - wrong, since a cookie's
974
974
  // `Expires` attribute itself contains a comma (so a client can't safely split it back). Emit one
975
975
  // header line per cookie via the un-joined `getSetCookie()` array (a response can set several:
976
976
  // e.g. a session cookie + a CSRF cookie).
@@ -1031,7 +1031,7 @@ function nodeResponseBody(response: Response): string | Uint8Array | undefined {
1031
1031
 
1032
1032
  type WsServerCtor = new (options: { noServer: true }) => WsServer
1033
1033
 
1034
- /** A non-literal specifier so TS treats `import(...)` as `any` — `ws` is an optional peer with no
1034
+ /** A non-literal specifier so TS treats `import(...)` as `any` - `ws` is an optional peer with no
1035
1035
  * `@types/ws` dependency here (the surface is structurally typed via {@link WsServer}/{@link WsSocket}). */
1036
1036
  const WS_MODULE_SPECIFIER = "ws"
1037
1037
 
@@ -1045,7 +1045,7 @@ async function loadWsServer(): Promise<WsServer | undefined> {
1045
1045
  const Ctor = mod.WebSocketServer ?? mod.default?.WebSocketServer
1046
1046
  return Ctor === undefined ? undefined : new Ctor({ noServer: true })
1047
1047
  } catch {
1048
- return undefined // `ws` not installed — caller responds 501
1048
+ return undefined // `ws` not installed - caller responds 501
1049
1049
  }
1050
1050
  }
1051
1051
 
@@ -1094,7 +1094,7 @@ async function handleUpgrade(
1094
1094
  }
1095
1095
 
1096
1096
  /** Wire a `ws` socket (already open in `handleUpgrade`'s callback) to a nifra WS handler. The Node copy
1097
- * of core's `attachWebSocket` — binary frames normalize to `Uint8Array`; a thrown/rejected callback
1097
+ * of core's `attachWebSocket` - binary frames normalize to `Uint8Array`; a thrown/rejected callback
1098
1098
  * routes to `error()` and never crashes the connection. */
1099
1099
  function attachNodeWebSocket(
1100
1100
  ws: WsSocket,
@@ -1119,7 +1119,7 @@ function attachNodeWebSocket(
1119
1119
  const r = handler.error(nifra, error)
1120
1120
  if (r instanceof Promise) r.catch(() => {})
1121
1121
  } catch {
1122
- /* the error handler itself failed — last resort, swallow */
1122
+ /* the error handler itself failed - last resort, swallow */
1123
1123
  }
1124
1124
  }
1125
1125
  const safe = (call: () => void | Promise<void>): void => {