@nifrajs/cli 2.2.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 +61 -22
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +332 -86
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +30 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +132 -36
- 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 +3 -3
- package/dist/doctor.js +8 -8
- 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 +6 -6
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +15 -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.js +9 -9
- 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 +6 -6
- 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 +75 -45
- 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 +1 -1
- package/dist/pipeline-guard.js +1 -1
- 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 +5 -5
- package/dist/types-search.js +7 -7
- 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 +174 -90
- package/docs/llms-full.txt +1696 -747
- package/docs/types.json +1892 -3105
- package/package.json +22 -11
- package/src/assure.ts +17 -18
- package/src/capabilities-tool.ts +82 -16
- package/src/check.ts +405 -103
- package/src/cli.ts +141 -38
- package/src/diff-tool.ts +8 -8
- package/src/docs-search.ts +6 -6
- package/src/doctor.ts +10 -10
- 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 +21 -12
- package/src/manifest-tool.ts +4 -2
- package/src/mcp-docs-tools.ts +9 -9
- package/src/mcp-http.ts +7 -7
- package/src/mcp-protocol.ts +2 -2
- package/src/mcp-render.ts +6 -6
- package/src/mcp-run.ts +2 -2
- package/src/mcp.ts +82 -45
- package/src/openapi-tool.ts +2 -2
- package/src/pipeline-guard.ts +1 -1
- 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 +8 -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,69 +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",
|
|
125
167
|
"slug": "deployment",
|
|
126
|
-
"code": "// _worker.ts
|
|
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",
|
|
173
|
+
"slug": "deployment",
|
|
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())"
|
|
127
181
|
},
|
|
128
182
|
{
|
|
129
183
|
"name": "VITE_PROD",
|
|
130
|
-
"topic": "Nifra
|
|
184
|
+
"topic": "Nifra - Dev & HMR",
|
|
131
185
|
"slug": "dev",
|
|
132
|
-
"code": "// vite.config.ts
|
|
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}"
|
|
133
187
|
},
|
|
134
188
|
{
|
|
135
189
|
"name": "LAB",
|
|
@@ -139,205 +193,235 @@
|
|
|
139
193
|
},
|
|
140
194
|
{
|
|
141
195
|
"name": "AUTOMATED",
|
|
142
|
-
"topic": "Nifra
|
|
196
|
+
"topic": "Nifra - Fonts",
|
|
143
197
|
"slug": "fonts",
|
|
144
|
-
"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))"
|
|
145
199
|
},
|
|
146
200
|
{
|
|
147
201
|
"name": "MANUAL",
|
|
148
|
-
"topic": "Nifra
|
|
202
|
+
"topic": "Nifra - Fonts",
|
|
149
203
|
"slug": "fonts",
|
|
150
|
-
"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})"
|
|
151
205
|
},
|
|
152
206
|
{
|
|
153
207
|
"name": "PRELOAD",
|
|
154
|
-
"topic": "Nifra
|
|
208
|
+
"topic": "Nifra - Fonts",
|
|
155
209
|
"slug": "fonts",
|
|
156
|
-
"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\" })] }"
|
|
157
211
|
},
|
|
158
212
|
{
|
|
159
213
|
"name": "SWAP",
|
|
160
|
-
"topic": "Nifra
|
|
214
|
+
"topic": "Nifra - Frameworks",
|
|
161
215
|
"slug": "frameworks",
|
|
162
|
-
"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})"
|
|
163
217
|
},
|
|
164
218
|
{
|
|
165
219
|
"name": "NEGOTIATE",
|
|
166
|
-
"topic": "Nifra
|
|
220
|
+
"topic": "Nifra - i18n",
|
|
167
221
|
"slug": "i18n",
|
|
168
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}"
|
|
169
223
|
},
|
|
170
224
|
{
|
|
171
225
|
"name": "DIMENSIONS",
|
|
172
|
-
"topic": "Nifra
|
|
226
|
+
"topic": "Nifra - images",
|
|
173
227
|
"slug": "images",
|
|
174
|
-
"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)"
|
|
175
229
|
},
|
|
176
230
|
{
|
|
177
231
|
"name": "LOADERS",
|
|
178
|
-
"topic": "Nifra
|
|
232
|
+
"topic": "Nifra - images",
|
|
179
233
|
"slug": "images",
|
|
180
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}` : \"\"}`"
|
|
181
235
|
},
|
|
182
236
|
{
|
|
183
237
|
"name": "SIGNING",
|
|
184
|
-
"topic": "Nifra
|
|
238
|
+
"topic": "Nifra - images",
|
|
185
239
|
"slug": "images",
|
|
186
|
-
"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})"
|
|
187
241
|
},
|
|
188
242
|
{
|
|
189
243
|
"name": "CLIENT",
|
|
190
|
-
"topic": "Nifra
|
|
244
|
+
"topic": "Nifra - Getting started",
|
|
191
245
|
"slug": "index",
|
|
192
|
-
"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"
|
|
193
247
|
},
|
|
194
248
|
{
|
|
195
249
|
"name": "HELLO",
|
|
196
|
-
"topic": "Nifra
|
|
250
|
+
"topic": "Nifra - Getting started",
|
|
197
251
|
"slug": "index",
|
|
198
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)"
|
|
199
253
|
},
|
|
200
254
|
{
|
|
201
255
|
"name": "CACHING",
|
|
202
|
-
"topic": "Nifra
|
|
256
|
+
"topic": "Nifra - Plugins & middleware",
|
|
203
257
|
"slug": "plugins",
|
|
204
|
-
"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)"
|
|
205
259
|
},
|
|
206
260
|
{
|
|
207
261
|
"name": "IDENTITY",
|
|
208
|
-
"topic": "Nifra
|
|
262
|
+
"topic": "Nifra - Plugins & middleware",
|
|
209
263
|
"slug": "plugins",
|
|
210
|
-
"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 }))"
|
|
211
265
|
},
|
|
212
266
|
{
|
|
213
267
|
"name": "JWT_BASIC",
|
|
214
|
-
"topic": "Nifra
|
|
268
|
+
"topic": "Nifra - Plugins & middleware",
|
|
215
269
|
"slug": "plugins",
|
|
216
|
-
"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\" }))"
|
|
217
271
|
},
|
|
218
272
|
{
|
|
219
273
|
"name": "MIDDLEWARE",
|
|
220
|
-
"topic": "Nifra
|
|
274
|
+
"topic": "Nifra - Plugins & middleware",
|
|
221
275
|
"slug": "plugins",
|
|
222
|
-
"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 }))"
|
|
223
277
|
},
|
|
224
278
|
{
|
|
225
279
|
"name": "OFFICIAL",
|
|
226
|
-
"topic": "Nifra
|
|
280
|
+
"topic": "Nifra - Plugins & middleware",
|
|
227
281
|
"slug": "plugins",
|
|
228
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"
|
|
229
283
|
},
|
|
230
284
|
{
|
|
231
285
|
"name": "PERF",
|
|
232
|
-
"topic": "Nifra
|
|
286
|
+
"topic": "Nifra - Plugins & middleware",
|
|
233
287
|
"slug": "plugins",
|
|
234
|
-
"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))"
|
|
235
289
|
},
|
|
236
290
|
{
|
|
237
291
|
"name": "RELIABILITY",
|
|
238
|
-
"topic": "Nifra
|
|
292
|
+
"topic": "Nifra - Plugins & middleware",
|
|
239
293
|
"slug": "plugins",
|
|
240
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() }))"
|
|
241
295
|
},
|
|
242
296
|
{
|
|
243
297
|
"name": "SHAPING",
|
|
244
|
-
"topic": "Nifra
|
|
298
|
+
"topic": "Nifra - Plugins & middleware",
|
|
245
299
|
"slug": "plugins",
|
|
246
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."
|
|
247
301
|
},
|
|
248
302
|
{
|
|
249
303
|
"name": "BUILD",
|
|
250
|
-
"topic": "Nifra
|
|
304
|
+
"topic": "Nifra - Rendering: SSG & ISR",
|
|
251
305
|
"slug": "rendering",
|
|
252
|
-
"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 })))"
|
|
253
307
|
},
|
|
254
308
|
{
|
|
255
309
|
"name": "FONTS",
|
|
256
|
-
"topic": "Nifra
|
|
310
|
+
"topic": "Nifra - Rendering: SSG & ISR",
|
|
257
311
|
"slug": "rendering",
|
|
258
|
-
"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\" })] }"
|
|
259
313
|
},
|
|
260
314
|
{
|
|
261
315
|
"name": "ISR",
|
|
262
|
-
"topic": "Nifra
|
|
316
|
+
"topic": "Nifra - Rendering: SSG & ISR",
|
|
263
317
|
"slug": "rendering",
|
|
264
|
-
"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) })"
|
|
265
319
|
},
|
|
266
320
|
{
|
|
267
321
|
"name": "BOUNDED",
|
|
268
|
-
"topic": "Nifra
|
|
322
|
+
"topic": "Nifra - security & hardening",
|
|
269
323
|
"slug": "security",
|
|
270
|
-
"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})"
|
|
271
325
|
},
|
|
272
326
|
{
|
|
273
327
|
"name": "GATING",
|
|
274
|
-
"topic": "Nifra
|
|
328
|
+
"topic": "Nifra - security & hardening",
|
|
275
329
|
"slug": "security",
|
|
276
|
-
"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 }))"
|
|
277
331
|
},
|
|
278
332
|
{
|
|
279
333
|
"name": "LOGGING",
|
|
280
|
-
"topic": "Nifra
|
|
334
|
+
"topic": "Nifra - security & hardening",
|
|
281
335
|
"slug": "security",
|
|
282
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] }"
|
|
283
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
|
+
},
|
|
284
356
|
{
|
|
285
357
|
"name": "SSE",
|
|
286
|
-
"topic": "Nifra
|
|
358
|
+
"topic": "Nifra - Streaming",
|
|
287
359
|
"slug": "streaming",
|
|
288
|
-
"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)"
|
|
289
361
|
},
|
|
290
362
|
{
|
|
291
363
|
"name": "BASIC",
|
|
292
|
-
"topic": "Nifra
|
|
364
|
+
"topic": "Nifra - Contract-derived adversarial testing",
|
|
293
365
|
"slug": "testing",
|
|
294
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})"
|
|
295
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
|
+
},
|
|
296
380
|
{
|
|
297
381
|
"name": "CONTRACT_FIRST",
|
|
298
|
-
"topic": "Nifra
|
|
382
|
+
"topic": "Nifra - Troubleshooting",
|
|
299
383
|
"slug": "troubleshooting",
|
|
300
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})"
|
|
301
385
|
},
|
|
302
386
|
{
|
|
303
387
|
"name": "MERGE_SPLIT",
|
|
304
|
-
"topic": "Nifra
|
|
388
|
+
"topic": "Nifra - Troubleshooting",
|
|
305
389
|
"slug": "troubleshooting",
|
|
306
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)"
|
|
307
391
|
},
|
|
308
392
|
{
|
|
309
393
|
"name": "SERVER_ONLY_MARKER",
|
|
310
|
-
"topic": "Nifra
|
|
394
|
+
"topic": "Nifra - Troubleshooting",
|
|
311
395
|
"slug": "troubleshooting",
|
|
312
|
-
"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."
|
|
313
397
|
},
|
|
314
398
|
{
|
|
315
399
|
"name": "CLIENT",
|
|
316
|
-
"topic": "Nifra
|
|
400
|
+
"topic": "Nifra - Types-first architecture",
|
|
317
401
|
"slug": "types-first",
|
|
318
|
-
"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}"
|
|
319
403
|
},
|
|
320
404
|
{
|
|
321
405
|
"name": "OPENAPI",
|
|
322
|
-
"topic": "Nifra
|
|
406
|
+
"topic": "Nifra - Types-first architecture",
|
|
323
407
|
"slug": "types-first",
|
|
324
|
-
"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`)"
|
|
325
409
|
},
|
|
326
410
|
{
|
|
327
411
|
"name": "ROUTE",
|
|
328
|
-
"topic": "Nifra
|
|
412
|
+
"topic": "Nifra - Types-first architecture",
|
|
329
413
|
"slug": "types-first",
|
|
330
|
-
"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})"
|
|
331
415
|
},
|
|
332
416
|
{
|
|
333
417
|
"name": "SCHEMA",
|
|
334
|
-
"topic": "Nifra
|
|
418
|
+
"topic": "Nifra - Types-first architecture",
|
|
335
419
|
"slug": "types-first",
|
|
336
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}"
|
|
337
421
|
},
|
|
338
422
|
{
|
|
339
423
|
"name": "BASIC",
|
|
340
|
-
"topic": "Nifra
|
|
424
|
+
"topic": "Nifra - WebSockets",
|
|
341
425
|
"slug": "websockets",
|
|
342
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"
|
|
343
427
|
}
|