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.
- package/README.md +3 -3
- package/dist/agent-files.d.ts +41 -26
- package/dist/agent-files.d.ts.map +1 -1
- package/dist/agent-files.js +80 -47
- package/dist/agent-files.js.map +1 -1
- package/dist/agents.d.ts +3 -3
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +65 -69
- package/dist/agents.js.map +1 -1
- package/dist/auth.d.ts +5 -4
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +27 -14
- package/dist/auth.js.map +1 -1
- package/dist/cli.d.ts +23 -25
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +141 -130
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +49 -48
- package/dist/db.js.map +1 -1
- package/dist/scaffold/frameworks.d.ts +13 -38
- package/dist/scaffold/frameworks.d.ts.map +1 -1
- package/dist/scaffold/frameworks.js +9 -32
- package/dist/scaffold/frameworks.js.map +1 -1
- package/dist/scaffold/route-types.d.ts +9 -0
- package/dist/scaffold/route-types.d.ts.map +1 -0
- package/dist/scaffold/route-types.js +61 -0
- package/dist/scaffold/route-types.js.map +1 -0
- package/dist/scaffold/site-files.d.ts +14 -8
- package/dist/scaffold/site-files.d.ts.map +1 -1
- package/dist/scaffold/site-files.js +29 -60
- package/dist/scaffold/site-files.js.map +1 -1
- package/dist/scaffold/site.d.ts +8 -5
- package/dist/scaffold/site.d.ts.map +1 -1
- package/dist/scaffold/site.js +47 -34
- package/dist/scaffold/site.js.map +1 -1
- package/dist/scaffold/targets.d.ts +39 -0
- package/dist/scaffold/targets.d.ts.map +1 -0
- package/dist/scaffold/targets.js +116 -0
- package/dist/scaffold/targets.js.map +1 -0
- package/package.json +4 -1
- package/src/agent-files.ts +89 -47
- package/template/README.md +12 -6
- package/template/{src → backend}/app.test.ts +2 -2
- package/template/{src → backend}/app.ts +8 -3
- package/template/{src → backend}/index.ts +2 -2
- package/template/{src → backend}/routes.ts +1 -1
- package/template/nifra.assurance.ts +16 -14
- package/template/package.json +7 -7
- package/template/tsconfig.json +1 -1
- package/template-batteries/README.md +10 -6
- package/template-batteries/{src → backend}/app.test.ts +23 -23
- package/template-batteries/{src → backend}/app.ts +8 -3
- package/template-batteries/{src → backend}/index.ts +2 -2
- package/template-batteries/{src → backend}/notes.ts +1 -1
- package/template-batteries/nifra.assurance.ts +16 -14
- package/template-batteries/package.json +10 -9
- package/template-batteries/tsconfig.json +1 -1
- package/template-isr/README.md +3 -0
- package/template-isr/{backend.ts → backend/app.ts} +2 -0
- package/template-isr/{server.ts → backend/dev-server.ts} +4 -4
- package/template-isr/{worker.ts → backend/worker.ts} +4 -2
- package/template-isr/build.ts +3 -1
- package/template-isr/gitignore +7 -1
- package/template-isr/nifra.assurance.ts +15 -13
- package/template-isr/package.json +10 -9
- package/template-isr/routes/index.backend.ts +14 -0
- package/template-isr/routes/index.tsx +5 -17
- package/template-isr/tsconfig.json +5 -4
- package/template-isr/wrangler.toml +6 -1
- package/template-site/README.md +24 -23
- package/template-site/{backend.ts → backend/app.ts} +6 -4
- package/template-site/gitignore +5 -10
- package/template-site/nifra.assurance.ts +15 -13
- package/template-site/nifra.config.ts +4 -4
- package/template-site/routes/index.backend.ts +18 -0
- package/template-site/routes/index.tsx +2 -15
- package/template-site-preact/README.md +21 -20
- package/template-site-preact/nifra.config.ts +4 -4
- package/template-site-preact/routes/_layout.tsx +3 -1
- package/template-site-preact/routes/index.tsx +2 -15
- package/template-site-solid/README.md +21 -19
- package/template-site-solid/nifra.config.ts +5 -4
- package/template-site-solid/routes/index.tsx +2 -15
- package/template-site-svelte/README.md +21 -19
- package/template-site-svelte/nifra.config.ts +3 -2
- package/template-site-svelte/routes/index.svelte +3 -16
- package/template-site-vue/README.md +21 -20
- package/template-site-vue/nifra.config.ts +4 -3
- package/template-site-vue/routes/_layout.vue +21 -25
- package/template-site-vue/routes/index.vue +12 -18
- package/dist/scaffold/site-build.d.ts +0 -60
- package/dist/scaffold/site-build.d.ts.map +0 -1
- package/dist/scaffold/site-build.js +0 -198
- package/dist/scaffold/site-build.js.map +0 -1
- package/template-site/.dockerignore +0 -14
- package/template-site/Dockerfile +0 -25
- package/template-site/_worker.ts +0 -17
- package/template-site/deno.json +0 -7
- package/template-site/server-bun.ts +0 -35
- package/template-site/server-deno.ts +0 -38
- package/template-site/server-node.ts +0 -40
- package/template-site/server-vercel.ts +0 -17
- package/template-site/wrangler.toml +0 -4
- /package/template-isr/{page.ts → backend/page.ts} +0 -0
- /package/template-site/{counter.ts → backend/counter.ts} +0 -0
package/template-isr/README.md
CHANGED
|
@@ -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 "./
|
|
11
|
+
import { backend } from "./app"
|
|
12
12
|
|
|
13
|
-
const publicDir = `${import.meta.dir}
|
|
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}
|
|
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 "./
|
|
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
|
-
|
|
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`
|
package/template-isr/build.ts
CHANGED
|
@@ -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
|
})
|
package/template-isr/gitignore
CHANGED
|
@@ -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": "^
|
|
14
|
-
"@nifrajs/core": "^
|
|
15
|
-
"@nifrajs/middleware": "^
|
|
16
|
-
"@nifrajs/schema": "^
|
|
17
|
-
"@nifrajs/web": "^
|
|
18
|
-
"@nifrajs/web-react": "^
|
|
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": "^
|
|
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 {
|
|
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
|
-
|
|
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
|
|
28
|
-
<code>x-nifra-isr: hit</code>). After the window, the next request gets the
|
|
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
|
-
|
|
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
|
package/template-site/README.md
CHANGED
|
@@ -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
|
|
20
|
-
(`about.tsx` → `/about`, `[id].tsx` → `:id`, nested `_layout.tsx`
|
|
21
|
-
|
|
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
|
|
23
|
+
## Deploy
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
| **
|
|
28
|
-
| **
|
|
29
|
-
| **
|
|
30
|
-
| **
|
|
31
|
-
| **
|
|
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
|
-
|
|
34
|
-
`
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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({
|
package/template-site/gitignore
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
node_modules
|
|
2
2
|
dist
|
|
3
|
-
|
|
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`
|
|
2
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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
|
|
22
|
+
## Deploy
|
|
24
23
|
|
|
25
|
-
|
|
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
|
-
| **
|
|
28
|
-
| **
|
|
29
|
-
| **
|
|
30
|
-
| **
|
|
31
|
-
| **
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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`
|
|
2
|
-
//
|
|
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>
|
|
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 " 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">
|