create-nifra 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.
Files changed (106) hide show
  1. package/README.md +3 -3
  2. package/dist/agent-files.d.ts +41 -26
  3. package/dist/agent-files.d.ts.map +1 -1
  4. package/dist/agent-files.js +80 -47
  5. package/dist/agent-files.js.map +1 -1
  6. package/dist/agents.d.ts +3 -3
  7. package/dist/agents.d.ts.map +1 -1
  8. package/dist/agents.js +65 -69
  9. package/dist/agents.js.map +1 -1
  10. package/dist/auth.d.ts +5 -4
  11. package/dist/auth.d.ts.map +1 -1
  12. package/dist/auth.js +27 -14
  13. package/dist/auth.js.map +1 -1
  14. package/dist/cli.d.ts +23 -25
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +141 -130
  17. package/dist/cli.js.map +1 -1
  18. package/dist/db.d.ts.map +1 -1
  19. package/dist/db.js +49 -48
  20. package/dist/db.js.map +1 -1
  21. package/dist/scaffold/frameworks.d.ts +13 -38
  22. package/dist/scaffold/frameworks.d.ts.map +1 -1
  23. package/dist/scaffold/frameworks.js +9 -32
  24. package/dist/scaffold/frameworks.js.map +1 -1
  25. package/dist/scaffold/route-types.d.ts +9 -0
  26. package/dist/scaffold/route-types.d.ts.map +1 -0
  27. package/dist/scaffold/route-types.js +61 -0
  28. package/dist/scaffold/route-types.js.map +1 -0
  29. package/dist/scaffold/site-files.d.ts +14 -8
  30. package/dist/scaffold/site-files.d.ts.map +1 -1
  31. package/dist/scaffold/site-files.js +29 -60
  32. package/dist/scaffold/site-files.js.map +1 -1
  33. package/dist/scaffold/site.d.ts +8 -5
  34. package/dist/scaffold/site.d.ts.map +1 -1
  35. package/dist/scaffold/site.js +47 -34
  36. package/dist/scaffold/site.js.map +1 -1
  37. package/dist/scaffold/targets.d.ts +39 -0
  38. package/dist/scaffold/targets.d.ts.map +1 -0
  39. package/dist/scaffold/targets.js +116 -0
  40. package/dist/scaffold/targets.js.map +1 -0
  41. package/package.json +4 -1
  42. package/src/agent-files.ts +89 -47
  43. package/template/README.md +12 -6
  44. package/template/{src → backend}/app.test.ts +2 -2
  45. package/template/{src → backend}/app.ts +8 -3
  46. package/template/{src → backend}/index.ts +2 -2
  47. package/template/{src → backend}/routes.ts +1 -1
  48. package/template/nifra.assurance.ts +16 -14
  49. package/template/package.json +7 -7
  50. package/template/tsconfig.json +1 -1
  51. package/template-batteries/README.md +10 -6
  52. package/template-batteries/{src → backend}/app.test.ts +23 -23
  53. package/template-batteries/{src → backend}/app.ts +8 -3
  54. package/template-batteries/{src → backend}/index.ts +2 -2
  55. package/template-batteries/{src → backend}/notes.ts +1 -1
  56. package/template-batteries/nifra.assurance.ts +16 -14
  57. package/template-batteries/package.json +10 -9
  58. package/template-batteries/tsconfig.json +1 -1
  59. package/template-isr/README.md +3 -0
  60. package/template-isr/{backend.ts → backend/app.ts} +2 -0
  61. package/template-isr/{server.ts → backend/dev-server.ts} +4 -4
  62. package/template-isr/{worker.ts → backend/worker.ts} +4 -2
  63. package/template-isr/build.ts +3 -1
  64. package/template-isr/gitignore +7 -1
  65. package/template-isr/nifra.assurance.ts +15 -13
  66. package/template-isr/package.json +10 -9
  67. package/template-isr/routes/index.backend.ts +14 -0
  68. package/template-isr/routes/index.tsx +5 -17
  69. package/template-isr/tsconfig.json +5 -4
  70. package/template-isr/wrangler.toml +6 -1
  71. package/template-site/README.md +24 -23
  72. package/template-site/{backend.ts → backend/app.ts} +6 -4
  73. package/template-site/gitignore +5 -10
  74. package/template-site/nifra.assurance.ts +15 -13
  75. package/template-site/nifra.config.ts +4 -4
  76. package/template-site/routes/index.backend.ts +18 -0
  77. package/template-site/routes/index.tsx +2 -15
  78. package/template-site-preact/README.md +21 -20
  79. package/template-site-preact/nifra.config.ts +4 -4
  80. package/template-site-preact/routes/_layout.tsx +3 -1
  81. package/template-site-preact/routes/index.tsx +2 -15
  82. package/template-site-solid/README.md +21 -19
  83. package/template-site-solid/nifra.config.ts +5 -4
  84. package/template-site-solid/routes/index.tsx +2 -15
  85. package/template-site-svelte/README.md +21 -19
  86. package/template-site-svelte/nifra.config.ts +3 -2
  87. package/template-site-svelte/routes/index.svelte +3 -16
  88. package/template-site-vue/README.md +21 -20
  89. package/template-site-vue/nifra.config.ts +4 -3
  90. package/template-site-vue/routes/_layout.vue +21 -25
  91. package/template-site-vue/routes/index.vue +12 -18
  92. package/dist/scaffold/site-build.d.ts +0 -60
  93. package/dist/scaffold/site-build.d.ts.map +0 -1
  94. package/dist/scaffold/site-build.js +0 -198
  95. package/dist/scaffold/site-build.js.map +0 -1
  96. package/template-site/.dockerignore +0 -14
  97. package/template-site/Dockerfile +0 -25
  98. package/template-site/_worker.ts +0 -17
  99. package/template-site/deno.json +0 -7
  100. package/template-site/server-bun.ts +0 -35
  101. package/template-site/server-deno.ts +0 -38
  102. package/template-site/server-node.ts +0 -40
  103. package/template-site/server-vercel.ts +0 -17
  104. package/template-site/wrangler.toml +0 -4
  105. /package/template-isr/{page.ts → backend/page.ts} +0 -0
  106. /package/template-site/{counter.ts → backend/counter.ts} +0 -0
