@nifrajs/cli 0.1.0-alpha.1 → 0.1.0-alpha.2
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 +8 -0
- package/dist/check.d.ts +82 -3
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +326 -11
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +11 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +178 -17
- package/dist/cli.js.map +1 -1
- package/dist/init-agents.d.ts +58 -0
- package/dist/init-agents.d.ts.map +1 -0
- package/dist/init-agents.js +126 -0
- package/dist/init-agents.js.map +1 -0
- package/dist/introspect.d.ts +77 -2
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +138 -9
- package/dist/introspect.js.map +1 -1
- package/dist/mcp-render.d.ts.map +1 -1
- package/dist/mcp-render.js +99 -8
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +49 -21
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +12 -3
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js +21 -4
- package/dist/openapi-tool.js.map +1 -1
- package/dist/port.d.ts +144 -0
- package/dist/port.d.ts.map +1 -0
- package/dist/port.js +498 -0
- package/dist/port.js.map +1 -0
- package/docs/examples.json +91 -133
- package/docs/llms-full.txt +1090 -35
- package/package.json +3 -2
- package/src/check.ts +398 -14
- package/src/cli.ts +176 -21
- package/src/init-agents.ts +176 -0
- package/src/introspect.ts +208 -8
- package/src/mcp-render.ts +106 -11
- package/src/mcp.ts +56 -20
- package/src/openapi-tool.ts +24 -4
- package/src/port.ts +663 -0
package/docs/llms-full.txt
CHANGED
|
@@ -361,6 +361,7 @@ and `c.query` are the _validated_ types, and a bad request gets a structured
|
|
|
361
361
|
`422` automatically. Path params (`:id`) are typed from the pattern.
|
|
362
362
|
|
|
363
363
|
```ts
|
|
364
|
+
// doc-check: skip — uses the third-party `zod` schema lib (any Standard Schema works); install it to run this.
|
|
364
365
|
import { server } from "@nifrajs/core"
|
|
365
366
|
import { z } from "zod" // any Standard Schema works: zod, valibot, arktype…
|
|
366
367
|
|
|
@@ -412,6 +413,7 @@ body, or return type is a compile error. The result is the same `app` the inline
|
|
|
412
413
|
builder produces.
|
|
413
414
|
|
|
414
415
|
```ts
|
|
416
|
+
// doc-check: skip — uses the third-party `zod` schema lib + an illustrative `users` repo; install zod to run this.
|
|
415
417
|
import { defineContract, implement } from "@nifrajs/core"
|
|
416
418
|
import { z } from "zod"
|
|
417
419
|
|
|
@@ -574,6 +576,7 @@ and the UI reflects it immediately. When the action resolves the real data takes
|
|
|
574
576
|
failed submit **reverts** automatically — no manual rollback bookkeeping.
|
|
575
577
|
|
|
576
578
|
```ts
|
|
579
|
+
// doc-check: skip — component-body fragment: `id` and `todo` are the row's props.
|
|
577
580
|
import { useFetcher } from "@nifrajs/web-react/fetcher"
|
|
578
581
|
|
|
579
582
|
// Optimistic UI: while a submit is in flight, read the expected value from the in-flight
|
|
@@ -691,8 +694,10 @@ carries an `x-nifra-isr: hit | stale | miss` header.
|
|
|
691
694
|
```ts
|
|
692
695
|
// server.ts — wrap the app with Incremental Static Regeneration.
|
|
693
696
|
import { createWebApp, withISR, MemoryCacheStore } from "@nifrajs/web"
|
|
697
|
+
import { reactAdapter } from "@nifrajs/web-react"
|
|
698
|
+
import { clientEntry, manifest } from "./server-manifest" // generated by buildServer
|
|
694
699
|
|
|
695
|
-
const app = createWebApp({ adapter, manifest, clientEntry
|
|
700
|
+
const app = createWebApp({ adapter: reactAdapter, manifest, clientEntry })
|
|
696
701
|
const store = new MemoryCacheStore() // dev / single-instance only
|
|
697
702
|
|
|
698
703
|
// GET text/html responses are cached + served stale-while-revalidate. Default freshness 60s.
|
|
@@ -719,6 +724,7 @@ before it's trusted, so a corrupt or version-skewed entry is treated as a miss,
|
|
|
719
724
|
broken.
|
|
720
725
|
|
|
721
726
|
```ts
|
|
727
|
+
// doc-check: skip — Workers entry: `Env`/`ExecutionContext` globals + your `app` from server.ts.
|
|
722
728
|
// worker.ts — production uses a SHARED store so the cache + purges hold across instances.
|
|
723
729
|
import { withISR, KVCacheStore, revalidateEndpoint } from "@nifrajs/web"
|
|
724
730
|
|
|
@@ -760,6 +766,7 @@ cache. Pass the same `draftSecret` to `createWebApp` and
|
|
|
760
766
|
`withISR`; a forged or tampered cookie fails the constant-time signature check.
|
|
761
767
|
|
|
762
768
|
```ts
|
|
769
|
+
// doc-check: skip — fragment spanning three files: your `app`, `env`, `redirect`, and route `slug`.
|
|
763
770
|
// 1. A route you gate yourself flips draft mode on for the editor.
|
|
764
771
|
import { enableDraft, disableDraft } from "@nifrajs/web"
|
|
765
772
|
|
|
@@ -860,7 +867,11 @@ resolves, you call `stream.close()`, or the client disconnects (
|
|
|
860
867
|
Node, Deno, and the edge — no `new Function`, no per-runtime API.
|
|
861
868
|
|
|
862
869
|
```ts
|
|
863
|
-
import { sse } from "@nifrajs/core"
|
|
870
|
+
import { server, sse } from "@nifrajs/core"
|
|
871
|
+
|
|
872
|
+
const app = server()
|
|
873
|
+
// Your pub-sub of choice — subscribe returns an unsubscribe function.
|
|
874
|
+
declare const notifications: { subscribe(on: (n: { id: string }) => void): () => void }
|
|
864
875
|
|
|
865
876
|
// A live feed — push events until the client disconnects.
|
|
866
877
|
app.get("/notifications", (c) =>
|
|
@@ -1023,12 +1034,12 @@ routes, data, streaming — runs on a different framework, unchanged.
|
|
|
1023
1034
|
// the same routes, loaders, actions, streaming, <Await>, fetchers, query cache.
|
|
1024
1035
|
import { createWebApp } from "@nifrajs/web"
|
|
1025
1036
|
import { reactAdapter } from "@nifrajs/web-react" // ← or web-solid · web-vue · web-preact · web-svelte
|
|
1037
|
+
import { clientEntry, manifest } from "./server-manifest" // generated by buildServer
|
|
1026
1038
|
|
|
1027
1039
|
const app = createWebApp({
|
|
1028
1040
|
adapter: reactAdapter, // the one line that changes per framework
|
|
1029
1041
|
manifest,
|
|
1030
1042
|
clientEntry, // the built client bundle (from buildClient's manifest)
|
|
1031
|
-
api,
|
|
1032
1043
|
})
|
|
1033
1044
|
```
|
|
1034
1045
|
|
|
@@ -1081,6 +1092,7 @@ defined after** `app.use(plugin)` — no extra generics. Wrap a plugin with
|
|
|
1081
1092
|
plugins both depend on it), it wires its hooks once.
|
|
1082
1093
|
|
|
1083
1094
|
```ts
|
|
1095
|
+
// doc-check: skip — fragment: the outer `app`, `verify`, and `db` are your application's.
|
|
1084
1096
|
import { definePlugin } from "@nifrajs/core"
|
|
1085
1097
|
|
|
1086
1098
|
// A plugin is just (app) => app — call use/derive/decorate or register routes.
|
|
@@ -1130,13 +1142,15 @@ short-circuit), `beforeHandle`/`afterHandle` (around the handler),
|
|
|
1130
1142
|
error, 404). Hardening middleware uses the same hook model:
|
|
1131
1143
|
|
|
1132
1144
|
```ts
|
|
1133
|
-
import {
|
|
1145
|
+
import { server } from "@nifrajs/core"
|
|
1146
|
+
import { cors, rateLimit, securityHeaders, MemoryStore } from "@nifrajs/middleware"
|
|
1134
1147
|
|
|
1135
1148
|
// Hardening middleware is a hook bundle (context-agnostic) — same app.use():
|
|
1136
|
-
app
|
|
1149
|
+
const app = server()
|
|
1137
1150
|
.use(securityHeaders())
|
|
1138
|
-
.use(cors({
|
|
1139
|
-
|
|
1151
|
+
.use(cors({ origin: ["https://app.example"] }))
|
|
1152
|
+
// MemoryStore is dev/single-instance only — use a shared store (Redis, etc.) in production.
|
|
1153
|
+
.use(rateLimit({ store: new MemoryStore(), max: 100, windowMs: 60_000 }))
|
|
1140
1154
|
```
|
|
1141
1155
|
|
|
1142
1156
|
### Official plugins
|
|
@@ -1144,9 +1158,10 @@ app
|
|
|
1144
1158
|
`@nifrajs/middleware` seeds a few `definePlugin` plugins to build on:
|
|
1145
1159
|
|
|
1146
1160
|
```ts
|
|
1161
|
+
import { server } from "@nifrajs/core"
|
|
1147
1162
|
import { requestId, logger, etag } from "@nifrajs/middleware"
|
|
1148
1163
|
|
|
1149
|
-
app
|
|
1164
|
+
const app = server()
|
|
1150
1165
|
.use(requestId()) // reuse/generate x-request-id → c.requestId (typed) + response header
|
|
1151
1166
|
.use(logger()) // one structured line/request: { method, path, status, ms }
|
|
1152
1167
|
.use(etag()) // content-hash ETag on GET 200s → 304 on matching If-None-Match
|
|
@@ -1175,6 +1190,7 @@ cached. For full session-based auth (OAuth, magic links, 2FA), see
|
|
|
1175
1190
|
[Auth & sessions](/docs/auth).
|
|
1176
1191
|
|
|
1177
1192
|
```ts
|
|
1193
|
+
// doc-check: skip — fragment: `app`, `lookupUser`, and the `db` lookup are your application's.
|
|
1178
1194
|
import { bearer, apiKey } from "@nifrajs/middleware"
|
|
1179
1195
|
|
|
1180
1196
|
// Bearer tokens — verify returns your principal (its type is inferred), 401s missing/invalid:
|
|
@@ -1207,9 +1223,10 @@ does custom, typed verification.
|
|
|
1207
1223
|
clobbering one a handler already set.
|
|
1208
1224
|
|
|
1209
1225
|
```ts
|
|
1226
|
+
import { server } from "@nifrajs/core"
|
|
1210
1227
|
import { compression, cacheControl } from "@nifrajs/middleware"
|
|
1211
1228
|
|
|
1212
|
-
app
|
|
1229
|
+
const app = server()
|
|
1213
1230
|
.use(compression()) // gzip compressible responses (Accept-Encoding)
|
|
1214
1231
|
.use(cacheControl("public, max-age=60")) // Cache-Control on GET/HEAD 2xx (won't clobber)
|
|
1215
1232
|
// …or a per-path policy — return undefined to leave a response untouched:
|
|
@@ -1234,6 +1251,7 @@ that Standard Schema can't expose. For full request/response _schemas_, generate
|
|
|
1234
1251
|
exported for build-time generation too.
|
|
1235
1252
|
|
|
1236
1253
|
```ts
|
|
1254
|
+
// doc-check: skip — fragment: `app`, `db`, and `redis` are your application's clients.
|
|
1237
1255
|
import { healthcheck, openapi } from "@nifrajs/middleware"
|
|
1238
1256
|
|
|
1239
1257
|
app
|
|
@@ -1252,10 +1270,12 @@ connection. It short-circuits before the handler; a concurrent retry gets a `409
|
|
|
1252
1270
|
guidance (shared store + DB constraint), and the `Set-Cookie` rule.
|
|
1253
1271
|
|
|
1254
1272
|
```ts
|
|
1273
|
+
import { server } from "@nifrajs/core"
|
|
1255
1274
|
import { idempotency, MemoryIdempotencyStore } from "@nifrajs/middleware"
|
|
1256
1275
|
|
|
1257
1276
|
// A retried POST with the same Idempotency-Key replays the first response instead of
|
|
1258
1277
|
// re-running the side effect. Shared store in production (atomic claim); dev-only memory store.
|
|
1278
|
+
const app = server()
|
|
1259
1279
|
app.use(idempotency({ store: new MemoryIdempotencyStore() }))
|
|
1260
1280
|
```
|
|
1261
1281
|
|
|
@@ -1270,11 +1290,12 @@ Read the typed claims off the returned plugin — `auth.requireClaims(c.req)` (t
|
|
|
1270
1290
|
compares static credentials in **constant time** (or takes a `verify` callback).
|
|
1271
1291
|
|
|
1272
1292
|
```ts
|
|
1293
|
+
import { server } from "@nifrajs/core"
|
|
1273
1294
|
import { jwt, jwks, basicAuth } from "@nifrajs/middleware"
|
|
1274
1295
|
|
|
1275
1296
|
// JWT (WebCrypto): an explicit algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected.
|
|
1276
1297
|
const auth = jwt({ key: process.env.JWT_SECRET!, algorithms: ["HS256"], issuer: "my-app" })
|
|
1277
|
-
app
|
|
1298
|
+
const app = server()
|
|
1278
1299
|
.use(auth) // 401s missing/invalid (optional:true lets them through)
|
|
1279
1300
|
.get("/me", (c) => auth.requireClaims(c.req)) // typed claims, or throws 401; auth.claims(req) is nullable
|
|
1280
1301
|
// Asymmetric (rotating keys): key: jwks({ url: "https://issuer/.well-known/jwks.json" }) — https-only, cached.
|
|
@@ -1293,10 +1314,12 @@ user's response to another. `MemoryResponseCache` is per-instance and refuses
|
|
|
1293
1314
|
pretty-prints JSON responses (capped, with an optional query toggle).
|
|
1294
1315
|
|
|
1295
1316
|
```ts
|
|
1317
|
+
import { server } from "@nifrajs/core"
|
|
1296
1318
|
import { cache, MemoryResponseCache, prettyJson } from "@nifrajs/middleware"
|
|
1297
1319
|
|
|
1298
1320
|
// Full response cache: pluggable store, Vary-aware keys, byte cap. Bypasses Set-Cookie and respects
|
|
1299
1321
|
// Cache-Control (no-store/private). MemoryResponseCache is per-instance — refuses prod unless opted in.
|
|
1322
|
+
const app = server()
|
|
1300
1323
|
app.use(cache({ store: new MemoryResponseCache(), ttlMs: 30_000, vary: ["accept-language"] }))
|
|
1301
1324
|
app.use(prettyJson()) // pretty-print JSON responses (size-capped; optional ?pretty query toggle)
|
|
1302
1325
|
```
|
|
@@ -1313,9 +1336,10 @@ no open redirect); `language` negotiates `Accept-Language` into
|
|
|
1313
1336
|
is opt-in; `combine` bundles several middleware into one.
|
|
1314
1337
|
|
|
1315
1338
|
```ts
|
|
1339
|
+
import { server } from "@nifrajs/core"
|
|
1316
1340
|
import { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from "@nifrajs/middleware"
|
|
1317
1341
|
|
|
1318
|
-
app
|
|
1342
|
+
const app = server()
|
|
1319
1343
|
.use(methodOverride()) // POST + X-HTTP-Method-Override → PUT/PATCH/DELETE (a real pre-routing request rewrite)
|
|
1320
1344
|
.use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods
|
|
1321
1345
|
.use(language({ supported: ["en", "fr"], defaultLanguage: "en" })) // Accept-Language → c.language + Content-Language
|
|
@@ -1365,7 +1389,7 @@ bypasses that read path. `c.boundedBody(maxBytes?)` and
|
|
|
1365
1389
|
`c.boundedJson<T>(maxBytes?)` extend the **same** cap to those routes.
|
|
1366
1390
|
|
|
1367
1391
|
```ts
|
|
1368
|
-
import { server
|
|
1392
|
+
import { server } from "@nifrajs/core"
|
|
1369
1393
|
|
|
1370
1394
|
const app = server()
|
|
1371
1395
|
|
|
@@ -1378,12 +1402,13 @@ app.post("/import", async (c) => {
|
|
|
1378
1402
|
// Responses (caught by the lifecycle like `throw redirect()`), so a handler can't
|
|
1379
1403
|
// accidentally ignore the cap. The over-cap length is rejected BEFORE buffering;
|
|
1380
1404
|
// a chunked / length-less body is aborted mid-stream once it crosses the cap.
|
|
1381
|
-
return
|
|
1405
|
+
return { received: bytes.byteLength } // a returned object is serialized as JSON 200
|
|
1382
1406
|
})
|
|
1383
1407
|
|
|
1384
1408
|
app.post("/rpc", async (c) => {
|
|
1385
1409
|
const body = await c.boundedJson<{ method: string }>() // default: the server's maxBodyBytes
|
|
1386
1410
|
// …bad JSON → 400. Then validate `body` with your schema before trusting it.
|
|
1411
|
+
return { method: body.method }
|
|
1387
1412
|
})
|
|
1388
1413
|
```
|
|
1389
1414
|
|
|
@@ -1400,12 +1425,13 @@ size cap and sniffs the **real** type from magic bytes — never the client-set
|
|
|
1400
1425
|
`Blob` is rejected by its `.size` before it's ever buffered.
|
|
1401
1426
|
|
|
1402
1427
|
```ts
|
|
1428
|
+
// doc-check: skip — fragment: `app`, `save`, `id`, and `env` are your application's.
|
|
1403
1429
|
import { validateUpload, signDownloadUrl } from "@nifrajs/uploads"
|
|
1404
1430
|
|
|
1405
1431
|
app.post("/avatar", async (c) => {
|
|
1406
1432
|
const form = await c.req.formData()
|
|
1407
1433
|
const file = form.get("file")
|
|
1408
|
-
if (!(file instanceof Blob))
|
|
1434
|
+
if (!(file instanceof Blob)) { c.set.status = 400; return { ok: false, error: "no_file" } }
|
|
1409
1435
|
|
|
1410
1436
|
// Size cap + REAL type by magic bytes — a .exe renamed .png (or a spoofed
|
|
1411
1437
|
// Content-Type) is caught, because the bytes win. An oversized Blob is rejected
|
|
@@ -1414,14 +1440,14 @@ app.post("/avatar", async (c) => {
|
|
|
1414
1440
|
maxBytes: 2_000_000,
|
|
1415
1441
|
accept: ["image/png", "image/jpeg"], // exact, or "image/*"
|
|
1416
1442
|
})
|
|
1417
|
-
if (!result.ok)
|
|
1443
|
+
if (!result.ok) { c.set.status = 400; return { ok: false, error: result.reason } }
|
|
1418
1444
|
// reason: "too_large" | "empty" | "unrecognized" | "type_not_allowed"
|
|
1419
1445
|
|
|
1420
1446
|
await save(result.bytes, `${id}.${result.ext}`) // result.mime / .ext are trustworthy
|
|
1421
1447
|
|
|
1422
1448
|
// Hand back a short-TTL, tamper-evident URL (HMAC over path + expiry):
|
|
1423
1449
|
const url = await signDownloadUrl(`/files/${id}`, env.FILE_SECRET, { expiresInSeconds: 300 })
|
|
1424
|
-
return
|
|
1450
|
+
return { ok: true, url } // a returned object is serialized as JSON 200
|
|
1425
1451
|
})
|
|
1426
1452
|
```
|
|
1427
1453
|
|
|
@@ -1435,6 +1461,7 @@ download links (HMAC-SHA256 over the path + expiry, constant-time verify). And
|
|
|
1435
1461
|
[@nifrajs/image](/docs/images) backend, with no dependency on it:
|
|
1436
1462
|
|
|
1437
1463
|
```ts
|
|
1464
|
+
// doc-check: skip — fragment: continues the upload handler above (`result.bytes`).
|
|
1438
1465
|
import { stripImageMetadata } from "@nifrajs/uploads"
|
|
1439
1466
|
import { bunImageBackend } from "@nifrajs/image/backends"
|
|
1440
1467
|
|
|
@@ -1452,19 +1479,20 @@ payload. `verifyWebhook` reads the raw body bounded, verifies the HMAC, and hand
|
|
|
1452
1479
|
the verified text for you to parse with your own schema.
|
|
1453
1480
|
|
|
1454
1481
|
```ts
|
|
1482
|
+
// doc-check: skip — fragment: `app`, `env`, `StripeEvent`, and the rotation keys are your application's.
|
|
1455
1483
|
import { verifyWebhook } from "@nifrajs/core"
|
|
1456
1484
|
|
|
1457
1485
|
app.post("/webhooks/stripe", async (c) => {
|
|
1458
1486
|
// Reads the raw body BOUNDED (DoS guard), verifies the HMAC CONSTANT-TIME, and only
|
|
1459
1487
|
// then returns the payload. Never JSON.parse a webhook before the signature checks out.
|
|
1460
1488
|
const r = await verifyWebhook(c.req, env.STRIPE_WEBHOOK_SECRET, { provider: "stripe" })
|
|
1461
|
-
if (!r.ok)
|
|
1489
|
+
if (!r.ok) { c.set.status = 400; return { ok: false, error: r.reason } }
|
|
1462
1490
|
// reason: "missing_signature" | "invalid_signature" | "timestamp_out_of_tolerance"
|
|
1463
1491
|
// | "malformed_signature" | "payload_too_large" | "invalid_content_length"
|
|
1464
1492
|
|
|
1465
1493
|
const event = StripeEvent.parse(JSON.parse(r.payload)) // validate at the trust boundary
|
|
1466
1494
|
// …handle event… (pair with idempotency below so a redelivery doesn't double-process)
|
|
1467
|
-
return
|
|
1495
|
+
return { ok: true }
|
|
1468
1496
|
})
|
|
1469
1497
|
|
|
1470
1498
|
// GitHub (sha256=…hex), or any provider via the generic preset:
|
|
@@ -1488,6 +1516,7 @@ A dropped connection or an impatient double-tap shouldn't double-charge a card.
|
|
|
1488
1516
|
re-running the side effect. It short-circuits in `onRequest`, _before_ the handler.
|
|
1489
1517
|
|
|
1490
1518
|
```ts
|
|
1519
|
+
// doc-check: skip — fragment: `app`, `chargeCard`, and `id` are your application's.
|
|
1491
1520
|
import { idempotency, MemoryIdempotencyStore } from "@nifrajs/middleware"
|
|
1492
1521
|
|
|
1493
1522
|
// Dev / single-instance. In production use a SHARED store (Redis, etc.) with an atomic
|
|
@@ -1496,7 +1525,7 @@ app.use(idempotency({ store: new MemoryIdempotencyStore() }))
|
|
|
1496
1525
|
|
|
1497
1526
|
app.post("/charge", async (c) => {
|
|
1498
1527
|
await chargeCard(/* … */) // the side effect
|
|
1499
|
-
return
|
|
1528
|
+
return { ok: true, id }
|
|
1500
1529
|
})
|
|
1501
1530
|
|
|
1502
1531
|
// A client retrying POST /charge with the same `Idempotency-Key` header gets the FIRST
|
|
@@ -1531,13 +1560,14 @@ Transient `5xx` aren't cached, so a failed call stays retryable.
|
|
|
1531
1560
|
Every one is constant-time where it compares secrets and **fails closed** by default.
|
|
1532
1561
|
|
|
1533
1562
|
```ts
|
|
1563
|
+
import { server } from "@nifrajs/core"
|
|
1534
1564
|
import { jwt, csrf, ipRestriction, bodyLimit } from "@nifrajs/middleware"
|
|
1535
1565
|
|
|
1536
|
-
app
|
|
1566
|
+
const app = server()
|
|
1537
1567
|
// JWT: the algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected; exp enforced.
|
|
1538
|
-
.use(jwt({ key: env.JWT_SECRET
|
|
1568
|
+
.use(jwt({ key: process.env.JWT_SECRET!, algorithms: ["HS256"], issuer: "my-app" }))
|
|
1539
1569
|
// Signed double-submit CSRF (HMAC) + Origin/Referer check on unsafe methods. Secret must be >= 32 bytes.
|
|
1540
|
-
.use(csrf({ secret: env.CSRF_SECRET }))
|
|
1570
|
+
.use(csrf({ secret: process.env.CSRF_SECRET! }))
|
|
1541
1571
|
// Allow/deny by IPv4/IPv6 + CIDR. FAILS CLOSED with no trusted client IP; X-Forwarded-For is ignored
|
|
1542
1572
|
// unless trustedProxies > 0 (set it to the number of proxies you actually run in front of the app).
|
|
1543
1573
|
.use(ipRestriction({ allow: ["10.0.0.0/8", "::1"], trustedProxies: 1 }))
|
|
@@ -1617,6 +1647,7 @@ your nifra server. Read the session with `getSession(auth, request)` (typed
|
|
|
1617
1647
|
your tests need no database — and your Better Auth types flow through by inference.
|
|
1618
1648
|
|
|
1619
1649
|
```ts
|
|
1650
|
+
// doc-check: skip — needs the third-party `better-auth` package + your `db`; install it to run this.
|
|
1620
1651
|
// auth.ts — your configured Better Auth instance (database, providers, …):
|
|
1621
1652
|
import { betterAuth as createBetterAuth } from "better-auth"
|
|
1622
1653
|
export const auth = createBetterAuth({ database: db, emailAndPassword: { enabled: true } })
|
|
@@ -1648,11 +1679,11 @@ Stores mirror the ISR discipline: `MemorySessionStore` is prod-guarded;
|
|
|
1648
1679
|
```ts
|
|
1649
1680
|
// auth.ts — one session manager. LAZY so a route module can import it without shipping it to the
|
|
1650
1681
|
// browser (see the warning below). Store mode keeps data server-side; cookie mode (no store) is stateless.
|
|
1651
|
-
import { createSessions, MemorySessionStore
|
|
1682
|
+
import { createSessions, MemorySessionStore } from "@nifrajs/auth"
|
|
1652
1683
|
|
|
1653
|
-
let manager
|
|
1684
|
+
let manager: ReturnType<typeof createSessions> | undefined
|
|
1654
1685
|
export const getSessions = () => (manager ??= createSessions({
|
|
1655
|
-
secret: process.env.SESSION_SECRET
|
|
1686
|
+
secret: process.env.SESSION_SECRET!, // ≥ 16 chars; rotating it invalidates all sessions
|
|
1656
1687
|
store: new MemorySessionStore(), // prod: new KVSessionStore(env.SESSIONS)
|
|
1657
1688
|
// cookie: { secure: false }, // local http dev only
|
|
1658
1689
|
}))
|
|
@@ -1694,6 +1725,7 @@ createWebApp({ /* … */ api: sessions }) // inject the manager
|
|
|
1694
1725
|
as-is, so the guard short-circuits the loader.
|
|
1695
1726
|
|
|
1696
1727
|
```ts
|
|
1728
|
+
// doc-check: skip — fragment: `api` is the session manager createWebApp injected (see setup above).
|
|
1697
1729
|
// A protected route's loader — reads the session and redirects when absent.
|
|
1698
1730
|
import { requireUser } from "@nifrajs/auth" // browser-safe; OK to import in a route module
|
|
1699
1731
|
|
|
@@ -1900,6 +1932,7 @@ Deno-Deploy), where there's no native codec. `@nifrajs/image/backends` has no
|
|
|
1900
1932
|
built in (it never decodes just to read dimensions).
|
|
1901
1933
|
|
|
1902
1934
|
```ts
|
|
1935
|
+
// doc-check: skip — illustrates third-party codecs (sharp, @jsquash/*); install them to run it.
|
|
1903
1936
|
import { createImageHandler } from "@nifrajs/image/server"
|
|
1904
1937
|
import { sharpImageBackend, wasmImageBackend } from "@nifrajs/image/backends"
|
|
1905
1938
|
|
|
@@ -1939,15 +1972,17 @@ loader can sign inline — even on the edge.
|
|
|
1939
1972
|
import { selfHostedLoader, signImageUrl } from "@nifrajs/image"
|
|
1940
1973
|
import { createImageHandler } from "@nifrajs/image/server"
|
|
1941
1974
|
|
|
1942
|
-
const
|
|
1975
|
+
const IMAGE_SECRET = process.env.IMAGE_SECRET! // server-only; on Workers read c.env.IMAGE_SECRET instead
|
|
1976
|
+
|
|
1977
|
+
const resize = selfHostedLoader({ endpoint: "/_image", secret: IMAGE_SECRET })
|
|
1943
1978
|
// → /_image?src=…&w=800&s=<hmac> (stable: an SSR-signed URL hydrates + caches identically)
|
|
1944
1979
|
|
|
1945
|
-
const image = createImageHandler({ root: "./public", signing: { secret:
|
|
1980
|
+
const image = createImageHandler({ root: "./public", signing: { secret: IMAGE_SECRET } })
|
|
1946
1981
|
// any request without a valid &s= (forged / tampered / expired) → 403, before any fetch or decode
|
|
1947
1982
|
|
|
1948
1983
|
// Time-limited links to private images (server-side only):
|
|
1949
1984
|
const url = signImageUrl("/_image", { src: "/private/a.jpg", width: 800 }, {
|
|
1950
|
-
secret:
|
|
1985
|
+
secret: IMAGE_SECRET,
|
|
1951
1986
|
expiresIn: 300, // seconds → adds &exp=
|
|
1952
1987
|
})
|
|
1953
1988
|
```
|
|
@@ -1974,8 +2009,10 @@ const info = await readImageDimensions(Bun.file("public/hero.jpg"))
|
|
|
1974
2009
|
// → { width: 1200, height: 630, format: "jpeg" } (null if unrecognized)
|
|
1975
2010
|
|
|
1976
2011
|
// Build-time tooling: pre-read sizes into a manifest so <Image> is CLS-safe
|
|
1977
|
-
// without hardcoding width/height at every call site.
|
|
1978
|
-
|
|
2012
|
+
// without hardcoding width/height at every call site. The sync variant takes the
|
|
2013
|
+
// raw header bytes you already have in memory (no file read):
|
|
2014
|
+
const headerBytes = await Bun.file("public/hero.jpg").bytes()
|
|
2015
|
+
imageDimensions(headerBytes)
|
|
1979
2016
|
```
|
|
1980
2017
|
|
|
1981
2018
|
### Notes
|
|
@@ -2015,7 +2052,7 @@ messages.
|
|
|
2015
2052
|
import { negotiateLocale } from "@nifrajs/i18n"
|
|
2016
2053
|
import { catalogs, locales } from "../catalogs"
|
|
2017
2054
|
|
|
2018
|
-
export async function loader({ request }) {
|
|
2055
|
+
export async function loader({ request }: { request: Request }) {
|
|
2019
2056
|
const q = new URL(request.url).searchParams.get("lang")
|
|
2020
2057
|
const locale = q && locales.includes(q) ? q : negotiateLocale(request, { locales, defaultLocale: "en", cookie: "lang" })
|
|
2021
2058
|
return { locale, messages: catalogs[locale] } // cookie → Accept-Language → default
|
|
@@ -2143,6 +2180,7 @@ handler for a `[triggers]` schedule. It gets the platform controller plus the sa
|
|
|
2143
2180
|
typed `env` and `waitUntil` as your request handlers.
|
|
2144
2181
|
|
|
2145
2182
|
```ts
|
|
2183
|
+
// doc-check: skip — `env.KV` is typed by your app's `Env` (the ./app import), supplied by the reader.
|
|
2146
2184
|
import { toFetchHandler } from "@nifrajs/core"
|
|
2147
2185
|
import { app } from "./app"
|
|
2148
2186
|
|
|
@@ -2302,10 +2340,11 @@ Deploy: `wrangler pages deploy dist` (Pages) or `wrangler deploy`
|
|
|
2302
2340
|
`server.ts` to hand-write. (`create-nifra` scaffolds the conventions.)
|
|
2303
2341
|
|
|
2304
2342
|
```ts
|
|
2305
|
-
nifra dev # true-HMR dev server (Vite middleware + nifra SSR) — http://localhost:
|
|
2343
|
+
nifra dev # true-HMR dev server (Vite middleware + nifra SSR) — http://localhost:4321
|
|
2306
2344
|
nifra build # bundle the client (content-hashed, code-split) + write dist/manifest.json (incl. CSS)
|
|
2307
2345
|
nifra start # serve the built client + SSR on Bun (assets, <link> stylesheets, matched-route preload)
|
|
2308
2346
|
|
|
2347
|
+
# dev + start share the default port 4321. Override per run: --port <n> (alias -p) or the PORT env var.
|
|
2309
2348
|
# flags: --port <n> (dev/start) · --out <dir> (build/start) · --poll (dev; containers/sandboxes)
|
|
2310
2349
|
```
|
|
2311
2350
|
|
|
@@ -2331,6 +2370,7 @@ required; the plugin/condition fields are extras the compiler frameworks need (R
|
|
|
2331
2370
|
JSX is Bun-native, so they need none beyond the Vite plugin for dev HMR).
|
|
2332
2371
|
|
|
2333
2372
|
```ts
|
|
2373
|
+
// doc-check: skip — needs the third-party @vitejs/plugin-react; install it to run this.
|
|
2334
2374
|
// framework.ts — the one place an app names its framework. The CLI reads these:
|
|
2335
2375
|
import react from "@vitejs/plugin-react"
|
|
2336
2376
|
import { reactAdapter } from "@nifrajs/web-react"
|
|
@@ -2389,6 +2429,7 @@ without a full page reload. Pass the official plugin for your UI framework, keep
|
|
|
2389
2429
|
Nifra routes and loaders, and run the dev server during local development.
|
|
2390
2430
|
|
|
2391
2431
|
```ts
|
|
2432
|
+
// doc-check: skip — needs the third-party @vitejs/plugin-react + your ./backend; install it to run this.
|
|
2392
2433
|
// dev.ts — state-preserving HMR for supported UI adapters
|
|
2393
2434
|
import react from "@vitejs/plugin-react" // your framework's official Vite plugin
|
|
2394
2435
|
import { createWebApp } from "@nifrajs/web"
|
|
@@ -2403,7 +2444,7 @@ const server = await createViteDevServer({
|
|
|
2403
2444
|
routesDir,
|
|
2404
2445
|
clientModule: "@nifrajs/web-react/client",
|
|
2405
2446
|
plugins: [react()], // Vue: @vitejs/plugin-vue, Svelte: …, etc.
|
|
2406
|
-
port: Number(Bun.env.PORT ??
|
|
2447
|
+
port: Number(Bun.env.PORT ?? 4321), // nifra's default; --port / PORT override it
|
|
2407
2448
|
createApp: (clientEntry, importQuery) =>
|
|
2408
2449
|
createWebApp({
|
|
2409
2450
|
adapter: reactAdapter,
|
|
@@ -2509,6 +2550,7 @@ instead.
|
|
|
2509
2550
|
serves SSR, watches the routes, and does a full-page reload on change.
|
|
2510
2551
|
|
|
2511
2552
|
```ts
|
|
2553
|
+
// doc-check: skip — fragment: routesDir/outDir/clientModule/createApp are your app's dev config.
|
|
2512
2554
|
// dev.ts — Bun-native live reload
|
|
2513
2555
|
import { createDevServer } from "@nifrajs/web/dev"
|
|
2514
2556
|
// builds the client on boot, serves SSR, watches routes, full-page live-reload on change.
|
|
@@ -2584,6 +2626,675 @@ own stylesheet. Pass `manifest.routeStyles` to `createWebApp` alongside
|
|
|
2584
2626
|
`styles`, and Nifra links the matched route's CSS during SSR. In
|
|
2585
2627
|
**dev**, Vite injects per-module CSS.
|
|
2586
2628
|
|
|
2629
|
+
## Nifra — Troubleshooting
|
|
2630
|
+
|
|
2631
|
+
> Fixes keyed on the literal error strings Nifra prints: `reached the client bundle` (a node:/native import in the browser bundle), `server-only module reached the client bundle` (the server-only marker), and `resolveDispatcher` / `Invalid hook call` (duplicate React).
|
|
2632
|
+
|
|
2633
|
+
Hit an error? Search this page for the literal message. Each section is keyed on the exact
|
|
2634
|
+
string Nifra prints, with what it means and the fix. The build, `nifra check`, and
|
|
2635
|
+
the runtime all use the same wording so you can grep for it.
|
|
2636
|
+
|
|
2637
|
+
### `reached the client bundle` — a `node:` / native import leaked to the browser
|
|
2638
|
+
|
|
2639
|
+
The client build refuses to ship a Node built-in (`node:fs`, `node:crypto`,
|
|
2640
|
+
`bun:sqlite`, a native driver like `pg`) to the browser. Bun would
|
|
2641
|
+
silently substitute a polyfill that breaks — or leaks server code — at runtime, so Nifra fails
|
|
2642
|
+
the build instead. The message names the offending builtin and the **import chain**
|
|
2643
|
+
that pulled it in:
|
|
2644
|
+
|
|
2645
|
+
[nifra/web] Node built-in(s) in the client bundle — move them behind a server-only path
|
|
2646
|
+
|
|
2647
|
+
- node:crypto reached the client bundle via routes/x.tsx → ../data.ts → ../db.ts
|
|
2648
|
+
(chunk: x-abc123.js)
|
|
2649
|
+
|
|
2650
|
+
Read the chain right-to-left: `../db.ts` imports `node:crypto`, and a
|
|
2651
|
+
top-level import in `routes/x.tsx` dragged it into the browser. **Fix it one
|
|
2652
|
+
of two ways:**
|
|
2653
|
+
|
|
2654
|
+
-
|
|
2655
|
+
**Move the code into a `*.server.ts` module** (the recommended
|
|
2656
|
+
default). The client build empties `*.server` modules, so their `node:`
|
|
2657
|
+
/ native imports never reach the browser. Import the server module only from a loader/action
|
|
2658
|
+
(which run on the server).
|
|
2659
|
+
|
|
2660
|
+
-
|
|
2661
|
+
**Reach the server code from a loader/action, not a route's top level.** A
|
|
2662
|
+
loader runs on the server during SSR; importing the `node:` module inside (or via)
|
|
2663
|
+
a loader keeps it out of the client graph.
|
|
2664
|
+
|
|
2665
|
+
```ts
|
|
2666
|
+
// doc-check: skip — illustrative two-file layout (a relative cross-file import).
|
|
2667
|
+
// db.server.ts — the `.server` convention: the client build EMPTIES this module, so its
|
|
2668
|
+
// `node:` / native imports never ship to the browser. No marker import needed — the filename is it.
|
|
2669
|
+
import { Database } from "bun:sqlite"
|
|
2670
|
+
export const db = new Database("app.db")
|
|
2671
|
+
|
|
2672
|
+
// routes/notes.tsx — import the server module from a loader; it runs only on the server during SSR.
|
|
2673
|
+
import type { LoaderContext } from "@nifrajs/web"
|
|
2674
|
+
import { db } from "../db.server"
|
|
2675
|
+
export async function loader(_ctx: LoaderContext) {
|
|
2676
|
+
return { notes: db.query("select * from notes").all() }
|
|
2677
|
+
}
|
|
2678
|
+
```
|
|
2679
|
+
|
|
2680
|
+
[!TIP]
|
|
2681
|
+
|
|
2682
|
+
Run `nifra check` (or `nifra check --json` for agents) to catch this
|
|
2683
|
+
_before_ the build: it reports the same transitive chain (
|
|
2684
|
+
`routes/x → ../data → ./db → node:crypto`) by walking the local module graph, so
|
|
2685
|
+
you see the leak as a lint result, not a failed build.
|
|
2686
|
+
|
|
2687
|
+
### `server-only module reached the client bundle` — the server-only marker fired
|
|
2688
|
+
|
|
2689
|
+
This is the companion guard for **pure server logic that carries no `node:`
|
|
2690
|
+
import** — a secret-bearing constant, a server-only API call — so the node-builtin guard
|
|
2691
|
+
above has nothing to catch and the `.server` convention needs the file to be
|
|
2692
|
+
_named_ `*.server`. You opt a module in with a side-effect import, and the
|
|
2693
|
+
client build fails loud (with the import chain) if it ever lands in a browser chunk:
|
|
2694
|
+
|
|
2695
|
+
[nifra/web] server-only module(s) in the client bundle — a module marked
|
|
2696
|
+
`import "@nifrajs/web/server-only"` reached the browser.
|
|
2697
|
+
|
|
2698
|
+
- server-only module reached the client bundle via routes/x.tsx → ./secrets.ts (marked
|
|
2699
|
+
server-only)
|
|
2700
|
+
|
|
2701
|
+
There are three markers; reach for them by intent:
|
|
2702
|
+
|
|
2703
|
+
marker
|
|
2704
|
+
enforcement
|
|
2705
|
+
use when
|
|
2706
|
+
|
|
2707
|
+
`*.server.ts` filename
|
|
2708
|
+
|
|
2709
|
+
client build auto-empties the module
|
|
2710
|
+
|
|
2711
|
+
a dedicated server module (a DB client, a `node:` helper) you can name
|
|
2712
|
+
`*.server`
|
|
2713
|
+
|
|
2714
|
+
`import "@nifrajs/web/server-only"`
|
|
2715
|
+
|
|
2716
|
+
client build fails loud, with the chain, if it leaks
|
|
2717
|
+
|
|
2718
|
+
pure server logic with **no `node:` import to catch** (a secret,
|
|
2719
|
+
a server-only call) that you can't / don't want to rename
|
|
2720
|
+
|
|
2721
|
+
`ServerOnly` type
|
|
2722
|
+
|
|
2723
|
+
type-level intent only — does NOT keep it out of the bundle
|
|
2724
|
+
|
|
2725
|
+
documenting that a value must not cross to the browser; pair it with one of the two
|
|
2726
|
+
runtime markers
|
|
2727
|
+
|
|
2728
|
+
A worked example — a secret with no `node:` import, marked so a leak fails the build
|
|
2729
|
+
rather than shipping the key to every visitor:
|
|
2730
|
+
|
|
2731
|
+
```ts
|
|
2732
|
+
// secrets.ts — pure server logic: a secret constant, no `node:` import to catch.
|
|
2733
|
+
import "@nifrajs/web/server-only" // ← fails the CLIENT build (loud, with the import chain)
|
|
2734
|
+
import type { ServerOnly } from "@nifrajs/web" // ← type-level intent: this value is server-only
|
|
2735
|
+
|
|
2736
|
+
// `ServerOnly<string>` is structurally `string` (the brand is an optional phantom field), so it
|
|
2737
|
+
// stays assignment-compatible — it documents intent without obstructing real use.
|
|
2738
|
+
export const apiKey: ServerOnly<string> = process.env.SECRET_API_KEY!
|
|
2739
|
+
|
|
2740
|
+
// If this module ever reaches a browser chunk, buildClient fails with:
|
|
2741
|
+
// server-only module reached the client bundle via routes/x.tsx → ./secrets.ts (marked server-only)
|
|
2742
|
+
// Reach it from a loader/action (server-only) instead, and the secret never ships to the client.
|
|
2743
|
+
```
|
|
2744
|
+
|
|
2745
|
+
**Fix:** reach the module from a loader/action (server-only), or rename it
|
|
2746
|
+
`*.server.ts` so the client build empties it. `nifra check` reports the
|
|
2747
|
+
same transitive chain pre-build. The `ServerOnly` brand on its own is purely
|
|
2748
|
+
type-level (it erases at build), so always back it with the import marker or the
|
|
2749
|
+
`.server` filename.
|
|
2750
|
+
|
|
2751
|
+
### `resolveDispatcher` / `Invalid hook call` — duplicate React
|
|
2752
|
+
|
|
2753
|
+
If SSR throws `Cannot read properties of null (reading 'useState')` inside
|
|
2754
|
+
`resolveDispatcher`, or React logs **"Invalid hook call. Hooks can only be
|
|
2755
|
+
called inside the body of a function component"**, you almost certainly have
|
|
2756
|
+
**two copies of React** in one render. React's hook dispatcher is module-level
|
|
2757
|
+
global state; a second copy nulls it out and every hook throws.
|
|
2758
|
+
|
|
2759
|
+
Nifra **dedupes React** in both the production build and the Vite dev server, so
|
|
2760
|
+
the framework itself won't load two copies. The usual culprit is a
|
|
2761
|
+
**`file:`-linked package** (a local component library you
|
|
2762
|
+
`bun link` or reference with `file:../lib`) that bundles its own React in
|
|
2763
|
+
its own `node_modules`:
|
|
2764
|
+
|
|
2765
|
+
-
|
|
2766
|
+
Make React a **peer dependency** of the linked package (not a regular
|
|
2767
|
+
dependency), so it resolves to the app's single copy.
|
|
2768
|
+
|
|
2769
|
+
-
|
|
2770
|
+
Ensure one React version across the workspace — pin it in the root
|
|
2771
|
+
`package.json` `overrides` (Nifra's own repo pins
|
|
2772
|
+
`react` / `react-dom` this way) so every package resolves the same
|
|
2773
|
+
copy.
|
|
2774
|
+
|
|
2775
|
+
-
|
|
2776
|
+
Delete the linked package's nested `node_modules/react` after linking if your
|
|
2777
|
+
package manager duplicated it.
|
|
2778
|
+
|
|
2779
|
+
[!NOTE]
|
|
2780
|
+
|
|
2781
|
+
This applies to every framework with module-global render state, not just React (Preact,
|
|
2782
|
+
Vue, Solid, Svelte). Nifra dedupes the active adapter's runtime in build and dev; a
|
|
2783
|
+
`file:`-linked package shipping its own copy is the thing to fix. See
|
|
2784
|
+
[Dev & HMR](/docs/dev) and the `file:`-linked-package note in
|
|
2785
|
+
`AGENTS.md`.
|
|
2786
|
+
|
|
2787
|
+
### Still stuck?
|
|
2788
|
+
|
|
2789
|
+
Run `nifra check --json` as the done-gate — it surfaces the import-chain leaks,
|
|
2790
|
+
typed-client drift, and raw-`Response`-from-a-route issues before you ship. The
|
|
2791
|
+
full machine-readable contract is at [/llms-full.txt](/llms-full.txt), and each
|
|
2792
|
+
package ships a tight `LLM.md` contract card.
|
|
2793
|
+
|
|
2794
|
+
## Nifra — Backends & API (dev and prod)
|
|
2795
|
+
|
|
2796
|
+
> How a Nifra backend reaches your app: inProcessClient feeds ctx.api to loaders, and createWebApp auto-mounts the backend over HTTP at /api/* — in `nifra dev` and production alike, no hand-dispatch.
|
|
2797
|
+
|
|
2798
|
+
Your `@nifrajs/core` backend reaches a Nifra app two ways from a single wiring.
|
|
2799
|
+
`inProcessClient(backend)` is fed to every loader and action as
|
|
2800
|
+
`ctx.api` — and `createWebApp` now **auto-mounts** that
|
|
2801
|
+
backend over HTTP at `/api/*`, so the browser can call the very same routes. No
|
|
2802
|
+
hand-written `if (pathname.startsWith("/api/"))` branch in your server entry.
|
|
2803
|
+
|
|
2804
|
+
### One backend, two call paths
|
|
2805
|
+
|
|
2806
|
+
Write the backend once. It defines its routes at the full `/api/…` path (the mount
|
|
2807
|
+
does no path stripping). Loaders call it _in-process_ during SSR; the browser calls it
|
|
2808
|
+
_over HTTP_. Both run the identical lifecycle — validation, middleware, contracts.
|
|
2809
|
+
|
|
2810
|
+
```ts
|
|
2811
|
+
// backend.ts — a normal @nifrajs/core server. Routes live at the full /api/... path.
|
|
2812
|
+
import { server } from "@nifrajs/core"
|
|
2813
|
+
import { t } from "@nifrajs/schema"
|
|
2814
|
+
|
|
2815
|
+
export const backend = server()
|
|
2816
|
+
.post("/api/sync", { body: t.object({ cursor: t.string() }) }, async (c) => {
|
|
2817
|
+
// validated body; runs the same whether called in-process (a loader) or over HTTP (a client fetch)
|
|
2818
|
+
return { applied: 12, nextCursor: c.body.cursor }
|
|
2819
|
+
})
|
|
2820
|
+
.get("/api/me", (c) => ({ id: c.cookies.session ?? null }))
|
|
2821
|
+
```
|
|
2822
|
+
|
|
2823
|
+
### ctx.api — the in-process loader client
|
|
2824
|
+
|
|
2825
|
+
Pass `inProcessClient(backend)` as `createWebApp`'s `api`.
|
|
2826
|
+
Inside a loader or action, `ctx.api` is that typed client, and a call goes
|
|
2827
|
+
**straight to the backend's `fetch` in-process** — no network hop, no
|
|
2828
|
+
port, the full real lifecycle. This is the SSR data path; it never touches the HTTP mount.
|
|
2829
|
+
|
|
2830
|
+
```ts
|
|
2831
|
+
// routes/index.tsx — a loader calls the backend IN-PROCESS via ctx.api (no network).
|
|
2832
|
+
import type { LoaderContext } from "@nifrajs/web"
|
|
2833
|
+
|
|
2834
|
+
export async function loader(ctx: LoaderContext) {
|
|
2835
|
+
const api = ctx.api as { me: { get(): Promise<{ data: { id: string | null } }> } }
|
|
2836
|
+
const res = await api.me.get() // in-process: full validation/middleware, no HTTP round-trip
|
|
2837
|
+
return { me: res.data }
|
|
2838
|
+
}
|
|
2839
|
+
```
|
|
2840
|
+
|
|
2841
|
+
### The auto-mounted /api/* (the new part)
|
|
2842
|
+
|
|
2843
|
+
Before, `inProcessClient` fed `ctx.api` but did _not_ serve the
|
|
2844
|
+
backend over HTTP — so a browser `POST /api/sync` hit the page router and 404/405'd
|
|
2845
|
+
until you hand-wrote a dispatch branch in `server-bun.ts`. Now
|
|
2846
|
+
`createWebApp` mounts it for you: a request whose pathname is exactly
|
|
2847
|
+
`apiPrefix` (default `/api`) or starts with
|
|
2848
|
+
`apiPrefix + "/"` is dispatched to `api.fetch(req.url, req)`
|
|
2849
|
+
**before** page routing, and the backend's `Response` is returned
|
|
2850
|
+
untouched (the request body is passed through, never pre-read).
|
|
2851
|
+
|
|
2852
|
+
```ts
|
|
2853
|
+
// server.ts (prod) — createWebApp serves pages AND auto-mounts the backend at /api/*.
|
|
2854
|
+
import { inProcessClient } from "@nifrajs/client"
|
|
2855
|
+
import { createWebApp } from "@nifrajs/web"
|
|
2856
|
+
import { reactAdapter } from "@nifrajs/web-react"
|
|
2857
|
+
import { backend } from "./backend"
|
|
2858
|
+
import { clientEntry, manifest } from "./server-manifest"
|
|
2859
|
+
|
|
2860
|
+
export const app = createWebApp({
|
|
2861
|
+
adapter: reactAdapter,
|
|
2862
|
+
manifest,
|
|
2863
|
+
clientEntry,
|
|
2864
|
+
api: inProcessClient(backend), // → ctx.api in loaders/actions AND auto-mounted at /api/*
|
|
2865
|
+
// apiPrefix: "/api", // the default; pass "" to disable the HTTP mount (pages only)
|
|
2866
|
+
})
|
|
2867
|
+
|
|
2868
|
+
// Bun: Bun.serve({ fetch: app.fetch }). No `if (pathname.startsWith("/api/")) …` branch needed —
|
|
2869
|
+
// POST /api/sync, GET /api/me, etc. are dispatched to the backend BEFORE the page router sees them.
|
|
2870
|
+
```
|
|
2871
|
+
|
|
2872
|
+
The dispatch runs in `createWebApp`'s request lifecycle, ahead of the page wildcard,
|
|
2873
|
+
for **every method** — so `GET`/`POST`/`PUT`/… all
|
|
2874
|
+
reach the backend, and an unknown `/api/…` path returns the _backend's_ 404,
|
|
2875
|
+
not the page's. A sibling path that merely shares the prefix string (e.g.
|
|
2876
|
+
`/apidocs`) is **not** captured — only the `/api` boundary
|
|
2877
|
+
is. Pass `apiPrefix: ""` to turn the mount off and keep `ctx.api` as a
|
|
2878
|
+
loader-only client.
|
|
2879
|
+
|
|
2880
|
+
[!NOTE] The mount lives in `createWebApp`, and `nifra dev` (the
|
|
2881
|
+
Vite-backed dev server) routes every request through that same app's `fetch`. So
|
|
2882
|
+
the `/api/*` routes are served identically in development and production — there is
|
|
2883
|
+
nothing extra to wire for the dev loop.
|
|
2884
|
+
|
|
2885
|
+
### Calling /api/* from the browser
|
|
2886
|
+
|
|
2887
|
+
With the routes mounted, a client island or component hits them with the same typed
|
|
2888
|
+
`client<typeof backend>` — same-origin, no separate API server.
|
|
2889
|
+
|
|
2890
|
+
```ts
|
|
2891
|
+
// A browser island / client component hits the mounted HTTP routes with the typed client.
|
|
2892
|
+
import { client } from "@nifrajs/client"
|
|
2893
|
+
import type { backend } from "./backend"
|
|
2894
|
+
|
|
2895
|
+
const api = client<typeof backend>("") // same-origin: /api/sync is served by createWebApp's mount
|
|
2896
|
+
export async function runSync(cursor: string) {
|
|
2897
|
+
const { data, error } = await api.api.sync.post({ cursor })
|
|
2898
|
+
return error ? { applied: 0 } : data
|
|
2899
|
+
}
|
|
2900
|
+
```
|
|
2901
|
+
|
|
2902
|
+
### Route actions vs the in-process backend (for mutations)
|
|
2903
|
+
|
|
2904
|
+
Two valid ways to mutate; pick by where the call originates.
|
|
2905
|
+
|
|
2906
|
+
-
|
|
2907
|
+
**Route `action`** — the form/SSR path. A
|
|
2908
|
+
`` (or the client submit) runs the route's
|
|
2909
|
+
`action(ctx)`, which typically calls `ctx.api` in-process and returns
|
|
2910
|
+
`actionData` (or a `redirect`). Progressive-enhancement: works with JS
|
|
2911
|
+
off, and the loader revalidates after. Reach for this for page-driven mutations tied to a
|
|
2912
|
+
route's UI.
|
|
2913
|
+
|
|
2914
|
+
-
|
|
2915
|
+
**The mounted `/api/*` backend** — the programmatic path. A browser
|
|
2916
|
+
island, a third party, a webhook, or a non-page client calls the HTTP route directly. Reach
|
|
2917
|
+
for this when the caller isn't a Nifra route's form — an RPC the page makes on an
|
|
2918
|
+
interaction, an external integration, a mobile client.
|
|
2919
|
+
|
|
2920
|
+
They share the backend: a route `action` and a browser `fetch` can both
|
|
2921
|
+
call `POST /api/sync` — one in-process, one over the mount — and get identical
|
|
2922
|
+
validation and behavior.
|
|
2923
|
+
|
|
2924
|
+
## Nifra — Framework contract
|
|
2925
|
+
|
|
2926
|
+
> The one-page contract: LoaderContext shape, loader/action/meta/head signatures, the client↔server boundary, and the build-output contract — every signature verified against the source.
|
|
2927
|
+
|
|
2928
|
+
Everything you'd otherwise read the source to learn, on one page: the
|
|
2929
|
+
`LoaderContext` shape, the loader / action / meta / head signatures, what runs on
|
|
2930
|
+
the server vs the client, the two ways to handle a request, the dynamic-route 404 rule, and
|
|
2931
|
+
the build-output contract. Every signature here is verified against the
|
|
2932
|
+
`@nifrajs/web` source.
|
|
2933
|
+
|
|
2934
|
+
### LoaderContext
|
|
2935
|
+
|
|
2936
|
+
One context object is passed to every `loader` and every `action`. The
|
|
2937
|
+
same five fields, always — `params`, `request`, `api`,
|
|
2938
|
+
`env`, and `draft`.
|
|
2939
|
+
|
|
2940
|
+
```ts
|
|
2941
|
+
// The single context object passed to every loader AND action (@nifrajs/web).
|
|
2942
|
+
interface LoaderContext {
|
|
2943
|
+
params: Record<string, string> // matched route params (:id, *path)
|
|
2944
|
+
request: Request // the standard Web Request — read headers/body/URL off it
|
|
2945
|
+
api: unknown // the in-process backend client createWebApp was given
|
|
2946
|
+
env: unknown // platform bindings forwarded from c.env (Workers KV/D1/…)
|
|
2947
|
+
draft: boolean // true only when a valid draft cookie is present (draftSecret set)
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2950
|
+
// api + env are typed per-route via @nifrajs/client's LoaderArgs<Api, Env>; the agnostic core
|
|
2951
|
+
// keeps them `unknown`. Branch on `draft` to load unpublished content for preview/editors.
|
|
2952
|
+
```
|
|
2953
|
+
|
|
2954
|
+
### Loader, action & meta signatures
|
|
2955
|
+
|
|
2956
|
+
A route module co-locates its data, mutation, head, and view. Only the
|
|
2957
|
+
`default` export (the component) is required; `loader`,
|
|
2958
|
+
`action`, `meta`, and `hydrate` are all optional. A
|
|
2959
|
+
`loader` runs on **GET** and feeds `props.data`; an
|
|
2960
|
+
`action` runs on **POST** and feeds `props.actionData`.
|
|
2961
|
+
|
|
2962
|
+
```ts
|
|
2963
|
+
// routes/users/[id].tsx — a route module's full contract (every export optional but `default`).
|
|
2964
|
+
// doc-check: skip — full-contract sketch; ctx.api's routes and meta's loader-data shape come from
|
|
2965
|
+
// the reader's own backend type, so it can't compile standalone (see the typed examples elsewhere).
|
|
2966
|
+
import type { LoaderContext } from "@nifrajs/web"
|
|
2967
|
+
|
|
2968
|
+
// GET → data for the page. Runs in-process on the server during SSR (no HTTP hop).
|
|
2969
|
+
export async function loader(ctx: LoaderContext) {
|
|
2970
|
+
return { user: await ctx.api.users.get({ id: ctx.params.id }) }
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
// POST → a mutation. Same context. Return a Response (e.g. a redirect — passed straight
|
|
2974
|
+
// through) OR data, which reaches the component as `actionData`.
|
|
2975
|
+
export async function action(ctx: LoaderContext) {
|
|
2976
|
+
const form = await ctx.request.formData()
|
|
2977
|
+
return { ok: true }
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
// Static OR a function of { data, params, origin }. Static meta is serialized once; function meta
|
|
2981
|
+
// recomputes per request (its content can vary with loader data). `origin` is the request's
|
|
2982
|
+
// scheme+host (server-resolved, matches the client's location.origin) — use it for ABSOLUTE
|
|
2983
|
+
// canonical / og:url / og:image URLs without threading siteUrl through loader data.
|
|
2984
|
+
export const meta = ({ data, params, origin }) => ({
|
|
2985
|
+
title: `User ${data.user.name}`,
|
|
2986
|
+
meta: [{ name: "description", content: data.user.bio }],
|
|
2987
|
+
link: [{ rel: "canonical", href: `${origin}/users/${params.id}` }],
|
|
2988
|
+
})
|
|
2989
|
+
|
|
2990
|
+
export const hydrate = false // opt out of full-document hydration (static / island pages)
|
|
2991
|
+
export default function User(props) { /* props.data, props.actionData, props.params */ }
|
|
2992
|
+
```
|
|
2993
|
+
|
|
2994
|
+
[!NOTE] An `action` may return a `Response` (a `redirect()`,
|
|
2995
|
+
say) — it's passed through untouched. Any other return is serialized as
|
|
2996
|
+
`actionData`.
|
|
2997
|
+
|
|
2998
|
+
### Two ways to handle a request
|
|
2999
|
+
|
|
3000
|
+
A page's data comes from a `loader` calling the **in-process backend**
|
|
3001
|
+
(`ctx.api`) — but that backend is **loaders-only**: it registers
|
|
3002
|
+
`GET` handlers, so a `.post()` to `ctx.api` answers
|
|
3003
|
+
**405**. A **public POST** (a browser form or `fetch`) is
|
|
3004
|
+
a route **action** export, not an `inProcessClient(backend).post()`
|
|
3005
|
+
call.
|
|
3006
|
+
|
|
3007
|
+
```ts
|
|
3008
|
+
// TWO ways to handle a request — pick by who is calling.
|
|
3009
|
+
import type { LoaderContext } from "@nifrajs/web"
|
|
3010
|
+
|
|
3011
|
+
// (1) A page's own data + mutations → route exports. A PUBLIC POST is a route ACTION
|
|
3012
|
+
// (routes/contact.tsx) — the browser POSTs the route's own URL, nifra runs `action`:
|
|
3013
|
+
export async function action(ctx: LoaderContext) {
|
|
3014
|
+
const form = await ctx.request.formData() // the browser POSTed this route
|
|
3015
|
+
return { ok: true, body: String(form.get("body")) } // → props.actionData on re-render
|
|
3016
|
+
}
|
|
3017
|
+
|
|
3018
|
+
// (2) ctx.api is the IN-PROCESS backend client (the inProcessClient(server) the app was given).
|
|
3019
|
+
// It is LOADERS-ONLY: the SSR backend registers GET handlers, so a .post() to ctx.api 405s.
|
|
3020
|
+
export async function loader(ctx: LoaderContext) {
|
|
3021
|
+
// ✅ a GET in-process — no HTTP hop. (A .post() to ctx.api would 405; write an `action` instead.)
|
|
3022
|
+
return { id: ctx.params.id }
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
// Rule of thumb: data INTO a page → loader + ctx.api (GET). A mutation FROM the browser
|
|
3026
|
+
// (a form/fetch POST) → an `action` export on the route. Don't `inProcessClient(api).post()`.
|
|
3027
|
+
```
|
|
3028
|
+
|
|
3029
|
+
### Dynamic routes & the 404 / control-flow rule
|
|
3030
|
+
|
|
3031
|
+
A `[param]` route's loader runs for _any_ value on plain SSR —
|
|
3032
|
+
`getStaticPaths`'s `fallback: "404"` is enforced only by the prerender /
|
|
3033
|
+
CDN layer, never by the on-demand worker. So a loader that can't find its record
|
|
3034
|
+
**must** 404 itself. The mechanism is the throw contract:
|
|
3035
|
+
|
|
3036
|
+
```ts
|
|
3037
|
+
// routes/users/[id].tsx — plain SSR runs the loader for ANY :id value. `fallback: "404"`
|
|
3038
|
+
// only takes effect under prerender/CDN, so on on-demand SSR you MUST guard and 404 yourself.
|
|
3039
|
+
import type { LoaderContext } from "@nifrajs/web"
|
|
3040
|
+
|
|
3041
|
+
declare function lookupUser(id: string): Promise<{ id: string } | null>
|
|
3042
|
+
|
|
3043
|
+
export async function loader(ctx: LoaderContext) {
|
|
3044
|
+
// ctx.params.id is `string | undefined` (an optional segment) — default it before the lookup.
|
|
3045
|
+
const user = await lookupUser(ctx.params.id ?? "")
|
|
3046
|
+
if (!user) {
|
|
3047
|
+
// CONTROL FLOW: a thrown Response is an INTENTIONAL HTTP response — it propagates untouched
|
|
3048
|
+
// (no _error boundary, no 500). This is how you 404 a missing record.
|
|
3049
|
+
throw new Response("Not Found", { status: 404 })
|
|
3050
|
+
}
|
|
3051
|
+
return { user }
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
declare function lookupUser(id: string): Promise<{ name: string } | null>
|
|
3055
|
+
|
|
3056
|
+
// The throw contract, stated once:
|
|
3057
|
+
// • throw a Response → that exact HTTP response is sent (404 / redirect() / 403 / …). Control flow.
|
|
3058
|
+
// • throw an Error → renders the nearest _error boundary if one exists, else a 500. A bug.
|
|
3059
|
+
// So NEVER `throw new Error("not found")` to mean 404 — that is a 500. Throw a 404 Response.
|
|
3060
|
+
```
|
|
3061
|
+
|
|
3062
|
+
**Throwing a `Response` is control flow** — that exact HTTP response
|
|
3063
|
+
(a 404, a `redirect()`, a 403) is sent untouched, bypassing the
|
|
3064
|
+
`_error` boundary. **Throwing an `Error` is a fault** — it
|
|
3065
|
+
renders the nearest `_error` boundary (or a 500 if there is none). Never throw an
|
|
3066
|
+
`Error` to signal a 404.
|
|
3067
|
+
|
|
3068
|
+
### Meta & head rendering rules
|
|
3069
|
+
|
|
3070
|
+
A route's `<head>` is its **layout chain's** head merged with
|
|
3071
|
+
the page's — so a `_layout.tsx` can export `meta` to put sitewide tags
|
|
3072
|
+
(`hreflang`, `preconnect`, a section `<title>`) on every
|
|
3073
|
+
page below it.
|
|
3074
|
+
|
|
3075
|
+
```ts
|
|
3076
|
+
// routes/_layout.tsx — a layout can export `meta` too. Its tags are SITEWIDE: they land in
|
|
3077
|
+
// the <head> of every page below it — the home for hreflang / preconnect / a section <title>.
|
|
3078
|
+
export const meta = {
|
|
3079
|
+
link: [
|
|
3080
|
+
{ rel: "preconnect", href: "https://cdn.example.com", crossorigin: "anonymous" },
|
|
3081
|
+
{ rel: "alternate", hreflang: "es", href: "https://example.com/es" },
|
|
3082
|
+
],
|
|
3083
|
+
title: "Docs", // section default — a child page's title overrides it
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
// The route's final <head> = its LAYOUT CHAIN's meta merged with the page's:
|
|
3087
|
+
// • title (and other scalars): NEAREST-WINS — page overrides inner layout overrides outer.
|
|
3088
|
+
// An undefined page title keeps the layout's.
|
|
3089
|
+
// • meta / link arrays: CONCATENATED outermost-layout → … → page (so the page's canonical
|
|
3090
|
+
// comes after the layout's links).
|
|
3091
|
+
// <link> attributes are typed (LinkDescriptor: a partial like { rel, href, hreflang } is assignable),
|
|
3092
|
+
// name-validated (any letter/digit/hyphen name) and value-escaped against XSS — so
|
|
3093
|
+
// rel/href/hreflang/crossorigin/media/sizes/as/integrity/fetchpriority/… all survive. A boolean
|
|
3094
|
+
// attribute (e.g. `disabled: true`) renders bare; `false`/`undefined` is omitted.
|
|
3095
|
+
```
|
|
3096
|
+
|
|
3097
|
+
The merge is **nearest-wins** for scalars (the page's `title` beats an
|
|
3098
|
+
inner layout's, which beats an outer one) and **concatenated** for the
|
|
3099
|
+
`meta` / `link` arrays (outermost layout first, page last). A
|
|
3100
|
+
`link` entry is a typed `LinkDescriptor` — a partial like
|
|
3101
|
+
`{ rel, href, hreflang }` is assignable, custom / `data-*` attrs
|
|
3102
|
+
pass through, and every `<link>` attribute with a normal name —
|
|
3103
|
+
`rel`, `href`, `hreflang`, `crossorigin`,
|
|
3104
|
+
`media`, `sizes`, `type`, `as`,
|
|
3105
|
+
`integrity`, `referrerpolicy`, `fetchpriority`,
|
|
3106
|
+
`imagesrcset` — survives into the SSR'd tag; values are HTML-escaped against XSS.
|
|
3107
|
+
|
|
3108
|
+
### SEO: Open Graph, Twitter cards & JSON-LD
|
|
3109
|
+
|
|
3110
|
+
A route's `meta` emits the full social / structured-data head with three public
|
|
3111
|
+
helpers — `canonical()`, `openGraph()`, and `jsonLd()` — and
|
|
3112
|
+
builds **absolute** URLs from the `origin` argument. Because
|
|
3113
|
+
`meta()` runs in **both** SSR and client navigation it has
|
|
3114
|
+
**no server env**: never read `process.env` or the request inside it.
|
|
3115
|
+
The framework injects `origin` (the site's scheme + host, e.g.
|
|
3116
|
+
`https://news.example.com`) from the request on the server and from
|
|
3117
|
+
`location.origin` on the client, so the two match and an absolute
|
|
3118
|
+
`og:url` / `canonical` / `og:image` never drifts between the
|
|
3119
|
+
server-rendered `<head>` and a soft-nav. Anything else server-only (an API
|
|
3120
|
+
base, a CDN host) belongs in the `loader`, not `meta()`.
|
|
3121
|
+
|
|
3122
|
+
```ts
|
|
3123
|
+
// routes/articles/[slug].tsx — a complete SEO head: canonical + Open Graph + Twitter + JSON-LD,
|
|
3124
|
+
// with ABSOLUTE URLs built from `origin`. The three helpers (canonical/openGraph/jsonLd) are public
|
|
3125
|
+
// exports of @nifrajs/web; `origin` is the third MetaArgs field (after data + params).
|
|
3126
|
+
import { canonical, jsonLd, openGraph, type MetaArgs } from "@nifrajs/web"
|
|
3127
|
+
|
|
3128
|
+
// CAVEAT: meta() runs in BOTH SSR and client navigation, so it has NO server env — never read
|
|
3129
|
+
// `process.env` or the request here. The framework injects `origin` (the site's scheme+host, e.g.
|
|
3130
|
+
// "https://news.example.com") server-side from the request AND on the client from location.origin, so
|
|
3131
|
+
// the two match and an absolute og:url/canonical/og:image never drifts between SSR and a soft-nav. For
|
|
3132
|
+
// anything else server-only (an API base, a CDN host), thread it through the LOADER, not meta().
|
|
3133
|
+
export const meta = ({ data, params, origin }: MetaArgs) => {
|
|
3134
|
+
const article = data as { title: string; summary: string; image: string; published: string }
|
|
3135
|
+
const url = `${origin}/articles/${params.slug}` // absolute, from the injected origin
|
|
3136
|
+
const image = `${origin}${article.image}` // og:image MUST be absolute for crawlers
|
|
3137
|
+
return {
|
|
3138
|
+
title: article.title,
|
|
3139
|
+
meta: [
|
|
3140
|
+
{ name: "description", content: article.summary },
|
|
3141
|
+
// og:* — openGraph emits only the props you pass (+ og:type, here "article").
|
|
3142
|
+
...openGraph({
|
|
3143
|
+
title: article.title,
|
|
3144
|
+
description: article.summary,
|
|
3145
|
+
url,
|
|
3146
|
+
image,
|
|
3147
|
+
type: "article",
|
|
3148
|
+
}),
|
|
3149
|
+
// twitter:* — a large-image summary card sharing the same absolute image.
|
|
3150
|
+
{ name: "twitter:card", content: "summary_large_image" },
|
|
3151
|
+
{ name: "twitter:title", content: article.title },
|
|
3152
|
+
{ name: "twitter:description", content: article.summary },
|
|
3153
|
+
{ name: "twitter:image", content: image },
|
|
3154
|
+
],
|
|
3155
|
+
link: [canonical(url)], // <link rel="canonical"> — the authoritative URL
|
|
3156
|
+
// JSON-LD structured data — escaped for safe <script> embedding by the head renderer.
|
|
3157
|
+
script: [
|
|
3158
|
+
jsonLd({
|
|
3159
|
+
"@context": "https://schema.org",
|
|
3160
|
+
"@type": "NewsArticle",
|
|
3161
|
+
headline: article.title,
|
|
3162
|
+
image,
|
|
3163
|
+
datePublished: article.published,
|
|
3164
|
+
mainEntityOfPage: url,
|
|
3165
|
+
}),
|
|
3166
|
+
],
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
```
|
|
3170
|
+
|
|
3171
|
+
[!NOTE] `og:image` (and the Twitter image) **must be absolute** for
|
|
3172
|
+
crawlers — build it from `origin`. `openGraph()` emits only the
|
|
3173
|
+
properties you pass (plus `og:type`); `jsonLd()`'s payload is escaped for
|
|
3174
|
+
safe `<script>` embedding by the head renderer.
|
|
3175
|
+
|
|
3176
|
+
### The client ↔ server boundary
|
|
3177
|
+
|
|
3178
|
+
Loaders, actions, and head resolution run on the server. The client receives the loader's
|
|
3179
|
+
_return value_ (serialized into the document), never the loader, the `api`,
|
|
3180
|
+
or `env`. Keep server-only imports out of a route file's top level.
|
|
3181
|
+
|
|
3182
|
+
```ts
|
|
3183
|
+
// What runs WHERE:
|
|
3184
|
+
//
|
|
3185
|
+
// SERVER (Bun / edge) CLIENT (browser)
|
|
3186
|
+
// ───────────────────── ──────────────────────────
|
|
3187
|
+
// loader / action event handlers, useState/effects
|
|
3188
|
+
// meta / head resolution soft-nav re-fetch of loader data
|
|
3189
|
+
// SSR of the layout chain hydration + Fast Refresh (dev)
|
|
3190
|
+
// backend (api, env, draft) —
|
|
3191
|
+
//
|
|
3192
|
+
// The client gets the loader's RETURN VALUE (serialized into the document), never the loader
|
|
3193
|
+
// itself, the api, or env. So:
|
|
3194
|
+
// 1. Never import server-only modules (Bun, a DB client, secrets) at the top level of a route
|
|
3195
|
+
// file — the bundler pulls it into the client chunk and it crashes / leaks. (The build now
|
|
3196
|
+
// FAILS with a named error if a `node:` built-in reaches a client chunk — see below.)
|
|
3197
|
+
// 2. A client soft-nav re-runs the loader over the network (X-Nifra-Data) and re-merges the
|
|
3198
|
+
// same layout-chain head, so sitewide tags persist across navigation — no page-only flash.
|
|
3199
|
+
```
|
|
3200
|
+
|
|
3201
|
+
### The build-output contract
|
|
3202
|
+
|
|
3203
|
+
`nifra build` emits a content-hashed `dist/` plus a
|
|
3204
|
+
`manifest.json` you hand to `createWebApp` (`entry`,
|
|
3205
|
+
`routes` for per-route preload, `css` + `routeStyles` for
|
|
3206
|
+
`<link>` injection).
|
|
3207
|
+
|
|
3208
|
+
```ts
|
|
3209
|
+
// `nifra build` → buildClient (Bun.build) writes dist/ + manifest.json. The contract:
|
|
3210
|
+
{
|
|
3211
|
+
"entry": "/assets/_nifra-entry-HASH.js", // the bootstrap module script
|
|
3212
|
+
"assets": ["/assets/…"], // every emitted chunk URL
|
|
3213
|
+
"routes": { "users/[id]": ["/assets/_layout-H.js", "/assets/_slug_-H.js"] },
|
|
3214
|
+
"css": ["/assets/app-H.css"], // aggregate stylesheets (fallback)
|
|
3215
|
+
"routeStyles": { "users/[id]": ["/assets/_layout-H.css"] } // per-route CSS (chain only)
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
// Invariants worth knowing:
|
|
3219
|
+
// • Chunk names are URL-PATH-SAFE. A dynamic-route file [slug].tsx would emit `[slug]-H.js`,
|
|
3220
|
+
// whose [ ] make a static server 400 the request. The build renames it (`[slug]` → `_slug_`)
|
|
3221
|
+
// and rewrites every reference, so the lazy import resolves and the route hydrates.
|
|
3222
|
+
// • `process.env` is compiled away. `process.env` → `({})` and `process.env.NODE_ENV` → the
|
|
3223
|
+
// build mode; every other `process.env.X` becomes undefined — EXCEPT names you opt in with the
|
|
3224
|
+
// PUBLIC_ prefix (Vite/Next convention), which are baked in with their value (see below). No
|
|
3225
|
+
// `process is not defined` crash, and no unprefixed (secret) env leaking into the client bundle.
|
|
3226
|
+
// • A `node:` built-in in the CLIENT bundle FAILS the build with a named error (move it behind a
|
|
3227
|
+
// loader/action — server-only). It builds via a browser polyfill otherwise, then breaks at runtime.
|
|
3228
|
+
// • Assets are content-hashed + immutable. Serve /assets/* with a long-lived cache header.
|
|
3229
|
+
```
|
|
3230
|
+
|
|
3231
|
+
### Public env in the client bundle
|
|
3232
|
+
|
|
3233
|
+
`process.env` is compiled away in the browser bundle, so a bare read can't crash
|
|
3234
|
+
hydration and an unprefixed (secret) var resolves to `undefined` — it never reaches
|
|
3235
|
+
the client. To expose a value, give it the `PUBLIC_` prefix (the Vite / Next
|
|
3236
|
+
convention) in the build environment and it's baked in by value. The prefix is overridable via
|
|
3237
|
+
`buildClient`'s `publicEnvPrefix` (`""` disables it).
|
|
3238
|
+
|
|
3239
|
+
```ts
|
|
3240
|
+
// process.env in the CLIENT bundle is compiled at build time:
|
|
3241
|
+
// process.env → ({}) (a bare read won't crash hydration)
|
|
3242
|
+
// process.env.NODE_ENV → "production" (the build mode — frameworks' prod/dev branch)
|
|
3243
|
+
// process.env.SECRET_KEY → undefined (unprefixed → never exposed; no secret leak)
|
|
3244
|
+
// process.env.PUBLIC_API_URL → "https://api.example.com" (PUBLIC_-prefixed → baked in by value)
|
|
3245
|
+
//
|
|
3246
|
+
// So to ship a value to the browser, give it a PUBLIC_ prefix in the BUILD environment:
|
|
3247
|
+
// PUBLIC_API_URL=https://api.example.com bun run build
|
|
3248
|
+
// then read process.env.PUBLIC_API_URL in app code — it becomes a string literal after the build.
|
|
3249
|
+
import { buildClient } from "@nifrajs/web/build"
|
|
3250
|
+
|
|
3251
|
+
// Override the prefix (or disable auto-exposure) via buildClient:
|
|
3252
|
+
await buildClient({
|
|
3253
|
+
routesDir: "./routes",
|
|
3254
|
+
outDir: "./dist",
|
|
3255
|
+
clientModule: "@nifrajs/web-react/client",
|
|
3256
|
+
publicEnvPrefix: "NIFRA_PUBLIC_", // default "PUBLIC_"; "" disables auto-exposure entirely
|
|
3257
|
+
})
|
|
3258
|
+
```
|
|
3259
|
+
|
|
3260
|
+
### Static emit (prerender)
|
|
3261
|
+
|
|
3262
|
+
Opt a static route into SSG with `export const prerender = true` (or a dynamic
|
|
3263
|
+
route with `export const getStaticPaths`), then call `prerenderRoutes` —
|
|
3264
|
+
a **public** export of `@nifrajs/web/build`. It drives the app's own
|
|
3265
|
+
`fetch` to write `index.html` + `_data.json` per route;
|
|
3266
|
+
`cloudflarePagesRoutes` emits the `_routes.json` for a hybrid CDN +
|
|
3267
|
+
worker deploy. No need to boot a server and curl it.
|
|
3268
|
+
|
|
3269
|
+
```ts
|
|
3270
|
+
// build.ts — emit static HTML for opted-in routes. prerenderRoutes + cloudflarePagesRoutes
|
|
3271
|
+
// are PUBLIC (exported from @nifrajs/web/build) — no need to boot a server and curl it.
|
|
3272
|
+
import { buildClient, prerenderRoutes, cloudflarePagesRoutes } from "@nifrajs/web/build"
|
|
3273
|
+
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
3274
|
+
|
|
3275
|
+
// 1. Build the client bundle first (so the app references the hashed entry).
|
|
3276
|
+
await buildClient({ routesDir: "./routes", outDir: "./dist", clientModule: "@nifrajs/web-react/client" })
|
|
3277
|
+
|
|
3278
|
+
// 2. Drive the app's own fetch to render each opted-in route → dist/<path>/index.html + _data.json.
|
|
3279
|
+
// Static routes opt in with `export const prerender = true`; dynamic routes enumerate concrete
|
|
3280
|
+
// params with `export const getStaticPaths` (+ a `fallback: "ssr" | "404"`).
|
|
3281
|
+
const { app } = await import("./server")
|
|
3282
|
+
const { prerendered } = await prerenderRoutes({
|
|
3283
|
+
app, // the built createWebApp (a { fetch } is enough)
|
|
3284
|
+
routes: discoverRoutes("./routes").routes,
|
|
3285
|
+
outDir: "./dist",
|
|
3286
|
+
})
|
|
3287
|
+
|
|
3288
|
+
// 3. Hybrid deploy: a Cloudflare Pages _routes.json that serves the prerendered HTML + _data.json
|
|
3289
|
+
// from the CDN and falls everything else through to the SSR worker.
|
|
3290
|
+
const paths = prerendered.map((p) => p.path)
|
|
3291
|
+
await Bun.write("./dist/_routes.json", JSON.stringify(cloudflarePagesRoutes({ prerendered: paths })))
|
|
3292
|
+
```
|
|
3293
|
+
|
|
3294
|
+
See [SSG & ISR](/docs/rendering) for prerendering, [Dev & HMR](/docs/dev) for the
|
|
3295
|
+
two dev loops and the `--port` flag, and [Loaders & actions](/docs/data) for the
|
|
3296
|
+
data layer in depth.
|
|
3297
|
+
|
|
2587
3298
|
## Nifra — Databases
|
|
2588
3299
|
|
|
2589
3300
|
> Nifra is database-agnostic: use SQLite, Postgres, or any ORM (Drizzle, Prisma, Kysely) from your handlers and loaders, on Bun, Node, Deno, and the edge.
|
|
@@ -2644,7 +3355,7 @@ import { Database } from "bun:sqlite" // Node: better-sqlite3 · edge: Tu
|
|
|
2644
3355
|
|
|
2645
3356
|
// Open the connection ONCE at module scope (the driver pools internally).
|
|
2646
3357
|
const db = new Database("app.db")
|
|
2647
|
-
const byId = db.query<{ id: number; name: string }>("SELECT * FROM users WHERE id = ?")
|
|
3358
|
+
const byId = db.query<{ id: number; name: string }, [number]>("SELECT * FROM users WHERE id = ?")
|
|
2648
3359
|
|
|
2649
3360
|
export const app = server().get("/users/:id", (c) =>
|
|
2650
3361
|
// Parameterized (?) — never interpolate user input into SQL.
|
|
@@ -2772,6 +3483,7 @@ If you prefer parameterized queries over an ORM, use `postgres` or `pg`
|
|
|
2772
3483
|
directly:
|
|
2773
3484
|
|
|
2774
3485
|
```ts
|
|
3486
|
+
// doc-check: skip — illustrates a third-party driver (postgres + drizzle); install those to run it.
|
|
2775
3487
|
import { server } from "@nifrajs/core"
|
|
2776
3488
|
import { drizzle } from "drizzle-orm/postgres-js" // or node-postgres / neon-http
|
|
2777
3489
|
import postgres from "postgres"
|
|
@@ -3145,6 +3857,125 @@ backend you can also ship on its own. Start with [Getting started](/docs).
|
|
|
3145
3857
|
Moving a backend (Express, Hono, Fastify, Elysia) instead? See
|
|
3146
3858
|
[Migrating a backend](/docs/migrate-backend).
|
|
3147
3859
|
|
|
3860
|
+
## Nifra — Types-first architecture
|
|
3861
|
+
|
|
3862
|
+
> One schema is the single source of truth: it drives runtime validation, inferred TypeScript types, the no-codegen typed client, an OpenAPI document, and the MCP contract your agents read.
|
|
3863
|
+
|
|
3864
|
+
In Nifra a single schema is the **source of truth**. The same definition drives runtime
|
|
3865
|
+
validation, inferred TypeScript types, the no-codegen typed client, an OpenAPI document, and
|
|
3866
|
+
the contract your coding agents read — so the five never drift apart.
|
|
3867
|
+
|
|
3868
|
+
### One schema
|
|
3869
|
+
|
|
3870
|
+
Define request inputs and the response shape once with `t`. Nothing here is
|
|
3871
|
+
framework-specific; it's a plain object you attach to a route.
|
|
3872
|
+
|
|
3873
|
+
```ts
|
|
3874
|
+
import { t } from "@nifrajs/schema"
|
|
3875
|
+
|
|
3876
|
+
// One contract, defined once. Everything below is derived from it.
|
|
3877
|
+
export const GetUser = {
|
|
3878
|
+
params: t.object({ id: t.string() }),
|
|
3879
|
+
response: t.object({
|
|
3880
|
+
id: t.string(),
|
|
3881
|
+
name: t.string(),
|
|
3882
|
+
role: t.union([t.literal("admin"), t.literal("user")]),
|
|
3883
|
+
}),
|
|
3884
|
+
}
|
|
3885
|
+
```
|
|
3886
|
+
|
|
3887
|
+
### Runtime validation
|
|
3888
|
+
|
|
3889
|
+
Attach the schema to a route. Path params, query, and body are validated at the runtime
|
|
3890
|
+
boundary _before_ your handler runs — invalid input is rejected with a 400, so the
|
|
3891
|
+
handler only ever sees well-formed data.
|
|
3892
|
+
|
|
3893
|
+
```ts
|
|
3894
|
+
import { server } from "@nifrajs/core"
|
|
3895
|
+
import { GetUser } from "./schema"
|
|
3896
|
+
|
|
3897
|
+
export const app = server().get("/users/:id", GetUser, (c) => {
|
|
3898
|
+
// c.params.id is typed `string` — parsed from the path and validated at the boundary.
|
|
3899
|
+
return { id: c.params.id, name: "Ada", role: "admin" as const }
|
|
3900
|
+
// ^ the return is checked against GetUser.response — a wrong shape is a tsc error.
|
|
3901
|
+
})
|
|
3902
|
+
```
|
|
3903
|
+
|
|
3904
|
+
### Inferred types
|
|
3905
|
+
|
|
3906
|
+
The same schema types the handler: `c.params.id` is `string`, and the
|
|
3907
|
+
return value is checked against `response`. Change the schema and the handler
|
|
3908
|
+
stops compiling until it matches — types and validation can't disagree.
|
|
3909
|
+
|
|
3910
|
+
### The typed client
|
|
3911
|
+
|
|
3912
|
+
The client is inferred from the server's _type_ — no generators, no build step, no
|
|
3913
|
+
SDK to regenerate. Paths and params autocomplete; the response is typed from the route. A
|
|
3914
|
+
backend change that breaks a call is a compile error on the frontend.
|
|
3915
|
+
|
|
3916
|
+
```ts
|
|
3917
|
+
import { client } from "@nifrajs/client"
|
|
3918
|
+
import type { app } from "./server" // a TYPE import — server code never ships to the client
|
|
3919
|
+
|
|
3920
|
+
const api = client<typeof app>("https://api.example.com")
|
|
3921
|
+
|
|
3922
|
+
const res = await api.users({ id: "42" }).get() // path + params autocomplete, no codegen
|
|
3923
|
+
if (res.ok) {
|
|
3924
|
+
res.data.name // typed from the route's response schema
|
|
3925
|
+
} else {
|
|
3926
|
+
res.error // client-call failures are returned, never thrown
|
|
3927
|
+
}
|
|
3928
|
+
```
|
|
3929
|
+
|
|
3930
|
+
### OpenAPI
|
|
3931
|
+
|
|
3932
|
+
The `openapi()` middleware builds an OpenAPI 3.1 document from your registered
|
|
3933
|
+
routes and their schemas — generated lazily on first request, never hand-written. Pass
|
|
3934
|
+
`ui: true` to also serve a Scalar reference page.
|
|
3935
|
+
|
|
3936
|
+
```ts
|
|
3937
|
+
import { server } from "@nifrajs/core"
|
|
3938
|
+
import { openapi } from "@nifrajs/middleware"
|
|
3939
|
+
import { GetUser } from "./schema" // the contract defined above
|
|
3940
|
+
|
|
3941
|
+
// Generates an OpenAPI 3.1 document from your registered routes — lazily, on first request.
|
|
3942
|
+
export const app = server()
|
|
3943
|
+
.use(openapi({ info: { title: "My API", version: "1.0.0" }, ui: true }))
|
|
3944
|
+
.get("/users/:id", GetUser, (c) => ({ id: c.params.id, name: "Ada", role: "admin" as const }))
|
|
3945
|
+
|
|
3946
|
+
// → GET /openapi.json (the spec, generated from your schemas)
|
|
3947
|
+
// → GET /reference (a Scalar API-reference page, because `ui: true`)
|
|
3948
|
+
```
|
|
3949
|
+
|
|
3950
|
+
### The MCP contract
|
|
3951
|
+
|
|
3952
|
+
The same routes and schemas feed coding agents. `nifra context` prints the live
|
|
3953
|
+
API surface as compact text, and `nifra mcp` serves it over the Model Context
|
|
3954
|
+
Protocol so Claude Code or Cursor read the real contract instead of guessing.
|
|
3955
|
+
|
|
3956
|
+
```ts
|
|
3957
|
+
$ nifra context # the same contract as compact text — pipe into any agent prompt
|
|
3958
|
+
GET /users/:id → params { id: string } response { id, name, role }
|
|
3959
|
+
|
|
3960
|
+
$ nifra mcp # the same data over an MCP server — Claude Code & Cursor read it
|
|
3961
|
+
```
|
|
3962
|
+
|
|
3963
|
+
### Known limitations
|
|
3964
|
+
|
|
3965
|
+
-
|
|
3966
|
+
Validation only covers what you put in a schema. A raw-body, file-upload, or
|
|
3967
|
+
bring-your-own-validation route reads the body directly — cap and validate those yourself
|
|
3968
|
+
(see [Security](/docs/security)'s `c.boundedBody`).
|
|
3969
|
+
|
|
3970
|
+
-
|
|
3971
|
+
The typed client infers from the server _type_, so it needs an
|
|
3972
|
+
`import type` of your app and TypeScript on the frontend. There is no runtime
|
|
3973
|
+
coupling — server code never ships to the client.
|
|
3974
|
+
|
|
3975
|
+
-
|
|
3976
|
+
The generated OpenAPI document is a structural subset of 3.1 derived from your schemas;
|
|
3977
|
+
it reflects exactly what the routes declare, not hand-authored prose.
|
|
3978
|
+
|
|
3148
3979
|
---
|
|
3149
3980
|
|
|
3150
3981
|
# Package reference (READMEs)
|
|
@@ -3168,6 +3999,14 @@ bun add @nifrajs/auth
|
|
|
3168
3999
|
|
|
3169
4000
|
MIT
|
|
3170
4001
|
|
|
4002
|
+
## For AI agents
|
|
4003
|
+
|
|
4004
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4005
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4006
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4007
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4008
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4009
|
+
|
|
3171
4010
|
## @nifrajs/better-auth
|
|
3172
4011
|
|
|
3173
4012
|
Mount [better-auth](https://better-auth.com) into a [nifra](https://github.com/nifra) app: one
|
|
@@ -3261,6 +4100,14 @@ export const loader = async ({ request }) => {
|
|
|
3261
4100
|
|
|
3262
4101
|
MIT
|
|
3263
4102
|
|
|
4103
|
+
## For AI agents
|
|
4104
|
+
|
|
4105
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4106
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4107
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4108
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4109
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4110
|
+
|
|
3264
4111
|
## @nifrajs/cli
|
|
3265
4112
|
|
|
3266
4113
|
The `nifra` CLI — zero-config dev/build/start for a nifra app, plus the agent surfaces (`nifra context`,
|
|
@@ -3361,6 +4208,14 @@ claude mcp add nifra -- nifra mcp
|
|
|
3361
4208
|
Run the client from (or point it at) your project root. The protocol is hand-rolled
|
|
3362
4209
|
(newline-delimited JSON-RPC 2.0) — no SDK dependency.
|
|
3363
4210
|
|
|
4211
|
+
## For AI agents
|
|
4212
|
+
|
|
4213
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4214
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4215
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4216
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4217
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4218
|
+
|
|
3364
4219
|
## @nifrajs/client
|
|
3365
4220
|
|
|
3366
4221
|
A browser-safe, **end-to-end-typed** client for [nifra](../../README.md) servers — an
|
|
@@ -3392,6 +4247,14 @@ const { ok, status, data, error } = await api.users({ id: "42" }).get()
|
|
|
3392
4247
|
|
|
3393
4248
|
ESM-only. MIT.
|
|
3394
4249
|
|
|
4250
|
+
## For AI agents
|
|
4251
|
+
|
|
4252
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4253
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4254
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4255
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4256
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4257
|
+
|
|
3395
4258
|
## @nifrajs/content
|
|
3396
4259
|
|
|
3397
4260
|
Typed, schema-validated content collections for nifra — Markdown + frontmatter, framework-agnostic.
|
|
@@ -3411,6 +4274,14 @@ bun add @nifrajs/content
|
|
|
3411
4274
|
|
|
3412
4275
|
MIT
|
|
3413
4276
|
|
|
4277
|
+
## For AI agents
|
|
4278
|
+
|
|
4279
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4280
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4281
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4282
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4283
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4284
|
+
|
|
3414
4285
|
## @nifrajs/core
|
|
3415
4286
|
|
|
3416
4287
|
The Bun-native, contract-first HTTP framework at the heart of [nifra](../../README.md):
|
|
@@ -3447,6 +4318,14 @@ export type App = typeof app // hand this to @nifrajs/client for end-to-end type
|
|
|
3447
4318
|
|
|
3448
4319
|
ESM-only; requires Bun at runtime. MIT.
|
|
3449
4320
|
|
|
4321
|
+
## For AI agents
|
|
4322
|
+
|
|
4323
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4324
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4325
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4326
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4327
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4328
|
+
|
|
3450
4329
|
## @nifrajs/cron
|
|
3451
4330
|
|
|
3452
4331
|
Runtime-agnostic **in-process cron** for long-running nifra servers (Bun / Node / Deno). Standard
|
|
@@ -3508,6 +4387,14 @@ Workers has no long-lived process, so in-process cron doesn't apply. Use the pla
|
|
|
3508
4387
|
trigger instead: `toFetchHandler(app, { scheduled })` from `@nifrajs/core` + a `[triggers]` cron in
|
|
3509
4388
|
`wrangler.toml`. This package is for Bun/Node/Deno servers.
|
|
3510
4389
|
|
|
4390
|
+
## For AI agents
|
|
4391
|
+
|
|
4392
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4393
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4394
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4395
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4396
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4397
|
+
|
|
3511
4398
|
## @nifrajs/deno
|
|
3512
4399
|
|
|
3513
4400
|
Run a [nifra](../../README.md) app on [Deno](https://deno.com) via `Deno.serve`.
|
|
@@ -3544,6 +4431,14 @@ cap live inside `app.fetch`, so they apply through this adapter automatically.
|
|
|
3544
4431
|
|
|
3545
4432
|
Works with any `{ fetch(req): Promise<Response> }` handler, not just nifra. ESM/TS. MIT.
|
|
3546
4433
|
|
|
4434
|
+
## For AI agents
|
|
4435
|
+
|
|
4436
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4437
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4438
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4439
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4440
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4441
|
+
|
|
3547
4442
|
## @nifrajs/env
|
|
3548
4443
|
|
|
3549
4444
|
Typed, validated environment variables. Define a schema once at startup; get a **frozen, typed**
|
|
@@ -3612,6 +4507,14 @@ pass the request's bindings: `defineEnv(shape, { source: env })`.
|
|
|
3612
4507
|
Error messages name the offending variable and the reason — **never its value** (it may be a
|
|
3613
4508
|
secret). `defineEnv` returns a frozen object, so a validated config can't be mutated downstream.
|
|
3614
4509
|
|
|
4510
|
+
## For AI agents
|
|
4511
|
+
|
|
4512
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4513
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4514
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4515
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4516
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4517
|
+
|
|
3615
4518
|
## @nifrajs/i18n
|
|
3616
4519
|
|
|
3617
4520
|
Framework-agnostic i18n for nifra — locale negotiation + a tiny ICU message formatter on the platform Intl. Dependency-free.
|
|
@@ -3631,6 +4534,14 @@ bun add @nifrajs/i18n
|
|
|
3631
4534
|
|
|
3632
4535
|
MIT
|
|
3633
4536
|
|
|
4537
|
+
## For AI agents
|
|
4538
|
+
|
|
4539
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4540
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4541
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4542
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4543
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4544
|
+
|
|
3634
4545
|
## @nifrajs/image
|
|
3635
4546
|
|
|
3636
4547
|
Image optimization for nifra — a CLS-safe responsive <Image> + pluggable loaders (CDN or self-hosted) + pure-JS dimension reading. Zero-dependency core; optional Bun.Image-backed resize endpoint at @nifrajs/image/server.
|
|
@@ -3650,6 +4561,14 @@ bun add @nifrajs/image
|
|
|
3650
4561
|
|
|
3651
4562
|
MIT
|
|
3652
4563
|
|
|
4564
|
+
## For AI agents
|
|
4565
|
+
|
|
4566
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4567
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4568
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4569
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4570
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4571
|
+
|
|
3653
4572
|
## @nifrajs/islets
|
|
3654
4573
|
|
|
3655
4574
|
Fine-grained signals + declarative DOM bindings for islands — **interactivity in ~1.4 KB gz**, no
|
|
@@ -3712,6 +4631,14 @@ into one flush. Island-scale by design: if a widget outgrows this (lists, owners
|
|
|
3712
4631
|
async orchestration), that's the signal to use the Solid adapter for that page — both tiers are
|
|
3713
4632
|
first-class.
|
|
3714
4633
|
|
|
4634
|
+
## For AI agents
|
|
4635
|
+
|
|
4636
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4637
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4638
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4639
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4640
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4641
|
+
|
|
3715
4642
|
## @nifrajs/middleware
|
|
3716
4643
|
|
|
3717
4644
|
Composable, dependency-light middleware for [nifra](../../README.md) — CORS, security headers, body
|
|
@@ -3792,6 +4719,14 @@ const app = server({ requestTimeoutMs: 5_000 })
|
|
|
3792
4719
|
|
|
3793
4720
|
`@nifrajs/core` is a peer dependency. ESM-only. MIT.
|
|
3794
4721
|
|
|
4722
|
+
## For AI agents
|
|
4723
|
+
|
|
4724
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4725
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4726
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4727
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4728
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4729
|
+
|
|
3795
4730
|
## @nifrajs/node
|
|
3796
4731
|
|
|
3797
4732
|
Run a [nifra](../../README.md) app on Node's `http` server — nifra's ergonomics, deployed
|
|
@@ -3855,6 +4790,14 @@ with no extra wiring. Slow-client / slow-loris protection is Node's built-in
|
|
|
3855
4790
|
|
|
3856
4791
|
Works with any `{ fetch(req): Promise<Response> }` handler, not just nifra. ESM-only. MIT.
|
|
3857
4792
|
|
|
4793
|
+
## For AI agents
|
|
4794
|
+
|
|
4795
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4796
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4797
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4798
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4799
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4800
|
+
|
|
3858
4801
|
## @nifrajs/otel
|
|
3859
4802
|
|
|
3860
4803
|
Distributed tracing for nifra. The `tracing()` plugin continues (or starts) a **W3C trace** per
|
|
@@ -3907,6 +4850,14 @@ Use `traceparent` and the built-in semantic attributes in every request span, th
|
|
|
3907
4850
|
your own collector through an exporter. Keep the package edge-safe by installing the OpenTelemetry
|
|
3908
4851
|
SDK only in apps that need that exporter.
|
|
3909
4852
|
|
|
4853
|
+
## For AI agents
|
|
4854
|
+
|
|
4855
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4856
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4857
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4858
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4859
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4860
|
+
|
|
3910
4861
|
## @nifrajs/runner
|
|
3911
4862
|
|
|
3912
4863
|
Run requests through a nifra app and capture **structured results** — the shared engine behind the
|
|
@@ -3937,6 +4888,14 @@ It's a **runner, not a security sandbox**: it calls the app you hand it in-proce
|
|
|
3937
4888
|
Isolation, when you need it, comes from the host — the browser tab for the playground, your own
|
|
3938
4889
|
process/CI for the agent runner. Don't feed it code you wouldn't already run.
|
|
3939
4890
|
|
|
4891
|
+
## For AI agents
|
|
4892
|
+
|
|
4893
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4894
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4895
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4896
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4897
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4898
|
+
|
|
3940
4899
|
## @nifrajs/schema
|
|
3941
4900
|
|
|
3942
4901
|
The optional, batteries-included schema builder for [nifra](../../README.md): `t` is
|
|
@@ -3988,6 +4947,14 @@ are still emitted without body/response detail (Standard Schema exposes no JSON
|
|
|
3988
4947
|
|
|
3989
4948
|
`@nifrajs/core` is a peer dependency. ESM-only. MIT.
|
|
3990
4949
|
|
|
4950
|
+
## For AI agents
|
|
4951
|
+
|
|
4952
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4953
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4954
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4955
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4956
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4957
|
+
|
|
3991
4958
|
## @nifrajs/uploads
|
|
3992
4959
|
|
|
3993
4960
|
File-upload hardening for nifra — magic-byte MIME detection, size + type validation, short-TTL signed download URLs, and EXIF/metadata stripping. Dependency-free, edge-safe.
|
|
@@ -4007,6 +4974,14 @@ bun add @nifrajs/uploads
|
|
|
4007
4974
|
|
|
4008
4975
|
MIT
|
|
4009
4976
|
|
|
4977
|
+
## For AI agents
|
|
4978
|
+
|
|
4979
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
4980
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
4981
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
4982
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
4983
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
4984
|
+
|
|
4010
4985
|
## @nifrajs/web
|
|
4011
4986
|
|
|
4012
4987
|
Framework-agnostic SSR core for nifra — the render seam + HTML document orchestration.
|
|
@@ -4026,6 +5001,14 @@ bun add @nifrajs/web
|
|
|
4026
5001
|
|
|
4027
5002
|
MIT
|
|
4028
5003
|
|
|
5004
|
+
## For AI agents
|
|
5005
|
+
|
|
5006
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5007
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5008
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5009
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5010
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5011
|
+
|
|
4029
5012
|
## @nifrajs/web-preact
|
|
4030
5013
|
|
|
4031
5014
|
Preact render adapter for @nifrajs/web — streaming SSR + hydration (no build plugin).
|
|
@@ -4045,6 +5028,14 @@ bun add @nifrajs/web-preact
|
|
|
4045
5028
|
|
|
4046
5029
|
MIT
|
|
4047
5030
|
|
|
5031
|
+
## For AI agents
|
|
5032
|
+
|
|
5033
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5034
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5035
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5036
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5037
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5038
|
+
|
|
4048
5039
|
## @nifrajs/web-react
|
|
4049
5040
|
|
|
4050
5041
|
React render adapter for @nifrajs/web — SSR + hydration (Bun-native JSX, no Babel).
|
|
@@ -4064,6 +5055,14 @@ bun add @nifrajs/web-react
|
|
|
4064
5055
|
|
|
4065
5056
|
MIT
|
|
4066
5057
|
|
|
5058
|
+
## For AI agents
|
|
5059
|
+
|
|
5060
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5061
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5062
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5063
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5064
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5065
|
+
|
|
4067
5066
|
## @nifrajs/web-solid
|
|
4068
5067
|
|
|
4069
5068
|
Solid render adapter for @nifrajs/web — SSR + hydration + the Solid Babel build plugin.
|
|
@@ -4083,6 +5082,14 @@ bun add @nifrajs/web-solid
|
|
|
4083
5082
|
|
|
4084
5083
|
MIT
|
|
4085
5084
|
|
|
5085
|
+
## For AI agents
|
|
5086
|
+
|
|
5087
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5088
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5089
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5090
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5091
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5092
|
+
|
|
4086
5093
|
## @nifrajs/web-svelte
|
|
4087
5094
|
|
|
4088
5095
|
Svelte 5 render adapter for @nifrajs/web — SSR + hydration + the .svelte compiler Bun-plugin.
|
|
@@ -4102,6 +5109,14 @@ bun add @nifrajs/web-svelte
|
|
|
4102
5109
|
|
|
4103
5110
|
MIT
|
|
4104
5111
|
|
|
5112
|
+
## For AI agents
|
|
5113
|
+
|
|
5114
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5115
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5116
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5117
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5118
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5119
|
+
|
|
4105
5120
|
## @nifrajs/web-vanilla
|
|
4106
5121
|
|
|
4107
5122
|
Zero-framework render adapter for @nifrajs/web — auto-escaping tagged-template HTML, ~0 KB client JS, interactivity via @nifrajs/web/islands.
|
|
@@ -4121,6 +5136,14 @@ bun add @nifrajs/web-vanilla
|
|
|
4121
5136
|
|
|
4122
5137
|
MIT
|
|
4123
5138
|
|
|
5139
|
+
## For AI agents
|
|
5140
|
+
|
|
5141
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5142
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5143
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5144
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5145
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5146
|
+
|
|
4124
5147
|
## @nifrajs/web-vue
|
|
4125
5148
|
|
|
4126
5149
|
Vue render adapter for @nifrajs/web — streaming SSR + hydration, with a .vue SFC compiler Bun plugin (@nifrajs/web-vue/plugin).
|
|
@@ -4140,6 +5163,14 @@ bun add @nifrajs/web-vue
|
|
|
4140
5163
|
|
|
4141
5164
|
MIT
|
|
4142
5165
|
|
|
5166
|
+
## For AI agents
|
|
5167
|
+
|
|
5168
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5169
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5170
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5171
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5172
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5173
|
+
|
|
4143
5174
|
## @nifrajs/workers
|
|
4144
5175
|
|
|
4145
5176
|
Cloudflare Workers helpers for nifra — a Durable Object hub for cross-connection WebSocket broadcast.
|
|
@@ -4159,6 +5190,14 @@ bun add @nifrajs/workers
|
|
|
4159
5190
|
|
|
4160
5191
|
MIT
|
|
4161
5192
|
|
|
5193
|
+
## For AI agents
|
|
5194
|
+
|
|
5195
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5196
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5197
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5198
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5199
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5200
|
+
|
|
4162
5201
|
## create-nifra
|
|
4163
5202
|
|
|
4164
5203
|
Scaffold a new [nifra](../../README.md) app.
|
|
@@ -4193,6 +5232,14 @@ bun run dev
|
|
|
4193
5232
|
|
|
4194
5233
|
MIT.
|
|
4195
5234
|
|
|
5235
|
+
## For AI agents
|
|
5236
|
+
|
|
5237
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5238
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5239
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5240
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5241
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5242
|
+
|
|
4196
5243
|
## nifra
|
|
4197
5244
|
|
|
4198
5245
|
The unscoped meta-entry for the **nifra** full-stack framework. It re-exports [`@nifrajs/core`](https://www.npmjs.com/package/@nifrajs/core), so:
|
|
@@ -4220,6 +5267,14 @@ Docs + an AI-readable reference: [`/llms.txt`](https://nifra.dev/llms.txt) · [`
|
|
|
4220
5267
|
|
|
4221
5268
|
MIT.
|
|
4222
5269
|
|
|
5270
|
+
## For AI agents
|
|
5271
|
+
|
|
5272
|
+
Start with [`LLM.md`](./LLM.md) — this package's contract card (the exports you call + its footguns),
|
|
5273
|
+
one cheap read instead of the whole corpus. For the wider framework: the repo's
|
|
5274
|
+
[`AGENTS.md`](../../AGENTS.md) is the copy-paste quick reference, and
|
|
5275
|
+
[`llms-full.txt`](../../llms-full.txt) is the full machine-readable corpus. Run `nifra check` as the
|
|
5276
|
+
done-gate, or `nifra mcp` to give the agent live project tools.
|
|
5277
|
+
|
|
4223
5278
|
---
|
|
4224
5279
|
|
|
4225
5280
|
# Complete export index (generated from each package's src/index.ts)
|
|
@@ -4294,7 +5349,7 @@ MIT.
|
|
|
4294
5349
|
|
|
4295
5350
|
## @nifrajs/web
|
|
4296
5351
|
|
|
4297
|
-
`Action`, `ACTION_GLOBAL`, `buildManifest`, `CachedResponse`, `CacheStore`, `clientEntry`, `ClientRouter`, `ClientRouterOptions`, `createClientRouter`, `createMatcher`, `createQueryClient`, `createWebApp`, `CreateWebAppOptions`, `DATA_GLOBAL`, `DATA_HEADER`, `defer`, `Deferred`, `disableDraft`, `DRAFT_COOKIE`, `DraftCookieControls`, `enableDraft`, `EnableDraftOptions`, `enumeratePrerenderedPaths`, `enumerateStaticRoutes`, `Fetcher`, `FetcherState`, `FetchRouteData`, `filePathToPattern`, `filePathToPatterns`, `FontDisplay`, `fontFace`, `FontFace`, `fontPreload`, `FontPreloadInput`, `FontSource`, `generateClientEntry`, `GenerateClientEntryOptions`, `generateServerManifest`, `GenerateServerManifestOptions`, `GetStaticPaths`, `hashQueryKey`, `isDraftEnabled`, `ISR_REVALIDATE_HEADER`, `ISR_STATUS_HEADER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LayoutEntry`, `Loader`, `LoaderContext`, `manifest`, `Manifest`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `Meta`, `MetaArgs`, `MetaInput`, `MountRouterOptions`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryState`, `QueryStatus`, `redirect`, `REDIRECT_HEADER`, `RedirectOptions`, `RenderAdapter`, `RenderedPage`, `renderPage`, `RenderPageOptions`, `renderPageResult`, `RenderProps`, `resolveMeta`, `revalidate`, `REVALIDATE_HEADER`, `revalidateEndpoint`, `RevalidateEndpointOptions`, `RevalidateResult`, `ROUTE_GLOBAL`, `RouteEntry`, `RouteMatch`, `RouteModule`, `RoutePattern`, `RouterState`, `serializeData`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `Submission`, `SubmitOptions`, `withISR`
|
|
5352
|
+
`Action`, `ACTION_GLOBAL`, `apiKey`, `buildManifest`, `CachedResponse`, `CacheStore`, `canonical`, `clientEntry`, `ClientRouter`, `ClientRouterOptions`, `createClientRouter`, `createMatcher`, `createQueryClient`, `createWebApp`, `CreateWebAppOptions`, `DATA_GLOBAL`, `DATA_HEADER`, `DEFAULT_DEV_PORT`, `defer`, `Deferred`, `disableDraft`, `DRAFT_COOKIE`, `DraftCookieControls`, `enableDraft`, `EnableDraftOptions`, `enumeratePrerenderedPaths`, `enumerateStaticRoutes`, `Fetcher`, `FetcherState`, `FetchRouteData`, `filePathToPattern`, `filePathToPatterns`, `FontDisplay`, `fontFace`, `FontFace`, `fontPreload`, `FontPreloadInput`, `FontSource`, `generateClientEntry`, `GenerateClientEntryOptions`, `generateServerManifest`, `GenerateServerManifestOptions`, `GetStaticPaths`, `hashQueryKey`, `isDraftEnabled`, `ISR_REVALIDATE_HEADER`, `ISR_STATUS_HEADER`, `ISRApp`, `ISROptions`, `ISRPlatform`, `jsonLd`, `KVCacheStore`, `KVCacheStoreOptions`, `KVNamespaceLike`, `LayoutEntry`, `LinkDescriptor`, `Loader`, `LoaderContext`, `manifest`, `Manifest`, `MemoryCacheStore`, `MemoryCacheStoreOptions`, `mergeHeads`, `meta`, `Meta`, `MetaArgs`, `MetaInput`, `MountRouterOptions`, `openGraph`, `OpenGraphInput`, `QueryClient`, `QueryClientOptions`, `QueryHandle`, `QueryState`, `QueryStatus`, `redirect`, `REDIRECT_HEADER`, `RedirectOptions`, `RenderAdapter`, `RenderedPage`, `renderPage`, `RenderPageOptions`, `renderPageResult`, `RenderProps`, `resolveMeta`, `revalidate`, `REVALIDATE_HEADER`, `revalidateEndpoint`, `RevalidateEndpointOptions`, `RevalidateResult`, `ROUTE_GLOBAL`, `RouteEntry`, `RouteMatch`, `RouteModule`, `RoutePattern`, `RouterState`, `ScriptDescriptor`, `serializeData`, `ServerOnly`, `StaticPath`, `StaticPaths`, `StaticRoutes`, `Submission`, `SubmitOptions`, `withISR`
|
|
4298
5353
|
|
|
4299
5354
|
## @nifrajs/web-preact
|
|
4300
5355
|
|