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.
- package/LICENSE +21 -0
- package/README.md +33 -0
- package/dist/agents.d.ts +22 -0
- package/dist/agents.d.ts.map +1 -0
- package/dist/agents.js +259 -0
- package/dist/agents.js.map +1 -0
- package/dist/auth.d.ts +30 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +90 -0
- package/dist/auth.js.map +1 -0
- package/dist/cli.d.ts +101 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +450 -0
- package/dist/cli.js.map +1 -0
- package/dist/db.d.ts +39 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +467 -0
- package/dist/db.js.map +1 -0
- package/package.json +50 -0
- package/template/README.md +24 -0
- package/template/gitignore +6 -0
- package/template/package.json +20 -0
- package/template/src/app.test.ts +8 -0
- package/template/src/app.ts +8 -0
- package/template/src/index.ts +5 -0
- package/template/tsconfig.json +16 -0
- package/template-isr/README.md +46 -0
- package/template-isr/backend.ts +12 -0
- package/template-isr/build.ts +23 -0
- package/template-isr/gitignore +7 -0
- package/template-isr/package.json +24 -0
- package/template-isr/routes/_404.tsx +10 -0
- package/template-isr/routes/_layout.tsx +18 -0
- package/template-isr/routes/index.tsx +34 -0
- package/template-isr/server.ts +56 -0
- package/template-isr/tsconfig.json +28 -0
- package/template-isr/worker.ts +52 -0
- package/template-isr/wrangler.toml +20 -0
- package/template-site/.dockerignore +14 -0
- package/template-site/Dockerfile +20 -0
- package/template-site/README.md +44 -0
- package/template-site/_worker.ts +17 -0
- package/template-site/backend.ts +12 -0
- package/template-site/build-bun.ts +27 -0
- package/template-site/build-deno.ts +25 -0
- package/template-site/build-node.ts +26 -0
- package/template-site/build-vercel.ts +45 -0
- package/template-site/build.ts +29 -0
- package/template-site/deno.json +7 -0
- package/template-site/framework.ts +6 -0
- package/template-site/gitignore +16 -0
- package/template-site/nifra.config.ts +9 -0
- package/template-site/package.json +37 -0
- package/template-site/routes/_404.tsx +20 -0
- package/template-site/routes/_layout.tsx +40 -0
- package/template-site/routes/index.tsx +53 -0
- package/template-site/server-bun.ts +35 -0
- package/template-site/server-deno.ts +38 -0
- package/template-site/server-node.ts +40 -0
- package/template-site/server-vercel.ts +17 -0
- package/template-site/tsconfig.json +37 -0
- package/template-site/wrangler.toml +4 -0
- package/template-site-preact/.dockerignore +14 -0
- package/template-site-preact/Dockerfile +20 -0
- package/template-site-preact/README.md +43 -0
- package/template-site-preact/_worker.ts +17 -0
- package/template-site-preact/backend.ts +12 -0
- package/template-site-preact/build-bun.ts +27 -0
- package/template-site-preact/build-deno.ts +25 -0
- package/template-site-preact/build-node.ts +26 -0
- package/template-site-preact/build-vercel.ts +45 -0
- package/template-site-preact/build.ts +29 -0
- package/template-site-preact/deno.json +7 -0
- package/template-site-preact/framework.ts +6 -0
- package/template-site-preact/gitignore +16 -0
- package/template-site-preact/nifra.config.ts +9 -0
- package/template-site-preact/package.json +34 -0
- package/template-site-preact/routes/_404.tsx +20 -0
- package/template-site-preact/routes/_layout.tsx +40 -0
- package/template-site-preact/routes/index.tsx +53 -0
- package/template-site-preact/server-bun.ts +35 -0
- package/template-site-preact/server-deno.ts +38 -0
- package/template-site-preact/server-node.ts +40 -0
- package/template-site-preact/server-vercel.ts +17 -0
- package/template-site-preact/tsconfig.json +38 -0
- package/template-site-preact/wrangler.toml +4 -0
- package/template-site-solid/.dockerignore +14 -0
- package/template-site-solid/Dockerfile +20 -0
- package/template-site-solid/README.md +43 -0
- package/template-site-solid/_worker.ts +17 -0
- package/template-site-solid/backend.ts +12 -0
- package/template-site-solid/build-bun.ts +33 -0
- package/template-site-solid/build-deno.ts +32 -0
- package/template-site-solid/build-node.ts +33 -0
- package/template-site-solid/build-vercel.ts +47 -0
- package/template-site-solid/build.ts +36 -0
- package/template-site-solid/deno.json +7 -0
- package/template-site-solid/framework.ts +6 -0
- package/template-site-solid/gitignore +16 -0
- package/template-site-solid/nifra.config.ts +14 -0
- package/template-site-solid/package.json +34 -0
- package/template-site-solid/routes/_404.tsx +20 -0
- package/template-site-solid/routes/_layout.tsx +39 -0
- package/template-site-solid/routes/index.tsx +53 -0
- package/template-site-solid/server-bun.ts +35 -0
- package/template-site-solid/server-deno.ts +38 -0
- package/template-site-solid/server-node.ts +40 -0
- package/template-site-solid/server-vercel.ts +17 -0
- package/template-site-solid/tsconfig.json +38 -0
- package/template-site-solid/wrangler.toml +4 -0
- package/template-site-svelte/.dockerignore +14 -0
- package/template-site-svelte/Dockerfile +20 -0
- package/template-site-svelte/README.md +45 -0
- package/template-site-svelte/_worker.ts +17 -0
- package/template-site-svelte/backend.ts +12 -0
- package/template-site-svelte/build-bun.ts +32 -0
- package/template-site-svelte/build-deno.ts +31 -0
- package/template-site-svelte/build-node.ts +32 -0
- package/template-site-svelte/build-vercel.ts +46 -0
- package/template-site-svelte/build.ts +35 -0
- package/template-site-svelte/deno.json +7 -0
- package/template-site-svelte/framework.ts +6 -0
- package/template-site-svelte/gitignore +16 -0
- package/template-site-svelte/nifra.config.ts +13 -0
- package/template-site-svelte/package.json +34 -0
- package/template-site-svelte/routes/_404.svelte +16 -0
- package/template-site-svelte/routes/_layout.svelte +35 -0
- package/template-site-svelte/routes/index.svelte +47 -0
- package/template-site-svelte/server-bun.ts +35 -0
- package/template-site-svelte/server-deno.ts +38 -0
- package/template-site-svelte/server-node.ts +40 -0
- package/template-site-svelte/server-vercel.ts +17 -0
- package/template-site-svelte/tsconfig.json +37 -0
- package/template-site-svelte/wrangler.toml +4 -0
- package/template-site-vue/.dockerignore +14 -0
- package/template-site-vue/Dockerfile +20 -0
- package/template-site-vue/README.md +45 -0
- package/template-site-vue/_worker.ts +17 -0
- package/template-site-vue/backend.ts +12 -0
- package/template-site-vue/build-bun.ts +37 -0
- package/template-site-vue/build-deno.ts +36 -0
- package/template-site-vue/build-node.ts +37 -0
- package/template-site-vue/build-vercel.ts +54 -0
- package/template-site-vue/build.ts +40 -0
- package/template-site-vue/deno.json +7 -0
- package/template-site-vue/framework.ts +6 -0
- package/template-site-vue/gitignore +16 -0
- package/template-site-vue/nifra.config.ts +19 -0
- package/template-site-vue/package.json +35 -0
- package/template-site-vue/routes/_404.vue +18 -0
- package/template-site-vue/routes/_layout.vue +40 -0
- package/template-site-vue/routes/index.vue +56 -0
- package/template-site-vue/server-bun.ts +35 -0
- package/template-site-vue/server-deno.ts +38 -0
- package/template-site-vue/server-node.ts +40 -0
- package/template-site-vue/server-vercel.ts +17 -0
- package/template-site-vue/tsconfig.json +37 -0
- package/template-site-vue/wrangler.toml +4 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { server } from "@nifrajs/core"
|
|
2
|
+
|
|
3
|
+
// Your backend contract — page loaders/actions call it in-process during SSR (no network). This demo
|
|
4
|
+
// counts server renders so the ISR cache is observable: a cache HIT serves stored bytes (the loader
|
|
5
|
+
// doesn't run, so the number holds), while a MISS or background REGENERATION runs it (it bumps).
|
|
6
|
+
// Replace it with real data (KV/D1/Postgres/…); on the edge, reach bindings via `c.env`.
|
|
7
|
+
let renders = 0
|
|
8
|
+
|
|
9
|
+
export const backend = server().get("/page", () => {
|
|
10
|
+
renders += 1
|
|
11
|
+
return { renders }
|
|
12
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Build for Cloudflare Workers: (1) buildClient → public/assets (served by Workers Assets at
|
|
2
|
+
// /assets/*), and (2) buildServer → dist-server/worker.js (the ISR worker). `bun run build`.
|
|
3
|
+
import { buildClient, buildServer } from "@nifrajs/web/build"
|
|
4
|
+
|
|
5
|
+
const dir = import.meta.dir
|
|
6
|
+
|
|
7
|
+
const client = await buildClient({
|
|
8
|
+
routesDir: `${dir}/routes`,
|
|
9
|
+
outDir: `${dir}/public/assets`,
|
|
10
|
+
clientModule: "@nifrajs/web-react/client",
|
|
11
|
+
conditions: ["bun", "browser"],
|
|
12
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const { worker } = await buildServer({
|
|
16
|
+
routesDir: `${dir}/routes`,
|
|
17
|
+
serverEntry: `${dir}/worker.ts`,
|
|
18
|
+
outDir: `${dir}/dist-server`,
|
|
19
|
+
clientEntry: client.entry,
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
console.log("client entry:", client.entry)
|
|
23
|
+
console.log("worker bundle:", worker, "→ run: bunx wrangler dev")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nifra-isr",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "bun run build.ts",
|
|
8
|
+
"dev": "bunx wrangler dev",
|
|
9
|
+
"dev:bun": "bun run server.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@nifrajs/client": "^0.1.0",
|
|
13
|
+
"@nifrajs/core": "^0.1.0",
|
|
14
|
+
"@nifrajs/web": "^0.1.0",
|
|
15
|
+
"@nifrajs/web-react": "^0.1.0",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@types/react": "^19.0.0",
|
|
21
|
+
"@types/react-dom": "^19.0.0",
|
|
22
|
+
"typescript": "^6.0.3"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
/** Root layout — rendered inside the document shell `renderPage` emits. */
|
|
4
|
+
export default function Layout(props: { children?: ReactNode }) {
|
|
5
|
+
return (
|
|
6
|
+
<main
|
|
7
|
+
style={{
|
|
8
|
+
fontFamily: "system-ui, sans-serif",
|
|
9
|
+
maxWidth: 640,
|
|
10
|
+
margin: "3rem auto",
|
|
11
|
+
padding: "0 1rem",
|
|
12
|
+
}}
|
|
13
|
+
>
|
|
14
|
+
<h1>nifra + ISR</h1>
|
|
15
|
+
{props.children}
|
|
16
|
+
</main>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { LoaderArgs, LoaderData } from "@nifrajs/client"
|
|
2
|
+
import type { backend } from "../backend"
|
|
3
|
+
|
|
4
|
+
export const meta = {
|
|
5
|
+
title: "nifra + ISR",
|
|
6
|
+
meta: [{ name: "description", content: "Incremental Static Regeneration on nifra." }],
|
|
7
|
+
}
|
|
8
|
+
|
|
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.data?.renders ?? 0 }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export default function Home(props: { data: LoaderData<typeof loader> }) {
|
|
21
|
+
return (
|
|
22
|
+
<section>
|
|
23
|
+
<p>
|
|
24
|
+
server renders: <b>{props.data.renders}</b>
|
|
25
|
+
</p>
|
|
26
|
+
<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{" "}
|
|
30
|
+
<code>routes/index.tsx</code> to begin.
|
|
31
|
+
</p>
|
|
32
|
+
</section>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
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
|
|
4
|
+
* store. Watch the `x-nifra-isr` response header: miss → hit → stale.
|
|
5
|
+
*/
|
|
6
|
+
import { inProcessClient } from "@nifrajs/client"
|
|
7
|
+
import { createWebApp, MemoryCacheStore, revalidateEndpoint, withISR } from "@nifrajs/web"
|
|
8
|
+
import type { BuildManifest } from "@nifrajs/web/build"
|
|
9
|
+
import { discoverRoutes } from "@nifrajs/web/fs"
|
|
10
|
+
import { reactAdapter } from "@nifrajs/web-react"
|
|
11
|
+
import { backend } from "./backend"
|
|
12
|
+
|
|
13
|
+
const publicDir = `${import.meta.dir}/public`
|
|
14
|
+
const assets = JSON.parse(
|
|
15
|
+
await Bun.file(`${publicDir}/assets/manifest.json`)
|
|
16
|
+
.text()
|
|
17
|
+
.catch(() => '{"entry":"/assets/missing.js","assets":[],"routes":{}}'),
|
|
18
|
+
) as BuildManifest
|
|
19
|
+
|
|
20
|
+
const app = createWebApp({
|
|
21
|
+
adapter: reactAdapter,
|
|
22
|
+
manifest: discoverRoutes(`${import.meta.dir}/routes`),
|
|
23
|
+
clientEntry: assets.entry,
|
|
24
|
+
routePreload: assets.routes,
|
|
25
|
+
api: inProcessClient(backend),
|
|
26
|
+
title: "nifra + ISR",
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
app.get("/assets/*", async (c) => {
|
|
30
|
+
const file = Bun.file(`${publicDir}${new URL(c.req.url).pathname}`)
|
|
31
|
+
if (!(await file.exists())) return new Response("Not Found", { status: 404 })
|
|
32
|
+
return new Response(file, {
|
|
33
|
+
headers: {
|
|
34
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
35
|
+
"cache-control": "public, max-age=31536000, immutable",
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const store = new MemoryCacheStore()
|
|
41
|
+
const secret = Bun.env.REVALIDATE_SECRET ?? "dev-secret"
|
|
42
|
+
const purge = revalidateEndpoint({ store, secret })
|
|
43
|
+
app.post("/__nifra/revalidate", (c) => purge(c.req))
|
|
44
|
+
|
|
45
|
+
const isr = withISR(app, {
|
|
46
|
+
store,
|
|
47
|
+
revalidate: 60,
|
|
48
|
+
now: () => Date.now(),
|
|
49
|
+
key: (req) => {
|
|
50
|
+
const url = new URL(req.url)
|
|
51
|
+
return url.pathname.startsWith("/assets/") ? null : url.origin + url.pathname + url.search
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const running = Bun.serve({ port: Number(Bun.env.PORT ?? 3000), fetch: (req) => isr(req) })
|
|
56
|
+
console.log(`http://localhost:${running.port}`)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"types": ["react", "react-dom"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["**/*.ts", "**/*.tsx"],
|
|
16
|
+
"exclude": [
|
|
17
|
+
"node_modules",
|
|
18
|
+
"public",
|
|
19
|
+
"dist-server",
|
|
20
|
+
".build",
|
|
21
|
+
".nifra",
|
|
22
|
+
".wrangler",
|
|
23
|
+
"server-manifest.ts",
|
|
24
|
+
"server.ts",
|
|
25
|
+
"worker.ts",
|
|
26
|
+
"build.ts"
|
|
27
|
+
]
|
|
28
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Workers entry — the app, cached in Workers KV so cached pages and on-demand purges hold
|
|
3
|
+
* ACROSS instances. `ctx.waitUntil` keeps the worker alive while a stale page regenerates behind the
|
|
4
|
+
* response. `bun run build` then `bunx wrangler dev` (a local KV is provided automatically).
|
|
5
|
+
*/
|
|
6
|
+
import { inProcessClient } from "@nifrajs/client"
|
|
7
|
+
import {
|
|
8
|
+
createWebApp,
|
|
9
|
+
KVCacheStore,
|
|
10
|
+
type KVNamespaceLike,
|
|
11
|
+
revalidateEndpoint,
|
|
12
|
+
withISR,
|
|
13
|
+
} from "@nifrajs/web"
|
|
14
|
+
import { reactAdapter } from "@nifrajs/web-react"
|
|
15
|
+
import { backend } from "./backend"
|
|
16
|
+
// Generated by build.ts (buildServer): the static-import route manifest + the baked client entry URL.
|
|
17
|
+
import { clientEntry, manifest } from "./server-manifest"
|
|
18
|
+
|
|
19
|
+
const app = createWebApp({
|
|
20
|
+
adapter: reactAdapter,
|
|
21
|
+
manifest,
|
|
22
|
+
clientEntry,
|
|
23
|
+
api: inProcessClient(backend),
|
|
24
|
+
title: "nifra + ISR",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
/** Workers bindings (wrangler.toml → [[kv_namespaces]] + [vars]). */
|
|
28
|
+
interface Env {
|
|
29
|
+
readonly ISR_CACHE: KVNamespaceLike
|
|
30
|
+
readonly REVALIDATE_SECRET?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Minimal shape of the Workers execution context — just `waitUntil`. */
|
|
34
|
+
interface ExecutionContext {
|
|
35
|
+
waitUntil(promise: Promise<unknown>): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default {
|
|
39
|
+
async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
|
|
40
|
+
const store = new KVCacheStore(env.ISR_CACHE, { expirationTtl: 86_400 }) // 1-day GC backstop
|
|
41
|
+
|
|
42
|
+
// On-demand purge: POST /__nifra/revalidate?path=/ with the secret in x-nifra-revalidate-token.
|
|
43
|
+
if (new URL(req.url).pathname === "/__nifra/revalidate") {
|
|
44
|
+
return revalidateEndpoint({ store, secret: env.REVALIDATE_SECRET ?? "" })(req)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Cache GET documents stale-while-revalidate. Default 60s; a route's `export const revalidate`
|
|
48
|
+
// overrides it per-page.
|
|
49
|
+
const isr = withISR(app, { store, revalidate: 60, now: () => Date.now() })
|
|
50
|
+
return isr(req, { env, waitUntil: (p) => ctx.waitUntil(p) })
|
|
51
|
+
},
|
|
52
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name = "nifra-isr"
|
|
2
|
+
main = "dist-server/worker.js"
|
|
3
|
+
compatibility_date = "2025-01-01"
|
|
4
|
+
compatibility_flags = ["nodejs_compat"]
|
|
5
|
+
|
|
6
|
+
# Workers Assets: the content-hashed client bundle (buildClient → ./public/assets) is served at
|
|
7
|
+
# /assets/*. Non-asset paths fall through to the worker (SSR + ISR).
|
|
8
|
+
assets = { directory = "./public" }
|
|
9
|
+
|
|
10
|
+
# The ISR page cache. `wrangler dev` provides a LOCAL (miniflare) KV automatically — no Cloudflare
|
|
11
|
+
# account needed. For `wrangler deploy`, create a namespace and paste its id:
|
|
12
|
+
# bunx wrangler kv namespace create ISR_CACHE
|
|
13
|
+
[[kv_namespaces]]
|
|
14
|
+
binding = "ISR_CACHE"
|
|
15
|
+
id = "REPLACE_WITH_YOUR_KV_NAMESPACE_ID"
|
|
16
|
+
|
|
17
|
+
# On-demand revalidation secret. DEV placeholder — set a real one for production with
|
|
18
|
+
# `bunx wrangler secret put REVALIDATE_SECRET`.
|
|
19
|
+
[vars]
|
|
20
|
+
REVALIDATE_SECRET = "dev-secret"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# nifra on Node, containerized. Build with Bun (nifra's build toolchain), then run the self-contained
|
|
2
|
+
# bundle on a slim Node image — the final image carries no build tools or node_modules.
|
|
3
|
+
# docker build -t my-nifra-app .
|
|
4
|
+
# docker run -p 3000:3000 my-nifra-app
|
|
5
|
+
FROM oven/bun:1 AS build
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
# Copy manifests first so the install layer caches across source-only changes.
|
|
8
|
+
COPY package.json bun.lock* ./
|
|
9
|
+
RUN bun install
|
|
10
|
+
COPY . .
|
|
11
|
+
RUN bun run build:node
|
|
12
|
+
|
|
13
|
+
FROM node:22-slim AS run
|
|
14
|
+
WORKDIR /app
|
|
15
|
+
ENV NODE_ENV=production
|
|
16
|
+
ENV PORT=3000
|
|
17
|
+
# build:node emits a self-contained server-bundle + the client assets it serves from disk.
|
|
18
|
+
COPY --from=build /app/dist-node ./dist-node
|
|
19
|
+
EXPOSE 3000
|
|
20
|
+
CMD ["node", "dist-node/server-node.js"]
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# nifra site
|
|
2
|
+
|
|
3
|
+
A nifra + React SSR site — file-based routes, typed loaders/actions, hydration — that deploys to
|
|
4
|
+
**Bun, Node (Docker), Deno Deploy, Cloudflare Pages, and Vercel Edge from one source**. `app.fetch`
|
|
5
|
+
is Web-standard, so only the server entry + build target differ per platform.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun install
|
|
9
|
+
bun run dev # nifra dev → true-HMR dev server (Vite + 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
|
+
```
|
|
12
|
+
|
|
13
|
+
Edit `routes/index.tsx` (the landing + a live loader/action counter), add files under `routes/`
|
|
14
|
+
(`about.tsx` → `/about`, `[id].tsx` → `:id`, nested `_layout.tsx` for sections), and put your data
|
|
15
|
+
behind `backend.ts`.
|
|
16
|
+
|
|
17
|
+
## Deploy — pick a target
|
|
18
|
+
|
|
19
|
+
| target | build | deploy |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| **Bun** (flagship) | `bun run build:bun` | `bun run start` (`bun dist-bun/server-bun.js`) — any host |
|
|
22
|
+
| **Node** | `bun run build:node` | `docker build -t app . && docker run -p 3000:3000 app` (or `bun run start:node`) |
|
|
23
|
+
| **Deno Deploy** | `bun run build:deno` | `deployctl deploy --prod --entrypoint=dist-deno/server-deno.js` (or `deno task start`) |
|
|
24
|
+
| **Cloudflare Pages** | `bun run build` | `bun run deploy:cf` (`wrangler pages deploy dist`) |
|
|
25
|
+
| **Vercel Edge** | `bun run build:vercel` | `bun run deploy:vercel` (`vercel deploy --prebuilt`) |
|
|
26
|
+
|
|
27
|
+
The routes, `backend.ts`, and the client bundle are shared; each `build*` script swaps
|
|
28
|
+
`buildServer`'s target/conditions and the server entry. nifra never enters your cloud credentials — the
|
|
29
|
+
configs (`Dockerfile`, `deno.json`, `wrangler.toml`, Vercel Build Output API) are scaffolded; you run
|
|
30
|
+
the vendor CLI.
|
|
31
|
+
|
|
32
|
+
## Structure
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
routes/ index.tsx (landing), _layout.tsx (chrome), _404.tsx
|
|
36
|
+
framework.ts the render adapter (imported by the server entries — kept edge-bundlable)
|
|
37
|
+
nifra.config.ts the nifra CLI's dev/build config (adapter + clientModule + Vite plugin) — read by `nifra dev`
|
|
38
|
+
backend.ts your contract (loaders/actions call it in-process)
|
|
39
|
+
server-bun.ts Bun entry (Bun.serve) build-bun.ts → dist-bun/
|
|
40
|
+
_worker.ts Cloudflare Pages entry build.ts → dist/
|
|
41
|
+
server-node.ts Node entry (@nifrajs/node) build-node.ts → dist-node/ (Dockerfile)
|
|
42
|
+
server-deno.ts Deno entry (@nifrajs/deno) build-deno.ts → dist-deno/ (deno.json)
|
|
43
|
+
server-vercel.ts Vercel Edge function build-vercel.ts → .vercel/output/
|
|
44
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { inProcessClient } from "@nifrajs/client"
|
|
2
|
+
import { toFetchHandler } from "@nifrajs/core"
|
|
3
|
+
import { createWebApp } from "@nifrajs/web"
|
|
4
|
+
import { backend } from "./backend"
|
|
5
|
+
import { adapter } from "./framework"
|
|
6
|
+
import { clientEntry, manifest } from "./server-manifest"
|
|
7
|
+
|
|
8
|
+
const app = createWebApp({
|
|
9
|
+
adapter,
|
|
10
|
+
manifest,
|
|
11
|
+
clientEntry,
|
|
12
|
+
api: inProcessClient(backend),
|
|
13
|
+
title: "nifra site",
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Cloudflare Pages: _routes.json serves /assets/* statically; everything else hits this (SSR).
|
|
17
|
+
export default toFetchHandler(app)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { server } from "@nifrajs/core"
|
|
2
|
+
|
|
3
|
+
// Your backend contract — page loaders/actions call it in-process during SSR (no network). Replace
|
|
4
|
+
// the demo counter with real data (KV/D1/Postgres/…); on the edge, reach bindings via `c.env`.
|
|
5
|
+
let count = 0
|
|
6
|
+
|
|
7
|
+
export const backend = server()
|
|
8
|
+
.get("/count", () => ({ count }))
|
|
9
|
+
.post("/count", () => {
|
|
10
|
+
count += 1
|
|
11
|
+
return { count }
|
|
12
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Build for Bun (nifra's flagship runtime) → dist-bun/ (server-bun.js + client assets).
|
|
2
|
+
// `bun run build:bun`. Run: `bun dist-bun/server-bun.js`.
|
|
3
|
+
import { cpSync, mkdirSync, rmSync } from "node:fs"
|
|
4
|
+
import { buildClient, buildServer } from "@nifrajs/web/build"
|
|
5
|
+
|
|
6
|
+
const dir = import.meta.dir
|
|
7
|
+
const dist = `${dir}/dist-bun`
|
|
8
|
+
rmSync(dist, { recursive: true, force: true })
|
|
9
|
+
mkdirSync(`${dist}/assets`, { recursive: true })
|
|
10
|
+
|
|
11
|
+
const client = await buildClient({
|
|
12
|
+
routesDir: `${dir}/routes`,
|
|
13
|
+
outDir: `${dist}/assets`,
|
|
14
|
+
clientModule: "@nifrajs/web-react/client",
|
|
15
|
+
conditions: ["bun", "browser"],
|
|
16
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
17
|
+
})
|
|
18
|
+
const { worker } = await buildServer({
|
|
19
|
+
routesDir: `${dir}/routes`,
|
|
20
|
+
serverEntry: `${dir}/server-bun.ts`,
|
|
21
|
+
outDir: `${dir}/.build-bun`,
|
|
22
|
+
target: "bun",
|
|
23
|
+
clientEntry: client.entry,
|
|
24
|
+
})
|
|
25
|
+
cpSync(worker, `${dist}/server-bun.js`)
|
|
26
|
+
rmSync(`${dir}/.build-bun`, { recursive: true, force: true })
|
|
27
|
+
console.log("Bun output → dist-bun (run: bun dist-bun/server-bun.js)")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Build for Deno → dist-deno/ (server-deno.js + client assets). `bun run build:deno`.
|
|
2
|
+
import { cpSync, mkdirSync, rmSync } from "node:fs"
|
|
3
|
+
import { buildClient, buildServer } from "@nifrajs/web/build"
|
|
4
|
+
|
|
5
|
+
const dir = import.meta.dir
|
|
6
|
+
const dist = `${dir}/dist-deno`
|
|
7
|
+
rmSync(dist, { recursive: true, force: true })
|
|
8
|
+
mkdirSync(`${dist}/assets`, { recursive: true })
|
|
9
|
+
|
|
10
|
+
const client = await buildClient({
|
|
11
|
+
routesDir: `${dir}/routes`,
|
|
12
|
+
outDir: `${dist}/assets`,
|
|
13
|
+
clientModule: "@nifrajs/web-react/client",
|
|
14
|
+
conditions: ["bun", "browser"],
|
|
15
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
16
|
+
})
|
|
17
|
+
const { worker } = await buildServer({
|
|
18
|
+
routesDir: `${dir}/routes`,
|
|
19
|
+
serverEntry: `${dir}/server-deno.ts`,
|
|
20
|
+
outDir: `${dir}/.build-deno`,
|
|
21
|
+
clientEntry: client.entry,
|
|
22
|
+
})
|
|
23
|
+
cpSync(worker, `${dist}/server-deno.js`)
|
|
24
|
+
rmSync(`${dir}/.build-deno`, { recursive: true, force: true })
|
|
25
|
+
console.log("Deno output → dist-deno (run: deno run -A dist-deno/server-deno.js)")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Build for Node → dist-node/ (server-node.js + client assets). `bun run build:node`.
|
|
2
|
+
import { cpSync, mkdirSync, rmSync } from "node:fs"
|
|
3
|
+
import { buildClient, buildServer } from "@nifrajs/web/build"
|
|
4
|
+
|
|
5
|
+
const dir = import.meta.dir
|
|
6
|
+
const dist = `${dir}/dist-node`
|
|
7
|
+
rmSync(dist, { recursive: true, force: true })
|
|
8
|
+
mkdirSync(`${dist}/assets`, { recursive: true })
|
|
9
|
+
|
|
10
|
+
const client = await buildClient({
|
|
11
|
+
routesDir: `${dir}/routes`,
|
|
12
|
+
outDir: `${dist}/assets`,
|
|
13
|
+
clientModule: "@nifrajs/web-react/client",
|
|
14
|
+
conditions: ["bun", "browser"],
|
|
15
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
16
|
+
})
|
|
17
|
+
const { worker } = await buildServer({
|
|
18
|
+
routesDir: `${dir}/routes`,
|
|
19
|
+
serverEntry: `${dir}/server-node.ts`,
|
|
20
|
+
outDir: `${dir}/.build-node`,
|
|
21
|
+
target: "node",
|
|
22
|
+
clientEntry: client.entry,
|
|
23
|
+
})
|
|
24
|
+
cpSync(worker, `${dist}/server-node.js`)
|
|
25
|
+
rmSync(`${dir}/.build-node`, { recursive: true, force: true })
|
|
26
|
+
console.log("Node output → dist-node (run: node dist-node/server-node.js)")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Build for Vercel, emitting Vercel's Build Output API v3 at .vercel/output/ so it deploys with no
|
|
2
|
+
// framework preset: `vercel deploy --prebuilt`. Layout:
|
|
3
|
+
// .vercel/output/config.json — serve static files, else SSR via the function
|
|
4
|
+
// .vercel/output/static/assets/<client bundle> — Vercel's CDN serves these directly
|
|
5
|
+
// .vercel/output/functions/index.func/index.js — the Edge SSR function (+ .vc-config.json)
|
|
6
|
+
import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
7
|
+
import { buildClient, buildServer } from "@nifrajs/web/build"
|
|
8
|
+
|
|
9
|
+
const dir = import.meta.dir
|
|
10
|
+
const out = `${dir}/.vercel/output`
|
|
11
|
+
const fn = `${out}/functions/index.func`
|
|
12
|
+
rmSync(`${dir}/.vercel`, { recursive: true, force: true })
|
|
13
|
+
mkdirSync(`${out}/static/assets`, { recursive: true })
|
|
14
|
+
mkdirSync(fn, { recursive: true })
|
|
15
|
+
|
|
16
|
+
const client = await buildClient({
|
|
17
|
+
routesDir: `${dir}/routes`,
|
|
18
|
+
outDir: `${out}/static/assets`,
|
|
19
|
+
clientModule: "@nifrajs/web-react/client",
|
|
20
|
+
conditions: ["bun", "browser"],
|
|
21
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
22
|
+
})
|
|
23
|
+
const { worker } = await buildServer({
|
|
24
|
+
routesDir: `${dir}/routes`,
|
|
25
|
+
serverEntry: `${dir}/server-vercel.ts`,
|
|
26
|
+
outDir: `${dir}/.build-vercel`,
|
|
27
|
+
clientEntry: client.entry,
|
|
28
|
+
})
|
|
29
|
+
cpSync(worker, `${fn}/index.js`)
|
|
30
|
+
rmSync(`${dir}/.build-vercel`, { recursive: true, force: true })
|
|
31
|
+
|
|
32
|
+
// Build Output API v3: serve real files first (`handle: filesystem` → /assets/*), then SSR the rest.
|
|
33
|
+
writeFileSync(
|
|
34
|
+
`${fn}/.vc-config.json`,
|
|
35
|
+
`${JSON.stringify({ runtime: "edge", entrypoint: "index.js" }, null, 2)}\n`,
|
|
36
|
+
)
|
|
37
|
+
writeFileSync(
|
|
38
|
+
`${out}/config.json`,
|
|
39
|
+
`${JSON.stringify(
|
|
40
|
+
{ version: 3, routes: [{ handle: "filesystem" }, { src: "/(.*)", dest: "/index" }] },
|
|
41
|
+
null,
|
|
42
|
+
2,
|
|
43
|
+
)}\n`,
|
|
44
|
+
)
|
|
45
|
+
console.log("Vercel output → .vercel/output (deploy: vercel deploy --prebuilt)")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Build for Cloudflare Pages → dist/ (_worker.js + _routes.json + client assets). `bun run build`.
|
|
2
|
+
import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { buildClient, buildServer } from "@nifrajs/web/build"
|
|
4
|
+
|
|
5
|
+
const dir = import.meta.dir
|
|
6
|
+
const dist = `${dir}/dist`
|
|
7
|
+
rmSync(dist, { recursive: true, force: true })
|
|
8
|
+
mkdirSync(`${dist}/assets`, { recursive: true })
|
|
9
|
+
|
|
10
|
+
const client = await buildClient({
|
|
11
|
+
routesDir: `${dir}/routes`,
|
|
12
|
+
outDir: `${dist}/assets`,
|
|
13
|
+
clientModule: "@nifrajs/web-react/client",
|
|
14
|
+
conditions: ["bun", "browser"],
|
|
15
|
+
define: { "process.env.NODE_ENV": '"production"' },
|
|
16
|
+
})
|
|
17
|
+
const { worker } = await buildServer({
|
|
18
|
+
routesDir: `${dir}/routes`,
|
|
19
|
+
serverEntry: `${dir}/_worker.ts`,
|
|
20
|
+
outDir: `${dir}/.build`,
|
|
21
|
+
clientEntry: client.entry,
|
|
22
|
+
})
|
|
23
|
+
cpSync(worker, `${dist}/_worker.js`)
|
|
24
|
+
rmSync(`${dir}/.build`, { recursive: true, force: true })
|
|
25
|
+
writeFileSync(
|
|
26
|
+
`${dist}/_routes.json`,
|
|
27
|
+
`${JSON.stringify({ version: 1, include: ["/*"], exclude: ["/assets/*"] }, null, 2)}\n`,
|
|
28
|
+
)
|
|
29
|
+
console.log("Cloudflare Pages output → dist (deploy: wrangler pages deploy dist)")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"tasks": {
|
|
3
|
+
"build": "bun run build-deno.ts",
|
|
4
|
+
"start": "deno run --allow-net --allow-read --allow-env dist-deno/server-deno.js"
|
|
5
|
+
},
|
|
6
|
+
"//": "Build with Bun (nifra's toolchain), run/deploy the self-contained bundle on Deno. Deploy to Deno Deploy: deployctl deploy --prod --entrypoint=dist-deno/server-deno.js"
|
|
7
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// The frontend adapter for this app. `create-nifra --framework <react|preact|vue|solid|svelte>` swaps
|
|
2
|
+
// this one line (and the routes + build config); every server entry imports the adapter from here, so
|
|
3
|
+
// they stay framework-agnostic.
|
|
4
|
+
import { reactAdapter } from "@nifrajs/web-react"
|
|
5
|
+
|
|
6
|
+
export const adapter = reactAdapter
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
dist
|
|
3
|
+
dist-bun
|
|
4
|
+
dist-node
|
|
5
|
+
dist-deno
|
|
6
|
+
.vercel
|
|
7
|
+
.build
|
|
8
|
+
.build-bun
|
|
9
|
+
.build-node
|
|
10
|
+
.build-deno
|
|
11
|
+
.build-vercel
|
|
12
|
+
.wrangler
|
|
13
|
+
# generated by buildServer next to the entry
|
|
14
|
+
server-manifest.ts
|
|
15
|
+
# generated by `nifra dev` (the Vite dev client entry) at the project root
|
|
16
|
+
.nifra-vite-entry.tsx
|
|
@@ -0,0 +1,9 @@
|
|
|
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 dev/build tooling like the Vite plugin. React JSX is
|
|
4
|
+
// Bun-native, so the only extra is the Vite plugin for dev HMR (Fast Refresh).
|
|
5
|
+
import react from "@vitejs/plugin-react"
|
|
6
|
+
|
|
7
|
+
export { adapter } from "./framework"
|
|
8
|
+
export const clientModule = "@nifrajs/web-react/client"
|
|
9
|
+
export const vitePlugins = [react()]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nifra-site",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"private": true,
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "nifra dev",
|
|
8
|
+
"preview": "bunx wrangler pages dev dist",
|
|
9
|
+
"build": "bun run build.ts",
|
|
10
|
+
"build:bun": "bun run build-bun.ts",
|
|
11
|
+
"build:node": "bun run build-node.ts",
|
|
12
|
+
"build:deno": "bun run build-deno.ts",
|
|
13
|
+
"build:vercel": "bun run build-vercel.ts",
|
|
14
|
+
"start": "bun dist-bun/server-bun.js",
|
|
15
|
+
"start:node": "node dist-node/server-node.js",
|
|
16
|
+
"deploy:cf": "wrangler pages deploy dist",
|
|
17
|
+
"deploy:vercel": "vercel deploy --prebuilt"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@nifrajs/client": "^0.1.0",
|
|
21
|
+
"@nifrajs/core": "^0.1.0",
|
|
22
|
+
"@nifrajs/deno": "^0.1.0",
|
|
23
|
+
"@nifrajs/node": "^0.1.0",
|
|
24
|
+
"@nifrajs/web": "^0.1.0",
|
|
25
|
+
"@nifrajs/web-react": "^0.1.0",
|
|
26
|
+
"react": "^19.0.0",
|
|
27
|
+
"react-dom": "^19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"@types/react-dom": "^19.0.0",
|
|
32
|
+
"@nifrajs/cli": "^0.1.0",
|
|
33
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
34
|
+
"typescript": "^6.0.3",
|
|
35
|
+
"vite": "^8.0.0"
|
|
36
|
+
}
|
|
37
|
+
}
|