@nifrajs/cli 2.1.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -25
- package/dist/assure.d.ts +2 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +17 -10
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +7 -0
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +68 -16
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +64 -22
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +400 -83
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +37 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +291 -53
- package/dist/cli.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.js +7 -7
- package/dist/docs-search.d.ts +3 -3
- package/dist/docs-search.js +6 -6
- package/dist/doctor.d.ts +8 -4
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +74 -14
- package/dist/doctor.js.map +1 -1
- package/dist/examples.d.ts +5 -5
- package/dist/examples.js +5 -5
- package/dist/init-agents.d.ts +14 -14
- package/dist/init-agents.js +14 -14
- package/dist/internal/typescript-import.d.ts +24 -0
- package/dist/internal/typescript-import.d.ts.map +1 -0
- package/dist/internal/typescript-import.js +32 -0
- package/dist/internal/typescript-import.js.map +1 -0
- package/dist/introspect.d.ts +12 -12
- package/dist/introspect.js +24 -24
- package/dist/levels-tool.d.ts +8 -8
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -30
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +17 -6
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +45 -7
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +4 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-docs-tools.d.ts +1 -1
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +14 -10
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.d.ts +5 -5
- package/dist/mcp-http.js +7 -7
- package/dist/mcp-protocol.d.ts +2 -2
- package/dist/mcp-protocol.js +2 -2
- package/dist/mcp-render.d.ts +3 -3
- package/dist/mcp-render.js +15 -7
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-run.d.ts +2 -2
- package/dist/mcp-run.js +2 -2
- package/dist/mcp.d.ts +14 -14
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +119 -47
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +1 -1
- package/dist/openapi-tool.js +2 -2
- package/dist/pipeline-guard.d.ts +83 -0
- package/dist/pipeline-guard.d.ts.map +1 -0
- package/dist/pipeline-guard.js +136 -0
- package/dist/pipeline-guard.js.map +1 -0
- package/dist/port.d.ts +9 -9
- package/dist/port.js +27 -27
- package/dist/recipes/1.8.0.d.ts +2 -2
- package/dist/recipes/1.8.0.js +5 -5
- package/dist/recipes/index.d.ts +2 -2
- package/dist/recipes/index.js +1 -1
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +8 -8
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.d.ts.map +1 -0
- package/dist/sync-routes.js +55 -0
- package/dist/sync-routes.js.map +1 -0
- package/dist/types-search.d.ts +6 -6
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +53 -8
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +7 -7
- package/dist/upgrade.js +12 -12
- package/dist/verification.d.ts +51 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +80 -0
- package/dist/verification.js.map +1 -0
- package/docs/examples.json +178 -88
- package/docs/llms-full.txt +1766 -766
- package/docs/types.json +2171 -2807
- package/package.json +22 -11
- package/src/assure.ts +17 -18
- package/src/capabilities-tool.ts +82 -16
- package/src/check.ts +493 -100
- package/src/cli.ts +306 -54
- package/src/diff-tool.ts +8 -8
- package/src/docs-search.ts +6 -6
- package/src/doctor.ts +81 -16
- package/src/examples.ts +7 -7
- package/src/init-agents.ts +17 -17
- package/src/internal/typescript-import.ts +33 -0
- package/src/introspect.ts +28 -28
- package/src/levels-tool.ts +37 -31
- package/src/load.ts +65 -12
- package/src/manifest-tool.ts +4 -2
- package/src/mcp-docs-tools.ts +20 -10
- package/src/mcp-http.ts +7 -7
- package/src/mcp-protocol.ts +2 -2
- package/src/mcp-render.ts +15 -7
- package/src/mcp-run.ts +2 -2
- package/src/mcp.ts +127 -47
- package/src/openapi-tool.ts +2 -2
- package/src/pipeline-guard.ts +186 -0
- package/src/port.ts +31 -31
- package/src/recipes/1.8.0.ts +5 -5
- package/src/recipes/index.ts +2 -2
- package/src/scaffold.ts +8 -8
- package/src/sync-routes.ts +69 -0
- package/src/types-search.ts +57 -8
- package/src/upgrade.ts +12 -12
- package/src/verification.ts +123 -0
package/docs/examples.json
CHANGED
|
@@ -1,52 +1,88 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"name": "TELEMETRY",
|
|
4
|
+
"topic": "Nifra - Coding agents",
|
|
5
|
+
"slug": "agents",
|
|
6
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { agentTelemetry, consoleAgentExporter } from \"@nifrajs/agent-telemetry\"\n\nexport const app = server().use(agentTelemetry({ exporter: consoleAgentExporter() }))"
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"name": "CLIENT",
|
|
4
|
-
"topic": "Nifra
|
|
10
|
+
"topic": "Nifra - API & typed client",
|
|
5
11
|
"slug": "api",
|
|
6
|
-
"code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\"\n\n// Infers the server's types directly
|
|
12
|
+
"code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\"\n\n// Infers the server's types directly - no codegen, no schema duplication.\nconst api = client<typeof app>(\"https://api.example.com\")\n\nconst { data } = await api.users({ id: \"1\" }).get() // path param → /users/1\nawait api.users.post({ name: \"Ada\" }) // POST body\nawait api.search.get({ query: { page: \"3\" } }) // query string\nawait api.users({ id: \"1\" }).posts({ postId: \"2\" }).get() // nested params"
|
|
7
13
|
},
|
|
8
14
|
{
|
|
9
15
|
"name": "SETUP",
|
|
10
|
-
"topic": "Nifra
|
|
16
|
+
"topic": "Nifra - Auth & sessions",
|
|
11
17
|
"slug": "auth",
|
|
12
|
-
"code": "// auth.ts
|
|
18
|
+
"code": "// auth.ts - one session manager. LAZY so a route module can import it without shipping it to the\n// browser (see the warning below). Store mode keeps data server-side; cookie mode (no store) is stateless.\nimport { createSessions, MemorySessionStore } from \"@nifrajs/auth\"\n\nlet manager: ReturnType<typeof createSessions> | undefined\nexport const getSessions = () => (manager ??= createSessions({\n secret: process.env.SESSION_SECRET!, // ≥ 16 chars; rotating it invalidates all sessions\n store: new MemorySessionStore(), // prod: new KVSessionStore(env.SESSIONS)\n // cookie: { secure: false }, // local http dev only\n}))"
|
|
13
19
|
},
|
|
14
20
|
{
|
|
15
21
|
"name": "BACKEND",
|
|
16
|
-
"topic": "Nifra
|
|
22
|
+
"topic": "Nifra - Backends & API (dev and prod)",
|
|
17
23
|
"slug": "backends",
|
|
18
|
-
"code": "// backend.ts
|
|
24
|
+
"code": "// backend.ts - a normal @nifrajs/core server. Routes live at the full /api/... path.\nimport { server } from \"@nifrajs/core/server\"\nimport { t } from \"@nifrajs/schema\"\n\nexport const backend = server()\n .post(\"/api/sync\", { body: t.object({ cursor: t.string() }) }, async (c) => {\n // validated body; runs the same whether called in-process (a loader) or over HTTP (a client fetch)\n return { applied: 12, nextCursor: c.body.cursor }\n })\n .get(\"/api/me\", (c) => ({ id: c.cookies.session ?? null }))"
|
|
19
25
|
},
|
|
20
26
|
{
|
|
21
27
|
"name": "CLIENT",
|
|
22
|
-
"topic": "Nifra
|
|
28
|
+
"topic": "Nifra - Backends & API (dev and prod)",
|
|
23
29
|
"slug": "backends",
|
|
24
30
|
"code": "// A browser island / client component hits the mounted HTTP routes with the typed client.\nimport { client } from \"@nifrajs/client\"\nimport type { backend } from \"./backend\"\n\nconst api = client<typeof backend>(\"\") // same-origin: /api/sync is served by createWebApp's mount\nexport async function runSync(cursor: string) {\n const { data, error } = await api.api.sync.post({ cursor })\n return error ? { applied: 0 } : data\n}"
|
|
25
31
|
},
|
|
26
32
|
{
|
|
27
33
|
"name": "COMPOSE",
|
|
28
|
-
"topic": "Nifra
|
|
34
|
+
"topic": "Nifra - Backends & API (dev and prod)",
|
|
29
35
|
"slug": "backends",
|
|
30
36
|
"code": "// backend.ts - compose domains with .merge(). Each group owns its FULL absolute paths.\nimport { server } from \"@nifrajs/core/server\"\n\nconst listings = server().get(\"/api/listings\", (c) => {\n // c.req.url is the ORIGINAL request URL - .merge() and the /api mount never strip the prefix.\n const path = new URL(c.req.url).pathname // GET /api/listings -> \"/api/listings\"\n return { path }\n})\n\nconst agents = server().get(\"/api/agents\", (c) => ({ ok: new URL(c.req.url).pathname }))\n\n// merge() folds both groups into ONE flat path space at their defined paths; a path+method clash\n// throws a RouteConfigError right here at merge time (fail-closed), never a silent shadow at runtime.\nexport const backend = server().merge(listings).merge(agents)"
|
|
31
37
|
},
|
|
38
|
+
{
|
|
39
|
+
"name": "EVENTS",
|
|
40
|
+
"topic": "Nifra - Backends & API (dev and prod)",
|
|
41
|
+
"slug": "backends",
|
|
42
|
+
"code": "import { defineEventContract } from \"@nifrajs/events\"\nimport { t } from \"@nifrajs/schema\"\n\nexport const noteCreated = defineEventContract({\n type: \"note.created\",\n version: 1,\n payload: t.object({ id: t.string(), title: t.string() }),\n})\n\n// Producer: validates the payload and stamps a full envelope (id, occurredAt).\nexport const emit = (id: string, title: string) => noteCreated.create({ id, title })\n\n// Consumer: parse never throws - an event from outside the process is untrusted input,\n// so the failure is a value you have to handle rather than an exception you might not.\nexport function receive(input: unknown): string | undefined {\n const parsed = noteCreated.parse(input)\n return parsed.success ? parsed.envelope.payload.title : undefined\n}"
|
|
43
|
+
},
|
|
32
44
|
{
|
|
33
45
|
"name": "LOADER",
|
|
34
|
-
"topic": "Nifra
|
|
46
|
+
"topic": "Nifra - Backends & API (dev and prod)",
|
|
35
47
|
"slug": "backends",
|
|
36
|
-
"code": "// routes/index.tsx
|
|
48
|
+
"code": "// routes/index.tsx - a loader calls the backend IN-PROCESS via ctx.api (no network).\nimport type { LoaderContext } from \"@nifrajs/web\"\n\nexport async function loader(ctx: LoaderContext) {\n const api = ctx.api as { me: { get(): Promise<{ data: { id: string | null } }> } }\n const res = await api.me.get() // in-process: full validation/middleware, no HTTP round-trip\n return { me: res.data }\n}"
|
|
37
49
|
},
|
|
38
50
|
{
|
|
39
51
|
"name": "WIRE",
|
|
40
|
-
"topic": "Nifra
|
|
52
|
+
"topic": "Nifra - Backends & API (dev and prod)",
|
|
41
53
|
"slug": "backends",
|
|
42
|
-
"code": "// server.ts (prod)
|
|
54
|
+
"code": "// server.ts (prod) - createWebApp serves pages AND auto-mounts the backend at /api/*.\nimport { inProcessClient } from \"@nifrajs/client\"\nimport { createWebApp } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\nimport { backend } from \"./backend\"\nimport { clientEntry, manifest } from \"./server-manifest\"\n\nexport const app = createWebApp({\n adapter: reactAdapter,\n manifest,\n clientEntry,\n api: inProcessClient(backend), // → ctx.api in loaders/actions AND auto-mounted at /api/*\n // apiPrefix: \"/api\", // the default; pass \"\" to disable the HTTP mount (pages only)\n})\n\n// Bun: Bun.serve({ fetch: app.fetch }). No `if (pathname.startsWith(\"/api/\")) …` branch needed -\n// POST /api/sync, GET /api/me, etc. are dispatched to the backend BEFORE the page router sees them."
|
|
43
55
|
},
|
|
44
56
|
{
|
|
45
57
|
"name": "SERVER",
|
|
46
|
-
"topic": "Nifra
|
|
58
|
+
"topic": "Nifra - Request budgets",
|
|
47
59
|
"slug": "budgets",
|
|
48
60
|
"code": "import { server } from \"@nifrajs/core/server\"\n\nconst app = server({\n requestTimeoutMs: 2_000,\n acceptInboundDeadlines: true,\n maxInboundDeadlineMs: 5_000,\n}).get(\"/report\", async (c) => {\n // c.signal aborts at the SAME effective deadline.\n // remaining() is monotonic even if the wall clock jumps.\n return { remainingMs: c.budget.remaining() }\n})"
|
|
49
61
|
},
|
|
62
|
+
{
|
|
63
|
+
"name": "BEACON",
|
|
64
|
+
"topic": "Nifra - Effect provenance",
|
|
65
|
+
"slug": "capabilities",
|
|
66
|
+
"code": "import { useCapability } from \"@nifrajs/core/capabilities\"\nimport { createCache } from \"@nifrajs/cache\"\n\nconst cache = createCache({ beacon: useCapability })\n\n// Announces cache.write against THIS route before the operation, and throws if it was not declared.\nexport const write = async (c: object): Promise<void> => {\n await cache.for(c).set(\"k\", 1)\n}"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "DURABLE",
|
|
70
|
+
"topic": "Nifra - Effect provenance",
|
|
71
|
+
"slug": "capabilities",
|
|
72
|
+
"code": "import { server } from \"@nifrajs/core\"\nimport { executeCapability } from \"@nifrajs/core/capabilities\"\nimport { createDurableEffectJournal } from \"@nifrajs/core/durable-execution\"\nimport { durableCommand } from \"@nifrajs/middleware\"\n\ndeclare const store: import(\"@nifrajs/core/durable-execution\").DurableEffectStore\ndeclare const gateway: { charge(): Promise<{ id: string }> }\n\nconst commands = durableCommand({ journal: createDurableEffectJournal({ store }) })\n\n// Every executeCapability below this records intent before the effect and one outcome after.\nconst app = server()\n .use(commands)\n .post(\"/charge\", { capabilities: [\"billing.charge\"] }, (c: object) =>\n executeCapability(c, \"billing.charge\", {}, () => gateway.charge()),\n )"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"name": "POLICY",
|
|
76
|
+
"topic": "Nifra - Effect provenance",
|
|
77
|
+
"slug": "capabilities",
|
|
78
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { defineAssuranceConfig, NIFRA_ASSURANCE } from \"@nifrajs/core/assurance\"\n\nconst app = server()\n\nexport default defineAssuranceConfig({\n source: app,\n capabilities: {\n definitions: [\n { id: \"db.read\", zone: \"domain\", access: \"read\" },\n { id: \"db.write\", zone: \"domain\", access: \"write\" },\n ],\n provenance: {\n // Reaching one of these implies holding the capabilities beside it.\n imports: [\n { specifier: \"./read.ts\", capabilities: [\"db.read\"] },\n { specifier: \"./write.ts\", capabilities: [\"db.write\"] },\n { specifier: \"bun:sqlite\", capabilities: [\"db.read\", \"db.write\"] },\n ],\n forbiddenImports: [],\n },\n },\n policy: {\n rules: [\n // Not a list of token names: anything whose DEFINITION is a domain write.\n {\n name: \"authenticated-write\",\n match: { access: \"write\", zone: \"domain\" },\n require: [NIFRA_ASSURANCE.AUTHENTICATED],\n },\n { name: \"read\", match: { methods: [\"GET\"] }, require: [] },\n ],\n },\n})"
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"name": "ROOT",
|
|
82
|
+
"topic": "Nifra - Effect provenance",
|
|
83
|
+
"slug": "capabilities",
|
|
84
|
+
"code": "// src/app.ts - composition only. It merges route modules and registers none of its own.\nimport { server } from \"@nifrajs/core/server\"\nimport { routes } from \"./routes.ts\"\n\nexport const app = server().merge(routes)"
|
|
85
|
+
},
|
|
50
86
|
{
|
|
51
87
|
"name": "CONTINUE",
|
|
52
88
|
"topic": "Nifra - Execution causality",
|
|
@@ -67,63 +103,87 @@
|
|
|
67
103
|
},
|
|
68
104
|
{
|
|
69
105
|
"name": "DUAL_API",
|
|
70
|
-
"topic": "Nifra
|
|
106
|
+
"topic": "Nifra - Framework contract",
|
|
71
107
|
"slug": "contract",
|
|
72
|
-
"code": "// TWO ways to handle a request
|
|
108
|
+
"code": "// TWO ways to handle a request - pick by who is calling.\nimport type { LoaderContext } from \"@nifrajs/web\"\n\n// (1) A page's own data + mutations → route exports. A PUBLIC POST is a route ACTION\n// (routes/contact.tsx) - the browser POSTs the route's own URL, nifra runs `action`:\nexport async function action(ctx: LoaderContext) {\n const form = await ctx.request.formData() // the browser POSTed this route\n return { ok: true, body: String(form.get(\"body\")) } // → props.actionData on re-render\n}\n\n// (2) ctx.api is the IN-PROCESS backend client (the inProcessClient(server) the app was given).\n// It is LOADERS-ONLY: the SSR backend registers GET handlers, so a .post() to ctx.api 405s.\nexport async function loader(ctx: LoaderContext) {\n // ✅ a GET in-process - no HTTP hop. (A .post() to ctx.api would 405; write an `action` instead.)\n return { id: ctx.params.id }\n}\n\n// Rule of thumb: data INTO a page → loader + ctx.api (GET). A mutation FROM the browser\n// (a form/fetch POST) → an `action` export on the route. Don't `inProcessClient(api).post()`."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "INLINE_SCRIPT",
|
|
112
|
+
"topic": "Nifra - Framework contract",
|
|
113
|
+
"slug": "contract",
|
|
114
|
+
"code": "import { renderPage, unsafeInlineScript } from \"@nifrajs/web\"\n\ndeclare const adapter: import(\"@nifrajs/web\").RenderAdapter\ndeclare const nonce: string // one per response, from your CSP middleware\n\nconst page = renderPage({\n adapter,\n chain: [null],\n data: null,\n clientEntry: \"/assets/client.js\",\n nonce, // reaches the hydration bootstrap, the data script and every island tag\n head: {\n script: [{ content: JSON.stringify({ \"@type\": \"Article\" }) }], // inert: JSON only\n unsafeScript: [unsafeInlineScript(\"window.dataLayer = []\", { nonce })],\n },\n})"
|
|
73
115
|
},
|
|
74
116
|
{
|
|
75
117
|
"name": "PARAM_404",
|
|
76
|
-
"topic": "Nifra
|
|
118
|
+
"topic": "Nifra - Framework contract",
|
|
77
119
|
"slug": "contract",
|
|
78
|
-
"code": "// routes/users/[id].tsx
|
|
120
|
+
"code": "// routes/users/[id].tsx - plain SSR runs the loader for ANY :id value. `fallback: \"404\"`\n// only takes effect under prerender/CDN, so on on-demand SSR you MUST guard and 404 yourself.\nimport type { LoaderContext } from \"@nifrajs/web\"\n\ndeclare function lookupUser(id: string): Promise<{ id: string } | null>\n\nexport async function loader(ctx: LoaderContext) {\n // ctx.params.id is `string | undefined` (an optional segment) - default it before the lookup.\n const user = await lookupUser(ctx.params.id ?? \"\")\n if (!user) {\n // CONTROL FLOW: a thrown Response is an INTENTIONAL HTTP response - it propagates untouched\n // (no _error boundary, no 500). This is how you 404 a missing record.\n throw new Response(\"Not Found\", { status: 404 })\n }\n return { user }\n}\n\ndeclare function lookupUser(id: string): Promise<{ name: string } | null>\n\n// The throw contract, stated once:\n// • throw a Response → that exact HTTP response is sent (404 / redirect() / 403 / …). Control flow.\n// • throw an Error → renders the nearest _error boundary if one exists, else a 500. A bug.\n// So NEVER `throw new Error(\"not found\")` to mean 404 - that is a 500. Throw a 404 Response."
|
|
79
121
|
},
|
|
80
122
|
{
|
|
81
123
|
"name": "PUBLIC_ENV",
|
|
82
|
-
"topic": "Nifra
|
|
124
|
+
"topic": "Nifra - Framework contract",
|
|
83
125
|
"slug": "contract",
|
|
84
|
-
"code": "// process.env in the CLIENT bundle is compiled at build time:\n// process.env → ({}) (a bare read won't crash hydration)\n// process.env.NODE_ENV → \"production\" (the build mode
|
|
126
|
+
"code": "// process.env in the CLIENT bundle is compiled at build time:\n// process.env → ({}) (a bare read won't crash hydration)\n// process.env.NODE_ENV → \"production\" (the build mode - frameworks' prod/dev branch)\n// process.env.SECRET_KEY → undefined (unprefixed → never exposed; no secret leak)\n// process.env.PUBLIC_API_URL → \"https://api.example.com\" (PUBLIC_-prefixed → baked in by value)\n//\n// So to ship a value to the browser, give it a PUBLIC_ prefix in the BUILD environment:\n// PUBLIC_API_URL=https://api.example.com bun run build\n// then read process.env.PUBLIC_API_URL in app code - it becomes a string literal after the build.\nimport { buildClient } from \"@nifrajs/web/build\"\n\n// Override the prefix (or disable auto-exposure) via buildClient:\nawait buildClient({\n routesDir: \"./routes\",\n outDir: \"./dist\",\n clientModule: \"@nifrajs/web-react/client\",\n publicEnvPrefix: \"NIFRA_PUBLIC_\", // default \"PUBLIC_\"; \"\" disables auto-exposure entirely\n})"
|
|
85
127
|
},
|
|
86
128
|
{
|
|
87
129
|
"name": "REUSABLE_ROUTER",
|
|
88
|
-
"topic": "Nifra
|
|
130
|
+
"topic": "Nifra - Framework contract",
|
|
89
131
|
"slug": "contract",
|
|
90
132
|
"code": "// A PUBLISHABLE router - contract-first, so its types survive `.d.ts` emit for consumers.\nimport { client } from \"@nifrajs/client\"\nimport { defineContract, implement } from \"@nifrajs/core/contract\"\nimport { server } from \"@nifrajs/core/server\"\nimport { t } from \"@nifrajs/schema\"\n\n// --- in the package (say @your-org/billing) ---\n// 1. The CONTRACT is ONE plain declared object - methods, paths, schemas, no handlers.\n// Export it: it is the single type the package publishes, and a consumer's typed client is\n// built from THIS value (not from `typeof app`), so no internal server type crosses the seam.\nexport const billingContract = defineContract({\n getPlan: { method: \"GET\", path: \"/billing/plan\" },\n subscribe: { method: \"POST\", path: \"/billing/subscribe\", body: t.object({ plan: t.string() }) },\n})\n\n// 2. A FACTORY closes over the host's dependencies and returns a real, mountable router.\n// `implement` binds handlers to the contract; each handler's context is typed from the contract\n// alone (`c.body` is { plan: string } below) - no per-route `AddRoute` inference accreting.\nexport interface BillingDeps {\n charge(plan: string): Promise<{ id: string }>\n}\nexport function createBillingRouter(deps: BillingDeps) {\n return implement(billingContract, {\n getPlan: () => ({ plan: \"pro\" }),\n subscribe: (c) => deps.charge(c.body.plan),\n })\n}\n\n// --- in the consuming app ---\n// 3. Mount the router with ONE `.merge()` - no per-route `.get()/.post()` chain to re-infer.\nconst app = server().merge(createBillingRouter({ charge: async (plan) => ({ id: plan }) }))\n\n// 4. Build the client from the EXPORTED contract value - decoupled, no `typeof app` import needed.\nconst api = client(billingContract, \"https://api.example.com\")\nconst { data } = await api.billing.plan.get() // GET /billing/plan\nawait api.billing.subscribe.post({ plan: \"pro\" }) // POST /billing/subscribe, body typed"
|
|
91
133
|
},
|
|
92
134
|
{
|
|
93
135
|
"name": "SEO_EXAMPLE",
|
|
94
|
-
"topic": "Nifra
|
|
136
|
+
"topic": "Nifra - Framework contract",
|
|
95
137
|
"slug": "contract",
|
|
96
|
-
"code": "// routes/articles/[slug].tsx
|
|
138
|
+
"code": "// routes/articles/[slug].tsx - a complete SEO head: canonical + Open Graph + Twitter + JSON-LD,\n// with ABSOLUTE URLs built from `origin`. The three helpers (canonical/openGraph/jsonLd) are public\n// exports of @nifrajs/web; `origin` is the third MetaArgs field (after data + params).\nimport { canonical, jsonLd, openGraph, type MetaArgs } from \"@nifrajs/web\"\n\n// CAVEAT: meta() runs in BOTH SSR and client navigation, so it has NO server env - never read\n// `process.env` or the request here. The framework injects `origin` (the site's scheme+host, e.g.\n// \"https://news.example.com\") server-side from the request AND on the client from location.origin, so\n// the two match and an absolute og:url/canonical/og:image never drifts between SSR and a soft-nav. For\n// anything else server-only (an API base, a CDN host), thread it through the LOADER, not meta().\nexport const meta = ({ data, params, origin }: MetaArgs) => {\n const article = data as { title: string; summary: string; image: string; published: string }\n const url = `${origin}/articles/${params.slug}` // absolute, from the injected origin\n const image = `${origin}${article.image}` // og:image MUST be absolute for crawlers\n return {\n title: article.title,\n meta: [\n { name: \"description\", content: article.summary },\n // og:* - openGraph emits only the props you pass (+ og:type, here \"article\").\n ...openGraph({\n title: article.title,\n description: article.summary,\n url,\n image,\n type: \"article\",\n }),\n // twitter:* - a large-image summary card sharing the same absolute image.\n { name: \"twitter:card\", content: \"summary_large_image\" },\n { name: \"twitter:title\", content: article.title },\n { name: \"twitter:description\", content: article.summary },\n { name: \"twitter:image\", content: image },\n ],\n link: [canonical(url)], // <link rel=\"canonical\"> - the authoritative URL\n // JSON-LD structured data - escaped for safe <script> embedding by the head renderer.\n script: [\n jsonLd({\n \"@context\": \"https://schema.org\",\n \"@type\": \"NewsArticle\",\n headline: article.title,\n image,\n datePublished: article.published,\n mainEntityOfPage: url,\n }),\n ],\n }\n}"
|
|
97
139
|
},
|
|
98
140
|
{
|
|
99
141
|
"name": "STATIC_EMIT",
|
|
100
|
-
"topic": "Nifra
|
|
142
|
+
"topic": "Nifra - Framework contract",
|
|
101
143
|
"slug": "contract",
|
|
102
|
-
"code": "// build.ts
|
|
144
|
+
"code": "// build.ts - emit static HTML for opted-in routes. prerenderRoutes + cloudflarePagesRoutes\n// are PUBLIC (exported from @nifrajs/web/build) - no need to boot a server and curl it.\nimport { buildClient, prerenderRoutes, cloudflarePagesRoutes } from \"@nifrajs/web/build\"\nimport { discoverRoutes } from \"@nifrajs/web/fs\"\n\n// 1. Build the client bundle first (so the app references the hashed entry).\nawait buildClient({ routesDir: \"./routes\", outDir: \"./dist\", clientModule: \"@nifrajs/web-react/client\" })\n\n// 2. Drive the app's own fetch to render each opted-in route → dist/<path>/index.html + _data.json.\n// Static routes opt in with `export const prerender = true`; dynamic routes enumerate concrete\n// params with `export const getStaticPaths` (+ a `fallback: \"ssr\" | \"404\"`).\nconst { app } = await import(\"./server\")\nconst { prerendered } = await prerenderRoutes({\n app, // the built createWebApp (a { fetch } is enough)\n routes: discoverRoutes(\"./routes\").routes,\n outDir: \"./dist\",\n})\n\n// 3. Hybrid deploy: a Cloudflare Pages _routes.json that serves the prerendered HTML + _data.json\n// from the CDN and falls everything else through to the SSR worker.\nconst paths = prerendered.map((p) => p.path)\nawait Bun.write(\"./dist/_routes.json\", JSON.stringify(cloudflarePagesRoutes({ prerendered: paths })))"
|
|
103
145
|
},
|
|
104
146
|
{
|
|
105
147
|
"name": "CONTENT",
|
|
106
|
-
"topic": "Nifra
|
|
148
|
+
"topic": "Nifra - Loaders & actions",
|
|
107
149
|
"slug": "data",
|
|
108
|
-
"code": "// content.config.ts
|
|
150
|
+
"code": "// content.config.ts - a typed, validated collection over a folder of Markdown.\nimport { defineCollection } from \"@nifrajs/content/fs\"\nimport { t } from \"@nifrajs/schema\"\n\nexport const blog = defineCollection({\n dir: \"content/blog\",\n schema: t.object({ title: t.string(), date: t.string(), draft: t.boolean() }),\n})\n\n// a loader - typed + validated entries, no manual fs/frontmatter parsing:\nexport async function loader() {\n const posts = (await blog.all()).filter((p) => !p.frontmatter.draft)\n return { posts: posts.sort((a, b) => b.frontmatter.date.localeCompare(a.frontmatter.date)) }\n}\n// posts[0].frontmatter is { title; date; draft } (typed); posts[0].html is the rendered Markdown"
|
|
109
151
|
},
|
|
110
152
|
{
|
|
111
153
|
"name": "DECORATE",
|
|
112
|
-
"topic": "Nifra
|
|
154
|
+
"topic": "Nifra - Databases",
|
|
113
155
|
"slug": "database",
|
|
114
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { desc } from \"drizzle-orm\"\nimport { db, notes } from \"./db\" // your Drizzle client + schema (what the scaffold generates)\n\n// decorate() hangs the client on the context ONCE
|
|
156
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { desc } from \"drizzle-orm\"\nimport { db, notes } from \"./db\" // your Drizzle client + schema (what the scaffold generates)\n\n// decorate() hangs the client on the context ONCE - every handler then reads it as `c.db`, fully typed.\nexport const app = server()\n .decorate(\"db\", db)\n .get(\"/notes\", async (c) => c.db.select().from(notes).orderBy(desc(notes.createdAt)))\n\nexport type App = typeof app"
|
|
115
157
|
},
|
|
116
158
|
{
|
|
117
159
|
"name": "SQLITE",
|
|
118
|
-
"topic": "Nifra
|
|
160
|
+
"topic": "Nifra - Databases",
|
|
119
161
|
"slug": "database",
|
|
120
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { Database } from \"bun:sqlite\" // Node: better-sqlite3 · edge: Turso/libSQL\n\n// Open the connection ONCE at module scope (the driver pools internally).\nconst db = new Database(\"app.db\")\nconst byId = db.query<{ id: number; name: string }, [number]>(\"SELECT * FROM users WHERE id = ?\")\n\nexport const app = server().get(\"/users/:id\", (c) =>\n // Parameterized (?)
|
|
162
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { Database } from \"bun:sqlite\" // Node: better-sqlite3 · edge: Turso/libSQL\n\n// Open the connection ONCE at module scope (the driver pools internally).\nconst db = new Database(\"app.db\")\nconst byId = db.query<{ id: number; name: string }, [number]>(\"SELECT * FROM users WHERE id = ?\")\n\nexport const app = server().get(\"/users/:id\", (c) =>\n // Parameterized (?) - never interpolate user input into SQL.\n byId.get(Number(c.params.id)) ?? new Response(\"Not found\", { status: 404 }),\n)"
|
|
121
163
|
},
|
|
122
164
|
{
|
|
123
165
|
"name": "CF",
|
|
124
|
-
"topic": "Nifra
|
|
166
|
+
"topic": "Nifra - Deployment",
|
|
167
|
+
"slug": "deployment",
|
|
168
|
+
"code": "// _worker.ts - the edge SSR entry\nimport { toFetchHandler } from \"@nifrajs/core/server\"\nimport { createWebApp } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\nimport { clientEntry, manifest } from \"./server-manifest\" // generated by buildServer\n\nconst app = createWebApp({ adapter: reactAdapter, manifest, clientEntry })\nexport default toFetchHandler(app) // a Workers/Pages fetch handler\n\n// build.ts - buildClient (→ /assets) + buildServer (edge conditions → _worker.js)\nimport { buildClient, buildServer } from \"@nifrajs/web/build\""
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
"name": "SW",
|
|
172
|
+
"topic": "Nifra - Deployment",
|
|
125
173
|
"slug": "deployment",
|
|
126
|
-
"code": "//
|
|
174
|
+
"code": "// build.ts - after buildClient(), write the worker to the ORIGIN ROOT.\nimport { generateServiceWorker, serviceWorkerRegistration } from \"@nifrajs/web/service-worker\"\n\nexport async function emitServiceWorker(manifest: {\n entry: string\n assets: readonly string[]\n css?: readonly string[]\n}): Promise<string> {\n const sw = generateServiceWorker(manifest, {\n // Anything that changes exactly when the assets do: a content hash, a commit sha, a release.\n buildId: Bun.env.GIT_SHA ?? \"dev\",\n offlineUrl: \"/offline\",\n })\n await Bun.write(\"dist/sw.js\", sw)\n // Put this in a <script> in your document shell.\n return serviceWorkerRegistration(\"/sw.js\")\n}"
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"name": "DEVTOOLS",
|
|
178
|
+
"topic": "Nifra - Dev & HMR",
|
|
179
|
+
"slug": "dev",
|
|
180
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { devtools } from \"@nifrajs/devtools\"\n\n// Enabled only when NODE_ENV is \"development\" unless you say otherwise.\nexport const app = server().use(devtools())"
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "VITE_PROD",
|
|
184
|
+
"topic": "Nifra - Dev & HMR",
|
|
185
|
+
"slug": "dev",
|
|
186
|
+
"code": "// vite.config.ts - a Vite/Rollup PRODUCTION client build (the escape hatch, not the default).\n// Only reach for this when an app needs a Vite-only transform with no Bun equivalent; nifra's default\n// production bundler stays Bun (buildClient), which is faster and Bun-native.\nimport { viteLeakGuard } from \"@nifrajs/web/plugins/vite-leak-guard\"\n\nexport default {\n build: {\n // The SAME two client-leak guards nifra's Bun build runs - server-only code or a node: builtin\n // reaching the browser fails the build, with the identical error message. A second production\n // pipeline must not ship without them.\n rollupOptions: { plugins: [viteLeakGuard()] },\n },\n}"
|
|
127
187
|
},
|
|
128
188
|
{
|
|
129
189
|
"name": "LAB",
|
|
@@ -133,205 +193,235 @@
|
|
|
133
193
|
},
|
|
134
194
|
{
|
|
135
195
|
"name": "AUTOMATED",
|
|
136
|
-
"topic": "Nifra
|
|
196
|
+
"topic": "Nifra - Fonts",
|
|
137
197
|
"slug": "fonts",
|
|
138
|
-
"code": "// fonts.build.ts
|
|
198
|
+
"code": "// fonts.build.ts - run once at build time (a prebuild step). It hits the network.\nimport { loadGoogleFont } from \"@nifrajs/web/fonts\"\n\nconst inter = await loadGoogleFont(\n { family: \"Inter\", weights: [400, 700], subsets: [\"latin\"], sizeAdjust: \"107%\" },\n { outDir: \"public/fonts\", publicPath: \"/fonts\" },\n)\n\nawait Bun.write(\"app/fonts.css\", inter.css) // the @font-face stylesheet\nawait Bun.write(\"app/fonts.preloads.json\", JSON.stringify(inter.preloads))"
|
|
139
199
|
},
|
|
140
200
|
{
|
|
141
201
|
"name": "MANUAL",
|
|
142
|
-
"topic": "Nifra
|
|
202
|
+
"topic": "Nifra - Fonts",
|
|
143
203
|
"slug": "fonts",
|
|
144
|
-
"code": "// fonts.css
|
|
204
|
+
"code": "// fonts.css - you dropped inter.woff2 into public/fonts/ yourself\nimport { fontFace } from \"@nifrajs/web\"\n\nexport default fontFace({\n family: \"Inter\",\n src: [{ url: \"/fonts/inter.woff2\" }], // format() inferred from the extension\n weight: \"100 900\", // a variable-font range\n display: \"swap\", // the default - paints fallback text instantly\n sizeAdjust: \"107%\", // metric override → no fallback→web-font shift\n})"
|
|
145
205
|
},
|
|
146
206
|
{
|
|
147
207
|
"name": "PRELOAD",
|
|
148
|
-
"topic": "Nifra
|
|
208
|
+
"topic": "Nifra - Fonts",
|
|
149
209
|
"slug": "fonts",
|
|
150
|
-
"code": "// a root layout's meta
|
|
210
|
+
"code": "// a root layout's meta - becomes <link rel=\"preload\" as=\"font\" crossorigin> in <head>\nimport { fontPreload } from \"@nifrajs/web\"\n\nexport const meta = { link: [fontPreload({ href: \"/fonts/inter.woff2\" })] }"
|
|
151
211
|
},
|
|
152
212
|
{
|
|
153
213
|
"name": "SWAP",
|
|
154
|
-
"topic": "Nifra
|
|
214
|
+
"topic": "Nifra - Frameworks",
|
|
155
215
|
"slug": "frameworks",
|
|
156
|
-
"code": "// Server
|
|
216
|
+
"code": "// Server - pick an adapter. Everything else is identical across all five frameworks:\n// the same routes, loaders, actions, streaming, <Await>, fetchers, query cache.\nimport { createWebApp } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\" // ← or web-solid · web-vue · web-preact · web-svelte\nimport { clientEntry, manifest } from \"./server-manifest\" // generated by buildServer\n\nconst app = createWebApp({\n adapter: reactAdapter, // the one line that changes per framework\n manifest,\n clientEntry, // the built client bundle (from buildClient's manifest)\n})"
|
|
157
217
|
},
|
|
158
218
|
{
|
|
159
219
|
"name": "NEGOTIATE",
|
|
160
|
-
"topic": "Nifra
|
|
220
|
+
"topic": "Nifra - i18n",
|
|
161
221
|
"slug": "i18n",
|
|
162
222
|
"code": "// In a loader: resolve the locale + return only that catalog's messages.\nimport { negotiateLocale } from \"@nifrajs/i18n\"\nimport { catalogs, locales } from \"../catalogs\"\n\nexport async function loader({ request }: { request: Request }) {\n const q = new URL(request.url).searchParams.get(\"lang\")\n const locale = q && locales.includes(q) ? q : negotiateLocale(request, { locales, defaultLocale: \"en\", cookie: \"lang\" })\n return { locale, messages: catalogs[locale] } // cookie → Accept-Language → default\n}"
|
|
163
223
|
},
|
|
164
224
|
{
|
|
165
225
|
"name": "DIMENSIONS",
|
|
166
|
-
"topic": "Nifra
|
|
226
|
+
"topic": "Nifra - images",
|
|
167
227
|
"slug": "images",
|
|
168
|
-
"code": "import { imageDimensions, readImageDimensions } from \"@nifrajs/image\"\n\n// Pure-JS header read
|
|
228
|
+
"code": "import { imageDimensions, readImageDimensions } from \"@nifrajs/image\"\n\n// Pure-JS header read - PNG / JPEG / GIF / WebP. No decode, no codec, no deps.\nconst info = await readImageDimensions(Bun.file(\"public/hero.jpg\"))\n// → { width: 1200, height: 630, format: \"jpeg\" } (null if unrecognized)\n\n// Build-time tooling: pre-read sizes into a manifest so <Image> is CLS-safe\n// without hardcoding width/height at every call site. The sync variant takes the\n// raw header bytes you already have in memory (no file read):\nconst headerBytes = await Bun.file(\"public/hero.jpg\").bytes()\nimageDimensions(headerBytes)"
|
|
169
229
|
},
|
|
170
230
|
{
|
|
171
231
|
"name": "LOADERS",
|
|
172
|
-
"topic": "Nifra
|
|
232
|
+
"topic": "Nifra - images",
|
|
173
233
|
"slug": "images",
|
|
174
234
|
"code": "import type { ImageLoader } from \"@nifrajs/image\"\nimport { cloudflareLoader, identityLoader } from \"@nifrajs/image\"\n\n// Built-in: Cloudflare Images. `base` prepends an origin to bare paths.\nconst cf = cloudflareLoader({ base: \"https://assets.example.com\" })\n\n// The default when you pass no loader: no transform (still CLS-safe + lazy,\n// just no responsive variants).\nidentityLoader({ src: \"/a.png\", width: 800 }) // → \"/a.png\"\n\n// Any CDN is a pure (src, width, quality?) → URL builder:\nconst imgix: ImageLoader = ({ src, width, quality }) =>\n `https://my.imgix.net${src}?w=${width}&auto=format${quality ? `&q=${quality}` : \"\"}`"
|
|
175
235
|
},
|
|
176
236
|
{
|
|
177
237
|
"name": "SIGNING",
|
|
178
|
-
"topic": "Nifra
|
|
238
|
+
"topic": "Nifra - images",
|
|
179
239
|
"slug": "images",
|
|
180
|
-
"code": "// Lock the endpoint to URLs YOU minted
|
|
240
|
+
"code": "// Lock the endpoint to URLs YOU minted - kills resize-bombing (w/q enumeration).\n// SAME secret on loader + handler; it's server-only (inject from env, like a session secret).\nimport { selfHostedLoader, signImageUrl } from \"@nifrajs/image\"\nimport { createImageHandler } from \"@nifrajs/image/server\"\n\nconst IMAGE_SECRET = process.env.IMAGE_SECRET! // server-only; on Workers read c.env.IMAGE_SECRET instead\n\nconst resize = selfHostedLoader({ endpoint: \"/_image\", secret: IMAGE_SECRET })\n// → /_image?src=…&w=800&s=<hmac> (stable: an SSR-signed URL hydrates + caches identically)\n\nconst image = createImageHandler({ root: \"./public\", signing: { secret: IMAGE_SECRET } })\n// any request without a valid &s= (forged / tampered / expired) → 403, before any fetch or decode\n\n// Time-limited links to private images (server-side only):\nconst url = signImageUrl(\"/_image\", { src: \"/private/a.jpg\", width: 800 }, {\n secret: IMAGE_SECRET,\n expiresIn: 300, // seconds → adds &exp=\n})"
|
|
181
241
|
},
|
|
182
242
|
{
|
|
183
243
|
"name": "CLIENT",
|
|
184
|
-
"topic": "Nifra
|
|
244
|
+
"topic": "Nifra - Getting started",
|
|
185
245
|
"slug": "index",
|
|
186
|
-
"code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\"\n\n// The client infers the server's types
|
|
246
|
+
"code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\"\n\n// The client infers the server's types - no codegen. Never throws: { data, error }.\nconst api = client<typeof app>(\"http://localhost:3000\")\nconst { data, error } = await api.users({ id: \"7\" }).get()\n// ^? { id: string } | undefined"
|
|
187
247
|
},
|
|
188
248
|
{
|
|
189
249
|
"name": "HELLO",
|
|
190
|
-
"topic": "Nifra
|
|
250
|
+
"topic": "Nifra - Getting started",
|
|
191
251
|
"slug": "index",
|
|
192
252
|
"code": "import { server } from \"@nifrajs/core/server\"\n\nserver()\n .get(\"/\", () => ({ hello: \"world\" }))\n .get(\"/users/:id\", (c) => ({ id: c.params.id }))\n .listen(3000)"
|
|
193
253
|
},
|
|
194
254
|
{
|
|
195
255
|
"name": "CACHING",
|
|
196
|
-
"topic": "Nifra
|
|
256
|
+
"topic": "Nifra - Plugins & middleware",
|
|
197
257
|
"slug": "plugins",
|
|
198
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { cache, MemoryResponseCache, prettyJson } from \"@nifrajs/middleware\"\n\n// Full response cache: pluggable store, Vary-aware keys, byte cap. Bypasses Set-Cookie and respects\n// Cache-Control (no-store/private). MemoryResponseCache is per-instance
|
|
258
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { cache, MemoryResponseCache, prettyJson } from \"@nifrajs/middleware\"\n\n// Full response cache: pluggable store, Vary-aware keys, byte cap. Bypasses Set-Cookie and respects\n// Cache-Control (no-store/private). MemoryResponseCache is per-instance - refuses prod unless opted in.\nconst app = server()\napp.use(cache({ store: new MemoryResponseCache(), ttlMs: 30_000, vary: [\"accept-language\"] }))\napp.use(prettyJson()) // pretty-print JSON responses (size-capped; optional ?pretty query toggle)"
|
|
199
259
|
},
|
|
200
260
|
{
|
|
201
261
|
"name": "IDENTITY",
|
|
202
|
-
"topic": "Nifra
|
|
262
|
+
"topic": "Nifra - Plugins & middleware",
|
|
203
263
|
"slug": "plugins",
|
|
204
|
-
"code": "import { defineRouterPlugin, server } from \"@nifrajs/core/server\"\n\n// A plugin that mounts routes/hooks but adds NO context type: defineRouterPlugin (the clearer name for\n// defineIdentityPlugin) keeps app.use()'s return type EXACTLY the caller's server, so routes added after\n// .use() stay typed
|
|
264
|
+
"code": "import { defineRouterPlugin, server } from \"@nifrajs/core/server\"\n\n// A plugin that mounts routes/hooks but adds NO context type: defineRouterPlugin (the clearer name for\n// defineIdentityPlugin) keeps app.use()'s return type EXACTLY the caller's server, so routes added after\n// .use() stay typed - and the typed client derived from them stays intact.\nexport const scim = defineRouterPlugin(\"scim\", (app) => {\n app.get(\"/scim/v2/Users\", () => ({ Resources: [] })) // mount as a SIDE EFFECT (a runtime-only route)\n return app // return the app unchanged → caller's registry preserved\n})\n\n// /a AND /b stay fully typed across the .use():\nconst api = server().get(\"/a\", () => ({ a: 1 })).use(scim).get(\"/b\", () => ({ b: 2 }))"
|
|
205
265
|
},
|
|
206
266
|
{
|
|
207
267
|
"name": "JWT_BASIC",
|
|
208
|
-
"topic": "Nifra
|
|
268
|
+
"topic": "Nifra - Plugins & middleware",
|
|
209
269
|
"slug": "plugins",
|
|
210
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { jwt, jwks, basicAuth } from \"@nifrajs/middleware\"\n\n// JWT (WebCrypto): an explicit algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected.\nconst auth = jwt({ key: process.env.JWT_SECRET!, algorithms: [\"HS256\"], issuer: \"my-app\" })\nconst app = server()\n .use(auth) // 401s missing/invalid (optional:true lets them through)\n .get(\"/me\", (c) => auth.requireClaims(c.req)) // typed claims, or throws 401; auth.claims(req) is nullable\n// Asymmetric (rotating keys): key: jwks({ url: \"https://issuer/.well-known/jwks.json\" })
|
|
270
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { jwt, jwks, basicAuth } from \"@nifrajs/middleware\"\n\n// JWT (WebCrypto): an explicit algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected.\nconst auth = jwt({ key: process.env.JWT_SECRET!, algorithms: [\"HS256\"], issuer: \"my-app\" })\nconst app = server()\n .use(auth) // 401s missing/invalid (optional:true lets them through)\n .get(\"/me\", (c) => auth.requireClaims(c.req)) // typed claims, or throws 401; auth.claims(req) is nullable\n// Asymmetric (rotating keys): key: jwks({ url: \"https://issuer/.well-known/jwks.json\" }) - https-only, cached.\n\n// HTTP Basic - static creds compared in CONSTANT TIME (SHA-256 + timing-safe), or a verify callback.\napp.use(basicAuth({ username: \"admin\", password: process.env.PASS!, realm: \"staging\" }))"
|
|
211
271
|
},
|
|
212
272
|
{
|
|
213
273
|
"name": "MIDDLEWARE",
|
|
214
|
-
"topic": "Nifra
|
|
274
|
+
"topic": "Nifra - Plugins & middleware",
|
|
215
275
|
"slug": "plugins",
|
|
216
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { cors, rateLimit, securityHeaders, MemoryStore } from \"@nifrajs/middleware\"\n\n// Hardening middleware is a hook bundle (context-agnostic)
|
|
276
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { cors, rateLimit, securityHeaders, MemoryStore } from \"@nifrajs/middleware\"\n\n// Hardening middleware is a hook bundle (context-agnostic) - same app.use():\nconst app = server()\n .use(securityHeaders())\n .use(cors({ origin: [\"https://app.example\"] }))\n // MemoryStore is dev/single-instance only - use a shared store (Redis, etc.) in production.\n .use(rateLimit({ store: new MemoryStore(), max: 100, windowMs: 60_000 }))"
|
|
217
277
|
},
|
|
218
278
|
{
|
|
219
279
|
"name": "OFFICIAL",
|
|
220
|
-
"topic": "Nifra
|
|
280
|
+
"topic": "Nifra - Plugins & middleware",
|
|
221
281
|
"slug": "plugins",
|
|
222
282
|
"code": "import { server } from \"@nifrajs/core/server\"\nimport { requestId, logger, etag } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(requestId()) // reuse/generate x-request-id → c.requestId (typed) + response header\n .use(logger()) // one structured line/request: { method, path, status, ms }\n .use(etag()) // content-hash ETag on GET 200s → 304 on matching If-None-Match"
|
|
223
283
|
},
|
|
224
284
|
{
|
|
225
285
|
"name": "PERF",
|
|
226
|
-
"topic": "Nifra
|
|
286
|
+
"topic": "Nifra - Plugins & middleware",
|
|
227
287
|
"slug": "plugins",
|
|
228
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { compression, cacheControl } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(compression()) // gzip compressible responses (Accept-Encoding)\n .use(cacheControl(\"public, max-age=60\")) // Cache-Control on GET/HEAD 2xx (won't clobber)\n // …or a per-path policy
|
|
288
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { compression, cacheControl } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(compression()) // gzip compressible responses (Accept-Encoding)\n .use(cacheControl(\"public, max-age=60\")) // Cache-Control on GET/HEAD 2xx (won't clobber)\n // …or a per-path policy - return undefined to leave a response untouched:\n .use(cacheControl((req) =>\n new URL(req.url).pathname.startsWith(\"/assets/\")\n ? \"public, max-age=31536000, immutable\"\n : undefined))"
|
|
229
289
|
},
|
|
230
290
|
{
|
|
231
291
|
"name": "RELIABILITY",
|
|
232
|
-
"topic": "Nifra
|
|
292
|
+
"topic": "Nifra - Plugins & middleware",
|
|
233
293
|
"slug": "plugins",
|
|
234
294
|
"code": "import { server } from \"@nifrajs/core/server\"\nimport { idempotency, MemoryIdempotencyStore } from \"@nifrajs/middleware\"\n\n// A retried POST with the same Idempotency-Key replays the first response instead of\n// re-running the side effect. Shared store in production (atomic claim); dev-only memory store.\nconst app = server()\napp.use(idempotency({ store: new MemoryIdempotencyStore() }))"
|
|
235
295
|
},
|
|
236
296
|
{
|
|
237
297
|
"name": "SHAPING",
|
|
238
|
-
"topic": "Nifra
|
|
298
|
+
"topic": "Nifra - Plugins & middleware",
|
|
239
299
|
"slug": "plugins",
|
|
240
300
|
"code": "import { server } from \"@nifrajs/core/server\"\nimport { methodOverride, trimTrailingSlash, language, timing, poweredBy, combine } from \"@nifrajs/middleware\"\n\nconst app = server()\n .use(methodOverride()) // POST + X-HTTP-Method-Override → PUT/PATCH/DELETE (a real pre-routing request rewrite)\n .use(trimTrailingSlash()) // canonicalize URLs: 308 redirect (or rewrite), same-origin only, conservative methods\n .use(language({ supported: [\"en\", \"fr\"], defaultLanguage: \"en\" })) // Accept-Language → c.language + Content-Language\n .use(timing()) // Server-Timing header + typed c.timing marks/measures\n// poweredBy() is opt-in (nifra emits no X-Powered-By by default). combine(a, b, c) bundles several into one plugin."
|
|
241
301
|
},
|
|
242
302
|
{
|
|
243
303
|
"name": "BUILD",
|
|
244
|
-
"topic": "Nifra
|
|
304
|
+
"topic": "Nifra - Rendering: SSG & ISR",
|
|
245
305
|
"slug": "rendering",
|
|
246
|
-
"code": "// build.ts
|
|
306
|
+
"code": "// build.ts - buildClient, then prerender opted-in routes to static HTML.\nimport { buildClient, prerenderRoutes, cloudflarePagesRoutes } from \"@nifrajs/web/build\"\nimport { discoverRoutes } from \"@nifrajs/web/fs\"\n\nawait buildClient({ routesDir: \"./routes\", outDir: \"./dist\", clientModule: \"@nifrajs/web-react/client\" })\nconst { app } = await import(\"./server\")\nconst { prerendered } = await prerenderRoutes({\n app,\n routes: discoverRoutes(\"./routes\").routes,\n outDir: \"./dist\", // writes <path>/index.html + <path>/_data.json per prerendered route\n})\n\n// Hybrid deploy (Cloudflare Pages): serve prerendered HTML + _data.json from the CDN; everything\n// else falls through to the SSR worker.\nconst paths = prerendered.map((p) => p.path)\nBun.write(\"./dist/_routes.json\", JSON.stringify(cloudflarePagesRoutes({ prerendered: paths })))"
|
|
247
307
|
},
|
|
248
308
|
{
|
|
249
309
|
"name": "FONTS",
|
|
250
|
-
"topic": "Nifra
|
|
310
|
+
"topic": "Nifra - Rendering: SSG & ISR",
|
|
251
311
|
"slug": "rendering",
|
|
252
|
-
"code": "// fonts.css
|
|
312
|
+
"code": "// fonts.css - a CLS-safe @font-face for a self-hosted font (the pipeline bundles + hashes it).\nimport { fontFace } from \"@nifrajs/web\"\nexport default fontFace({\n family: \"Inter\",\n src: [{ url: \"/fonts/inter-var.woff2\" }], // self-host it - never hotlink a CDN\n weight: \"100 900\", // variable font\n sizeAdjust: \"100.06%\", ascentOverride: \"90%\", // optional: stop fallback->web-font layout shift\n})\n\n// a root layout - preload the file so it downloads WITH the document (not after CSS parse):\nimport { fontPreload } from \"@nifrajs/web\"\nexport const meta = { link: [fontPreload({ href: \"/fonts/inter-var.woff2\" })] }"
|
|
253
313
|
},
|
|
254
314
|
{
|
|
255
315
|
"name": "ISR",
|
|
256
|
-
"topic": "Nifra
|
|
316
|
+
"topic": "Nifra - Rendering: SSG & ISR",
|
|
257
317
|
"slug": "rendering",
|
|
258
|
-
"code": "// server.ts
|
|
318
|
+
"code": "// server.ts - wrap the app with Incremental Static Regeneration.\nimport { createWebApp, withISR, MemoryCacheStore } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\nimport { clientEntry, manifest } from \"./server-manifest\" // generated by buildServer\n\nconst app = createWebApp({ adapter: reactAdapter, manifest, clientEntry })\nconst store = new MemoryCacheStore() // dev / single-instance only\n\n// GET text/html responses are cached + served stale-while-revalidate. Default freshness 60s.\nconst isr = withISR(app, { store, revalidate: 60, now: () => Date.now() })\nBun.serve({ fetch: (req) => isr(req) })"
|
|
259
319
|
},
|
|
260
320
|
{
|
|
261
321
|
"name": "BOUNDED",
|
|
262
|
-
"topic": "Nifra
|
|
322
|
+
"topic": "Nifra - security & hardening",
|
|
263
323
|
"slug": "security",
|
|
264
|
-
"code": "import { server } from \"@nifrajs/core/server\"\n\nconst app = server()\n\n// A schema route is ALREADY bounded
|
|
324
|
+
"code": "import { server } from \"@nifrajs/core/server\"\n\nconst app = server()\n\n// A schema route is ALREADY bounded - the validated read enforces `maxBodyBytes`.\n// But a raw-body / file / BYO-validation route reads the body directly, which\n// `maxBodyBytes` does not cover. `c.boundedBody` caps that read:\napp.post(\"/import\", async (c) => {\n const bytes = await c.boundedBody(5 * 1024 * 1024) // cap THIS route at 5 MiB\n // Over-cap throws a flat 413; a malformed Content-Length a 400 - as control-flow\n // Responses (caught by the lifecycle like `throw redirect()`), so a handler can't\n // accidentally ignore the cap. The over-cap length is rejected BEFORE buffering;\n // a chunked / length-less body is aborted mid-stream once it crosses the cap.\n return { received: bytes.byteLength } // a returned object is serialized as JSON 200\n})\n\napp.post(\"/rpc\", async (c) => {\n const body = await c.boundedJson<{ method: string }>() // default: the server's maxBodyBytes\n // …bad JSON → 400. Then validate `body` with your schema before trusting it.\n return { method: body.method }\n})"
|
|
265
325
|
},
|
|
266
326
|
{
|
|
267
327
|
"name": "GATING",
|
|
268
|
-
"topic": "Nifra
|
|
328
|
+
"topic": "Nifra - security & hardening",
|
|
269
329
|
"slug": "security",
|
|
270
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { jwt, csrf, ipRestriction, bodyLimit } from \"@nifrajs/middleware\"\n\nconst app = server()\n // JWT: the algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected; exp enforced.\n .use(jwt({ key: process.env.JWT_SECRET!, algorithms: [\"HS256\"], issuer: \"my-app\" }))\n // Signed double-submit CSRF (HMAC) + Origin/Referer check on unsafe methods. Secret must be >= 32 bytes.\n .use(csrf({ secret: process.env.CSRF_SECRET! }))\n // Allow/deny by IPv4/IPv6 + CIDR. FAILS CLOSED with no trusted client IP; X-Forwarded-For is ignored\n // unless trustedProxies > 0 (set it to the number of proxies you actually run in front of the app).\n .use(ipRestriction({ allow: [\"10.0.0.0/8\", \"::1\"], trustedProxies: 1 }))\n // Reject oversized bodies at the EDGE by Content-Length, before routing
|
|
330
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { jwt, csrf, ipRestriction, bodyLimit } from \"@nifrajs/middleware\"\n\nconst app = server()\n // JWT: the algorithm allowlist is REQUIRED; alg:none and RSA/HMAC confusion are rejected; exp enforced.\n .use(jwt({ key: process.env.JWT_SECRET!, algorithms: [\"HS256\"], issuer: \"my-app\" }))\n // Signed double-submit CSRF (HMAC) + Origin/Referer check on unsafe methods. Secret must be >= 32 bytes.\n .use(csrf({ secret: process.env.CSRF_SECRET! }))\n // Allow/deny by IPv4/IPv6 + CIDR. FAILS CLOSED with no trusted client IP; X-Forwarded-For is ignored\n // unless trustedProxies > 0 (set it to the number of proxies you actually run in front of the app).\n .use(ipRestriction({ allow: [\"10.0.0.0/8\", \"::1\"], trustedProxies: 1 }))\n // Reject oversized bodies at the EDGE by Content-Length, before routing - fails closed (411) on a\n // length-less body. (The schema / c.boundedBody cap is the read-time guard; this is the cheap pre-filter.)\n .use(bodyLimit({ maxBytes: 1_000_000 }))"
|
|
271
331
|
},
|
|
272
332
|
{
|
|
273
333
|
"name": "LOGGING",
|
|
274
|
-
"topic": "Nifra
|
|
334
|
+
"topic": "Nifra - security & hardening",
|
|
275
335
|
"slug": "security",
|
|
276
336
|
"code": "import { server, jsonLogger, commonSecretPatterns } from \"@nifrajs/core/server\"\n\n// Key-name redaction is always on; valuePatterns adds opt-in value + message scanning.\nconst app = server({\n logger: jsonLogger(undefined, { valuePatterns: commonSecretPatterns }),\n})\n\n// logger.error(\"auth failed for user@example.com with Bearer abc.def\")\n// → { ...,\"message\":\"auth failed for [REDACTED] with [REDACTED]\" }\n// Add your own: { valuePatterns: [...commonSecretPatterns, /\\bord_[a-z0-9]+/g] }"
|
|
277
337
|
},
|
|
338
|
+
{
|
|
339
|
+
"name": "RESPONSE_CONTRACT",
|
|
340
|
+
"topic": "Nifra - security & hardening",
|
|
341
|
+
"slug": "security",
|
|
342
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { responseContract } from \"@nifrajs/core/response-contract\"\nimport { t } from \"@nifrajs/schema\"\n\nconst PublicUser = t.object({ id: t.string(), name: t.string() })\n\n// \"warn\" logs and changes nothing; \"enforce\" makes the contract the upper bound too.\n// Not installing the plugin is \"off\" - and keeps the lane out of your bundle entirely.\nexport const app = server().use(responseContract(\"enforce\")).get(\n \"/me\",\n { response: PublicUser },\n async () => {\n // Every column, including the ones the contract never declared.\n const user = { id: \"u1\", name: \"Ada\", email: \"a@b.c\", passwordHash: \"...\" }\n return user\n },\n)\n\n// off -> {\"id\":\"u1\",\"name\":\"Ada\",\"email\":\"a@b.c\",\"passwordHash\":\"...\"}\n// enforce -> {\"id\":\"u1\",\"name\":\"Ada\"}"
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"name": "DECLARE",
|
|
346
|
+
"topic": "Nifra - Server functions",
|
|
347
|
+
"slug": "server-functions",
|
|
348
|
+
"code": "// todos.fn.ts - the suffix is what tells the build to strip this module from the client.\nimport { serverFn } from \"@nifrajs/web/fn\"\nimport { t } from \"@nifrajs/schema\"\nimport { createTodo } from \"./db/write.ts\"\n\nexport const addTodo = serverFn(\n { input: t.object({ text: t.string({ minLength: 1 }) }), capabilities: [\"db.write\"] },\n async ({ text }) => createTodo({ text }),\n)"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
"name": "MOUNT",
|
|
352
|
+
"topic": "Nifra - Server functions",
|
|
353
|
+
"slug": "server-functions",
|
|
354
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { serverFunctions } from \"@nifrajs/web/fn\"\nimport * as todos from \"./todos.fn.ts\"\n\n// \"todos\" is the namespace segment; each export mounts at /_nifra/fn/todos/<exportName>.\nexport const app = server().use(serverFunctions(\"todos\", todos))"
|
|
355
|
+
},
|
|
278
356
|
{
|
|
279
357
|
"name": "SSE",
|
|
280
|
-
"topic": "Nifra
|
|
358
|
+
"topic": "Nifra - Streaming",
|
|
281
359
|
"slug": "streaming",
|
|
282
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { sse } from \"@nifrajs/core/sse\"\n\nconst app = server()\n// Your pub-sub of choice
|
|
360
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { sse } from \"@nifrajs/core/sse\"\n\nconst app = server()\n// Your pub-sub of choice - subscribe returns an unsubscribe function.\ndeclare const notifications: { subscribe(on: (n: { id: string }) => void): () => void }\n\n// A live feed - push events until the client disconnects.\napp.get(\"/notifications\", (c) =>\n sse(c, (stream) => {\n const off = notifications.subscribe((n) =>\n stream.send({ event: \"notification\", id: n.id, data: JSON.stringify(n) }),\n )\n // Keep the connection open until the client leaves, then tear down.\n return new Promise<void>((resolve) =>\n stream.signal.addEventListener(\"abort\", () => { off(); resolve() }, { once: true }),\n )\n }, { keepAlive: 15_000 }),\n)"
|
|
283
361
|
},
|
|
284
362
|
{
|
|
285
363
|
"name": "BASIC",
|
|
286
|
-
"topic": "Nifra
|
|
364
|
+
"topic": "Nifra - Contract-derived adversarial testing",
|
|
287
365
|
"slug": "testing",
|
|
288
366
|
"code": "import { assertAdversarialContract } from \"@nifrajs/testing\"\nimport { app } from \"../src/app\"\n\nconst { test } = await import(\"bun:test\")\n\ntest(\"the API contract withstands hostile inputs\", async () => {\n await assertAdversarialContract(app, { seed: 73 })\n})"
|
|
289
367
|
},
|
|
368
|
+
{
|
|
369
|
+
"name": "MOCK",
|
|
370
|
+
"topic": "Nifra - Contract-derived adversarial testing",
|
|
371
|
+
"slug": "testing",
|
|
372
|
+
"code": "import { createMockServer } from \"@nifrajs/mock\"\nimport { app } from \"./app.ts\"\n\n// Reads the routes' `response` schemas and generates data matching their shape.\n// The seed is fixed, so a snapshot taken today still matches tomorrow.\nconst mock = createMockServer(app, { seed: 42 })\n\nconst res = await mock.fetch(new Request(\"http://local/notes\"))"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
"name": "VALIDATE_RESPONSES",
|
|
376
|
+
"topic": "Nifra - Contract-derived adversarial testing",
|
|
377
|
+
"slug": "testing",
|
|
378
|
+
"code": "import { testClient } from \"@nifrajs/client\"\nimport { app } from \"./app\"\n\n// Every JSON response is checked against the route's declared schema for its status:\n// `response` for 2xx, `errors[status]` for declared failures. A mismatch THROWS.\nconst api = testClient<typeof app>(app, { validateResponses: true })\n\nconst res = await api.me.get() // ResponseContractViolation if the payload drifted"
|
|
379
|
+
},
|
|
290
380
|
{
|
|
291
381
|
"name": "CONTRACT_FIRST",
|
|
292
|
-
"topic": "Nifra
|
|
382
|
+
"topic": "Nifra - Troubleshooting",
|
|
293
383
|
"slug": "troubleshooting",
|
|
294
384
|
"code": "import { defineContract, implement } from \"@nifrajs/core/contract\"\n\n// One object type, declared upfront - NOT an N-deep stack of `Server<AddRoute<…>>` aliases.\nconst contract = defineContract({\n getUser: { method: \"GET\", path: \"/users/:id\" },\n listUsers: { method: \"GET\", path: \"/users\" },\n createUser: { method: \"POST\", path: \"/users\" },\n // ...hundreds more operations stay flat: the registry is one type, not a growing chain.\n})\n\nexport const app = implement(contract, {\n getUser: (c) => ({ id: c.params.id }),\n listUsers: () => ({ users: [] as string[] }),\n createUser: () => ({ created: true }),\n})"
|
|
295
385
|
},
|
|
296
386
|
{
|
|
297
387
|
"name": "MERGE_SPLIT",
|
|
298
|
-
"topic": "Nifra
|
|
388
|
+
"topic": "Nifra - Troubleshooting",
|
|
299
389
|
"slug": "troubleshooting",
|
|
300
390
|
"code": "import { server } from \"@nifrajs/core\"\n\n// Each domain is its OWN short server() chain, kept well under the ~95-route ceiling.\nconst users = server()\n .get(\"/users/:id\", (c) => ({ id: c.params.id }))\n .post(\"/users\", () => ({ created: true }))\n\nconst orders = server()\n .get(\"/orders/:id\", (c) => ({ id: c.params.id }))\n .post(\"/orders\", () => ({ placed: true }))\n\n// merge() adds ONE R & R2 intersection per group - no per-call context recompute - so the whole\n// app stays inside tsc's budget no matter how many groups (or routes) you compose.\nexport const app = server()\n .get(\"/health\", () => ({ ok: true }))\n .merge(users)\n .merge(orders)"
|
|
301
391
|
},
|
|
302
392
|
{
|
|
303
393
|
"name": "SERVER_ONLY_MARKER",
|
|
304
|
-
"topic": "Nifra
|
|
394
|
+
"topic": "Nifra - Troubleshooting",
|
|
305
395
|
"slug": "troubleshooting",
|
|
306
|
-
"code": "// secrets.ts
|
|
396
|
+
"code": "// secrets.ts - pure server logic: a secret constant, no `node:` import to catch.\nimport \"@nifrajs/web/server-only\" // ← fails the CLIENT build (loud, with the import chain)\nimport type { ServerOnly } from \"@nifrajs/web\" // ← type-level intent: this value is server-only\n\n// `ServerOnly<string>` is structurally `string` (the brand is an optional phantom field), so it\n// stays assignment-compatible - it documents intent without obstructing real use.\nexport const apiKey: ServerOnly<string> = process.env.SECRET_API_KEY!\n\n// If this module ever reaches a browser chunk, buildClient fails with:\n// server-only module reached the client bundle via routes/x.tsx → ./secrets.ts (marked server-only)\n// Reach it from a loader/action (server-only) instead, and the secret never ships to the client."
|
|
307
397
|
},
|
|
308
398
|
{
|
|
309
399
|
"name": "CLIENT",
|
|
310
|
-
"topic": "Nifra
|
|
400
|
+
"topic": "Nifra - Types-first architecture",
|
|
311
401
|
"slug": "types-first",
|
|
312
|
-
"code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\" // a TYPE import
|
|
402
|
+
"code": "import { client } from \"@nifrajs/client\"\nimport type { app } from \"./server\" // a TYPE import - server code never ships to the client\n\nconst api = client<typeof app>(\"https://api.example.com\")\n\nconst res = await api.users({ id: \"42\" }).get() // path + params autocomplete, no codegen\nif (res.ok) {\n res.data.name // typed from the route's response schema\n} else {\n res.error // client-call failures are returned, never thrown\n}"
|
|
313
403
|
},
|
|
314
404
|
{
|
|
315
405
|
"name": "OPENAPI",
|
|
316
|
-
"topic": "Nifra
|
|
406
|
+
"topic": "Nifra - Types-first architecture",
|
|
317
407
|
"slug": "types-first",
|
|
318
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { openapi } from \"@nifrajs/middleware\"\nimport { GetUser } from \"./schema\" // the contract defined above\n\n// Generates an OpenAPI 3.1 document from your registered routes
|
|
408
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { openapi } from \"@nifrajs/middleware\"\nimport { GetUser } from \"./schema\" // the contract defined above\n\n// Generates an OpenAPI 3.1 document from your registered routes - lazily, on first request.\nexport const app = server()\n .use(openapi({ info: { title: \"My API\", version: \"1.0.0\" }, ui: true }))\n .get(\"/users/:id\", GetUser, (c) => ({ id: c.params.id, name: \"Ada\", role: \"admin\" as const }))\n\n// → GET /openapi.json (the spec, generated from your schemas)\n// → GET /reference (a Scalar API-reference page, because `ui: true`)"
|
|
319
409
|
},
|
|
320
410
|
{
|
|
321
411
|
"name": "ROUTE",
|
|
322
|
-
"topic": "Nifra
|
|
412
|
+
"topic": "Nifra - Types-first architecture",
|
|
323
413
|
"slug": "types-first",
|
|
324
|
-
"code": "import { server } from \"@nifrajs/core/server\"\nimport { GetUser } from \"./schema\"\n\nexport const app = server().get(\"/users/:id\", GetUser, (c) => {\n // c.params.id is typed `string`
|
|
414
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { GetUser } from \"./schema\"\n\nexport const app = server().get(\"/users/:id\", GetUser, (c) => {\n // c.params.id is typed `string` - parsed from the path and validated at the boundary.\n return { id: c.params.id, name: \"Ada\", role: \"admin\" as const }\n // ^ the return is checked against GetUser.response - a wrong shape is a tsc error.\n})"
|
|
325
415
|
},
|
|
326
416
|
{
|
|
327
417
|
"name": "SCHEMA",
|
|
328
|
-
"topic": "Nifra
|
|
418
|
+
"topic": "Nifra - Types-first architecture",
|
|
329
419
|
"slug": "types-first",
|
|
330
420
|
"code": "import { t } from \"@nifrajs/schema\"\n\n// One contract, defined once. Everything below is derived from it.\nexport const GetUser = {\n params: t.object({ id: t.string() }),\n response: t.object({\n id: t.string(),\n name: t.string(),\n role: t.union([t.literal(\"admin\"), t.literal(\"user\")]),\n }),\n}"
|
|
331
421
|
},
|
|
332
422
|
{
|
|
333
423
|
"name": "BASIC",
|
|
334
|
-
"topic": "Nifra
|
|
424
|
+
"topic": "Nifra - WebSockets",
|
|
335
425
|
"slug": "websockets",
|
|
336
426
|
"code": "import { server } from \"@nifrajs/core/server\"\nimport { websocket } from \"@nifrajs/core/ws\" // .use(websocket()) enables app.ws()\n\nconst app = server()\n .use(websocket())\n .get(\"/\", () => ({ ok: true }))\n .ws(\"/echo\", {\n open: (ws) => ws.send(\"welcome\"),\n message: (ws, data) => ws.send(data), // data: string | Uint8Array\n })\n\napp.listen(3000) // Bun"
|
|
337
427
|
}
|