@nifrajs/cli 4.0.2 → 5.1.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 +10 -2
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +9 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +8 -23
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -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 +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +218 -44
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +94 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/dev-feed-tool.ts +112 -1
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/docs/examples.json
CHANGED
|
@@ -47,6 +47,12 @@
|
|
|
47
47
|
"slug": "api",
|
|
48
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
49
|
},
|
|
50
|
+
{
|
|
51
|
+
"name": "TYPED_ERRORS",
|
|
52
|
+
"topic": "Nifra - API & typed client",
|
|
53
|
+
"slug": "api",
|
|
54
|
+
"code": "import { client } from \"@nifrajs/client\"\nimport { server, status } from \"@nifrajs/core/server\"\nimport { t } from \"@nifrajs/schema\"\n\nconst conflict = t.object({ code: t.literal(\"conflict\"), retryAfter: t.number() })\nconst locked = new Set([\"7\"])\n\nexport const app = server().post(\"/orders/:id/pay\", { errors: { 409: conflict } }, (c) => {\n if (locked.has(c.params.id))\n // The body is the typed contract; the header serves proxies and plain HTTP clients.\n return status(409, { code: \"conflict\", retryAfter: 5 }, { headers: { \"retry-after\": \"5\" } })\n return { paid: true }\n})\n\nconst api = client<typeof app>(\"https://api.example.com\")\nconst res = await api.orders({ id: \"7\" }).pay.post()\nif (!res.ok && res.status === 409) {\n const wait: number = res.data.retryAfter // narrowed to the declared 409 body\n}"
|
|
55
|
+
},
|
|
50
56
|
{
|
|
51
57
|
"name": "GUARD",
|
|
52
58
|
"topic": "Nifra - Auth & sessions",
|
|
@@ -69,7 +75,7 @@
|
|
|
69
75
|
"name": "COMPOSE",
|
|
70
76
|
"topic": "Nifra - Backends & API (dev and prod)",
|
|
71
77
|
"slug": "backends",
|
|
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)"
|
|
78
|
+
"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.\n// A group holding .ws() routes or mount()s is refused the same way: add those on the parent.\nexport const backend = server().merge(listings).merge(agents)"
|
|
73
79
|
},
|
|
74
80
|
{
|
|
75
81
|
"name": "EVENTS",
|
|
@@ -87,7 +93,7 @@
|
|
|
87
93
|
"name": "WIRE",
|
|
88
94
|
"topic": "Nifra - Backends & API (dev and prod)",
|
|
89
95
|
"slug": "backends",
|
|
90
|
-
"code": "// backend/server.ts - a hand-written server: createWebApp serves pages AND
|
|
96
|
+
"code": "// backend/server.ts - a hand-written server: createWebApp serves pages AND 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\n apiPrefix: \"/api\", // AND served over HTTP at /api/*; without it the backend is loader-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."
|
|
91
97
|
},
|
|
92
98
|
{
|
|
93
99
|
"name": "SERVER",
|
|
@@ -195,7 +201,7 @@
|
|
|
195
201
|
"name": "SEO_EXAMPLE",
|
|
196
202
|
"topic": "Nifra - Framework contract",
|
|
197
203
|
"slug": "contract",
|
|
198
|
-
"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)
|
|
204
|
+
"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) come from\n// @nifrajs/web/seo, which imports nothing at runtime, so the page's browser bundle takes only them;\n// `origin` is a MetaArgs field beside data, pageData and params.\nimport type { MetaArgs } from \"@nifrajs/web\"\nimport { canonical, jsonLd, openGraph } from \"@nifrajs/web/seo\"\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}"
|
|
199
205
|
},
|
|
200
206
|
{
|
|
201
207
|
"name": "STATIC_EMIT",
|
|
@@ -203,11 +209,23 @@
|
|
|
203
209
|
"slug": "contract",
|
|
204
210
|
"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
211
|
},
|
|
212
|
+
{
|
|
213
|
+
"name": "SERVER",
|
|
214
|
+
"topic": "Nifra - Control APIs (one schema, every client)",
|
|
215
|
+
"slug": "control-api",
|
|
216
|
+
"code": "import { timingSafeEqual } from \"node:crypto\"\nimport { server } from \"@nifrajs/core\"\nimport { streaming } from \"@nifrajs/core/sse\"\nimport { t, toOpenAPI } from \"@nifrajs/schema\"\n\nconst token = process.env.CONTROL_TOKEN\nif (token === undefined || token.length < 32) throw new Error(\"CONTROL_TOKEN must be 32+ characters\")\nconst expected = Buffer.from(`Bearer ${token}`)\nconst authorized = (request: Request): boolean => {\n const given = Buffer.from(request.headers.get(\"authorization\") ?? \"\")\n return given.length === expected.length && timingSafeEqual(given, expected)\n}\n\nconst Session = t.object({\n id: t.string(),\n status: t.union([t.literal(\"idle\"), t.literal(\"running\")]),\n})\nconst TurnEvent = t.object({\n seq: t.integer({ minimum: 0 }),\n type: t.string(),\n text: t.optional(t.string()),\n})\n\nexport const app = server()\n .use(streaming())\n .onRequest((request) =>\n authorized(request) ? undefined : Response.json({ error: \"unauthorized\" }, { status: 401 }),\n )\n .post(\n \"/session/create\",\n { body: t.object({ cwd: t.string({ minLength: 1, maxLength: 4096 }) }), response: Session },\n () => ({ id: crypto.randomUUID(), status: \"idle\" as const }),\n )\n .post(\n \"/turn/send\",\n {\n body: t.object({\n sessionId: t.string(),\n message: t.string({ minLength: 1, maxLength: 256 * 1024 }),\n }),\n response: t.object({ turnId: t.string() }),\n bodyLimit: 512 * 1024,\n },\n () => ({ turnId: crypto.randomUUID() }),\n )\n .sse(\n \"/turn/events\",\n { query: t.object({ turnId: t.string() }), sse: TurnEvent },\n (c, stream) => {\n const after = Number(c.req.headers.get(\"last-event-id\") ?? -1)\n for (let seq = after + 1; seq < 3; seq++)\n stream.send({ seq, type: \"assistant.delta\", text: `part ${seq}` }, { id: String(seq) })\n stream.close()\n },\n )\n\nexport const spec = toOpenAPI(app, { openapiVersion: \"3.2.0\", title: \"Control API\", version: \"1.0.0\" })"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"name": "FULL_APP",
|
|
220
|
+
"topic": "Nifra - Custom server (createWebApp)",
|
|
221
|
+
"slug": "custom-server",
|
|
222
|
+
"code": "import { fileURLToPath } from \"node:url\"\nimport { createCspPolicy, createWebApp, servePublicDir } from \"@nifrajs/web\"\nimport type { Manifest, RenderAdapter } from \"@nifrajs/web\"\n\n// From your build: the framework adapter, the route manifest, and what buildClient() returned.\ndeclare const adapter: RenderAdapter\ndeclare const manifest: Manifest\ndeclare const build: {\n readonly entry: string\n readonly assets: readonly string[]\n readonly routes: Readonly<Record<string, readonly string[]>>\n readonly css?: readonly string[]\n}\ndeclare const errors: { capture(error: unknown, tags: Record<string, string>): void }\n\n// buildClient({ outDir: \"dist/assets\" }) writes the chunks where their /assets/ URLs point.\nconst bundle = servePublicDir({\n dir: fileURLToPath(new URL(\"./dist/\", import.meta.url)),\n hashedFiles: new Set(build.assets),\n})\nconst publicFiles = servePublicDir({ dir: fileURLToPath(new URL(\"./public/\", import.meta.url)) })\n\nconst csp = createCspPolicy({\n header: ({ sources }) => `default-src 'self'; script-src 'self' ${sources}; object-src 'none'`,\n})\n\nexport const app = createWebApp({\n adapter,\n manifest,\n clientEntry: build.entry,\n routePreload: build.routes,\n styles: build.css ?? [],\n csp,\n server: { requestTimeoutMs: 15_000, gracefulSignals: { drainMs: 8_000 } },\n use: (app) => {\n app.onRequest(async (request) => (await bundle(request)) ?? (await publicFiles(request)))\n },\n onLoaderError: (error, { route }) => errors.capture(error, { route }),\n})\n\napp.listen(3000)"
|
|
223
|
+
},
|
|
206
224
|
{
|
|
207
225
|
"name": "ACTION",
|
|
208
226
|
"topic": "Nifra - Loaders & actions",
|
|
209
227
|
"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
|
|
228
|
+
"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 const action: Route.Action = async ({ api, request }) => {\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}"
|
|
211
229
|
},
|
|
212
230
|
{
|
|
213
231
|
"name": "CONTENT",
|
|
@@ -219,7 +237,7 @@
|
|
|
219
237
|
"name": "LOADER",
|
|
220
238
|
"topic": "Nifra - Loaders & actions",
|
|
221
239
|
"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
|
|
240
|
+
"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\n// Route.Loader checks what this returns against loaderOutput at compile time.\nexport const loader: Route.Loader = async ({ params, api }) => {\n const res = await api.users({ id: params.id }).get()\n if (!res.ok) throw notFound()\n return res.data\n}"
|
|
223
241
|
},
|
|
224
242
|
{
|
|
225
243
|
"name": "SHOULD_REVALIDATE",
|
|
@@ -461,6 +479,12 @@
|
|
|
461
479
|
"slug": "plugins",
|
|
462
480
|
"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."
|
|
463
481
|
},
|
|
482
|
+
{
|
|
483
|
+
"name": "TIERS",
|
|
484
|
+
"topic": "Nifra - Plugins & middleware",
|
|
485
|
+
"slug": "plugins",
|
|
486
|
+
"code": "import { server } from \"@nifrajs/core/server\"\nimport { MemoryStore, rateLimitTiers } from \"@nifrajs/middleware\"\n\n// Each request is charged to ONE bucket: the longest matching prefix, else the floor.\n// A tier may sit above the floor - tiers never stack on it.\nconst app = server().use(\n rateLimitTiers({\n store: new MemoryStore(),\n floor: { max: 120, windowMs: 60_000 },\n tiers: [\n { prefix: \"/api/public\", max: 600, windowMs: 60_000 },\n { prefix: \"/auth/login\", max: 10, windowMs: 60_000 },\n ],\n skip: (req) => new URL(req.url).pathname === \"/health\", // never charged\n }),\n)"
|
|
487
|
+
},
|
|
464
488
|
{
|
|
465
489
|
"name": "BUILD",
|
|
466
490
|
"topic": "Nifra - Rendering: SSR, streaming, SSG & ISR",
|
|
@@ -549,7 +573,7 @@
|
|
|
549
573
|
"name": "RESPONSE_CONTRACT",
|
|
550
574
|
"topic": "Nifra - security & hardening",
|
|
551
575
|
"slug": "security",
|
|
552
|
-
"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
|
|
576
|
+
"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\", { extra: \"strip\" })).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 -> 200 {\"id\":\"u1\",\"name\":\"Ada\",\"email\":\"a@b.c\",\"passwordHash\":\"...\"}\n// enforce -> 500 {\"ok\":false,\"error\":\"internal_error\"}\n// (t.object is closed; the log names email and passwordHash)\n// enforce, { extra: \"strip\" } -> 200 {\"id\":\"u1\",\"name\":\"Ada\"}"
|
|
553
577
|
},
|
|
554
578
|
{
|
|
555
579
|
"name": "SECURITY_HEADERS",
|
|
@@ -557,6 +581,12 @@
|
|
|
557
581
|
"slug": "security",
|
|
558
582
|
"code": "import { server } from \"@nifrajs/core/server\"\nimport { securityHeaders } from \"@nifrajs/middleware\"\n\n// Always on (covering errors and 404s too): X-Content-Type-Options: nosniff,\n// X-Frame-Options: DENY, Referrer-Policy: no-referrer. The rest are opt-in - each is a\n// deliberate cross-origin decision, so nifra never turns them on behind your back:\nconst app = server().use(securityHeaders({\n crossOriginOpenerPolicy: \"same-origin\", // isolate the browsing-context group\n crossOriginResourcePolicy: \"same-origin\", // block cross-origin embedding of your responses\n crossOriginEmbedderPolicy: \"require-corp\", // enable crossOriginIsolated (SharedArrayBuffer, …)\n permissionsPolicy: \"camera=(), geolocation=()\",\n contentSecurityPolicy: \"default-src 'self'\",\n hsts: { maxAge: 63072000, includeSubDomains: true, preload: true }, // opt in once HTTPS-only\n}))"
|
|
559
583
|
},
|
|
584
|
+
{
|
|
585
|
+
"name": "DEFINE",
|
|
586
|
+
"topic": "Nifra - Server components",
|
|
587
|
+
"slug": "server-components",
|
|
588
|
+
"code": "// Product.component.server.ts - native Vue/Svelte/Solid view.\nimport { t } from \"@nifrajs/schema\"\nimport { defineServerComponent, type ServerComponentContext } from \"@nifrajs/web/server-components\"\nimport { nativeServerView } from \"@nifrajs/web/server-components/islands\"\n\n// Replace these declarations with your native view and scoped read service.\ndeclare const ProductView: unknown\ndeclare function readProduct(ctx: ServerComponentContext, id: string): Promise<{ title: string; internal: string }>\n\nexport const Product = defineServerComponent({\n input: t.object({ id: t.string({ minLength: 1 }) }),\n props: t.object({ title: t.string() }),\n authorize(ctx, input) {\n if (ctx.params.id !== input.id) throw new Response(null, { status: 403 })\n },\n load: (ctx, input) => readProduct(ctx, input.id),\n view: () => nativeServerView(ProductView),\n})"
|
|
589
|
+
},
|
|
560
590
|
{
|
|
561
591
|
"name": "DECLARE",
|
|
562
592
|
"topic": "Nifra - Server functions",
|
|
@@ -581,11 +611,17 @@
|
|
|
581
611
|
"slug": "streaming",
|
|
582
612
|
"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)"
|
|
583
613
|
},
|
|
614
|
+
{
|
|
615
|
+
"name": "TYPED_SSE",
|
|
616
|
+
"topic": "Nifra - Streaming",
|
|
617
|
+
"slug": "streaming",
|
|
618
|
+
"code": "import { client } from \"@nifrajs/client\"\nimport { server } from \"@nifrajs/core/server\"\nimport { streaming } from \"@nifrajs/core/sse\"\nimport { t } from \"@nifrajs/schema\"\n\ndeclare function generate(prompt: string): AsyncIterable<string>\n\nexport const app = server()\n .use(streaming())\n // A GET stream: the event payload is contracted by `sse`.\n .sse(\"/feed\", { sse: t.object({ id: t.integer(), title: t.string() }) }, (_c, stream) => {\n stream.send({ id: 1, title: \"hello\" })\n stream.close()\n })\n // A body makes it a POST stream, validated like any POST: a chat that posts its prompt.\n .sse(\n \"/chat\",\n { body: t.object({ prompt: t.string() }), sse: t.object({ token: t.string() }) },\n async (c, stream) => {\n for await (const token of generate(c.body.prompt)) stream.send({ token })\n stream.close()\n },\n )\n\nconst api = client<typeof app>(\"https://example.com\")\nconst feed = api.feed.subscribe((post) => console.log(post.title))\napi.chat.subscribe((event) => console.log(event.token), { body: { prompt: \"Hi\" } })\nfeed.close()"
|
|
619
|
+
},
|
|
584
620
|
{
|
|
585
621
|
"name": "BACKEND_HALF",
|
|
586
622
|
"topic": "Nifra - Project structure",
|
|
587
623
|
"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}"
|
|
624
|
+
"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; in development the console names\n// each dropped field, so a missing declaration never shows up as a silently blank page.\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
625
|
},
|
|
590
626
|
{
|
|
591
627
|
"name": "BASIC",
|
|
@@ -605,6 +641,12 @@
|
|
|
605
641
|
"slug": "testing",
|
|
606
642
|
"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"
|
|
607
643
|
},
|
|
644
|
+
{
|
|
645
|
+
"name": "WEB_ROUTES",
|
|
646
|
+
"topic": "Nifra - Contract-derived adversarial testing",
|
|
647
|
+
"slug": "testing",
|
|
648
|
+
"code": "// test/routes.test.ts - render the real routes in-process, each page with its backend half.\nimport { expect, test } from \"bun:test\"\nimport { createWebApp } from \"@nifrajs/web\"\nimport { discoverRoutes } from \"@nifrajs/web/fs\"\nimport { reactAdapter } from \"@nifrajs/web-react\"\n\nconst app = createWebApp({\n adapter: reactAdapter,\n // Scans routes/ the way the build does: a page and its .backend.ts half always arrive together.\n manifest: discoverRoutes(`${import.meta.dir}/../routes`),\n clientEntry: \"/client.js\",\n})\n\ntest(\"the users page runs its loader\", async () => {\n const res = await app.fetch(new Request(\"http://localhost/users/7\"))\n expect(res.status).toBe(200)\n})"
|
|
649
|
+
},
|
|
608
650
|
{
|
|
609
651
|
"name": "BACKEND_ONLY_MARKER",
|
|
610
652
|
"topic": "Nifra - Troubleshooting",
|
|
@@ -658,5 +700,11 @@
|
|
|
658
700
|
"topic": "Nifra - WebSockets",
|
|
659
701
|
"slug": "websockets",
|
|
660
702
|
"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"
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
"name": "COMPOSED",
|
|
706
|
+
"topic": "Nifra - WebSockets",
|
|
707
|
+
"slug": "websockets",
|
|
708
|
+
"code": "// server.ts - one listener; the chat child and a non-nifra child both take upgrades.\nimport { server } from \"@nifrajs/core/server\"\nimport { attachWebSocket, TopicRegistry, websocket, type WebSocketUpgradeOutcome } from \"@nifrajs/core/ws\"\n\nconst chat = server()\n .use(websocket())\n .ws(\"/room\", { message: (ws, data) => ws.send(data) })\n\n// Not a nifra server: it answers HTTP through fetch and builds its own upgrade outcome.\nconst topics = new TopicRegistry()\nconst legacy = {\n fetch: () => new Response(\"legacy\"),\n resolveWebSocketUpgrade: (request: Request): WebSocketUpgradeOutcome =>\n new URL(request.url).pathname === \"/feed\"\n ? { kind: \"upgrade\", handler: { open: (ws) => ws.send(\"hello\") }, data: undefined, pubsub: topics, attach: attachWebSocket }\n : { kind: \"pass\" },\n}\n\nserver()\n .use(websocket()) // Bun: the parent holds the socket table every mounted upgrade runs through\n .mount({ path: \"/chat\", app: chat, stripPrefix: true })\n .mount({ path: \"/legacy\", app: legacy, stripPrefix: true })\n .listen(3000)"
|
|
661
709
|
}
|
|
662
710
|
]
|