create-nifra 0.1.0-alpha.1

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 (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +33 -0
  3. package/dist/agents.d.ts +22 -0
  4. package/dist/agents.d.ts.map +1 -0
  5. package/dist/agents.js +259 -0
  6. package/dist/agents.js.map +1 -0
  7. package/dist/auth.d.ts +30 -0
  8. package/dist/auth.d.ts.map +1 -0
  9. package/dist/auth.js +90 -0
  10. package/dist/auth.js.map +1 -0
  11. package/dist/cli.d.ts +101 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +450 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/db.d.ts +39 -0
  16. package/dist/db.d.ts.map +1 -0
  17. package/dist/db.js +467 -0
  18. package/dist/db.js.map +1 -0
  19. package/package.json +50 -0
  20. package/template/README.md +24 -0
  21. package/template/gitignore +6 -0
  22. package/template/package.json +20 -0
  23. package/template/src/app.test.ts +8 -0
  24. package/template/src/app.ts +8 -0
  25. package/template/src/index.ts +5 -0
  26. package/template/tsconfig.json +16 -0
  27. package/template-isr/README.md +46 -0
  28. package/template-isr/backend.ts +12 -0
  29. package/template-isr/build.ts +23 -0
  30. package/template-isr/gitignore +7 -0
  31. package/template-isr/package.json +24 -0
  32. package/template-isr/routes/_404.tsx +10 -0
  33. package/template-isr/routes/_layout.tsx +18 -0
  34. package/template-isr/routes/index.tsx +34 -0
  35. package/template-isr/server.ts +56 -0
  36. package/template-isr/tsconfig.json +28 -0
  37. package/template-isr/worker.ts +52 -0
  38. package/template-isr/wrangler.toml +20 -0
  39. package/template-site/.dockerignore +14 -0
  40. package/template-site/Dockerfile +20 -0
  41. package/template-site/README.md +44 -0
  42. package/template-site/_worker.ts +17 -0
  43. package/template-site/backend.ts +12 -0
  44. package/template-site/build-bun.ts +27 -0
  45. package/template-site/build-deno.ts +25 -0
  46. package/template-site/build-node.ts +26 -0
  47. package/template-site/build-vercel.ts +45 -0
  48. package/template-site/build.ts +29 -0
  49. package/template-site/deno.json +7 -0
  50. package/template-site/framework.ts +6 -0
  51. package/template-site/gitignore +16 -0
  52. package/template-site/nifra.config.ts +9 -0
  53. package/template-site/package.json +37 -0
  54. package/template-site/routes/_404.tsx +20 -0
  55. package/template-site/routes/_layout.tsx +40 -0
  56. package/template-site/routes/index.tsx +53 -0
  57. package/template-site/server-bun.ts +35 -0
  58. package/template-site/server-deno.ts +38 -0
  59. package/template-site/server-node.ts +40 -0
  60. package/template-site/server-vercel.ts +17 -0
  61. package/template-site/tsconfig.json +37 -0
  62. package/template-site/wrangler.toml +4 -0
  63. package/template-site-preact/.dockerignore +14 -0
  64. package/template-site-preact/Dockerfile +20 -0
  65. package/template-site-preact/README.md +43 -0
  66. package/template-site-preact/_worker.ts +17 -0
  67. package/template-site-preact/backend.ts +12 -0
  68. package/template-site-preact/build-bun.ts +27 -0
  69. package/template-site-preact/build-deno.ts +25 -0
  70. package/template-site-preact/build-node.ts +26 -0
  71. package/template-site-preact/build-vercel.ts +45 -0
  72. package/template-site-preact/build.ts +29 -0
  73. package/template-site-preact/deno.json +7 -0
  74. package/template-site-preact/framework.ts +6 -0
  75. package/template-site-preact/gitignore +16 -0
  76. package/template-site-preact/nifra.config.ts +9 -0
  77. package/template-site-preact/package.json +34 -0
  78. package/template-site-preact/routes/_404.tsx +20 -0
  79. package/template-site-preact/routes/_layout.tsx +40 -0
  80. package/template-site-preact/routes/index.tsx +53 -0
  81. package/template-site-preact/server-bun.ts +35 -0
  82. package/template-site-preact/server-deno.ts +38 -0
  83. package/template-site-preact/server-node.ts +40 -0
  84. package/template-site-preact/server-vercel.ts +17 -0
  85. package/template-site-preact/tsconfig.json +38 -0
  86. package/template-site-preact/wrangler.toml +4 -0
  87. package/template-site-solid/.dockerignore +14 -0
  88. package/template-site-solid/Dockerfile +20 -0
  89. package/template-site-solid/README.md +43 -0
  90. package/template-site-solid/_worker.ts +17 -0
  91. package/template-site-solid/backend.ts +12 -0
  92. package/template-site-solid/build-bun.ts +33 -0
  93. package/template-site-solid/build-deno.ts +32 -0
  94. package/template-site-solid/build-node.ts +33 -0
  95. package/template-site-solid/build-vercel.ts +47 -0
  96. package/template-site-solid/build.ts +36 -0
  97. package/template-site-solid/deno.json +7 -0
  98. package/template-site-solid/framework.ts +6 -0
  99. package/template-site-solid/gitignore +16 -0
  100. package/template-site-solid/nifra.config.ts +14 -0
  101. package/template-site-solid/package.json +34 -0
  102. package/template-site-solid/routes/_404.tsx +20 -0
  103. package/template-site-solid/routes/_layout.tsx +39 -0
  104. package/template-site-solid/routes/index.tsx +53 -0
  105. package/template-site-solid/server-bun.ts +35 -0
  106. package/template-site-solid/server-deno.ts +38 -0
  107. package/template-site-solid/server-node.ts +40 -0
  108. package/template-site-solid/server-vercel.ts +17 -0
  109. package/template-site-solid/tsconfig.json +38 -0
  110. package/template-site-solid/wrangler.toml +4 -0
  111. package/template-site-svelte/.dockerignore +14 -0
  112. package/template-site-svelte/Dockerfile +20 -0
  113. package/template-site-svelte/README.md +45 -0
  114. package/template-site-svelte/_worker.ts +17 -0
  115. package/template-site-svelte/backend.ts +12 -0
  116. package/template-site-svelte/build-bun.ts +32 -0
  117. package/template-site-svelte/build-deno.ts +31 -0
  118. package/template-site-svelte/build-node.ts +32 -0
  119. package/template-site-svelte/build-vercel.ts +46 -0
  120. package/template-site-svelte/build.ts +35 -0
  121. package/template-site-svelte/deno.json +7 -0
  122. package/template-site-svelte/framework.ts +6 -0
  123. package/template-site-svelte/gitignore +16 -0
  124. package/template-site-svelte/nifra.config.ts +13 -0
  125. package/template-site-svelte/package.json +34 -0
  126. package/template-site-svelte/routes/_404.svelte +16 -0
  127. package/template-site-svelte/routes/_layout.svelte +35 -0
  128. package/template-site-svelte/routes/index.svelte +47 -0
  129. package/template-site-svelte/server-bun.ts +35 -0
  130. package/template-site-svelte/server-deno.ts +38 -0
  131. package/template-site-svelte/server-node.ts +40 -0
  132. package/template-site-svelte/server-vercel.ts +17 -0
  133. package/template-site-svelte/tsconfig.json +37 -0
  134. package/template-site-svelte/wrangler.toml +4 -0
  135. package/template-site-vue/.dockerignore +14 -0
  136. package/template-site-vue/Dockerfile +20 -0
  137. package/template-site-vue/README.md +45 -0
  138. package/template-site-vue/_worker.ts +17 -0
  139. package/template-site-vue/backend.ts +12 -0
  140. package/template-site-vue/build-bun.ts +37 -0
  141. package/template-site-vue/build-deno.ts +36 -0
  142. package/template-site-vue/build-node.ts +37 -0
  143. package/template-site-vue/build-vercel.ts +54 -0
  144. package/template-site-vue/build.ts +40 -0
  145. package/template-site-vue/deno.json +7 -0
  146. package/template-site-vue/framework.ts +6 -0
  147. package/template-site-vue/gitignore +16 -0
  148. package/template-site-vue/nifra.config.ts +19 -0
  149. package/template-site-vue/package.json +35 -0
  150. package/template-site-vue/routes/_404.vue +18 -0
  151. package/template-site-vue/routes/_layout.vue +40 -0
  152. package/template-site-vue/routes/index.vue +56 -0
  153. package/template-site-vue/server-bun.ts +35 -0
  154. package/template-site-vue/server-deno.ts +38 -0
  155. package/template-site-vue/server-node.ts +40 -0
  156. package/template-site-vue/server-vercel.ts +17 -0
  157. package/template-site-vue/tsconfig.json +37 -0
  158. package/template-site-vue/wrangler.toml +4 -0
package/dist/db.js ADDED
@@ -0,0 +1,467 @@
1
+ /**
2
+ * `--db <preset>` — wire a Drizzle data layer into a freshly scaffolded app, so a human (or a coding
3
+ * agent) starts from a correct, production-grade DB setup instead of inventing one. nifra ships NO DB
4
+ * abstraction (that belongs to Drizzle/libSQL, not a web framework) — this just bundles the existing
5
+ * tool, the Redwood-style "batteries available" move: a starter schema, a typed client, a migration
6
+ * config, scripts, and env, all rip-out-able.
7
+ *
8
+ * Three presets, by runtime fit:
9
+ * - drizzle-libsql — SQLite that runs EVERYWHERE incl. the edge (local file, or Turso). The default
10
+ * cross-runtime answer.
11
+ * - drizzle-postgres — Postgres (postgres.js) on Bun/Node/Deno.
12
+ * - drizzle-sqlite — Bun's built-in `bun:sqlite` (sync, local file). Bun-only.
13
+ *
14
+ * The starter `notes` table follows the production-grade DB defaults for its dialect; what a dialect
15
+ * can't express inline (CHECK constraints, RLS, uuidv7) is called out in a comment, per those rules.
16
+ */
17
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
18
+ import { dirname, join } from "node:path";
19
+ export const DB_CHOICES = [
20
+ "drizzle-libsql",
21
+ "drizzle-postgres",
22
+ "drizzle-sqlite",
23
+ "prisma-postgres",
24
+ "prisma-sqlite",
25
+ "kysely-postgres",
26
+ ];
27
+ const DRIZZLE_ORM = "^0.38.3";
28
+ const DRIZZLE_KIT = "^0.30.4";
29
+ const SCRIPTS = {
30
+ "db:generate": "drizzle-kit generate",
31
+ "db:migrate": "drizzle-kit migrate",
32
+ "db:studio": "drizzle-kit studio",
33
+ };
34
+ // --- starter schemas (one production-grade `notes` table per dialect) -------------------------------
35
+ const PG_SCHEMA = `import { pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core"
36
+
37
+ // Production-grade starter table. Postgres: UUID PK, TIMESTAMPTZ stamps, NOT NULL by default,
38
+ // soft-delete via deleted_at. Deferred to a migration (Drizzle can't express these inline; add them in
39
+ // the generated SQL): CHECK constraints (e.g. char_length(title) BETWEEN 1 AND 200), RLS for tenancy,
40
+ // and gen_uuidv7() for insert locality on hot tables (defaultRandom() = gen_random_uuid() for now).
41
+ export const notes = pgTable("notes", {
42
+ id: uuid("id").primaryKey().defaultRandom(),
43
+ title: text("title").notNull(),
44
+ body: text("body").notNull().default(""),
45
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
46
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
47
+ deletedAt: timestamp("deleted_at", { withTimezone: true }),
48
+ })
49
+
50
+ export type Note = typeof notes.$inferSelect
51
+ export type NewNote = typeof notes.$inferInsert
52
+ `;
53
+ // SQLite has no UUID/TIMESTAMPTZ/RLS — so: text id (app-generated UUID), millisecond-epoch timestamps
54
+ // (timestamp_ms mode → JS Date), NOT NULL by default, soft-delete via deleted_at. Shared by the
55
+ // bun:sqlite and libSQL presets (same dialect). CHECK constraints are deferred to the migration.
56
+ const SQLITE_SCHEMA = `import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core"
57
+
58
+ // Production-grade starter table (SQLite dialect — see the comment in your DB defaults for what the
59
+ // dialect can't enforce inline: CHECK constraints go in the generated migration).
60
+ export const notes = sqliteTable("notes", {
61
+ id: text("id")
62
+ .primaryKey()
63
+ .$defaultFn(() => crypto.randomUUID()),
64
+ title: text("title").notNull(),
65
+ body: text("body").notNull().default(""),
66
+ createdAt: integer("created_at", { mode: "timestamp_ms" })
67
+ .notNull()
68
+ .$defaultFn(() => new Date()),
69
+ updatedAt: integer("updated_at", { mode: "timestamp_ms" })
70
+ .notNull()
71
+ .$defaultFn(() => new Date()),
72
+ deletedAt: integer("deleted_at", { mode: "timestamp_ms" }),
73
+ })
74
+
75
+ export type Note = typeof notes.$inferSelect
76
+ export type NewNote = typeof notes.$inferInsert
77
+ `;
78
+ // --- typed clients (export \`db\` + re-export the schema) --------------------------------------------
79
+ // Each prints the same usage header so an agent sees how to wire it (decorate) and query it.
80
+ const WIRE_DOC = `// Wire it into your backend ONCE, then read \`c.db\` in handlers:
81
+ // import { server } from "@nifrajs/core"
82
+ // import { db, notes } from "./db"
83
+ // export const app = server().decorate("db", db)
84
+ // .get("/notes", async (c) => c.db.select().from(notes))
85
+ // Never top-level-import this into a routes/ page file (server-only) — reach it via c.db / ctx.api.`;
86
+ const PG_CLIENT = `import { drizzle } from "drizzle-orm/postgres-js"
87
+ import postgres from "postgres"
88
+ import * as schema from "./schema.ts"
89
+
90
+ ${WIRE_DOC}
91
+
92
+ const url = process.env.DATABASE_URL
93
+ if (url === undefined || url === "") {
94
+ throw new Error("DATABASE_URL is not set — copy .env.example to .env and fill it in.")
95
+ }
96
+
97
+ // One pooled client per process (per instance in a multi-instance deploy — fine for Postgres).
98
+ export const db = drizzle(postgres(url), { schema })
99
+ export * from "./schema.ts"
100
+ `;
101
+ const SQLITE_CLIENT = `import { Database } from "bun:sqlite"
102
+ import { drizzle } from "drizzle-orm/bun-sqlite"
103
+ import * as schema from "./schema.ts"
104
+
105
+ ${WIRE_DOC}
106
+
107
+ // Bun's built-in SQLite — sync + fast, local file. (Bun-only; for Node/Deno/edge use the libsql preset.)
108
+ export const db = drizzle(new Database(process.env.DATABASE_URL ?? "local.db"), { schema })
109
+ export * from "./schema.ts"
110
+ `;
111
+ const LIBSQL_CLIENT = `import { createClient } from "@libsql/client"
112
+ import { drizzle } from "drizzle-orm/libsql"
113
+ import * as schema from "./schema.ts"
114
+
115
+ ${WIRE_DOC}
116
+
117
+ // libSQL runs everywhere — a local file (file:local.db), a remote Turso URL (libsql://…) with an auth
118
+ // token, or an embedded replica. The one SQLite client that also works on the edge (Cloudflare Workers).
119
+ const token = process.env.DATABASE_AUTH_TOKEN
120
+ export const db = drizzle(
121
+ createClient({
122
+ url: process.env.DATABASE_URL ?? "file:local.db",
123
+ ...(token !== undefined && token !== "" ? { authToken: token } : {}),
124
+ }),
125
+ { schema },
126
+ )
127
+ export * from "./schema.ts"
128
+ `;
129
+ // --- drizzle-kit config (per dialect) ---------------------------------------------------------------
130
+ const PG_CONFIG = `import { defineConfig } from "drizzle-kit"
131
+
132
+ export default defineConfig({
133
+ schema: "./db/schema.ts",
134
+ out: "./db/migrations",
135
+ dialect: "postgresql",
136
+ // biome-ignore lint/style/noNonNullAssertion: drizzle-kit reads this from .env (Bun auto-loads it).
137
+ dbCredentials: { url: process.env.DATABASE_URL! },
138
+ })
139
+ `;
140
+ const SQLITE_CONFIG = `import { defineConfig } from "drizzle-kit"
141
+
142
+ export default defineConfig({
143
+ schema: "./db/schema.ts",
144
+ out: "./db/migrations",
145
+ dialect: "sqlite",
146
+ dbCredentials: { url: process.env.DATABASE_URL ?? "local.db" },
147
+ })
148
+ `;
149
+ const LIBSQL_CONFIG = `import { defineConfig } from "drizzle-kit"
150
+
151
+ export default defineConfig({
152
+ schema: "./db/schema.ts",
153
+ out: "./db/migrations",
154
+ dialect: "turso",
155
+ dbCredentials: {
156
+ url: process.env.DATABASE_URL ?? "file:local.db",
157
+ authToken: process.env.DATABASE_AUTH_TOKEN,
158
+ },
159
+ })
160
+ `;
161
+ // --- .env examples ----------------------------------------------------------------------------------
162
+ const PG_ENV = `# Postgres connection string (postgres.js). Never commit the real .env.
163
+ DATABASE_URL="postgres://user:password@localhost:5432/app"
164
+ `;
165
+ const SQLITE_ENV = `# Path to the local SQLite file (Bun's bun:sqlite). Never commit the real .env.
166
+ DATABASE_URL="local.db"
167
+ `;
168
+ const LIBSQL_ENV = `# Local file by default; point at Turso for a hosted/edge DB. Never commit the real .env.
169
+ DATABASE_URL="file:local.db"
170
+ # For Turso (or any remote libSQL):
171
+ # DATABASE_URL="libsql://your-db.turso.io"
172
+ # DATABASE_AUTH_TOKEN="your-token"
173
+ `;
174
+ // --- Prisma presets ---------------------------------------------------------------------------------
175
+ // Prisma owns its schema DSL (prisma/schema.prisma), client codegen, and migration engine — so the
176
+ // scaffold is schema + a singleton client + the prisma scripts (no drizzle-kit equivalent needed).
177
+ const PRISMA_CLIENT_VERSION = "^6.5.0";
178
+ const PRISMA_SCRIPTS = {
179
+ "db:generate": "prisma generate",
180
+ "db:migrate": "prisma migrate dev",
181
+ "db:studio": "prisma studio",
182
+ };
183
+ const PRISMA_PG_SCHEMA = `datasource db {
184
+ provider = "postgresql"
185
+ url = env("DATABASE_URL")
186
+ }
187
+
188
+ generator client {
189
+ provider = "prisma-client-js"
190
+ }
191
+
192
+ // Production-grade starter model. Postgres: UUID PK, TIMESTAMPTZ stamps (@db.Timestamptz), NOT NULL by
193
+ // default, soft-delete via deletedAt. Add in a migration what the DSL can't express inline: CHECK
194
+ // constraints (e.g. length caps on title), RLS for tenancy, and gen_uuidv7() for insert locality on hot
195
+ // tables (uuid() = gen_random_uuid() for now).
196
+ model Note {
197
+ id String @id @default(uuid()) @db.Uuid
198
+ title String
199
+ body String @default("")
200
+ createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
201
+ updatedAt DateTime @updatedAt @map("updated_at") @db.Timestamptz(6)
202
+ deletedAt DateTime? @map("deleted_at") @db.Timestamptz(6)
203
+
204
+ @@map("notes")
205
+ }
206
+ `;
207
+ const PRISMA_SQLITE_SCHEMA = `datasource db {
208
+ provider = "sqlite"
209
+ url = env("DATABASE_URL")
210
+ }
211
+
212
+ generator client {
213
+ provider = "prisma-client-js"
214
+ }
215
+
216
+ // Production-grade starter model (SQLite dialect — no native UUID/TIMESTAMPTZ/RLS; Prisma stores the
217
+ // String id + DateTime stamps for you). NOT NULL by default, soft-delete via deletedAt. CHECK
218
+ // constraints are deferred to the generated migration SQL.
219
+ model Note {
220
+ id String @id @default(uuid())
221
+ title String
222
+ body String @default("")
223
+ createdAt DateTime @default(now()) @map("created_at")
224
+ updatedAt DateTime @updatedAt @map("updated_at")
225
+ deletedAt DateTime? @map("deleted_at")
226
+
227
+ @@map("notes")
228
+ }
229
+ `;
230
+ const PRISMA_CLIENT = `import { PrismaClient } from "@prisma/client"
231
+
232
+ // Wire it into your backend ONCE, then read \`c.db\` in handlers:
233
+ // import { server } from "@nifrajs/core"
234
+ // import { db } from "./db"
235
+ // export const app = server().decorate("db", db)
236
+ // .get("/notes", async (c) => c.db.note.findMany({ where: { deletedAt: null } }))
237
+ // Never top-level-import this into a routes/ page file (server-only) — reach it via c.db / ctx.api.
238
+
239
+ // One PrismaClient per process. The globalThis guard stops dev hot-reload from opening a new connection
240
+ // pool on every reload (which exhausts the DB); production gets a single fresh instance.
241
+ const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
242
+ export const db = globalForPrisma.prisma ?? new PrismaClient()
243
+ if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db
244
+ `;
245
+ const PRISMA_PG_ENV = `# Postgres connection string (Prisma). Never commit the real .env.
246
+ DATABASE_URL="postgresql://user:password@localhost:5432/app"
247
+ `;
248
+ const PRISMA_SQLITE_ENV = `# Local SQLite file (Prisma). Never commit the real .env.
249
+ DATABASE_URL="file:./local.db"
250
+ `;
251
+ // --- Kysely preset ----------------------------------------------------------------------------------
252
+ // Kysely is a typed query builder, not an ORM: you describe the DB shape (db/schema.ts) and own the
253
+ // migrations. It ships no CLI, so the scaffold includes a Migrator runner (db/migrate.ts) + a starter
254
+ // migration. Postgres via the official PostgresDialect + node-postgres.
255
+ const KYSELY_VERSION = "^0.27.4";
256
+ const KYSELY_SCRIPTS = {
257
+ "db:migrate": "bun run db/migrate.ts",
258
+ };
259
+ const KYSELY_PG_SCHEMA = `import type { ColumnType, Generated } from "kysely"
260
+
261
+ // Kysely is types-only: describe the DB shape and keep it in sync with db/migrations (or generate it
262
+ // with \`kysely-codegen\`). There is no runtime schema object.
263
+ export interface NotesTable {
264
+ id: Generated<string> // uuid PK, DB-defaulted (gen_random_uuid())
265
+ title: string
266
+ body: Generated<string> // defaults to ''
267
+ // DB-managed stamps: never written by the app (never on insert/update), read back as Date.
268
+ created_at: ColumnType<Date, never, never>
269
+ updated_at: ColumnType<Date, never, Date>
270
+ deleted_at: Date | null // soft delete
271
+ }
272
+
273
+ export interface DB {
274
+ notes: NotesTable
275
+ }
276
+ `;
277
+ const KYSELY_PG_CLIENT = `import { Kysely, PostgresDialect } from "kysely"
278
+ import { Pool } from "pg"
279
+ import type { DB } from "./schema.ts"
280
+
281
+ // Wire it into your backend ONCE, then read \`c.db\` in handlers:
282
+ // import { server } from "@nifrajs/core"
283
+ // import { db } from "./db"
284
+ // export const app = server().decorate("db", db)
285
+ // .get("/notes", async (c) =>
286
+ // c.db.selectFrom("notes").selectAll().where("deleted_at", "is", null).execute())
287
+ // Never top-level-import this into a routes/ page file (server-only) — reach it via c.db / ctx.api.
288
+
289
+ const url = process.env.DATABASE_URL
290
+ if (url === undefined || url === "") {
291
+ throw new Error("DATABASE_URL is not set — copy .env.example to .env and fill it in.")
292
+ }
293
+
294
+ // One pooled client per process (per instance in a multi-instance deploy — fine for Postgres).
295
+ export const db = new Kysely<DB>({
296
+ dialect: new PostgresDialect({ pool: new Pool({ connectionString: url }) }),
297
+ })
298
+ export type { DB } from "./schema.ts"
299
+ `;
300
+ const KYSELY_MIGRATION = `import { type Kysely, sql } from "kysely"
301
+
302
+ // Production-grade starter table. Postgres: UUID PK (gen_random_uuid — swap to gen_uuidv7() for insert
303
+ // locality on hot tables once available), TIMESTAMPTZ stamps, NOT NULL by default, soft-delete via
304
+ // deleted_at. Add CHECK constraints (e.g. length caps) and RLS here per your DB defaults.
305
+ export async function up(db: Kysely<unknown>): Promise<void> {
306
+ await db.schema
307
+ .createTable("notes")
308
+ .addColumn("id", "uuid", (c) => c.primaryKey().defaultTo(sql\`gen_random_uuid()\`))
309
+ .addColumn("title", "text", (c) => c.notNull())
310
+ .addColumn("body", "text", (c) => c.notNull().defaultTo(""))
311
+ .addColumn("created_at", sql\`timestamptz\`, (c) => c.notNull().defaultTo(sql\`now()\`))
312
+ .addColumn("updated_at", sql\`timestamptz\`, (c) => c.notNull().defaultTo(sql\`now()\`))
313
+ .addColumn("deleted_at", sql\`timestamptz\`)
314
+ .execute()
315
+ }
316
+
317
+ export async function down(db: Kysely<unknown>): Promise<void> {
318
+ await db.schema.dropTable("notes").execute()
319
+ }
320
+ `;
321
+ const KYSELY_MIGRATE_RUNNER = `import { promises as fs } from "node:fs"
322
+ import * as path from "node:path"
323
+ import { FileMigrationProvider, Migrator } from "kysely"
324
+ import { db } from "./index.ts"
325
+
326
+ // Kysely ships no migration CLI, so this is the runner: it applies every pending migration in
327
+ // db/migrations (in filename order, each in its own transaction). Run with \`bun run db:migrate\`.
328
+ const migrator = new Migrator({
329
+ db,
330
+ provider: new FileMigrationProvider({
331
+ fs,
332
+ path,
333
+ migrationFolder: path.join(import.meta.dir, "migrations"),
334
+ }),
335
+ })
336
+
337
+ const { error, results } = await migrator.migrateToLatest()
338
+ for (const r of results ?? []) {
339
+ console.log(r.status === "Success" ? \`✓ \${r.migrationName}\` : \`✗ \${r.migrationName}\`)
340
+ }
341
+ await db.destroy()
342
+ if (error !== undefined) {
343
+ console.error("migration failed:", error)
344
+ process.exit(1)
345
+ }
346
+ `;
347
+ export const DB_PRESETS = {
348
+ "drizzle-libsql": {
349
+ label: "Drizzle + libSQL",
350
+ orm: "drizzle",
351
+ dialect: "sqlite",
352
+ note: "libSQL runs on every runtime incl. the edge (local file or Turso).",
353
+ deps: { "drizzle-orm": DRIZZLE_ORM, "@libsql/client": "^0.14.0" },
354
+ devDeps: { "drizzle-kit": DRIZZLE_KIT },
355
+ scripts: SCRIPTS,
356
+ files: {
357
+ "db/schema.ts": SQLITE_SCHEMA,
358
+ "db/index.ts": LIBSQL_CLIENT,
359
+ "drizzle.config.ts": LIBSQL_CONFIG,
360
+ ".env.example": LIBSQL_ENV,
361
+ },
362
+ },
363
+ "drizzle-postgres": {
364
+ label: "Drizzle + Postgres",
365
+ orm: "drizzle",
366
+ dialect: "postgres",
367
+ note: "Postgres (postgres.js) on Bun/Node/Deno — not the edge.",
368
+ deps: { "drizzle-orm": DRIZZLE_ORM, postgres: "^3.4.5" },
369
+ devDeps: { "drizzle-kit": DRIZZLE_KIT },
370
+ scripts: SCRIPTS,
371
+ files: {
372
+ "db/schema.ts": PG_SCHEMA,
373
+ "db/index.ts": PG_CLIENT,
374
+ "drizzle.config.ts": PG_CONFIG,
375
+ ".env.example": PG_ENV,
376
+ },
377
+ },
378
+ "drizzle-sqlite": {
379
+ label: "Drizzle + SQLite (bun:sqlite)",
380
+ orm: "drizzle",
381
+ dialect: "sqlite",
382
+ note: "Bun's built-in SQLite — local file, Bun-only.",
383
+ deps: { "drizzle-orm": DRIZZLE_ORM },
384
+ devDeps: { "drizzle-kit": DRIZZLE_KIT },
385
+ scripts: SCRIPTS,
386
+ files: {
387
+ "db/schema.ts": SQLITE_SCHEMA,
388
+ "db/index.ts": SQLITE_CLIENT,
389
+ "drizzle.config.ts": SQLITE_CONFIG,
390
+ ".env.example": SQLITE_ENV,
391
+ },
392
+ },
393
+ "prisma-postgres": {
394
+ label: "Prisma + Postgres",
395
+ orm: "prisma",
396
+ dialect: "postgres",
397
+ note: "Prisma ORM on Postgres (Bun/Node/Deno) — own schema DSL, migrate engine, and Studio.",
398
+ deps: { "@prisma/client": PRISMA_CLIENT_VERSION },
399
+ devDeps: { prisma: PRISMA_CLIENT_VERSION },
400
+ scripts: PRISMA_SCRIPTS,
401
+ files: {
402
+ "prisma/schema.prisma": PRISMA_PG_SCHEMA,
403
+ "db/index.ts": PRISMA_CLIENT,
404
+ ".env.example": PRISMA_PG_ENV,
405
+ },
406
+ },
407
+ "prisma-sqlite": {
408
+ label: "Prisma + SQLite",
409
+ orm: "prisma",
410
+ dialect: "sqlite",
411
+ note: "Prisma ORM on a local SQLite file — fastest start; migrate to Postgres later by swapping the datasource.",
412
+ deps: { "@prisma/client": PRISMA_CLIENT_VERSION },
413
+ devDeps: { prisma: PRISMA_CLIENT_VERSION },
414
+ scripts: PRISMA_SCRIPTS,
415
+ files: {
416
+ "prisma/schema.prisma": PRISMA_SQLITE_SCHEMA,
417
+ "db/index.ts": PRISMA_CLIENT,
418
+ ".env.example": PRISMA_SQLITE_ENV,
419
+ },
420
+ },
421
+ "kysely-postgres": {
422
+ label: "Kysely + Postgres",
423
+ orm: "kysely",
424
+ dialect: "postgres",
425
+ note: "Kysely typed query builder on Postgres (node-postgres) — you own the schema types + migrations.",
426
+ deps: { kysely: KYSELY_VERSION, pg: "^8.13.1" },
427
+ devDeps: { "@types/pg": "^8.11.10" },
428
+ scripts: KYSELY_SCRIPTS,
429
+ files: {
430
+ "db/schema.ts": KYSELY_PG_SCHEMA,
431
+ "db/index.ts": KYSELY_PG_CLIENT,
432
+ "db/migrate.ts": KYSELY_MIGRATE_RUNNER,
433
+ "db/migrations/0001_create_notes.ts": KYSELY_MIGRATION,
434
+ ".env.example": PG_ENV,
435
+ },
436
+ },
437
+ };
438
+ const GITIGNORE_BLOCK = `
439
+ # database (added by --db)
440
+ .env
441
+ local.db
442
+ *.db-shm
443
+ *.db-wal
444
+ db/migrations/meta
445
+ `;
446
+ /** Write a DB preset's files into `target` and ensure local DB artifacts + .env are gitignored. */
447
+ export async function writeDbFiles(target, choice) {
448
+ const preset = DB_PRESETS[choice];
449
+ for (const [rel, content] of Object.entries(preset.files)) {
450
+ const path = join(target, rel);
451
+ await mkdir(dirname(path), { recursive: true });
452
+ await writeFile(path, content);
453
+ }
454
+ // Append DB ignores once (the template's .gitignore already exists post-rename; never clobber it).
455
+ const gitignorePath = join(target, ".gitignore");
456
+ let current = "";
457
+ try {
458
+ current = await readFile(gitignorePath, "utf8");
459
+ }
460
+ catch {
461
+ // No .gitignore in this template — create one with just the DB block.
462
+ }
463
+ if (!current.includes("local.db")) {
464
+ await writeFile(gitignorePath, `${current}${GITIGNORE_BLOCK}`);
465
+ }
466
+ }
467
+ //# sourceMappingURL=db.js.map
package/dist/db.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AAC7D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEzC,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,gBAAgB;IAChB,kBAAkB;IAClB,gBAAgB;IAChB,iBAAiB;IACjB,eAAe;IACf,iBAAiB;CACT,CAAA;AAsBV,MAAM,WAAW,GAAG,SAAS,CAAA;AAC7B,MAAM,WAAW,GAAG,SAAS,CAAA;AAC7B,MAAM,OAAO,GAAqC;IAChD,aAAa,EAAE,sBAAsB;IACrC,YAAY,EAAE,qBAAqB;IACnC,WAAW,EAAE,oBAAoB;CAClC,CAAA;AAED,uGAAuG;AAEvG,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;CAiBjB,CAAA;AAED,sGAAsG;AACtG,gGAAgG;AAChG,iGAAiG;AACjG,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;CAqBrB,CAAA;AAED,wGAAwG;AACxG,6FAA6F;AAE7F,MAAM,QAAQ,GAAG;;;;;qGAKoF,CAAA;AAErG,MAAM,SAAS,GAAG;;;;EAIhB,QAAQ;;;;;;;;;;CAUT,CAAA;AAED,MAAM,aAAa,GAAG;;;;EAIpB,QAAQ;;;;;CAKT,CAAA;AAED,MAAM,aAAa,GAAG;;;;EAIpB,QAAQ;;;;;;;;;;;;;CAaT,CAAA;AAED,uGAAuG;AAEvG,MAAM,SAAS,GAAG;;;;;;;;;CASjB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;CAQrB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;;;;CAWrB,CAAA;AAED,uGAAuG;AAEvG,MAAM,MAAM,GAAG;;CAEd,CAAA;AACD,MAAM,UAAU,GAAG;;CAElB,CAAA;AACD,MAAM,UAAU,GAAG;;;;;CAKlB,CAAA;AAED,uGAAuG;AACvG,mGAAmG;AACnG,mGAAmG;AAEnG,MAAM,qBAAqB,GAAG,QAAQ,CAAA;AACtC,MAAM,cAAc,GAAqC;IACvD,aAAa,EAAE,iBAAiB;IAChC,YAAY,EAAE,oBAAoB;IAClC,WAAW,EAAE,eAAe;CAC7B,CAAA;AAED,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;;CAuBxB,CAAA;AAED,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsB5B,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;;;;;;;CAcrB,CAAA;AAED,MAAM,aAAa,GAAG;;CAErB,CAAA;AACD,MAAM,iBAAiB,GAAG;;CAEzB,CAAA;AAED,uGAAuG;AACvG,oGAAoG;AACpG,sGAAsG;AACtG,wEAAwE;AAExE,MAAM,cAAc,GAAG,SAAS,CAAA;AAChC,MAAM,cAAc,GAAqC;IACvD,YAAY,EAAE,uBAAuB;CACtC,CAAA;AAED,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;CAiBxB,CAAA;AAED,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;;;CAsBxB,CAAA;AAED,MAAM,gBAAgB,GAAG;;;;;;;;;;;;;;;;;;;;CAoBxB,CAAA;AAED,MAAM,qBAAqB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;CAyB7B,CAAA;AAED,MAAM,CAAC,MAAM,UAAU,GAAyC;IAC9D,gBAAgB,EAAE;QAChB,KAAK,EAAE,kBAAkB;QACzB,GAAG,EAAE,SAAS;QACd,OAAO,EAAE,QAAQ;QACjB,IAAI,EAAE,oEAAoE;QAC1E,IAAI,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE,gBAAgB,EAAE,SAAS,EAAE;QACjE,OAAO,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE;QACvC,OAAO,EAAE,OAAO;QAChB,KAAK,EAAE;YACL,cAAc,EAAE,aAAa;YAC7B,aAAa,EAAE,aAAa;YAC5B,mBAAmB,EAAE,aAAa;YAClC,cAAc,EAAE,UAAU;SAC3B;KACF;IACD,kBAAkB,EAAE;QAClB,KAAK,EAAE,oBAAoB;QAC3B,GAAG,EAAE,SAAS;QACd,OAAO,EAAE,UAAU;QACnB,IAAI,EAAE,yDAAyD;QAC/D,IAAI,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE;QACxD,OAAO,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE;QACvC,OAAO,EAAE,OAAO;QAChB,KAAK,EAAE;YACL,cAAc,EAAE,SAAS;YACzB,aAAa,EAAE,SAAS;YACxB,mBAAmB,EAAE,SAAS;YAC9B,cAAc,EAAE,MAAM;SACvB;KACF;IACD,gBAAgB,EAAE;QAChB,KAAK,EAAE,+BAA+B;QACtC,GAAG,EAAE,SAAS;QACd,OAAO,EAAE,QAAQ;QACjB,IAAI,EAAE,+CAA+C;QACrD,IAAI,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE;QACpC,OAAO,EAAE,EAAE,aAAa,EAAE,WAAW,EAAE;QACvC,OAAO,EAAE,OAAO;QAChB,KAAK,EAAE;YACL,cAAc,EAAE,aAAa;YAC7B,aAAa,EAAE,aAAa;YAC5B,mBAAmB,EAAE,aAAa;YAClC,cAAc,EAAE,UAAU;SAC3B;KACF;IACD,iBAAiB,EAAE;QACjB,KAAK,EAAE,mBAAmB;QAC1B,GAAG,EAAE,QAAQ;QACb,OAAO,EAAE,UAAU;QACnB,IAAI,EAAE,sFAAsF;QAC5F,IAAI,EAAE,EAAE,gBAAgB,EAAE,qBAAqB,EAAE;QACjD,OAAO,EAAE,EAAE,MAAM,EAAE,qBAAqB,EAAE;QAC1C,OAAO,EAAE,cAAc;QACvB,KAAK,EAAE;YACL,sBAAsB,EAAE,gBAAgB;YACxC,aAAa,EAAE,aAAa;YAC5B,cAAc,EAAE,aAAa;SAC9B;KACF;IACD,eAAe,EAAE;QACf,KAAK,EAAE,iBAAiB;QACxB,GAAG,EAAE,QAAQ;QACb,OAAO,EAAE,QAAQ;QACjB,IAAI,EAAE,0GAA0G;QAChH,IAAI,EAAE,EAAE,gBAAgB,EAAE,qBAAqB,EAAE;QACjD,OAAO,EAAE,EAAE,MAAM,EAAE,qBAAqB,EAAE;QAC1C,OAAO,EAAE,cAAc;QACvB,KAAK,EAAE;YACL,sBAAsB,EAAE,oBAAoB;YAC5C,aAAa,EAAE,aAAa;YAC5B,cAAc,EAAE,iBAAiB;SAClC;KACF;IACD,iBAAiB,EAAE;QACjB,KAAK,EAAE,mBAAmB;QAC1B,GAAG,EAAE,QAAQ;QACb,OAAO,EAAE,UAAU;QACnB,IAAI,EAAE,iGAAiG;QACvG,IAAI,EAAE,EAAE,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,SAAS,EAAE;QAC/C,OAAO,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE;QACpC,OAAO,EAAE,cAAc;QACvB,KAAK,EAAE;YACL,cAAc,EAAE,gBAAgB;YAChC,aAAa,EAAE,gBAAgB;YAC/B,eAAe,EAAE,qBAAqB;YACtC,oCAAoC,EAAE,gBAAgB;YACtD,cAAc,EAAE,MAAM;SACvB;KACF;CACF,CAAA;AAED,MAAM,eAAe,GAAG;;;;;;;CAOvB,CAAA;AAED,mGAAmG;AACnG,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,MAAc,EAAE,MAAgB;IACjE,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC,CAAA;IACjC,KAAK,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1D,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;QAC9B,MAAM,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;QAC/C,MAAM,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;IAChC,CAAC;IACD,mGAAmG;IACnG,MAAM,aAAa,GAAG,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,CAAA;IAChD,IAAI,OAAO,GAAG,EAAE,CAAA;IAChB,IAAI,CAAC;QACH,OAAO,GAAG,MAAM,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,CAAA;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,sEAAsE;IACxE,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,CAAC;QAClC,MAAM,SAAS,CAAC,aAAa,EAAE,GAAG,OAAO,GAAG,eAAe,EAAE,CAAC,CAAA;IAChE,CAAC;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "create-nifra",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Scaffold a new nifra app — `bun create nifra <dir>`.",
5
+ "keywords": [
6
+ "bun",
7
+ "typescript",
8
+ "nifra",
9
+ "web-framework",
10
+ "full-stack",
11
+ "edge",
12
+ "ssr",
13
+ "scaffold",
14
+ "create",
15
+ "starter",
16
+ "template",
17
+ "cli"
18
+ ],
19
+ "type": "module",
20
+ "bin": {
21
+ "create-nifra": "./dist/cli.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "template",
26
+ "template-site",
27
+ "template-site-vue",
28
+ "template-site-preact",
29
+ "template-site-solid",
30
+ "template-site-svelte",
31
+ "template-isr"
32
+ ],
33
+ "sideEffects": false,
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
36
+ },
37
+ "license": "MIT",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/nifrajs/nifra.git",
44
+ "directory": "packages/create-nifra"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/nifrajs/nifra/issues"
48
+ },
49
+ "homepage": "https://github.com/nifrajs/nifra#readme"
50
+ }
@@ -0,0 +1,24 @@
1
+ # nifra-app
2
+
3
+ A starter [nifra](https://github.com/nifra) app, scaffolded with `create-nifra`.
4
+
5
+ ```sh
6
+ bun install
7
+ bun run dev # watch + serve on http://localhost:3000
8
+ bun test # run the tests
9
+ bun run typecheck # type-check
10
+ ```
11
+
12
+ - `src/app.ts` — your routes (exported without `listen`, so tests drive it via `app.fetch`).
13
+ - `src/index.ts` — boots the server.
14
+ - `src/app.test.ts` — an example test.
15
+
16
+ Add a typed client from `typeof app`:
17
+
18
+ ```ts
19
+ import { client } from "@nifrajs/client"
20
+ import type { App } from "./src/app.ts"
21
+
22
+ const api = client<App>("http://localhost:3000")
23
+ const { data } = await api.users({ id: "42" }).get()
24
+ ```
@@ -0,0 +1,6 @@
1
+ node_modules/
2
+ dist/
3
+ *.log
4
+ .env
5
+ .env.*
6
+ !.env.example
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "nifra-app",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "private": true,
6
+ "scripts": {
7
+ "dev": "bun --watch run src/index.ts",
8
+ "start": "bun run src/index.ts",
9
+ "test": "bun test",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "dependencies": {
13
+ "@nifrajs/client": "^0.1.0",
14
+ "@nifrajs/core": "^0.1.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/bun": "latest",
18
+ "typescript": "^6.0.3"
19
+ }
20
+ }
@@ -0,0 +1,8 @@
1
+ import { expect, test } from "bun:test"
2
+ import { app } from "./app.ts"
3
+
4
+ test("GET /users/:id echoes the id", async () => {
5
+ const res = await app.fetch(new Request("http://localhost/users/42"))
6
+ expect(res.status).toBe(200)
7
+ expect(await res.json()).toEqual({ id: "42" })
8
+ })
@@ -0,0 +1,8 @@
1
+ import { server } from "@nifrajs/core"
2
+
3
+ /** Your app. Exported (without `listen`) so tests can drive it via `app.fetch`. */
4
+ export const app = server()
5
+ .get("/", () => ({ hello: "nifra" }))
6
+ .get("/users/:id", (c) => ({ id: c.params.id }))
7
+
8
+ export type App = typeof app
@@ -0,0 +1,5 @@
1
+ import { app } from "./app.ts"
2
+
3
+ const port = Number(process.env.PORT ?? 3000)
4
+ app.listen(port)
5
+ console.log(`▲ nifra listening on http://localhost:${port}`)
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ESNext"],
7
+ "strict": true,
8
+ "noUncheckedIndexedAccess": true,
9
+ "verbatimModuleSyntax": true,
10
+ "allowImportingTsExtensions": true,
11
+ "noEmit": true,
12
+ "types": ["bun"],
13
+ "skipLibCheck": true
14
+ },
15
+ "include": ["src/**/*"]
16
+ }
@@ -0,0 +1,46 @@
1
+ # nifra + ISR
2
+
3
+ A nifra + React app with **Incremental Static Regeneration** — pages are cached and served
4
+ stale-while-revalidate, backed by **Workers KV** in production (so the cache and on-demand purges hold
5
+ across instances) and an in-memory store for local dev.
6
+
7
+ ## Develop
8
+
9
+ On real workerd with a local KV (no Cloudflare account needed):
10
+
11
+ ```sh
12
+ bun install
13
+ bun run build # buildClient → public/assets, buildServer → dist-server/worker.js
14
+ bun run dev # bunx wrangler dev
15
+ ```
16
+
17
+ Or a plain Bun server with an in-memory store:
18
+
19
+ ```sh
20
+ bun run build
21
+ bun run dev:bun # http://localhost:3000
22
+ ```
23
+
24
+ Watch the `x-nifra-isr` response header as you reload: `miss` (rendered + stored) → `hit` (fresh, from
25
+ cache) → `stale` (served instantly while it regenerates). The page shows a server-render counter that
26
+ holds across `hit`s and bumps on a `miss`/regeneration.
27
+
28
+ ## How it works
29
+
30
+ - `withISR(app, { store, revalidate, now })` wraps the app's `fetch`. Only full-document `text/html`
31
+ `GET` `200`s are cached; assets, soft-nav data fetches, redirects, and errors pass through.
32
+ - A route sets its freshness with `export const revalidate = <seconds>` (see `routes/index.tsx`).
33
+ - `KVCacheStore(env.ISR_CACHE)` is the shared, durable store (production); `MemoryCacheStore` is dev.
34
+ - `revalidateEndpoint({ store, secret })` is the on-demand purge handler:
35
+
36
+ ```sh
37
+ curl -X POST 'http://localhost:3000/__velo/revalidate?path=/' -H 'x-nifra-revalidate-token: dev-secret'
38
+ ```
39
+
40
+ ## Deploy
41
+
42
+ ```sh
43
+ bunx wrangler kv namespace create ISR_CACHE # paste the id into wrangler.toml
44
+ bunx wrangler secret put REVALIDATE_SECRET # set a real purge secret
45
+ bun run build && bunx wrangler deploy
46
+ ```