@@ -12,5 +12,5 @@
12
12
  "types": ["bun"],
13
13
  "skipLibCheck": true
14
14
  },
15
- "include": ["src/**/*"]
15
+ "include": ["backend/**/*"]
16
16
  }
@@ -27,6 +27,9 @@ holds across `hit`s and bumps on a `miss`/regeneration.
27
27
 
28
28
  ## How it works
29
29
 
30
+ `backend/worker.ts` is the Workers entry and `backend/dev-server.ts` the local Bun server; both wrap
31
+ the same app, and `build.ts` builds the client bundle and the worker.
32
+
30
33
  - `withISR(app, { store, revalidate, now })` wraps the app's `fetch`. Only full-document `text/html`
31
34
  `GET` `200`s are cached; assets, soft-nav data fetches, redirects, and errors pass through.
32
35
  - A route sets its freshness with `export const revalidate = <seconds>` (see `routes/index.tsx`).
@@ -1,3 +1,4 @@
1
+ import { responseContract } from "@nifrajs/core/response-contract"
1
2
  import { server } from "@nifrajs/core/server"
2
3
  import {
3
4
  cors,
@@ -30,6 +31,7 @@ export const backend = server({
30
31
  requestTimeoutMs: 30_000,
31
32
  admission: createAdmissionController({ maxInFlight: 128, maxQueue: 0 }),
32
33
  })
34
+ .use(responseContract("enforce"))
33
35
  .use(securityHeaders())
34
36
  .use(
35
37
  cors({
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Local Bun dev server (no wrangler) - the same app with an in-memory ISR store. `bun run dev:bun`
3
- * after `bun run build`. Production uses Workers KV (worker.ts); the only line that changes is the
3
+ * after `bun run build`. Production uses Workers KV (backend/worker.ts); the only line that changes is the
4
4
  * store. Watch the `x-nifra-isr` response header: miss → hit → stale.
5
5
  */
6
6
  import { inProcessClient } from "@nifrajs/client"
@@ -8,9 +8,9 @@ import { createWebApp, MemoryCacheStore, revalidateEndpoint, withISR } from "@ni
8
8
  import type { BuildManifest } from "@nifrajs/web/build"
9
9
  import { discoverRoutes } from "@nifrajs/web/fs"
10
10
  import { reactAdapter } from "@nifrajs/web-react"
11
- import { backend } from "./backend"
11
+ import { backend } from "./app"
12
12
 
13
- const publicDir = `${import.meta.dir}/public`
13
+ const publicDir = `${import.meta.dir}/../public`
14
14
  const assets = JSON.parse(
15
15
  await Bun.file(`${publicDir}/assets/manifest.json`)
16
16
  .text()
@@ -19,7 +19,7 @@ const assets = JSON.parse(
19
19
 
20
20
  const app = createWebApp({
21
21
  adapter: reactAdapter,
22
- manifest: discoverRoutes(`${import.meta.dir}/routes`),
22
+ manifest: discoverRoutes(`${import.meta.dir}/../routes`),
23
23
  clientEntry: assets.entry,
24
24
  routePreload: assets.routes,
25
25
  api: inProcessClient(backend),
@@ -12,7 +12,7 @@ import {
12
12
  withISR,
13
13
  } from "@nifrajs/web"
14
14
  import { reactAdapter } from "@nifrajs/web-react"
15
- import { backend } from "./backend"
15
+ import { backend } from "./app"
16
16
  // Generated by build.ts (buildServer): the static-import route manifest + the baked client entry URL.
17
17
  import { clientEntry, manifest } from "./server-manifest"
18
18
 
@@ -41,7 +41,9 @@ export default {
41
41
 
42
42
  // On-demand purge: POST /__nifra/revalidate?path=/ with the secret in x-nifra-revalidate-token.
43
43
  if (new URL(req.url).pathname === "/__nifra/revalidate") {
44
- return revalidateEndpoint({ store, secret: env.REVALIDATE_SECRET ?? "" })(req)
44
+ // No secret configured means no purge endpoint - never an open one.
45
+ if (env.REVALIDATE_SECRET === undefined) return new Response("Not Found", { status: 404 })
46
+ return revalidateEndpoint({ store, secret: env.REVALIDATE_SECRET })(req)
45
47
  }
46
48
 
47
49
  // Cache GET documents stale-while-revalidate. Default 60s; a route's `export const revalidate`
@@ -7,6 +7,8 @@ const dir = import.meta.dir
7
7
  const client = await buildClient({
8
8
  routesDir: `${dir}/routes`,
9
9
  outDir: `${dir}/public/assets`,
10
+ // public/ is generated Workers Assets output; copying it into public/assets would recurse.
11
+ publicDir: false,
10
12
  clientModule: "@nifrajs/web-react/client",
11
13
  conditions: ["bun", "browser"],
12
14
  define: { "process.env.NODE_ENV": '"production"' },
@@ -14,7 +16,7 @@ const client = await buildClient({
14
16
 
15
17
  const { worker } = await buildServer({
16
18
  routesDir: `${dir}/routes`,
17
- serverEntry: `${dir}/worker.ts`,
19
+ serverEntry: `${dir}/backend/worker.ts`,
18
20
  outDir: `${dir}/dist-server`,
19
21
  clientEntry: client.entry,
20
22
  })
@@ -4,4 +4,10 @@ dist-server
4
4
  .build
5
5
  .wrangler
6
6
  # generated by buildServer next to the entry
7
- server-manifest.ts
7
+ backend/server-manifest.ts
8
+ # generated by nifra: route types and dev-server entries
9
+ .nifra/
10
+ # local secrets; commit .env.example instead
11
+ .env
12
+ .env.*
13
+ !.env.example
@@ -11,7 +11,7 @@
11
11
  * an exemption you get by leaving the rule off is one nobody ever sees.
12
12
  */
13
13
  import { defineAssuranceConfig, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
14
- import { backend } from "./backend"
14
+ import { backend } from "./backend/app"
15
15
 
16
16
  export default defineAssuranceConfig({
17
17
  source: backend,
@@ -34,19 +34,21 @@ export default defineAssuranceConfig({
34
34
  // the route modules - is what keeps every route's declaration equal to its reach. The drivers
35
35
  // below are the backstop for code that goes around the seam; they grant both, because an import
36
36
  // cannot tell a read from a write.
37
+ // Shared starter catalog: an absent database integration is optional, but any matching
38
+ // import still grants its capabilities. Remove optional for seams this app owns.
37
39
  imports: [
38
- { specifier: "./read.ts", capabilities: ["db.read"] },
39
- { specifier: "./write.ts", capabilities: ["db.write"] },
40
- { specifier: "bun:sqlite", capabilities: ["db.read", "db.write"] },
41
- { specifier: "node:sqlite", capabilities: ["db.read", "db.write"] },
42
- { specifier: "pg", capabilities: ["db.read", "db.write"] },
43
- { specifier: "postgres", capabilities: ["db.read", "db.write"] },
44
- { specifier: "mysql2", capabilities: ["db.read", "db.write"] },
45
- { specifier: "kysely", capabilities: ["db.read", "db.write"] },
46
- { specifier: "drizzle-orm", capabilities: ["db.read", "db.write"] },
47
- { specifier: "drizzle-orm/*", capabilities: ["db.read", "db.write"] },
48
- { specifier: "@libsql/*", capabilities: ["db.read", "db.write"] },
49
- { specifier: "@prisma/client", capabilities: ["db.read", "db.write"] },
40
+ { specifier: "./read.ts", capabilities: ["db.read"], optional: true },
41
+ { specifier: "./write.ts", capabilities: ["db.write"], optional: true },
42
+ { specifier: "bun:sqlite", capabilities: ["db.read", "db.write"], optional: true },
43
+ { specifier: "node:sqlite", capabilities: ["db.read", "db.write"], optional: true },
44
+ { specifier: "pg", capabilities: ["db.read", "db.write"], optional: true },
45
+ { specifier: "postgres", capabilities: ["db.read", "db.write"], optional: true },
46
+ { specifier: "mysql2", capabilities: ["db.read", "db.write"], optional: true },
47
+ { specifier: "kysely", capabilities: ["db.read", "db.write"], optional: true },
48
+ { specifier: "drizzle-orm", capabilities: ["db.read", "db.write"], optional: true },
49
+ { specifier: "drizzle-orm/*", capabilities: ["db.read", "db.write"], optional: true },
50
+ { specifier: "@libsql/*", capabilities: ["db.read", "db.write"], optional: true },
51
+ { specifier: "@prisma/client", capabilities: ["db.read", "db.write"], optional: true },
50
52
  ],
51
53
  // Add a driver here to force every query through the seam, and the check will name any route that
52
54
  // reaches around it: `{ specifier: "pg", reason: "query through db/read.ts or db/write.ts" }`.
@@ -5,22 +5,23 @@
5
5
  "private": true,
6
6
  "scripts": {
7
7
  "build": "bun run build.ts",
8
- "dev": "bunx wrangler dev",
9
- "dev:bun": "bun run server.ts",
8
+ "dev": "bunx wrangler dev --var NIFRA_ALLOW_MEMORY_RATE_LIMIT:true",
9
+ "dev:bun": "bun run backend/dev-server.ts",
10
10
  "check": "nifra check && nifra assure"
11
11
  },
12
12
  "dependencies": {
13
- "@nifrajs/client": "^3.5.0",
14
- "@nifrajs/core": "^3.5.0",
15
- "@nifrajs/middleware": "^3.5.0",
16
- "@nifrajs/schema": "^3.5.0",
17
- "@nifrajs/web": "^3.5.0",
18
- "@nifrajs/web-react": "^3.5.0",
13
+ "@nifrajs/client": "^4.0.2",
14
+ "@nifrajs/core": "^4.0.2",
15
+ "@nifrajs/middleware": "^4.0.2",
16
+ "@nifrajs/schema": "^4.0.2",
17
+ "@nifrajs/web": "^4.0.2",
18
+ "@nifrajs/web-react": "^4.0.2",
19
19
  "react": "^19.0.0",
20
20
  "react-dom": "^19.0.0"
21
21
  },
22
22
  "devDependencies": {
23
- "@nifrajs/cli": "^3.5.0",
23
+ "@nifrajs/cli": "^4.0.2",
24
+ "@types/bun": "^1.4.2",
24
25
  "@types/react": "^19.0.0",
25
26
  "@types/react-dom": "^19.0.0",
26
27
  "typescript": "^6.0.3"
@@ -0,0 +1,14 @@
1
+ import { t } from "@nifrajs/schema"
2
+ import type { Route } from "./+types/index"
3
+
4
+ // ISR: the `withISR` wrapper (worker.ts / server.ts) caches this page and serves it
5
+ // stale-while-revalidate. `revalidate` is the freshness window in SECONDS - nifra emits it as the
6
+ // `x-nifra-isr-revalidate` header, which the wrapper reads to set this page's TTL.
7
+ export const revalidate = 10
8
+
9
+ export const loaderOutput = t.object({ renders: t.number(), revalidate: t.number() })
10
+
11
+ export async function loader({ api }: Route.LoaderArgs) {
12
+ const res = await api.page.get()
13
+ return { renders: res.ok ? res.data.renders : 0, revalidate }
14
+ }
@@ -1,32 +1,20 @@
1
- import type { LoaderArgs, LoaderData } from "@nifrajs/client"
2
- import type { backend } from "../backend"
1
+ import type { Route } from "./+types/index"
3
2
 
4
3
  export const meta = {
5
4
  title: "nifra + ISR",
6
5
  meta: [{ name: "description", content: "Incremental Static Regeneration on nifra." }],
7
6
  }
8
7
 
9
- // ISR: the `withISR` wrapper (worker.ts / server.ts) caches this page and serves it
10
- // stale-while-revalidate. `revalidate` is the freshness window in SECONDS - nifra emits it as the
11
- // `x-nifra-isr-revalidate` header, which the wrapper reads to set this page's TTL. Loader/meta are
12
- // pure annotated exports, so they tree-shake out of the client bundle.
13
- export const revalidate = 10
14
-
15
- export async function loader({ api }: LoaderArgs<typeof backend>) {
16
- const res = await api.page.get()
17
- return { renders: res.ok ? res.data.renders : 0 }
18
- }
19
-
20
- export default function Home(props: { data: LoaderData<typeof loader> }) {
8
+ export default function Home(props: Route.ComponentProps) {
21
9
  return (
22
10
  <section>
23
11
  <p>
24
12
  server renders: <b>{props.data.renders}</b>
25
13
  </p>
26
14
  <p>
27
- Reload within {revalidate}s and this holds - you're served the cached page (response header{" "}
28
- <code>x-nifra-isr: hit</code>). After the window, the next request gets the stale page
29
- instantly (<code>stale</code>) while a fresh copy regenerates behind it. Edit{" "}
15
+ Reload within {props.data.revalidate}s and this holds - you're served the cached page
16
+ (response header <code>x-nifra-isr: hit</code>). After the window, the next request gets the
17
+ stale page instantly (<code>stale</code>) while a fresh copy regenerates behind it. Edit{" "}
30
18
  <code>routes/index.tsx</code> to begin.
31
19
  </p>
32
20
  </section>
@@ -10,7 +10,8 @@
10
10
  "skipLibCheck": true,
11
11
  "noEmit": true,
12
12
  "verbatimModuleSyntax": true,
13
- "types": ["react", "react-dom"]
13
+ "types": ["bun", "react", "react-dom"],
14
+ "rootDirs": [".", "./.nifra/types"]
14
15
  },
15
16
  "include": ["**/*.ts", "**/*.tsx"],
16
17
  "exclude": [
@@ -20,9 +21,9 @@
20
21
  ".build",
21
22
  ".nifra",
22
23
  ".wrangler",
23
- "server-manifest.ts",
24
- "server.ts",
25
- "worker.ts",
24
+ "backend/server-manifest.ts",
25
+ "backend/dev-server.ts",
26
+ "backend/worker.ts",
26
27
  "build.ts"
27
28
  ]
28
29
  }
@@ -1,6 +1,11 @@
1
1
  name = "nifra-isr"
2
2
  main = "dist-server/worker.js"
3
- compatibility_date = "2025-01-01"
3
+ # 2025-04-01 or later: from that date `nodejs_compat` fills `process.env` from the Worker's variables,
4
+ # where backend/app.ts reads NIFRA_ALLOW_MEMORY_RATE_LIMIT; until that variable is "true" the deployed
5
+ # Worker refuses to start. Set it (a Worker variable, or [vars] below) only if a per-isolate rate limit
6
+ # will do: Cloudflare runs many isolates and each counts alone. `bun run dev` sets it for its one local
7
+ # process.
8
+ compatibility_date = "2025-04-01"
4
9
  compatibility_flags = ["nodejs_compat"]
5
10
 
6
11
  # Workers Assets: the content-hashed client bundle (buildClient → ./public/assets) is served at
@@ -7,7 +7,6 @@ is Web-standard, so only the server entry + build target differ per platform.
7
7
  ```sh
8
8
  bun install
9
9
  bun run dev # nifra dev → true-HMR dev server (Bun + nifra SSR) at http://localhost:3000
10
- bun run preview # wrangler pages dev dist → preview the built Cloudflare bundle (run `bun run build` first)
11
10
  ```
12
11
 
13
12
  The backend starter includes response security headers, explicit-origin CORS, a 30-second request
@@ -16,35 +15,37 @@ production unless `NIFRA_ALLOW_MEMORY_RATE_LIMIT=true`; use a shared store for m
16
15
  For cookie sessions, add signed CSRF middleware and require both runtime authentication and CSRF
17
16
  evidence in `nifra.assurance.ts`.
18
17
 
19
- Edit `routes/index.tsx` (the landing + a live loader/action counter), add files under `routes/`
20
- (`about.tsx` → `/about`, `[id].tsx` → `:id`, nested `_layout.tsx` for sections), and put your data
21
- behind `backend.ts`.
18
+ Edit `routes/index.tsx` (the landing) and `routes/index.backend.ts` (its live loader/action
19
+ counter), add files under `routes/` (`about.tsx` → `/about`, `[id].tsx` → `:id`, nested `_layout.tsx`
20
+ for sections; a page's loader/action goes in its `x.backend.ts`), and put your data behind
21
+ `backend/app.ts`.
22
22
 
23
- ## Deploy - pick a target
23
+ ## Deploy
24
24
 
25
- | target | build | deploy |
25
+ The app deploys to the `target` in `nifra.config.ts` (chosen with `bun create nifra --target`, switched
26
+ with `nifra target <t>`). `bun run build` runs `nifra build`, which generates that target's server
27
+ entry from `backend/` and `routes/`, so the app carries no per-runtime entry or build script.
28
+
29
+ | target | `bun run build` emits | then |
26
30
  | --- | --- | --- |
27
- | **Bun** (flagship) | `bun run build:bun` | `bun run start` (`bun dist-bun/server-bun.js`) - any host |
28
- | **Node** | `bun run build:node` | `docker build -t app . && docker run -p 3000:3000 app` (or `bun run start:node`) |
29
- | **Deno Deploy** | `bun run build:deno` | `deployctl deploy --prod --entrypoint=dist-deno/server-deno.js` (or `deno task start`) |
30
- | **Cloudflare Pages** | `bun run build` | `bun run deploy:cf` (`wrangler pages deploy dist`) |
31
- | **Vercel Edge** | `bun run build:vercel` | `bun run deploy:vercel` (`vercel deploy --prebuilt`) |
31
+ | **bun** (default) | `dist/server.js` + assets | `bun dist/server.js` on any host (`--docker` adds a Dockerfile) |
32
+ | **node** | `dist/server.js` + assets | `node dist/server.js` (`--docker` adds a Dockerfile) |
33
+ | **deno** | `dist/server.js` + assets | `deployctl deploy --prod --entrypoint=dist/server.js` |
34
+ | **cloudflare** | `dist/` (`_worker.js`, `_routes.json`) | `wrangler pages deploy dist` |
35
+ | **vercel** | `.vercel/output/` (Build Output API) | `vercel deploy --prebuilt` |
32
36
 
33
- The routes, `backend.ts`, and the client bundle are shared; each `build*` script swaps
34
- `buildServer`'s target/conditions and the server entry. nifra never enters your cloud credentials - the
35
- configs (`Dockerfile`, `deno.json`, `wrangler.toml`, Vercel Build Output API) are scaffolded; you run
36
- the vendor CLI.
37
+ nifra never enters your cloud credentials: it writes the target's config (`wrangler.toml`,
38
+ `deno.json`, a `Dockerfile`) and you run the vendor CLI.
37
39
 
38
40
  ## Structure
39
41
 
40
42
  ```
41
43
  routes/ index.tsx (landing), _layout.tsx (chrome), _404.tsx
42
- framework.ts the render adapter (imported by the server entries - kept edge-bundlable)
43
- nifra.config.ts the nifra CLI's dev/build config (adapter + clientModule) - read by `nifra dev`
44
- backend.ts your contract (loaders/actions call it in-process)
45
- server-bun.ts Bun entry (Bun.serve) build-bun.ts → dist-bun/
46
- _worker.ts Cloudflare Pages entry build.ts → dist/
47
- server-node.ts Node entry (@nifrajs/node) build-node.ts → dist-node/ (Dockerfile)
48
- server-deno.ts Deno entry (@nifrajs/deno) build-deno.ts → dist-deno/ (deno.json)
49
- server-vercel.ts Vercel Edge function build-vercel.ts → .vercel/output/
44
+ index.backend.ts - the landing page's loader/action (server only)
45
+ backend/ app.ts - your contract (loaders/actions call it in-process)
46
+ framework.ts - the render adapter (imported by the server entry `nifra build` generates - kept edge-bundlable)
47
+ frontend/ browser-only code (components, hooks) - created when you need it
48
+ shared/ code both sides import (types, formatting, validation) - may import only shared code
49
+ nifra.config.ts the nifra CLI's dev/build config (adapter + clientModule) - read by `nifra dev` and `nifra build`,
50
+ including `target`, where `nifra build` deploys
50
51
  ```
@@ -1,4 +1,5 @@
1
1
  import { server } from "@nifrajs/core/server"
2
+ import { responseContract } from "@nifrajs/core/response-contract"
2
3
  import {
3
4
  MemoryStore,
4
5
  cors,
@@ -9,10 +10,10 @@ import {
9
10
  import { counter } from "./counter"
10
11
 
11
12
  // Your backend contract - page loaders/actions call it in-process during SSR (no network).
12
- // Name and location are load-bearing: `nifra dev|build|check` resolve `backend.ts` from the project
13
- // root, the same fixed convention as `routes/`, `framework.ts`, and `nifra.config.ts`. Rename or move
14
- // it and the CLI runs without a backend (no error - the file is optional). Only THIS entry file is
15
- // pinned to the root; the modules it merges can live in any directory you like.
13
+ // Name and location are load-bearing: `nifra dev|build|check` resolve `backend/app.ts`, the same
14
+ // fixed convention as `routes/`, `backend/framework.ts`, and `nifra.config.ts`. Rename or move it and
15
+ // the CLI runs without a backend (no error - the file is optional). The modules it merges live beside
16
+ // it under `backend/`, which never reaches the browser.
16
17
  //
17
18
  // Composition only: this module merges route modules and registers none of its own.
18
19
  //
@@ -30,6 +31,7 @@ export const backend = server({
30
31
  requestTimeoutMs: 30_000,
31
32
  admission: createAdmissionController({ maxInFlight: 128, maxQueue: 0 }),
32
33
  })
34
+ .use(responseContract("enforce"))
33
35
  .use(securityHeaders())
34
36
  .use(
35
37
  cors({
@@ -1,19 +1,14 @@
1
1
  node_modules
2
2
  dist
3
- dist-bun
4
- dist-node
5
- dist-deno
3
+ .nifra-build
6
4
  .vercel
7
- .build
8
- .build-bun
9
- .build-node
10
- .build-deno
11
- .build-vercel
12
5
  .wrangler
13
- # generated by buildServer next to the entry
14
- server-manifest.ts
15
6
  # generated by `nifra dev` at the project root: the Vite pipeline's client entry, the Bun pipeline's
16
7
  # client entry + HTML, and the Bun pipeline's generated bunfig/boundary plugin.
17
8
  .nifra-vite-entry.tsx
18
9
  .nifra-bun/
19
10
  .nifra/
11
+ # local secrets; commit .env.example instead
12
+ .env
13
+ .env.*
14
+ !.env.example
@@ -11,7 +11,7 @@
11
11
  * an exemption you get by leaving the rule off is one nobody ever sees.
12
12
  */
13
13
  import { defineAssuranceConfig, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
14
- import { backend } from "./backend"
14
+ import { backend } from "./backend/app"
15
15
 
16
16
  export default defineAssuranceConfig({
17
17
  source: backend,
@@ -34,19 +34,21 @@ export default defineAssuranceConfig({
34
34
  // the route modules - is what keeps every route's declaration equal to its reach. The drivers
35
35
  // below are the backstop for code that goes around the seam; they grant both, because an import
36
36
  // cannot tell a read from a write.
37
+ // Shared starter catalog: an absent database integration is optional, but any matching
38
+ // import still grants its capabilities. Remove optional for seams this app owns.
37
39
  imports: [
38
- { specifier: "./read.ts", capabilities: ["db.read"] },
39
- { specifier: "./write.ts", capabilities: ["db.write"] },
40
- { specifier: "bun:sqlite", capabilities: ["db.read", "db.write"] },
41
- { specifier: "node:sqlite", capabilities: ["db.read", "db.write"] },
42
- { specifier: "pg", capabilities: ["db.read", "db.write"] },
43
- { specifier: "postgres", capabilities: ["db.read", "db.write"] },
44
- { specifier: "mysql2", capabilities: ["db.read", "db.write"] },
45
- { specifier: "kysely", capabilities: ["db.read", "db.write"] },
46
- { specifier: "drizzle-orm", capabilities: ["db.read", "db.write"] },
47
- { specifier: "drizzle-orm/*", capabilities: ["db.read", "db.write"] },
48
- { specifier: "@libsql/*", capabilities: ["db.read", "db.write"] },
49
- { specifier: "@prisma/client", capabilities: ["db.read", "db.write"] },
40
+ { specifier: "./read.ts", capabilities: ["db.read"], optional: true },
41
+ { specifier: "./write.ts", capabilities: ["db.write"], optional: true },
42
+ { specifier: "bun:sqlite", capabilities: ["db.read", "db.write"], optional: true },
43
+ { specifier: "node:sqlite", capabilities: ["db.read", "db.write"], optional: true },
44
+ { specifier: "pg", capabilities: ["db.read", "db.write"], optional: true },
45
+ { specifier: "postgres", capabilities: ["db.read", "db.write"], optional: true },
46
+ { specifier: "mysql2", capabilities: ["db.read", "db.write"], optional: true },
47
+ { specifier: "kysely", capabilities: ["db.read", "db.write"], optional: true },
48
+ { specifier: "drizzle-orm", capabilities: ["db.read", "db.write"], optional: true },
49
+ { specifier: "drizzle-orm/*", capabilities: ["db.read", "db.write"], optional: true },
50
+ { specifier: "@libsql/*", capabilities: ["db.read", "db.write"], optional: true },
51
+ { specifier: "@prisma/client", capabilities: ["db.read", "db.write"], optional: true },
50
52
  ],
51
53
  // Add a driver here to force every query through the seam, and the check will name any route that
52
54
  // reaches around it: `{ specifier: "pg", reason: "query through db/read.ts or db/write.ts" }`.
@@ -1,7 +1,7 @@
1
- // The nifra CLI's config - read by `nifra dev|build|start`. Separate from `framework.ts` (which the
2
- // edge/server entries import, so it must stay edge-bundlable): THIS file is imported only by the CLI
3
- // (which runs on Bun), so it's the place for CLI-only build/dev tooling. React JSX is Bun-native,
1
+ // The nifra CLI's config - read by `nifra dev|build|start`. Separate from `backend/framework.ts`
2
+ // (which the generated server entry imports, so it must stay edge-bundlable): THIS file is imported only
3
+ // by the CLI (which runs on Bun), so it's the place for CLI-only build/dev tooling. React JSX is Bun-native,
4
4
  // so the default dev loop needs no Vite plugin.
5
5
 
6
- export { adapter } from "./framework"
6
+ export { adapter } from "./backend/framework"
7
7
  export const clientModule = "@nifrajs/web-react/client"
@@ -0,0 +1,18 @@
1
+ import { t } from "@nifrajs/schema"
2
+ import type { Route } from "./+types/index"
3
+
4
+ export const loaderOutput = t.object({ count: t.number() })
5
+
6
+ // Loader runs on the server (in-process during SSR). The action handles the form POST; after a
7
+ // client submit the loader revalidates with no full reload (progressive enhancement).
8
+ export async function loader({ api }: Route.LoaderArgs) {
9
+ const res = await api.count.get()
10
+ return { count: res.ok ? res.data.count : 0 }
11
+ }
12
+
13
+ export const actionOutput = t.object({ ok: t.boolean() })
14
+
15
+ export async function action({ api }: Route.ActionArgs) {
16
+ await api.count.post()
17
+ return { ok: true }
18
+ }
@@ -1,24 +1,11 @@
1
- import type { ActionArgs, LoaderArgs, LoaderData } from "@nifrajs/client"
2
- import type { backend } from "../backend"
1
+ import type { Route } from "./+types/index"
3
2
 
4
3
  export const meta = {
5
4
  title: "nifra site",
6
5
  meta: [{ name: "description", content: "A nifra + React SSR site, deployable to every runtime." }],
7
6
  }
8
7
 
9
- // Loader runs on the server (in-process during SSR). The action handles the form POST; after a
10
- // client submit the loader revalidates with no full reload (progressive enhancement).
11
- export async function loader({ api }: LoaderArgs<typeof backend>) {
12
- const res = await api.count.get()
13
- return { count: res.ok ? res.data.count : 0 }
14
- }
15
-
16
- export async function action({ api }: ActionArgs<typeof backend>) {
17
- await api.count.post()
18
- return { ok: true }
19
- }
20
-
21
- export default function Home(props: { data: LoaderData<typeof loader> }) {
8
+ export default function Home(props: Route.ComponentProps) {
22
9
  return (
23
10
  <>
24
11
  <section className="hero">
@@ -7,7 +7,6 @@ is Web-standard, so only the server entry + build target differ per platform.
7
7
  ```sh
8
8
  bun install
9
9
  bun run dev # nifra dev → true-HMR dev server (Bun + nifra SSR) at http://localhost:3000
10
- bun run preview # wrangler pages dev dist → preview the built Cloudflare bundle (run `bun run build` first)
11
10
  ```
12
11
 
13
12
  The backend starter includes response security headers, explicit-origin CORS, a 30-second request
@@ -18,32 +17,34 @@ evidence in `nifra.assurance.ts`.
18
17
 
19
18
  Routes are Preact function components (JSX, via `jsxImportSource: "preact"`) under `routes/` -
20
19
  `index.tsx` (landing + a live loader/action counter), `_layout.tsx` (chrome), `_404.tsx`. The
21
- frontend adapter is one line in `framework.ts`; data lives behind `backend.ts`.
20
+ frontend adapter is one line in `backend/framework.ts`; data lives behind `backend/app.ts`.
22
21
 
23
- ## Deploy - pick a target
22
+ ## Deploy
24
23
 
25
- | target | build | deploy |
24
+ The app deploys to the `target` in `nifra.config.ts` (chosen with `bun create nifra --target`, switched
25
+ with `nifra target <t>`). `bun run build` runs `nifra build`, which generates that target's server
26
+ entry from `backend/` and `routes/`, so the app carries no per-runtime entry or build script.
27
+
28
+ | target | `bun run build` emits | then |
26
29
  | --- | --- | --- |
27
- | **Bun** (flagship) | `bun run build:bun` | `bun run start` - any host |
28
- | **Node** | `bun run build:node` | `docker build -t app . && docker run -p 3000:3000 app` (or `bun run start:node`) |
29
- | **Deno Deploy** | `bun run build:deno` | `deployctl deploy --prod --entrypoint=dist-deno/server-deno.js` |
30
- | **Cloudflare Pages** | `bun run build` | `bun run deploy:cf` |
31
- | **Vercel Edge** | `bun run build:vercel` | `bun run deploy:vercel` |
30
+ | **bun** (default) | `dist/server.js` + assets | `bun dist/server.js` on any host (`--docker` adds a Dockerfile) |
31
+ | **node** | `dist/server.js` + assets | `node dist/server.js` (`--docker` adds a Dockerfile) |
32
+ | **deno** | `dist/server.js` + assets | `deployctl deploy --prod --entrypoint=dist/server.js` |
33
+ | **cloudflare** | `dist/` (`_worker.js`, `_routes.json`) | `wrangler pages deploy dist` |
34
+ | **vercel** | `.vercel/output/` (Build Output API) | `vercel deploy --prebuilt` |
32
35
 
33
- The routes, `backend.ts`, and the client bundle are shared; each `build*` script swaps `buildServer`'s
34
- target/conditions and the server entry. nifra never enters your cloud credentials - it scaffolds the
35
- configs (`Dockerfile`, `deno.json`, `wrangler.toml`, Vercel Build Output API); you run the vendor CLI.
36
+ nifra never enters your cloud credentials: it writes the target's config (`wrangler.toml`,
37
+ `deno.json`, a `Dockerfile`) and you run the vendor CLI.
36
38
 
37
39
  ## Structure
38
40
 
39
41
  ```
40
42
  routes/ index.tsx (landing), _layout.tsx (chrome), _404.tsx
41
- framework.ts the adapter (preactAdapter) - imported by the server entries (edge-bundlable)
42
- nifra.config.ts the nifra CLI's dev/build config (adapter + clientModule) - read by `nifra dev`
43
- backend.ts your contract (loaders/actions call it in-process)
44
- server-bun.ts Bun entry (Bun.serve) build-bun.ts → dist-bun/
45
- _worker.ts Cloudflare Pages entry build.ts → dist/
46
- server-node.ts Node entry (@nifrajs/node) build-node.ts → dist-node/ (Dockerfile)
47
- server-deno.ts Deno entry (@nifrajs/deno) build-deno.ts → dist-deno/ (deno.json)
48
- server-vercel.ts Vercel Edge function build-vercel.ts → .vercel/output/
43
+ index.backend.ts - the landing page's loader/action (server only)
44
+ backend/ app.ts - your contract (loaders/actions call it in-process)
45
+ framework.ts - the adapter (preactAdapter) - imported by the server entry `nifra build` generates (edge-bundlable)
46
+ frontend/ browser-only code (components, hooks) - created when you need it
47
+ shared/ code both sides import (types, formatting, validation) - may import only shared code
48
+ nifra.config.ts the nifra CLI's dev/build config (adapter + clientModule) - read by `nifra dev` and `nifra build`,
49
+ including `target`, where `nifra build` deploys
49
50
  ```
@@ -1,7 +1,7 @@
1
- // The nifra CLI's config - read by `nifra dev|build|start`. Separate from `framework.ts` (which the
2
- // edge/server entries import, so it must stay edge-bundlable): THIS file is imported only by the CLI
3
- // (which runs on Bun), so it's the place for CLI-only build/dev tooling. Preact JSX is Bun-native,
1
+ // The nifra CLI's config - read by `nifra dev|build|start`. Separate from `backend/framework.ts`
2
+ // (which the generated server entry imports, so it must stay edge-bundlable): THIS file is imported only
3
+ // by the CLI (which runs on Bun), so it's the place for CLI-only build/dev tooling. Preact JSX is Bun-native,
4
4
  // so the default dev loop needs no Vite plugin.
5
5
 
6
- export { adapter } from "./framework"
6
+ export { adapter } from "./backend/framework"
7
7
  export const clientModule = "@nifrajs/web-preact/client"
@@ -22,7 +22,9 @@ const css = `
22
22
  export default function Layout(props: { children?: ComponentChildren }) {
23
23
  return (
24
24
  <div id="app">
25
- <style>{css}</style>
25
+ {/* Raw: the server renderer escapes text children, and a <style> element's text is never
26
+ unescaped, so a quoted font name would arrive as &quot; and void the rule. */}
27
+ <style dangerouslySetInnerHTML={{ __html: css }} />
26
28
  <header className="site">
27
29
  <div className="wrap">
28
30
  <a href="/" className="logo">