@nifrajs/cli 3.5.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +62 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +275 -35
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +52 -13
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +48 -22
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +67 -17
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.js +1 -1
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-render.js +2 -2
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +110 -23
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +82 -9
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +1155 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +36 -0
- package/dist/recipes/4.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +71 -38
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +132 -87
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +44 -19
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +4 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +389 -48
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +53 -32
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- package/src/migrate-layout.ts +1338 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +5 -0
- package/src/recipes/4.0.0.ts +37 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +13 -8
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +71 -39
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +147 -95
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +50 -19
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
package/docs/examples.json
CHANGED
|
@@ -21,31 +21,55 @@
|
|
|
21
21
|
"name": "CLIENT",
|
|
22
22
|
"topic": "Nifra - API & typed client",
|
|
23
23
|
"slug": "api",
|
|
24
|
-
"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"
|
|
24
|
+
"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\nawait api.files(\"report.json\").get() // /files/:name.json → the segment as sent"
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
|
-
"name": "
|
|
27
|
+
"name": "COOKIES",
|
|
28
|
+
"topic": "Nifra - API & typed client",
|
|
29
|
+
"slug": "api",
|
|
30
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { t } from \"@nifrajs/schema\"\n\nexport const app = server().get(\n \"/dashboard\",\n { cookies: t.cookies({ session: t.string(), page: t.optional(t.integer()) }) },\n (c) => ({\n session: c.cookies.session, // string - a request without it is a 422 before this runs\n page: c.cookies.page ?? 1, // number | undefined - coerced from the cookie's text\n }),\n)"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "METHOD_ROUTES",
|
|
34
|
+
"topic": "Nifra - API & typed client",
|
|
35
|
+
"slug": "api",
|
|
36
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { all, method } from \"@nifrajs/core/methods\"\n\nexport const app = server()\n // GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS /echo\n .use(all(\"/echo\", (c) => ({ method: c.req.method })))\n // A method outside the standard seven\n .use(method(\"PURGE\", \"/cache/:key\", (c) => ({ purged: c.params.key })))\n // A list of methods, one handler\n .use(method([\"GET\", \"POST\"], \"/search\", (c) => ({ q: c.query.get(\"q\") })))"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "OPTIONAL_PARAMS",
|
|
40
|
+
"topic": "Nifra - API & typed client",
|
|
41
|
+
"slug": "api",
|
|
42
|
+
"code": "import { server } from \"@nifrajs/core/server\"\n\nexport const app = server()\n // GET /users and GET /users/42\n .get(\"/users/:id?\", (c) => (c.params.id === undefined ? { all: true } : { id: c.params.id }))\n // GET /archive, GET /archive/2026 and GET /archive/2026/09\n .get(\"/archive/:year?/:month?\", (c) => ({ year: c.params.year, month: c.params.month }))"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "PARAM_CONSTRAINTS",
|
|
46
|
+
"topic": "Nifra - API & typed client",
|
|
47
|
+
"slug": "api",
|
|
48
|
+
"code": "import { server } from \"@nifrajs/core/server\"\n\nexport const app = server()\n // /users/me is its own route; /users/42 is this one; /users/ada is a 404\n .get(\"/users/me\", () => ({ me: true }))\n .get(\"/users/:id{[0-9]+}\", (c) => ({ id: Number(c.params.id) }))\n // A list of values, and a count: exactly two capital letters\n .get(\"/img/:size{thumb|full}/:file\", (c) => ({ size: c.params.size, file: c.params.file }))\n .get(\"/countries/:code{[A-Z]{2}}\", (c) => ({ code: c.params.code }))\n // Inside a segment, and optional at the end of a path\n .get(\"/files/:name.:ext{png|jpg}\", (c) => ({ name: c.params.name, ext: c.params.ext }))\n .get(\"/posts/:page{[0-9]+}?\", (c) => ({ page: c.params.page ?? \"1\" }))"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "GUARD",
|
|
28
52
|
"topic": "Nifra - Auth & sessions",
|
|
29
53
|
"slug": "auth",
|
|
30
|
-
"code": "//
|
|
54
|
+
"code": "// routes/account.backend.ts - a protected route's loader reads the session and redirects when absent.\nimport { requireUser } from \"@nifrajs/auth\"\nimport { t } from \"@nifrajs/schema\"\nimport { getSessions } from \"../backend/auth\" // backend code: a backend half imports it directly\nimport type { Route } from \"./+types/account\"\n\nexport const loaderOutput = t.object({ userId: t.string() })\nexport async function loader({ request }: Route.LoaderArgs) {\n const session = await getSessions().read(request)\n // requireUser throws a 302 to /login when there's no session, and the loader stops there.\n const userId = requireUser(session, \"userId\", { redirectTo: \"/login\" })\n return { userId }\n}"
|
|
31
55
|
},
|
|
32
56
|
{
|
|
33
|
-
"name": "
|
|
34
|
-
"topic": "Nifra -
|
|
35
|
-
"slug": "
|
|
36
|
-
"code": "// backend.ts -
|
|
57
|
+
"name": "SETUP",
|
|
58
|
+
"topic": "Nifra - Auth & sessions",
|
|
59
|
+
"slug": "auth",
|
|
60
|
+
"code": "// backend/auth.ts - one session manager, created on first use so the secret is read at request time.\n// 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}))"
|
|
37
61
|
},
|
|
38
62
|
{
|
|
39
|
-
"name": "
|
|
63
|
+
"name": "BACKEND",
|
|
40
64
|
"topic": "Nifra - Backends & API (dev and prod)",
|
|
41
65
|
"slug": "backends",
|
|
42
|
-
"code": "//
|
|
66
|
+
"code": "// backend/app.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 }))"
|
|
43
67
|
},
|
|
44
68
|
{
|
|
45
69
|
"name": "COMPOSE",
|
|
46
70
|
"topic": "Nifra - Backends & API (dev and prod)",
|
|
47
71
|
"slug": "backends",
|
|
48
|
-
"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)"
|
|
72
|
+
"code": "// backend/app.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)"
|
|
49
73
|
},
|
|
50
74
|
{
|
|
51
75
|
"name": "EVENTS",
|
|
@@ -57,13 +81,13 @@
|
|
|
57
81
|
"name": "LOADER",
|
|
58
82
|
"topic": "Nifra - Backends & API (dev and prod)",
|
|
59
83
|
"slug": "backends",
|
|
60
|
-
"code": "// routes/index.
|
|
84
|
+
"code": "// routes/index.backend.ts - the loader calls the backend IN-PROCESS via api (no network).\nimport { t } from \"@nifrajs/schema\"\nimport type { Route } from \"./+types/index\"\n\nexport const loaderOutput = t.object({ me: t.object({ id: t.union([t.string(), t.null()]) }) })\nexport async function loader({ api }: Route.LoaderArgs) {\n const res = await api.api.me.get() // in-process: full validation/middleware, no HTTP round-trip\n return { me: res.data }\n}"
|
|
61
85
|
},
|
|
62
86
|
{
|
|
63
87
|
"name": "WIRE",
|
|
64
88
|
"topic": "Nifra - Backends & API (dev and prod)",
|
|
65
89
|
"slug": "backends",
|
|
66
|
-
"code": "// server.ts
|
|
90
|
+
"code": "// backend/server.ts - a hand-written server: createWebApp serves pages AND auto-mounts the\n// backend at /api/*. (`nifra build` emits this wiring for you; write it only for a custom host.)\nimport { inProcessClient } from \"@nifrajs/client\"\nimport { createWebApp } from \"@nifrajs/web\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\nimport { backend } from \"./app\"\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."
|
|
67
91
|
},
|
|
68
92
|
{
|
|
69
93
|
"name": "SERVER",
|
|
@@ -83,6 +107,12 @@
|
|
|
83
107
|
"slug": "capabilities",
|
|
84
108
|
"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 )"
|
|
85
109
|
},
|
|
110
|
+
{
|
|
111
|
+
"name": "OPAQUE",
|
|
112
|
+
"topic": "Nifra - Effect provenance",
|
|
113
|
+
"slug": "capabilities",
|
|
114
|
+
"code": "import { server } from \"@nifrajs/core\"\n\n// Stands in for a handler from another package (better-auth's): nifra cannot see what it reaches.\nconst authHandler = { fetch: (request: Request) => new Response(request.url) }\n\nexport const app = server().mount({\n path: \"/api/auth\",\n app: authHandler,\n opaque: \"better-auth's own handler; its effects are its own\",\n})"
|
|
115
|
+
},
|
|
86
116
|
{
|
|
87
117
|
"name": "POLICY",
|
|
88
118
|
"topic": "Nifra - Effect provenance",
|
|
@@ -93,7 +123,7 @@
|
|
|
93
123
|
"name": "ROOT",
|
|
94
124
|
"topic": "Nifra - Effect provenance",
|
|
95
125
|
"slug": "capabilities",
|
|
96
|
-
"code": "//
|
|
126
|
+
"code": "// backend/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 backend = server().merge(routes)"
|
|
97
127
|
},
|
|
98
128
|
{
|
|
99
129
|
"name": "CONTINUE",
|
|
@@ -119,23 +149,29 @@
|
|
|
119
149
|
"slug": "cli",
|
|
120
150
|
"code": "// nifra.config.ts - Bun's client and SSR build transforms.\nimport { stylexBunPlugin } from \"@nifrajs/web/plugins/stylex\"\n\nexport const clientPlugins = [stylexBunPlugin(\"dom\")]\nexport const serverPlugins = [stylexBunPlugin(\"ssr\")]"
|
|
121
151
|
},
|
|
152
|
+
{
|
|
153
|
+
"name": "CSP_POLICY",
|
|
154
|
+
"topic": "Nifra - Framework contract",
|
|
155
|
+
"slug": "contract",
|
|
156
|
+
"code": "import { createCspPolicy, createWebApp, MemoryCacheStore, withISR } from \"@nifrajs/web\"\n\ndeclare const adapter: import(\"@nifrajs/web\").RenderAdapter\ndeclare const manifest: import(\"@nifrajs/web\").Manifest\n\nconst csp = createCspPolicy({\n // `sources`: a sha256 hash per constant inline script nifra writes, plus 'nonce-…' only on a\n // page that needs one - it defer()s, or its meta names the nonce in unsafeInlineScript.\n header: ({ sources }) => `default-src 'self'; script-src 'self' ${sources}; object-src 'none'`,\n})\nconst app = createWebApp({ adapter, manifest, clientEntry: \"/assets/client.js\", csp })\n\n// Every other page is nonce-free, sends the same CSP header each time, and caches.\nexport const handler = withISR(app, {\n store: new MemoryCacheStore(),\n revalidate: 60,\n now: () => Date.now(),\n})"
|
|
157
|
+
},
|
|
122
158
|
{
|
|
123
159
|
"name": "DUAL_API",
|
|
124
160
|
"topic": "Nifra - Framework contract",
|
|
125
161
|
"slug": "contract",
|
|
126
|
-
"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 →
|
|
162
|
+
"code": "// TWO ways to handle a request - pick by who is calling.\nimport { t } from \"@nifrajs/schema\"\nimport type { LoaderContext } from \"@nifrajs/web\"\n\n// (1) A page's own data + mutations → its backend half's exports. A PUBLIC POST is a route ACTION\n// (routes/contact.backend.ts) - the browser POSTs the route's own URL, Nifra runs `action`:\nexport const actionOutput = t.object({ ok: t.boolean(), body: t.string() })\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 const loaderOutput = t.object({ id: t.string() })\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()`."
|
|
127
163
|
},
|
|
128
164
|
{
|
|
129
165
|
"name": "INLINE_SCRIPT",
|
|
130
166
|
"topic": "Nifra - Framework contract",
|
|
131
167
|
"slug": "contract",
|
|
132
|
-
"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,
|
|
168
|
+
"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, streamed scripts and every island tag\n head: {\n script: [{ content: JSON.stringify({ \"@type\": \"Article\" }) }], // inert: JSON only\n unsafeScript: [unsafeInlineScript(\"window.dataLayer = []\", { nonce })],\n },\n})"
|
|
133
169
|
},
|
|
134
170
|
{
|
|
135
171
|
"name": "PARAM_404",
|
|
136
172
|
"topic": "Nifra - Framework contract",
|
|
137
173
|
"slug": "contract",
|
|
138
|
-
"code": "// routes/users/[id].
|
|
174
|
+
"code": "// routes/users/[id].backend.ts - 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 { t } from \"@nifrajs/schema\"\nimport type { LoaderContext } from \"@nifrajs/web\"\n\ndeclare function lookupUser(id: string): Promise<{ id: string } | null>\n\nexport const loaderOutput = t.object({ user: t.object({ id: t.string() }) })\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."
|
|
139
175
|
},
|
|
140
176
|
{
|
|
141
177
|
"name": "PUBLIC_ENV",
|
|
@@ -143,6 +179,12 @@
|
|
|
143
179
|
"slug": "contract",
|
|
144
180
|
"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})"
|
|
145
181
|
},
|
|
182
|
+
{
|
|
183
|
+
"name": "RESPONSE_CONTROLS",
|
|
184
|
+
"topic": "Nifra - Framework contract",
|
|
185
|
+
"slug": "contract",
|
|
186
|
+
"code": "// routes/account.backend.ts - response headers and cookies, from a loader or an action.\nimport { t } from \"@nifrajs/schema\"\nimport { type LoaderContext, redirect } from \"@nifrajs/web\"\n\ndeclare function signIn(email: string, password: string): Promise<{ token: string } | null>\n\nexport const loaderOutput = t.object({ plan: t.string() })\nexport const actionOutput = t.object({ error: t.string() })\n\nexport async function loader(ctx: LoaderContext) {\n // Headers land on the rendered document.\n ctx.set.headers[\"cache-control\"] = \"public, max-age=60\"\n ctx.set.headers[\"x-robots-tag\"] = \"noindex\"\n return { plan: \"pro\" }\n}\n\nexport async function action(ctx: LoaderContext) {\n const form = await ctx.request.formData()\n const session = await signIn(String(form.get(\"email\")), String(form.get(\"password\")))\n if (session === null) return { error: \"Wrong email or password\" }\n // HttpOnly; Secure; SameSite=Lax; Path=/ unless the options say otherwise.\n ctx.set.cookie(\"session\", session.token, { maxAge: 60 * 60 * 24 * 7 })\n throw redirect(\"/dashboard\") // the cookie rides the redirect\n}"
|
|
187
|
+
},
|
|
146
188
|
{
|
|
147
189
|
"name": "REUSABLE_ROUTER",
|
|
148
190
|
"topic": "Nifra - Framework contract",
|
|
@@ -159,19 +201,37 @@
|
|
|
159
201
|
"name": "STATIC_EMIT",
|
|
160
202
|
"topic": "Nifra - Framework contract",
|
|
161
203
|
"slug": "contract",
|
|
162
|
-
"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 })))"
|
|
204
|
+
"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\"`) - both in the\n// route's .backend.ts.\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 })))"
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
"name": "ACTION",
|
|
208
|
+
"topic": "Nifra - Loaders & actions",
|
|
209
|
+
"slug": "data",
|
|
210
|
+
"code": "// routes/users/new.backend.ts - an action handles the route's POST.\nimport { t } from \"@nifrajs/schema\"\nimport type { Route } from \"./+types/new\"\n\nexport const actionOutput = t.object({ ok: t.boolean() })\n\nexport async function action({ api, request }: Route.ActionArgs) {\n const form = await request.formData()\n await api.users.post({ name: String(form.get(\"name\")) })\n return { ok: true } // the page's loaders revalidate automatically\n}"
|
|
163
211
|
},
|
|
164
212
|
{
|
|
165
213
|
"name": "CONTENT",
|
|
166
214
|
"topic": "Nifra - Loaders & actions",
|
|
167
215
|
"slug": "data",
|
|
168
|
-
"code": "// content.
|
|
216
|
+
"code": "// backend/content.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// routes/blog/index.backend.ts - typed + validated entries, no manual fs/frontmatter parsing:\nexport const loaderOutput = t.object({\n posts: t.array(t.object({ slug: t.string(), title: t.string(), html: t.string() })),\n})\n\nexport async function loader() {\n const posts = (await blog.all()).filter((p) => !p.frontmatter.draft)\n posts.sort((a, b) => b.frontmatter.date.localeCompare(a.frontmatter.date))\n return { posts: posts.map((p) => ({ slug: p.slug, title: p.frontmatter.title, html: p.html })) }\n}"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "LOADER",
|
|
220
|
+
"topic": "Nifra - Loaders & actions",
|
|
221
|
+
"slug": "data",
|
|
222
|
+
"code": "// routes/users/[id].backend.ts - a loader runs on the server: in-process during SSR (no network\n// round-trip), typed against your backend contract. This file never reaches the browser.\nimport { t } from \"@nifrajs/schema\"\nimport { notFound } from \"@nifrajs/web\"\nimport type { Route } from \"./+types/[id]\"\n\n// The page receives exactly this shape. Anything else the loader returns stays on the server.\nexport const loaderOutput = t.object({ id: t.string(), name: t.string() })\n\nexport async function loader({ params, api }: Route.LoaderArgs) {\n const res = await api.users({ id: params.id }).get()\n if (!res.ok) throw notFound()\n return res.data\n}"
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
"name": "SHOULD_REVALIDATE",
|
|
226
|
+
"topic": "Nifra - Loaders & actions",
|
|
227
|
+
"slug": "data",
|
|
228
|
+
"code": "// routes/orgs/[org]/_layout.backend.ts\nimport { t } from \"@nifrajs/schema\"\nimport type { ShouldRevalidate } from \"@nifrajs/web\"\n\nexport const loaderOutput = t.object({ name: t.string() })\n\nexport async function loader({ params }: { params: { org: string } }) {\n const res = await fetch(\"https://api.example.com/orgs/\" + encodeURIComponent(params.org))\n const org = (await res.json()) as { name: string }\n return { name: org.name }\n}\n\n// The org sidebar ignores the query: filtering a list beneath it keeps the org data,\n// switching orgs loads it again.\nexport const shouldRevalidate: ShouldRevalidate = ({ currentParams, nextParams }) =>\n currentParams.org !== nextParams.org"
|
|
169
229
|
},
|
|
170
230
|
{
|
|
171
231
|
"name": "DECORATE",
|
|
172
232
|
"topic": "Nifra - Databases",
|
|
173
233
|
"slug": "database",
|
|
174
|
-
"code": "
|
|
234
|
+
"code": "// backend/app.ts\nimport { 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 backend = server()\n .decorate(\"db\", db)\n .get(\"/notes\", async (c) => c.db.select().from(notes).orderBy(desc(notes.createdAt)))\n\nexport type Backend = typeof backend"
|
|
175
235
|
},
|
|
176
236
|
{
|
|
177
237
|
"name": "SQLITE",
|
|
@@ -183,7 +243,13 @@
|
|
|
183
243
|
"name": "CF",
|
|
184
244
|
"topic": "Nifra - Deployment",
|
|
185
245
|
"slug": "deployment",
|
|
186
|
-
"code": "//
|
|
246
|
+
"code": "// worker.ts - any nifra app as a Workers fetch handler\nimport { server, toFetchHandler } from \"@nifrajs/core/server\"\n\nconst app = server().get(\"/\", () => ({ ok: true }))\nexport default toFetchHandler(app)"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
"name": "HTTPS",
|
|
250
|
+
"topic": "Nifra - Deployment",
|
|
251
|
+
"slug": "deployment",
|
|
252
|
+
"code": "import { readFileSync } from \"node:fs\"\nimport { server } from \"@nifrajs/core\"\n\nconst app = server().get(\"/\", () => ({ ok: true }))\n\n// PEM text or the files' bytes.\nconst tls = { cert: readFileSync(\"cert.pem\"), key: readFileSync(\"key.pem\") }\n\napp.listen(443, { tls }) // Bun: https://, and wss:// for WebSockets, on one port"
|
|
187
253
|
},
|
|
188
254
|
{
|
|
189
255
|
"name": "LAMBDA",
|
|
@@ -207,7 +273,7 @@
|
|
|
207
273
|
"name": "VITE_PROD",
|
|
208
274
|
"topic": "Nifra - Dev & HMR",
|
|
209
275
|
"slug": "dev",
|
|
210
|
-
"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 {
|
|
276
|
+
"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 {\n viteAssetUrlGuard,\n viteBareBuiltinExternal,\n viteLeakGuard,\n} from \"@nifrajs/web/plugins/vite-leak-guard\"\n\nexport default {\n // Keeps a bare built-in (`fs/promises`) named instead of an empty stub, so the guard sees it.\n // viteAssetUrlGuard refuses a new URL(\"../backend/x.ts\", import.meta.url) - a file Vite would copy or\n // inline with no import for the graph guard to see. Last, after any framework plugin.\n plugins: [viteBareBuiltinExternal(), viteAssetUrlGuard()],\n build: {\n // The SAME client-leak guards Nifra's Bun build runs - backend code (a route's .backend.ts half,\n // backend/) or a node: builtin reaching the browser fails the build, with the identical message. A second production\n // pipeline must not ship without them. `node:` stays external so the guard can name it.\n rollupOptions: { external: [/^node:/], plugins: [viteLeakGuard()] },\n },\n}"
|
|
211
277
|
},
|
|
212
278
|
{
|
|
213
279
|
"name": "LAB",
|
|
@@ -239,17 +305,47 @@
|
|
|
239
305
|
"slug": "frameworks",
|
|
240
306
|
"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})"
|
|
241
307
|
},
|
|
308
|
+
{
|
|
309
|
+
"name": "ALTERNATES",
|
|
310
|
+
"topic": "Nifra - i18n",
|
|
311
|
+
"slug": "i18n",
|
|
312
|
+
"code": "// routes/[lang]/kundli.tsx - canonical + hreflang links and the document language.\nimport { defineLocales } from \"@nifrajs/i18n\"\nimport { defineI18nRouting } from \"@nifrajs/i18n/routing\"\nimport type { Meta, MetaArgs } from \"@nifrajs/web\"\n\nconst urls = defineI18nRouting(\n defineLocales({ default: \"en\", locales: { en: {}, hi: { hreflang: \"hi-IN\" }, ur: { tag: \"ur-PK\" } } }),\n)\n\nexport function meta({ params, origin }: MetaArgs): Meta {\n const lang = params.lang ?? \"\"\n const locale = urls.locales.isServed(lang) ? lang : urls.locales.default\n // Every page of this cluster passes the same locales, so the alternates stay reciprocal.\n const { canonical, links } = urls.alternates(urls.localizePathname(\"/kundli\", locale), { origin })\n return {\n ...urls.locales.documentMeta(locale),\n link: [\n { rel: \"canonical\", href: canonical },\n ...links.map((link) => ({ rel: \"alternate\", hreflang: link.hreflang, href: link.href })),\n ],\n }\n}"
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"name": "CATALOG",
|
|
316
|
+
"topic": "Nifra - i18n",
|
|
317
|
+
"slug": "i18n",
|
|
318
|
+
"code": "// shared/messages/en.ts - the default locale's catalog: ICU strings, lists and nested blocks.\nimport { createFormatter, type PartialMessages } from \"@nifrajs/i18n\"\n\nexport const en = {\n cart: \"{count, plural, =0 {empty} one {# item} other {# items}}\",\n place: \"You finished {rank, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}\",\n home: { title: \"Welcome back, {name}\" },\n faq: [{ q: \"Is it free?\", a: \"Yes.\" }],\n}\n\n// Another locale is partial; a key it lacks falls back to the next catalog.\nexport const fr: PartialMessages<typeof en> = {\n cart: \"{count, plural, =0 {vide} one {# article} other {# articles}}\",\n}\n\nconst t = createFormatter<typeof en>(\"fr\", fr, {\n fallback: [en],\n onMissing: (key, locale) => console.warn(`missing ${locale} translation: ${key}`),\n timeZone: \"Europe/Paris\",\n})\nt.t(\"cart\", { count: 1200 }) // \"1 200 articles\"\nt.t(\"home.title\", { name: \"Ada\" }) // \"Welcome back, Ada\", from en\nconst questions = t.get(\"faq\")?.map((item) => item.q)\nconsole.log(questions)"
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
"name": "CHECK_TEST",
|
|
322
|
+
"topic": "Nifra - i18n",
|
|
323
|
+
"slug": "i18n",
|
|
324
|
+
"code": "// Or as a test, with no CLI: the same checks, pure.\nimport { expect, test } from \"bun:test\"\nimport { defineLocales } from \"@nifrajs/i18n\"\nimport { checkCatalogs } from \"@nifrajs/i18n/check\"\n\nconst locales = defineLocales({ default: \"en\", locales: { en: {}, gu: {} } })\n\ntest(\"catalogs are consistent\", () => {\n const { findings } = checkCatalogs({\n locales,\n catalogs: { en: { hi: \"Hi {name}\" }, gu: { hi: \"નમસ્તે {name}\" } },\n })\n expect(findings.filter((finding) => finding.severity === \"error\")).toEqual([])\n})"
|
|
325
|
+
},
|
|
242
326
|
{
|
|
243
327
|
"name": "DETECTOR",
|
|
244
328
|
"topic": "Nifra - i18n",
|
|
245
329
|
"slug": "i18n",
|
|
246
330
|
"code": "// Or as a server plugin: c.locale on every handler, Content-Language on every response.\nimport { server } from \"@nifrajs/core\"\nimport { localeDetector } from \"@nifrajs/i18n/detector\"\n\nconst app = server().use(localeDetector({\n locales: [\"en\", \"fr\", \"de\"],\n defaultLocale: \"en\",\n queryParam: \"lang\",\n cookie: \"locale\",\n persist: true, // pin an explicit ?lang= choice into the cookie\n})).get(\"/\", (c) => c.json({ locale: c.locale, via: c.localeSource }))"
|
|
247
331
|
},
|
|
332
|
+
{
|
|
333
|
+
"name": "GUARD",
|
|
334
|
+
"topic": "Nifra - i18n",
|
|
335
|
+
"slug": "i18n",
|
|
336
|
+
"code": "// routes/[lang]/_layout.backend.ts - 404 unknown and draft locales, redirect /en/... to /...\nimport { defineLocales } from \"@nifrajs/i18n\"\nimport { defineI18nRouting } from \"@nifrajs/i18n/routing\"\nimport { notFound, type RouteMiddleware, redirect } from \"@nifrajs/web\"\n\nconst urls = defineI18nRouting(\n defineLocales({ default: \"en\", locales: { en: {}, hi: {}, gu: { draft: true } } }),\n)\n\nexport const middleware: RouteMiddleware = (ctx) => {\n const { pathname, search } = new URL(ctx.request.url)\n const match = urls.matchSegment(ctx.params.lang, pathname + search)\n if (match.kind === \"not-found\") notFound()\n if (match.kind === \"redirect\") return redirect(match.location, { status: 308 })\n return undefined\n}"
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
"name": "LOCALES",
|
|
340
|
+
"topic": "Nifra - i18n",
|
|
341
|
+
"slug": "i18n",
|
|
342
|
+
"code": "// shared/i18n.ts - each locale declared once; routing, alternates and <html lang>/<html dir> read it.\nimport { defineLocales } from \"@nifrajs/i18n\"\nimport { defineI18nRouting } from \"@nifrajs/i18n/routing\"\n\nexport const locales = defineLocales({\n default: \"en\",\n locales: {\n en: { hreflang: \"en-IN\" },\n hi: { hreflang: \"hi-IN\" },\n ur: { tag: \"ur-PK\", hreflang: \"ur\" }, // dir \"rtl\", derived from the tag\n gu: { draft: true }, // catalogs in progress: never routed, never an alternate\n },\n})\nexport const urls = defineI18nRouting(locales)\n\nurls.localizePathname(\"/kundli\", \"hi\") // \"/hi/kundli\"\nlocales.chain(\"hi\") // [\"hi\", \"en\"] - the catalog fallback order"
|
|
343
|
+
},
|
|
248
344
|
{
|
|
249
345
|
"name": "NEGOTIATE",
|
|
250
346
|
"topic": "Nifra - i18n",
|
|
251
347
|
"slug": "i18n",
|
|
252
|
-
"code": "//
|
|
348
|
+
"code": "// routes/index.backend.ts - resolve the locale + return only that catalog's messages.\nimport { negotiateLocale } from \"@nifrajs/i18n\"\nimport { t } from \"@nifrajs/schema\"\nimport { catalogs, locales } from \"../shared/catalogs\"\nimport type { Route } from \"./+types/index\"\n\n// A catalog is open-ended, so its schema is the explicit opt-out of the strict default.\nexport const loaderOutput = t.object({ locale: t.string(), messages: t.looseObject({}) })\nexport async function loader({ request }: Route.LoaderArgs) {\n const locale = negotiateLocale(request, { locales, defaultLocale: \"en\", queryParam: \"lang\", cookie: \"lang\" })\n return { locale, messages: catalogs[locale] } // ?lang= → cookie → Accept-Language → default\n}"
|
|
253
349
|
},
|
|
254
350
|
{
|
|
255
351
|
"name": "DIMENSIONS",
|
|
@@ -281,6 +377,12 @@
|
|
|
281
377
|
"slug": "index",
|
|
282
378
|
"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)"
|
|
283
379
|
},
|
|
380
|
+
{
|
|
381
|
+
"name": "LOADER",
|
|
382
|
+
"topic": "Nifra - Getting started",
|
|
383
|
+
"slug": "index",
|
|
384
|
+
"code": "// routes/index.backend.ts - the loader runs on the server (in-process during SSR, no network),\n// fully typed against your contract. This file never reaches the browser.\nimport { t } from \"@nifrajs/schema\"\nimport type { Route } from \"./+types/index\"\n\n// The page receives exactly this shape; anything else the loader returns stays on the server.\nexport const loaderOutput = t.object({ id: t.string() })\n\nexport async function loader({ api }: Route.LoaderArgs) {\n const res = await api.users({ id: \"7\" }).get()\n return { id: res.ok ? res.data.id : \"unknown\" }\n}"
|
|
385
|
+
},
|
|
284
386
|
{
|
|
285
387
|
"name": "BUS_PATTERN",
|
|
286
388
|
"topic": "Nifra - Islands: the AI-safe interactivity lane",
|
|
@@ -377,17 +479,59 @@
|
|
|
377
479
|
"slug": "rendering",
|
|
378
480
|
"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) })"
|
|
379
481
|
},
|
|
482
|
+
{
|
|
483
|
+
"name": "PRERENDER",
|
|
484
|
+
"topic": "Nifra - Rendering: SSR, streaming, SSG & ISR",
|
|
485
|
+
"slug": "rendering",
|
|
486
|
+
"code": "// routes/blog/index.backend.ts - a static route: render it to index.html at build time.\nimport { t } from \"@nifrajs/schema\"\nimport type { Route } from \"./+types/index\"\n\nexport const prerender = true\n\nexport const loaderOutput = t.object({\n posts: t.array(t.object({ slug: t.string(), title: t.string() })),\n})\n\nexport async function loader({ api }: Route.LoaderArgs) {\n const res = await api.posts.get() // runs at BUILD (no per-request secrets)\n return { posts: res.ok ? res.data : [] }\n}"
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
"name": "PROGRESSIVE",
|
|
490
|
+
"topic": "Nifra - Rendering: SSR, streaming, SSG & ISR",
|
|
491
|
+
"slug": "rendering",
|
|
492
|
+
"code": "// routes/products/[id].backend.ts\nimport { t } from \"@nifrajs/schema\"\nimport { defer } from \"@nifrajs/web\"\nimport type { Route } from \"./+types/[id]\"\n\nconst Review = t.object({ author: t.string(), body: t.string() })\ndeclare function loadReviews(productId: string): Promise<{ author: string; body: string }[]>\n\n// A deferred value is held to the same output contract as the rest of the data.\nexport const loaderOutput = t.object({\n product: t.object({ name: t.string() }),\n reviews: t.deferred(t.array(Review)),\n})\n\nexport async function loader({ api, params }: Route.LoaderArgs) {\n const product = await api.products({ id: params.id }).get()\n return {\n product: { name: product.ok ? product.data.name : \"\" }, // critical: in the first shell\n reviews: defer(loadReviews(params.id)), // non-critical: streamed when ready\n }\n}"
|
|
493
|
+
},
|
|
494
|
+
{
|
|
495
|
+
"name": "VITALS",
|
|
496
|
+
"topic": "Nifra - Rendering: SSR, streaming, SSG & ISR",
|
|
497
|
+
"slug": "rendering",
|
|
498
|
+
"code": "import { reportWebVitals, type WebVitalsMetric } from \"@nifrajs/web/vitals\"\n\nfunction send(metric: WebVitalsMetric): void {\n // name: \"LCP\" | \"INP\" | \"CLS\" | \"FCP\" | \"TTFB\"; route: the matched route's id, e.g. \"users/[id]\"\n const { name, value, rating, id, route } = metric\n const body = JSON.stringify({ name, value, rating, id, route })\n void fetch(\"/api/vitals\", { method: \"POST\", body, keepalive: true }) // survives the page closing\n}\n\n// Call it from the root layout's mount effect; the stop function it returns is the cleanup.\n// React, Preact: useEffect(() => reportVitals(), [])\n// Svelte: onMount(reportVitals) Solid: onMount(() => onCleanup(reportVitals()))\n// Vue: onMounted(() => (stop = reportVitals())), onUnmounted(() => stop())\nexport const reportVitals = () => reportWebVitals(send, { softNavigations: true })"
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
"name": "CATCHALL",
|
|
502
|
+
"topic": "Nifra - Routing",
|
|
503
|
+
"slug": "routing",
|
|
504
|
+
"code": "// routes/files/[...path].backend.ts - the page is routes/files/[...path].tsx, and together they\n// match /files/a, /files/a/b/c.txt, …\nimport { t } from \"@nifrajs/schema\"\n\ndeclare function read(path: string): Promise<string>\n\nexport const loaderOutput = t.object({ file: t.string() })\n\nexport async function loader({ params }: { params: { path: string } }) {\n const path = params.path // \"a/b/c.txt\" - the matched tail, as one string\n return { file: await read(path) }\n}\n// A catch-all needs ≥1 segment (/files alone won't match) and must be the last segment."
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
"name": "MIDDLEWARE",
|
|
508
|
+
"topic": "Nifra - Routing",
|
|
509
|
+
"slug": "routing",
|
|
510
|
+
"code": "// routes/account/_layout.backend.ts - runs before every page under /account\nimport { type RouteMiddleware, redirect } from \"@nifrajs/web\"\n\nexport const middleware: RouteMiddleware = ({ request, set }) => {\n set.headers[\"cache-control\"] = \"private, no-store\"\n const signedIn = request.headers.get(\"cookie\")?.includes(\"session=\") ?? false\n return signedIn ? undefined : redirect(\"/login\")\n}"
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
"name": "ROUTE_BACKEND",
|
|
514
|
+
"topic": "Nifra - Routing",
|
|
515
|
+
"slug": "routing",
|
|
516
|
+
"code": "// routes/users/[id].backend.ts - its server half\nimport { t } from \"@nifrajs/schema\"\nimport type { Route } from \"./+types/[id]\"\n\nexport const loaderOutput = t.object({ name: t.string() })\n\nexport async function loader({ params }: Route.LoaderArgs) {\n return { name: `User ${params.id}` }\n}"
|
|
517
|
+
},
|
|
380
518
|
{
|
|
381
519
|
"name": "BOUNDED",
|
|
382
520
|
"topic": "Nifra - security & hardening",
|
|
383
521
|
"slug": "security",
|
|
384
522
|
"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})"
|
|
385
523
|
},
|
|
524
|
+
{
|
|
525
|
+
"name": "FORM",
|
|
526
|
+
"topic": "Nifra - security & hardening",
|
|
527
|
+
"slug": "security",
|
|
528
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { t } from \"@nifrajs/schema/form\"\n\nconst app = server()\n\napp.post(\n \"/listings\",\n {\n body: t.form(\n {\n title: t.string({ minLength: 1, maxLength: 120 }),\n price: t.integer({ minimum: 0 }), // form values arrive as text; \"1200\" becomes 1200\n cover: t.file({ maxBytes: 5_000_000, accept: [\"image/png\", \"image/jpeg\"] }),\n photos: t.array(t.file({ maxBytes: 5_000_000, accept: [\"image/*\"] }), { maxItems: 8 }),\n floorPlan: t.optional(t.file({ accept: [\"application/pdf\"] })),\n },\n { maxFiles: 10, maxFields: 20 },\n ),\n bodyLimit: 50_000_000, // the default body cap is 1 MB: raise it on the routes that take files\n },\n (c) => {\n const { title, cover, photos } = c.body // cover: File, photos: File[], floorPlan?: File\n // Name the stored object yourself. `cover.name` is whatever the client typed.\n const key = `${crypto.randomUUID()}.${cover.type === \"image/png\" ? \"png\" : \"jpg\"}`\n return { title, key, photos: photos.length }\n },\n)"
|
|
529
|
+
},
|
|
386
530
|
{
|
|
387
531
|
"name": "GATING",
|
|
388
532
|
"topic": "Nifra - security & hardening",
|
|
389
533
|
"slug": "security",
|
|
390
|
-
"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
|
|
534
|
+
"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, judged against c.clientIp: the socket peer, or the chain your\n // server({ clientIp }) trust declaration names. FAILS CLOSED with no caller IP; a raw\n // X-Forwarded-For is never believed.\n .use(ipRestriction({ allow: [\"10.0.0.0/8\", \"::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 }))"
|
|
391
535
|
},
|
|
392
536
|
{
|
|
393
537
|
"name": "LOGGING",
|
|
@@ -425,12 +569,24 @@
|
|
|
425
569
|
"slug": "server-functions",
|
|
426
570
|
"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))"
|
|
427
571
|
},
|
|
572
|
+
{
|
|
573
|
+
"name": "DEFER",
|
|
574
|
+
"topic": "Nifra - Streaming",
|
|
575
|
+
"slug": "streaming",
|
|
576
|
+
"code": "// routes/index.backend.ts - send the page shell immediately; stream the slow part when it resolves.\nimport { t } from \"@nifrajs/schema\"\nimport { defer } from \"@nifrajs/web\"\nimport type { Route } from \"./+types/index\"\n\ndeclare function loadFeed(userId: string): Promise<string[]>\n\nexport const loaderOutput = t.object({\n name: t.string(), // awaited - in the shell\n feed: t.deferred(t.array(t.string())), // deferred - streamed later\n})\n\nexport async function loader({ api }: Route.LoaderArgs) {\n const user = await api.users({ id: \"7\" }).get()\n return {\n name: user.ok ? user.data.name : \"\",\n feed: defer(loadFeed(\"7\")),\n }\n}"
|
|
577
|
+
},
|
|
428
578
|
{
|
|
429
579
|
"name": "SSE",
|
|
430
580
|
"topic": "Nifra - Streaming",
|
|
431
581
|
"slug": "streaming",
|
|
432
582
|
"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)"
|
|
433
583
|
},
|
|
584
|
+
{
|
|
585
|
+
"name": "BACKEND_HALF",
|
|
586
|
+
"topic": "Nifra - Project structure",
|
|
587
|
+
"slug": "structure",
|
|
588
|
+
"code": "// routes/users/[id].backend.ts - the page's server half. It never reaches the browser.\nimport { t } from \"@nifrajs/schema\"\nimport { notFound } from \"@nifrajs/web\"\nimport type { Route } from \"./+types/[id]\"\n\n// What the page receives - and the ONLY fields that leave the server. A field the loader returns\n// but the schema does not declare is dropped before rendering.\nexport const loaderOutput = t.object({ name: t.string(), avatarUrl: t.string() })\n\nexport async function loader({ params, api }: Route.LoaderArgs) {\n const res = await api.users({ id: params.id }).get()\n if (!res.ok) throw notFound()\n return res.data\n}"
|
|
589
|
+
},
|
|
434
590
|
{
|
|
435
591
|
"name": "BASIC",
|
|
436
592
|
"topic": "Nifra - Contract-derived adversarial testing",
|
|
@@ -450,22 +606,22 @@
|
|
|
450
606
|
"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"
|
|
451
607
|
},
|
|
452
608
|
{
|
|
453
|
-
"name": "
|
|
609
|
+
"name": "BACKEND_ONLY_MARKER",
|
|
454
610
|
"topic": "Nifra - Troubleshooting",
|
|
455
611
|
"slug": "troubleshooting",
|
|
456
|
-
"code": "
|
|
612
|
+
"code": "// packages/billing/src/keys.ts - a workspace package both sides import, holding one server value.\nimport \"@nifrajs/web/backend-only\" // ← a browser build that reaches this module fails\nimport type { BackendOnly } from \"@nifrajs/web\" // ← type-level intent: this value stays on the server\n\n// `BackendOnly<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: BackendOnly<string> = process.env.SECRET_API_KEY!"
|
|
457
613
|
},
|
|
458
614
|
{
|
|
459
|
-
"name": "
|
|
615
|
+
"name": "CONTRACT_FIRST",
|
|
460
616
|
"topic": "Nifra - Troubleshooting",
|
|
461
617
|
"slug": "troubleshooting",
|
|
462
|
-
"code": "import {
|
|
618
|
+
"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})"
|
|
463
619
|
},
|
|
464
620
|
{
|
|
465
|
-
"name": "
|
|
621
|
+
"name": "MERGE_SPLIT",
|
|
466
622
|
"topic": "Nifra - Troubleshooting",
|
|
467
623
|
"slug": "troubleshooting",
|
|
468
|
-
"code": "
|
|
624
|
+
"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)"
|
|
469
625
|
},
|
|
470
626
|
{
|
|
471
627
|
"name": "CLIENT",
|