create-nifra 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/agent-files.d.ts +14 -14
- package/dist/agent-files.js +29 -29
- package/dist/agents.d.ts +4 -4
- package/dist/agents.js +63 -63
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +8 -8
- package/dist/cli.d.ts +6 -6
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +56 -35
- package/dist/cli.js.map +1 -1
- package/dist/db.d.ts +6 -6
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +138 -41
- package/dist/db.js.map +1 -1
- package/dist/scaffold/features.d.ts +41 -0
- package/dist/scaffold/features.d.ts.map +1 -0
- package/dist/scaffold/features.js +48 -0
- package/dist/scaffold/features.js.map +1 -0
- package/dist/scaffold/frameworks.d.ts +73 -0
- package/dist/scaffold/frameworks.d.ts.map +1 -0
- package/dist/scaffold/frameworks.js +100 -0
- package/dist/scaffold/frameworks.js.map +1 -0
- package/dist/scaffold/site-build.d.ts +60 -0
- package/dist/scaffold/site-build.d.ts.map +1 -0
- package/dist/scaffold/site-build.js +198 -0
- package/dist/scaffold/site-build.js.map +1 -0
- package/dist/scaffold/site-files.d.ts +16 -0
- package/dist/scaffold/site-files.d.ts.map +1 -0
- package/dist/scaffold/site-files.js +112 -0
- package/dist/scaffold/site-files.js.map +1 -0
- package/dist/scaffold/site.d.ts +33 -0
- package/dist/scaffold/site.d.ts.map +1 -0
- package/dist/scaffold/site.js +99 -0
- package/dist/scaffold/site.js.map +1 -0
- package/package.json +2 -2
- package/src/agent-files.ts +29 -29
- package/template/README.md +3 -3
- package/template/nifra.assurance.ts +80 -0
- package/template/package.json +5 -3
- package/template/src/app.ts +10 -11
- package/template/src/routes.ts +13 -0
- package/template-isr/README.md +1 -1
- package/template-isr/backend.ts +11 -15
- package/template-isr/nifra.assurance.ts +81 -0
- package/template-isr/package.json +8 -7
- package/template-isr/page.ts +12 -0
- package/template-isr/routes/_404.tsx +1 -1
- package/template-isr/routes/_layout.tsx +1 -1
- package/template-isr/routes/index.tsx +2 -2
- package/template-isr/server.ts +1 -1
- package/template-isr/worker.ts +2 -2
- package/template-isr/wrangler.toml +2 -2
- package/template-site/Dockerfile +1 -1
- package/template-site/README.md +6 -6
- package/template-site/backend.ts +11 -13
- package/template-site/counter.ts +16 -0
- package/template-site/nifra.assurance.ts +83 -0
- package/template-site/nifra.config.ts +1 -1
- package/template-site/routes/index.tsx +1 -1
- package/template-site/server-node.ts +1 -1
- package/template-site/server-vercel.ts +1 -1
- package/template-site-preact/README.md +7 -7
- package/template-site-preact/nifra.config.ts +1 -1
- package/template-site-preact/routes/index.tsx +1 -1
- package/template-site-solid/README.md +7 -7
- package/template-site-solid/nifra.config.ts +2 -2
- package/template-site-solid/routes/index.tsx +1 -1
- package/template-site-svelte/README.md +7 -7
- package/template-site-svelte/nifra.config.ts +2 -2
- package/template-site-svelte/routes/_layout.svelte +1 -1
- package/template-site-svelte/routes/index.svelte +1 -1
- package/template-site-vue/README.md +7 -7
- package/template-site-vue/nifra.config.ts +2 -2
- package/template-site-vue/routes/_layout.vue +2 -2
- package/template-site-vue/routes/index.vue +3 -3
- package/template-site/build-bun.ts +0 -27
- package/template-site/build-deno.ts +0 -25
- package/template-site/build-node.ts +0 -26
- package/template-site/build-vercel.ts +0 -45
- package/template-site/build.ts +0 -29
- package/template-site/framework.ts +0 -6
- package/template-site/package.json +0 -38
- package/template-site/tsconfig.json +0 -37
- package/template-site-preact/.dockerignore +0 -14
- package/template-site-preact/Dockerfile +0 -20
- package/template-site-preact/_worker.ts +0 -17
- package/template-site-preact/backend.ts +0 -15
- package/template-site-preact/build-bun.ts +0 -27
- package/template-site-preact/build-deno.ts +0 -25
- package/template-site-preact/build-node.ts +0 -26
- package/template-site-preact/build-vercel.ts +0 -45
- package/template-site-preact/build.ts +0 -29
- package/template-site-preact/deno.json +0 -7
- package/template-site-preact/framework.ts +0 -6
- package/template-site-preact/gitignore +0 -16
- package/template-site-preact/package.json +0 -35
- package/template-site-preact/server-bun.ts +0 -35
- package/template-site-preact/server-deno.ts +0 -38
- package/template-site-preact/server-node.ts +0 -40
- package/template-site-preact/server-vercel.ts +0 -17
- package/template-site-preact/tsconfig.json +0 -38
- package/template-site-preact/wrangler.toml +0 -4
- package/template-site-solid/.dockerignore +0 -14
- package/template-site-solid/Dockerfile +0 -20
- package/template-site-solid/_worker.ts +0 -17
- package/template-site-solid/backend.ts +0 -15
- package/template-site-solid/build-bun.ts +0 -33
- package/template-site-solid/build-deno.ts +0 -32
- package/template-site-solid/build-node.ts +0 -33
- package/template-site-solid/build-vercel.ts +0 -47
- package/template-site-solid/build.ts +0 -36
- package/template-site-solid/deno.json +0 -7
- package/template-site-solid/framework.ts +0 -6
- package/template-site-solid/gitignore +0 -16
- package/template-site-solid/package.json +0 -35
- package/template-site-solid/server-bun.ts +0 -35
- package/template-site-solid/server-deno.ts +0 -38
- package/template-site-solid/server-node.ts +0 -40
- package/template-site-solid/server-vercel.ts +0 -17
- package/template-site-solid/tsconfig.json +0 -38
- package/template-site-solid/wrangler.toml +0 -4
- package/template-site-svelte/.dockerignore +0 -14
- package/template-site-svelte/Dockerfile +0 -20
- package/template-site-svelte/_worker.ts +0 -17
- package/template-site-svelte/backend.ts +0 -15
- package/template-site-svelte/build-bun.ts +0 -32
- package/template-site-svelte/build-deno.ts +0 -31
- package/template-site-svelte/build-node.ts +0 -32
- package/template-site-svelte/build-vercel.ts +0 -46
- package/template-site-svelte/build.ts +0 -35
- package/template-site-svelte/deno.json +0 -7
- package/template-site-svelte/framework.ts +0 -6
- package/template-site-svelte/gitignore +0 -16
- package/template-site-svelte/package.json +0 -35
- package/template-site-svelte/server-bun.ts +0 -35
- package/template-site-svelte/server-deno.ts +0 -38
- package/template-site-svelte/server-node.ts +0 -40
- package/template-site-svelte/server-vercel.ts +0 -17
- package/template-site-svelte/tsconfig.json +0 -37
- package/template-site-svelte/wrangler.toml +0 -4
- package/template-site-vue/.dockerignore +0 -14
- package/template-site-vue/Dockerfile +0 -20
- package/template-site-vue/_worker.ts +0 -17
- package/template-site-vue/backend.ts +0 -15
- package/template-site-vue/build-bun.ts +0 -37
- package/template-site-vue/build-deno.ts +0 -36
- package/template-site-vue/build-node.ts +0 -37
- package/template-site-vue/build-vercel.ts +0 -54
- package/template-site-vue/build.ts +0 -40
- package/template-site-vue/deno.json +0 -7
- package/template-site-vue/framework.ts +0 -6
- package/template-site-vue/gitignore +0 -16
- package/template-site-vue/package.json +0 -36
- package/template-site-vue/server-bun.ts +0 -35
- package/template-site-vue/server-deno.ts +0 -38
- package/template-site-vue/server-node.ts +0 -40
- package/template-site-vue/server-vercel.ts +0 -17
- package/template-site-vue/tsconfig.json +0 -37
- package/template-site-vue/wrangler.toml +0 -4
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose a site scaffold: shared base, generated files, framework overlay.
|
|
3
|
+
*
|
|
4
|
+
* A site is 26 files. Thirteen are the same whatever you render with - the Dockerfile, every server
|
|
5
|
+
* entry, the worker, the assurance config - eight are mechanical enough to emit from a model, and five
|
|
6
|
+
* are genuinely the framework's own. Keeping five copies of all 26 is what let `.vercel` reach four
|
|
7
|
+
* tsconfigs and not the fifth, and a Vercel comment reach one build entry and not the other four.
|
|
8
|
+
*
|
|
9
|
+
* The five that stay literal stay literal on purpose. `nifra.config.ts` explains why Solid wants a
|
|
10
|
+
* `solid` resolve condition and what `@preact/preset-vite` is; the routes are the app a user reads
|
|
11
|
+
* first. That prose belongs in a file you can open, not in a TypeScript string.
|
|
12
|
+
*/
|
|
13
|
+
import { cp, mkdir, writeFile } from "node:fs/promises";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
import { frameworkSpec } from "./frameworks.js";
|
|
17
|
+
import { BUILD_TARGETS, renderBuildFile } from "./site-build.js";
|
|
18
|
+
import { renderFrameworkModule, renderPackageJson, renderTsconfig } from "./site-files.js";
|
|
19
|
+
/**
|
|
20
|
+
* Files identical in every site scaffold, taken from the base directory.
|
|
21
|
+
*
|
|
22
|
+
* Listed rather than globbed: a glob would silently absorb a file someone adds to the base for one
|
|
23
|
+
* framework's benefit, which is the failure this is meant to end. Adding a shared file means saying so
|
|
24
|
+
* here, and `site-composition.test.ts` fails if this list and the directory disagree.
|
|
25
|
+
*/
|
|
26
|
+
export const SHARED_SITE_FILES = [
|
|
27
|
+
".dockerignore",
|
|
28
|
+
"Dockerfile",
|
|
29
|
+
"_worker.ts",
|
|
30
|
+
"backend.ts",
|
|
31
|
+
"counter.ts",
|
|
32
|
+
"deno.json",
|
|
33
|
+
"gitignore",
|
|
34
|
+
"nifra.assurance.ts",
|
|
35
|
+
"server-bun.ts",
|
|
36
|
+
"server-deno.ts",
|
|
37
|
+
"server-node.ts",
|
|
38
|
+
"server-vercel.ts",
|
|
39
|
+
"wrangler.toml",
|
|
40
|
+
];
|
|
41
|
+
/** Files each framework supplies itself, relative to its overlay directory. */
|
|
42
|
+
export const FRAMEWORK_SITE_FILES = ["README.md", "nifra.config.ts"];
|
|
43
|
+
/** Route files, whose extension is the framework's own. */
|
|
44
|
+
export const ROUTE_BASENAMES = ["_404", "_layout", "index"];
|
|
45
|
+
/** Extension the framework's route files carry. */
|
|
46
|
+
export function routeExtension(framework) {
|
|
47
|
+
if (framework.id === "svelte")
|
|
48
|
+
return "svelte";
|
|
49
|
+
if (framework.id === "vue")
|
|
50
|
+
return "vue";
|
|
51
|
+
return "tsx";
|
|
52
|
+
}
|
|
53
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
/** The base directory: the shared files plus React's own, which is also the React overlay. */
|
|
55
|
+
export const SITE_BASE_DIR = join(here, "..", "..", "template-site");
|
|
56
|
+
export function overlayDir(framework) {
|
|
57
|
+
return framework.id === "react"
|
|
58
|
+
? SITE_BASE_DIR
|
|
59
|
+
: join(here, "..", "..", `template-site-${framework.id}`);
|
|
60
|
+
}
|
|
61
|
+
/** Every file a site scaffold emits, and the text it holds. Generated files only. */
|
|
62
|
+
export function generatedSiteFiles(framework) {
|
|
63
|
+
const files = new Map();
|
|
64
|
+
for (const target of BUILD_TARGETS)
|
|
65
|
+
files.set(target.file, renderBuildFile(target, framework));
|
|
66
|
+
files.set("framework.ts", renderFrameworkModule(framework));
|
|
67
|
+
files.set("package.json", renderPackageJson(framework));
|
|
68
|
+
files.set("tsconfig.json", renderTsconfig(framework));
|
|
69
|
+
return files;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Write a complete site scaffold into `target`.
|
|
73
|
+
*
|
|
74
|
+
* Refusing an occupied destination is the default and is preserved deliberately: `cp`'s
|
|
75
|
+
* `errorOnExist` did that before this was generated, and losing it would turn a mistyped path into
|
|
76
|
+
* silent data loss in someone's working directory. `--force` is what `bun create nifra .` needs.
|
|
77
|
+
*/
|
|
78
|
+
export async function materializeSite(target, id, options = {}) {
|
|
79
|
+
const framework = frameworkSpec(id);
|
|
80
|
+
const overlay = overlayDir(framework);
|
|
81
|
+
const force = options.force === true;
|
|
82
|
+
const copy = (from, to) => cp(from, to, force ? { force: true } : { errorOnExist: true, force: false });
|
|
83
|
+
// `wx` fails when the file exists, which is the write-side equivalent of `errorOnExist`.
|
|
84
|
+
const emit = (to, contents) => writeFile(to, contents, force ? {} : { flag: "wx" });
|
|
85
|
+
await mkdir(join(target, "routes"), { recursive: true });
|
|
86
|
+
for (const file of SHARED_SITE_FILES)
|
|
87
|
+
await copy(join(SITE_BASE_DIR, file), join(target, file));
|
|
88
|
+
for (const [file, contents] of generatedSiteFiles(framework)) {
|
|
89
|
+
await emit(join(target, file), contents);
|
|
90
|
+
}
|
|
91
|
+
for (const file of FRAMEWORK_SITE_FILES)
|
|
92
|
+
await copy(join(overlay, file), join(target, file));
|
|
93
|
+
const extension = routeExtension(framework);
|
|
94
|
+
for (const base of ROUTE_BASENAMES) {
|
|
95
|
+
const name = join("routes", `${base}.${extension}`);
|
|
96
|
+
await copy(join(overlay, name), join(target, name));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=site.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site.js","sourceRoot":"","sources":["../../src/scaffold/site.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAA;AACxC,OAAO,EAAsB,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACnE,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AAChE,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAE1F;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAsB;IAClD,eAAe;IACf,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,YAAY;IACZ,WAAW;IACX,WAAW;IACX,oBAAoB;IACpB,eAAe;IACf,gBAAgB;IAChB,gBAAgB;IAChB,kBAAkB;IAClB,eAAe;CAChB,CAAA;AAED,+EAA+E;AAC/E,MAAM,CAAC,MAAM,oBAAoB,GAAsB,CAAC,WAAW,EAAE,iBAAiB,CAAC,CAAA;AAEvF,2DAA2D;AAC3D,MAAM,CAAC,MAAM,eAAe,GAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,CAAA;AAE9E,mDAAmD;AACnD,MAAM,UAAU,cAAc,CAAC,SAAwB;IACrD,IAAI,SAAS,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,QAAQ,CAAA;IAC9C,IAAI,SAAS,CAAC,EAAE,KAAK,KAAK;QAAE,OAAO,KAAK,CAAA;IACxC,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;AACpD,8FAA8F;AAC9F,MAAM,CAAC,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,eAAe,CAAC,CAAA;AACpE,MAAM,UAAU,UAAU,CAAC,SAAwB;IACjD,OAAO,SAAS,CAAC,EAAE,KAAK,OAAO;QAC7B,CAAC,CAAC,aAAa;QACf,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,iBAAiB,SAAS,CAAC,EAAE,EAAE,CAAC,CAAA;AAC7D,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,kBAAkB,CAAC,SAAwB;IACzD,MAAM,KAAK,GAAG,IAAI,GAAG,EAAkB,CAAA;IACvC,KAAK,MAAM,MAAM,IAAI,aAAa;QAAE,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,eAAe,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAA;IAC9F,KAAK,CAAC,GAAG,CAAC,cAAc,EAAE,qBAAqB,CAAC,SAAS,CAAC,CAAC,CAAA;IAC3D,KAAK,CAAC,GAAG,CAAC,cAAc,EAAE,iBAAiB,CAAC,SAAS,CAAC,CAAC,CAAA;IACvD,KAAK,CAAC,GAAG,CAAC,eAAe,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC,CAAA;IACrD,OAAO,KAAK,CAAA;AACd,CAAC;AAOD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,EAAU,EACV,UAA8B,EAAE;IAEhC,MAAM,SAAS,GAAG,aAAa,CAAC,EAAE,CAAC,CAAA;IACnC,MAAM,OAAO,GAAG,UAAU,CAAC,SAAS,CAAC,CAAA;IACrC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,KAAK,IAAI,CAAA;IACpC,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAU,EAAiB,EAAE,CACvD,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAA;IAC9E,yFAAyF;IACzF,MAAM,IAAI,GAAG,CAAC,EAAU,EAAE,QAAgB,EAAiB,EAAE,CAC3D,SAAS,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAA;IAEtD,MAAM,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IACxD,KAAK,MAAM,IAAI,IAAI,iBAAiB;QAAE,MAAM,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IAC/F,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,kBAAkB,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,EAAE,QAAQ,CAAC,CAAA;IAC1C,CAAC;IACD,KAAK,MAAM,IAAI,IAAI,oBAAoB;QAAE,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IAC5F,MAAM,SAAS,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;IAC3C,KAAK,MAAM,IAAI,IAAI,eAAe,EAAE,CAAC;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,GAAG,IAAI,IAAI,SAAS,EAAE,CAAC,CAAA;QACnD,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAA;IACrD,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
package/src/agent-files.ts
CHANGED
|
@@ -2,23 +2,23 @@
|
|
|
2
2
|
* The canonical generators for the agent-discovery files an app ships so a coding agent auto-discovers
|
|
3
3
|
* the project's nifra MCP server and prefers it over writing nifra from memory:
|
|
4
4
|
*
|
|
5
|
-
* .mcp.json
|
|
6
|
-
* .cursor/mcp.json
|
|
7
|
-
* CLAUDE.md
|
|
8
|
-
* AGENTS.md "MCP"
|
|
5
|
+
* .mcp.json - Claude Code's project MCP registry ({ mcpServers: { nifra: { command, args } } })
|
|
6
|
+
* .cursor/mcp.json - Cursor's MCP registry (same server config)
|
|
7
|
+
* CLAUDE.md - Claude's preamble (use the MCP first; gate on `nifra check`) + `@AGENTS.md` import
|
|
8
|
+
* AGENTS.md "MCP" - a section for non-Claude agents pointing at the same server
|
|
9
9
|
*
|
|
10
10
|
* Single source of truth: both `create-nifra` (scaffold time) and `@nifrajs/cli`'s `nifra init-agents`
|
|
11
11
|
* (retrofit an existing app) import these, so the four files can never drift apart. This module is
|
|
12
|
-
* dependency-free (pure string + object generation) on purpose
|
|
12
|
+
* dependency-free (pure string + object generation) on purpose - `create-nifra` ships with no runtime
|
|
13
13
|
* deps, and `@nifrajs/cli` imports it as a workspace dependency.
|
|
14
14
|
*
|
|
15
15
|
* Why `bunx @nifrajs/cli mcp` (not `bunx nifra mcp`): the `nifra` binary is provided by the
|
|
16
16
|
* `@nifrajs/cli` package. A scaffolded `site` app carries `@nifrajs/cli` as a devDependency, so either
|
|
17
|
-
* spelling would resolve `node_modules/.bin/nifra` locally
|
|
17
|
+
* spelling would resolve `node_modules/.bin/nifra` locally - BUT the `api`/`isr` templates do NOT carry
|
|
18
18
|
* `@nifrajs/cli`, and the bare npm package literally named `nifra` (this monorepo's `@nifrajs/web` shim)
|
|
19
19
|
* exposes NO `nifra` bin. So `bunx nifra mcp` would fetch the wrong package and fail there. Naming the
|
|
20
20
|
* bin-owning package, `bunx @nifrajs/cli mcp`, resolves the locally-installed bin when present and
|
|
21
|
-
* otherwise fetches the package that actually provides the `nifra` command
|
|
21
|
+
* otherwise fetches the package that actually provides the `nifra` command - robust across every
|
|
22
22
|
* template and for any existing app.
|
|
23
23
|
*/
|
|
24
24
|
|
|
@@ -29,10 +29,10 @@ import { readFileSync } from "node:fs"
|
|
|
29
29
|
export const MCP_SERVER_COMMAND = "bunx" as const
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* The `@nifrajs/cli` version the launch command pins to
|
|
32
|
+
* The `@nifrajs/cli` version the launch command pins to - DERIVED at load time from this package's own
|
|
33
33
|
* `version`, never hardcoded. `fixed` changeset versioning ([["@nifrajs/*", "create-nifra", "nifra"]] in
|
|
34
34
|
* `.changeset/config.json`) bumps `create-nifra` and `@nifrajs/cli` in lockstep, so `create-nifra@x`
|
|
35
|
-
* always means `@nifrajs/cli@x`. Deriving it here means a release bump has nothing to forget to update
|
|
35
|
+
* always means `@nifrajs/cli@x`. Deriving it here means a release bump has nothing to forget to update -
|
|
36
36
|
* there is no stale literal to drift (the 1.0.0 cut shipped a beta pin exactly this way). `check:publish`
|
|
37
37
|
* still asserts it equals `@nifrajs/cli`'s version as a belt-and-suspenders guard on the `fixed` link.
|
|
38
38
|
*
|
|
@@ -43,11 +43,11 @@ export const MCP_SERVER_COMMAND = "bunx" as const
|
|
|
43
43
|
*
|
|
44
44
|
* Why pin at all instead of `bunx @nifrajs/cli mcp`: `bunx` keys its cache on the exact version spec.
|
|
45
45
|
* An UNPINNED spec resolves to the `latest` tag once, then `bunx` reuses that cached copy on every
|
|
46
|
-
* later spawn WITHOUT re-checking the registry
|
|
46
|
+
* later spawn WITHOUT re-checking the registry - so an editor that once launched an older `@nifrajs/cli`
|
|
47
47
|
* keeps respawning the stale binary even after a newer one is published (the MCP server silently runs
|
|
48
48
|
* old code). Pinning the exact version makes the version part of the cache key, so each release fetches
|
|
49
49
|
* fresh and a stale cache can never shadow it. The cost: an already-scaffolded app's `.mcp.json` freezes
|
|
50
|
-
* at its scaffold-time version until `nifra init-agents` is re-run
|
|
50
|
+
* at its scaffold-time version until `nifra init-agents` is re-run - an acceptable, deterministic trade.
|
|
51
51
|
*/
|
|
52
52
|
export const MCP_CLI_VERSION: string = (
|
|
53
53
|
JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as {
|
|
@@ -67,7 +67,7 @@ export interface McpConfig {
|
|
|
67
67
|
readonly mcpServers: Readonly<Record<string, McpServerConfig>>
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/** The one canonical MCP config object both registries serialize
|
|
70
|
+
/** The one canonical MCP config object both registries serialize - the anti-drift seam. */
|
|
71
71
|
export const MCP_CONFIG: McpConfig = {
|
|
72
72
|
mcpServers: {
|
|
73
73
|
nifra: { command: MCP_SERVER_COMMAND, args: [...MCP_SERVER_ARGS] },
|
|
@@ -81,31 +81,31 @@ export function mcpJson(): string {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
84
|
-
* `CLAUDE.md`
|
|
84
|
+
* `CLAUDE.md` - Claude Code reads this automatically. It is deliberately NOT a copy of `AGENTS.md`:
|
|
85
85
|
* a short preamble that (1) tells Claude this project ships a nifra MCP, registered in `.mcp.json`, and
|
|
86
86
|
* to PREFER it, and (2) pulls in the full cookbook with Claude Code's `@file` import directive on its
|
|
87
|
-
* own line
|
|
87
|
+
* own line - `@AGENTS.md` - so the conventions live in exactly one place and can't drift between the
|
|
88
88
|
* two files. Keep this short; the depth is in `AGENTS.md`.
|
|
89
89
|
*/
|
|
90
90
|
export function claudeMd(): string {
|
|
91
91
|
return `# CLAUDE.md
|
|
92
92
|
|
|
93
93
|
This project ships a **nifra MCP server**, registered for Claude Code in \`.mcp.json\` (it launches with
|
|
94
|
-
\`${MCP_SERVER_COMMAND} ${MCP_SERVER_ARGS.join(" ")}\`). **Prefer the MCP tools** over writing nifra from memory
|
|
94
|
+
\`${MCP_SERVER_COMMAND} ${MCP_SERVER_ARGS.join(" ")}\`). **Prefer the MCP tools** over writing nifra from memory - they are
|
|
95
95
|
typechecked against *this* project and this installed version, so they beat training-data recall:
|
|
96
96
|
|
|
97
|
-
- \`nifra_docs\` / \`nifra_example\`
|
|
97
|
+
- \`nifra_docs\` / \`nifra_example\` - exact signatures + verified, compiling snippets. Reach for these
|
|
98
98
|
before hand-writing a route, a loader, or a client call; a remembered API is often stale or wrong.
|
|
99
|
-
- \`nifra_context\`
|
|
100
|
-
- \`nifra_types\`
|
|
101
|
-
Call it for a precise type; **never read \`node_modules/@nifrajs/**/*.d.ts\`**
|
|
99
|
+
- \`nifra_context\` - this project's route index + conventions, so changes fit the existing surface.
|
|
100
|
+
- \`nifra_types\` - the EXACT TypeScript of any \`@nifrajs/*\` symbol (e.g. \`nifra_types({ name: "RateLimitStore" })\`).
|
|
101
|
+
Call it for a precise type; **never read \`node_modules/@nifrajs/**/*.d.ts\`** - the tool is the authoritative, complete source.
|
|
102
102
|
- **\`nifra_check\` is the done-gate.** Before declaring any change complete, run \`nifra_check\` (or
|
|
103
103
|
\`nifra check --json\` in a terminal). It typechecks the frontend↔backend contract and flags drift
|
|
104
104
|
(hand-rolled \`fetch()\` to this app's own API, server-only imports in \`routes/\`). A failing check
|
|
105
|
-
means the work isn't done
|
|
105
|
+
means the work isn't done - fix it, don't ship around it.
|
|
106
106
|
|
|
107
|
-
The full nifra cookbook for this app
|
|
108
|
-
the gotchas
|
|
107
|
+
The full nifra cookbook for this app - backend rules, the typed never-throwing client, file routing, and
|
|
108
|
+
the gotchas - lives in \`AGENTS.md\`, imported here so it stays the single source of truth:
|
|
109
109
|
|
|
110
110
|
@AGENTS.md
|
|
111
111
|
`
|
|
@@ -119,16 +119,16 @@ the gotchas — lives in \`AGENTS.md\`, imported here so it stays the single sou
|
|
|
119
119
|
export function agentsMcpSection(): string {
|
|
120
120
|
return `## MCP server
|
|
121
121
|
|
|
122
|
-
This project ships a **nifra MCP server**
|
|
122
|
+
This project ships a **nifra MCP server** - launch it with \`${MCP_SERVER_COMMAND} ${MCP_SERVER_ARGS.join(" ")}\`. It's registered for
|
|
123
123
|
Claude Code in \`.mcp.json\` and for Cursor in \`.cursor/mcp.json\`; other agents can point their MCP client
|
|
124
|
-
at that command. **Prefer its tools** over writing nifra from memory
|
|
124
|
+
at that command. **Prefer its tools** over writing nifra from memory - they're typechecked against this
|
|
125
125
|
project + the installed version, so they beat training-data recall:
|
|
126
126
|
|
|
127
|
-
- \`nifra_docs\` / \`nifra_example\`
|
|
128
|
-
- \`nifra_context\` / \`nifra_routes\`
|
|
129
|
-
- \`nifra_types\`
|
|
130
|
-
precise type; **never read \`@nifrajs\` \`.d.ts\` files**
|
|
131
|
-
- \`nifra_check\`
|
|
127
|
+
- \`nifra_docs\` / \`nifra_example\` - exact signatures + verified, compiling snippets.
|
|
128
|
+
- \`nifra_context\` / \`nifra_routes\` - this project's route index + per-route schemas.
|
|
129
|
+
- \`nifra_types\` - the EXACT TypeScript of any \`@nifrajs/*\` symbol (interface/type/function). Call it for a
|
|
130
|
+
precise type; **never read \`@nifrajs\` \`.d.ts\` files** - this is the authoritative, complete source.
|
|
131
|
+
- \`nifra_check\` - the done-gate (typecheck + drift lints). Run it before calling work complete; a
|
|
132
132
|
failing check means it isn't done. (\`nifra check --json\` in a terminal does the same.)`
|
|
133
133
|
}
|
|
134
134
|
|
package/template/README.md
CHANGED
|
@@ -9,9 +9,9 @@ bun test # run the tests
|
|
|
9
9
|
bun run typecheck # type-check
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
- `src/app.ts`
|
|
13
|
-
- `src/index.ts`
|
|
14
|
-
- `src/app.test.ts`
|
|
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
15
|
|
|
16
16
|
Add a typed client from `typeof app`:
|
|
17
17
|
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this app's routes are required to PROVE, checked by `nifra assure` (and level L1 of
|
|
3
|
+
* `nifra levels`). Evidence is collected from the routes themselves - a `body` schema records
|
|
4
|
+
* `body-bounded`, an auth middleware records `authenticated` - so this file states the requirement
|
|
5
|
+
* and the framework reports whether the route actually carries it.
|
|
6
|
+
*
|
|
7
|
+
* The starter rules below are deliberately small and true of THIS app. Grow them as the app grows:
|
|
8
|
+
* once there is auth, add `NIFRA_ASSURANCE.AUTHENTICATED` to `mutation`, and every route that does
|
|
9
|
+
* not carry it fails the check rather than shipping.
|
|
10
|
+
*
|
|
11
|
+
* Rules are first-match-wins, so order matters: put narrow exemptions above the general rule.
|
|
12
|
+
*/
|
|
13
|
+
import { defineAssuranceConfig, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
|
|
14
|
+
import { app } from "./src/app.ts"
|
|
15
|
+
|
|
16
|
+
export default defineAssuranceConfig({
|
|
17
|
+
source: app,
|
|
18
|
+
// What each effect IS, so the policy below can be written about a CLASS of effect rather than a list
|
|
19
|
+
// of token names: `{ access: "write", zone: "domain" }` covers `payments.charge` the day someone adds
|
|
20
|
+
// it, where a rule naming `db.write` would not.
|
|
21
|
+
capabilities: {
|
|
22
|
+
definitions: [
|
|
23
|
+
{ id: "db.read", zone: "domain", access: "read" },
|
|
24
|
+
{ id: "db.write", zone: "domain", access: "write" },
|
|
25
|
+
],
|
|
26
|
+
provenance: {
|
|
27
|
+
// Reaching one of these modules means holding the capabilities beside it, and `nifra check` fails
|
|
28
|
+
// any route that can reach further than it declared - so wiring a database into a handler and
|
|
29
|
+
// forgetting to say so is a failing check rather than something review has to catch.
|
|
30
|
+
//
|
|
31
|
+
// The first two are the seam `--db` scaffolds, split by access on purpose: reach is computed per
|
|
32
|
+
// MODULE, and a GET route may not declare a domain write, so a module that can reach both has GET
|
|
33
|
+
// routes with no legal declaration. Keeping reads and writes import-disjoint - at the seam and at
|
|
34
|
+
// the route modules - is what keeps every route's declaration equal to its reach. The drivers
|
|
35
|
+
// below are the backstop for code that goes around the seam; they grant both, because an import
|
|
36
|
+
// cannot tell a read from a write.
|
|
37
|
+
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"] },
|
|
50
|
+
],
|
|
51
|
+
// Add a driver here to force every query through the seam, and the check will name any route that
|
|
52
|
+
// reaches around it: `{ specifier: "pg", reason: "query through db/read.ts or db/write.ts" }`.
|
|
53
|
+
forbiddenImports: [],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
policy: {
|
|
57
|
+
rules: [
|
|
58
|
+
// Anything that writes business state must prove who asked. Matched on what the capability IS
|
|
59
|
+
// rather than on its name or its path, so a token introduced later - `payments.charge`,
|
|
60
|
+
// `orders.write` - is covered the day it is declared instead of the day someone remembers to
|
|
61
|
+
// widen this rule. A server function is a public POST endpoint like any other, so this is what
|
|
62
|
+
// stops one shipping unauthenticated.
|
|
63
|
+
{
|
|
64
|
+
name: "authenticated-write",
|
|
65
|
+
match: { access: "write", zone: "domain" },
|
|
66
|
+
require: [NIFRA_ASSURANCE.AUTHENTICATED],
|
|
67
|
+
},
|
|
68
|
+
// Anything that changes state must validate its input at the boundary. Adding a POST without a
|
|
69
|
+
// `body` schema fails this - which is the point: the check is what notices, not review.
|
|
70
|
+
{
|
|
71
|
+
name: "mutation",
|
|
72
|
+
match: { methods: ["POST", "PUT", "PATCH", "DELETE"] },
|
|
73
|
+
require: [NIFRA_ASSURANCE.BODY_BOUNDED],
|
|
74
|
+
},
|
|
75
|
+
// Reads carry no requirement yet. When some become private, split this into a public rule and
|
|
76
|
+
// an authenticated one rather than weakening the whole class.
|
|
77
|
+
{ name: "read", match: { methods: ["GET"] }, require: [] },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
})
|
package/template/package.json
CHANGED
|
@@ -6,15 +6,17 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "bun --watch run src/index.ts",
|
|
8
8
|
"start": "bun run src/index.ts",
|
|
9
|
+
"check": "nifra check && nifra assure",
|
|
9
10
|
"test": "bun test",
|
|
10
11
|
"typecheck": "tsc --noEmit"
|
|
11
12
|
},
|
|
12
13
|
"dependencies": {
|
|
13
|
-
"@nifrajs/client": "^2.
|
|
14
|
-
"@nifrajs/core": "^2.
|
|
15
|
-
"@nifrajs/schema": "^2.
|
|
14
|
+
"@nifrajs/client": "^2.3.0",
|
|
15
|
+
"@nifrajs/core": "^2.3.0",
|
|
16
|
+
"@nifrajs/schema": "^2.3.0"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
19
|
+
"@nifrajs/cli": "^2.3.0",
|
|
18
20
|
"@types/bun": "latest",
|
|
19
21
|
"typescript": "^6.0.3"
|
|
20
22
|
}
|
package/template/src/app.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
import { server } from "@nifrajs/core/server"
|
|
2
|
-
import {
|
|
2
|
+
import { routes } from "./routes.ts"
|
|
3
3
|
|
|
4
4
|
/** Your app. Exported (without `listen`) so tests can drive it via the in-process test client. */
|
|
5
5
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// `
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.post("/echo", { body: EchoInput, response: EchoReply }, (c) => ({ echoed: c.body.message }))
|
|
6
|
+
// Composition only: this module merges route modules and registers none of its own.
|
|
7
|
+
//
|
|
8
|
+
// That is not a style preference. `nifra check` works out what a route can reach from the module that
|
|
9
|
+
// REGISTERS it, following that module's imports - so a file that registers routes AND imports a
|
|
10
|
+
// database gives every route in it database reach, and a GET route may not declare a domain write at
|
|
11
|
+
// all. Keeping the root pure means each route's reach is its own module's, which is what lets the
|
|
12
|
+
// capability declarations in `nifra.assurance.ts` stay true as the app grows. Add a feature as a
|
|
13
|
+
// module, merge it here.
|
|
14
|
+
export const app = server().merge(routes)
|
|
16
15
|
|
|
17
16
|
export type App = typeof app
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { server } from "@nifrajs/core/server"
|
|
2
|
+
import { t } from "@nifrajs/schema"
|
|
3
|
+
|
|
4
|
+
// Contract-first: the schema validates the body at the boundary, types `c.body` in the
|
|
5
|
+
// handler, types the response, and flows into the typed client (`testClient` in app.test.ts,
|
|
6
|
+
// `client<App>(baseUrl)` in a frontend) - no codegen step.
|
|
7
|
+
const EchoInput = t.object({ message: t.string({ minLength: 1 }) })
|
|
8
|
+
const EchoReply = t.object({ echoed: t.string() })
|
|
9
|
+
|
|
10
|
+
export const routes = server()
|
|
11
|
+
.get("/", () => ({ hello: "nifra" }))
|
|
12
|
+
.get("/users/:id", (c) => ({ id: c.params.id }))
|
|
13
|
+
.post("/echo", { body: EchoInput, response: EchoReply }, (c) => ({ echoed: c.body.message }))
|
package/template-isr/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# nifra + ISR
|
|
2
2
|
|
|
3
|
-
A nifra + React app with **Incremental Static Regeneration**
|
|
3
|
+
A nifra + React app with **Incremental Static Regeneration** - pages are cached and served
|
|
4
4
|
stale-while-revalidate, backed by **Workers KV** in production (so the cache and on-demand purges hold
|
|
5
5
|
across instances) and an in-memory store for local dev.
|
|
6
6
|
|
package/template-isr/backend.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { server } from "@nifrajs/core/server"
|
|
2
|
-
import {
|
|
2
|
+
import { page } from "./page"
|
|
3
3
|
|
|
4
|
-
// Your backend contract
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
renders += 1
|
|
15
|
-
return { renders }
|
|
16
|
-
},
|
|
17
|
-
)
|
|
4
|
+
// Your backend contract - page loaders/actions call it in-process during SSR (no network).
|
|
5
|
+
// Composition only: this module merges route modules and registers none of its own.
|
|
6
|
+
//
|
|
7
|
+
// That is not a style preference. `nifra check` works out what a route can reach from the module that
|
|
8
|
+
// REGISTERS it, following that module's imports - so a file that registers routes AND imports a
|
|
9
|
+
// database gives every route in it database reach, and a GET route may not declare a domain write at
|
|
10
|
+
// all. Keeping this file pure means each route's reach is its own module's, which is what lets the
|
|
11
|
+
// capability declarations in `nifra.assurance.ts` stay true as the app grows. Add a feature as a
|
|
12
|
+
// module, merge it here.
|
|
13
|
+
export const backend = server().merge(page)
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this app's routes are required to PROVE, checked by `nifra assure` (and level L1 of
|
|
3
|
+
* `nifra levels`). Evidence is collected from the routes themselves - a `body` schema records
|
|
4
|
+
* `body-bounded`, an auth middleware records `authenticated` - so this file states the requirement
|
|
5
|
+
* and the framework reports whether the route actually carries it.
|
|
6
|
+
*
|
|
7
|
+
* Page routes under `routes/` are not classified here; this is the backend contract those pages call.
|
|
8
|
+
*
|
|
9
|
+
* Rules are first-match-wins, so order matters: narrow exemptions go above the general rule, and each
|
|
10
|
+
* one is named and justified. An exemption you have to write down is one a reviewer can argue with;
|
|
11
|
+
* an exemption you get by leaving the rule off is one nobody ever sees.
|
|
12
|
+
*/
|
|
13
|
+
import { defineAssuranceConfig, NIFRA_ASSURANCE } from "@nifrajs/core/assurance"
|
|
14
|
+
import { backend } from "./backend"
|
|
15
|
+
|
|
16
|
+
export default defineAssuranceConfig({
|
|
17
|
+
source: backend,
|
|
18
|
+
// What each effect IS, so the policy below can be written about a CLASS of effect rather than a list
|
|
19
|
+
// of token names: `{ access: "write", zone: "domain" }` covers `payments.charge` the day someone adds
|
|
20
|
+
// it, where a rule naming `db.write` would not.
|
|
21
|
+
capabilities: {
|
|
22
|
+
definitions: [
|
|
23
|
+
{ id: "db.read", zone: "domain", access: "read" },
|
|
24
|
+
{ id: "db.write", zone: "domain", access: "write" },
|
|
25
|
+
],
|
|
26
|
+
provenance: {
|
|
27
|
+
// Reaching one of these modules means holding the capabilities beside it, and `nifra check` fails
|
|
28
|
+
// any route that can reach further than it declared - so wiring a database into a handler and
|
|
29
|
+
// forgetting to say so is a failing check rather than something review has to catch.
|
|
30
|
+
//
|
|
31
|
+
// The first two are the seam `--db` scaffolds, split by access on purpose: reach is computed per
|
|
32
|
+
// MODULE, and a GET route may not declare a domain write, so a module that can reach both has GET
|
|
33
|
+
// routes with no legal declaration. Keeping reads and writes import-disjoint - at the seam and at
|
|
34
|
+
// the route modules - is what keeps every route's declaration equal to its reach. The drivers
|
|
35
|
+
// below are the backstop for code that goes around the seam; they grant both, because an import
|
|
36
|
+
// cannot tell a read from a write.
|
|
37
|
+
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"] },
|
|
50
|
+
],
|
|
51
|
+
// Add a driver here to force every query through the seam, and the check will name any route that
|
|
52
|
+
// reaches around it: `{ specifier: "pg", reason: "query through db/read.ts or db/write.ts" }`.
|
|
53
|
+
forbiddenImports: [],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
policy: {
|
|
57
|
+
rules: [
|
|
58
|
+
// Anything that writes business state must prove who asked. Matched on what the capability IS
|
|
59
|
+
// rather than on its name or its path, so a token introduced later - `payments.charge`,
|
|
60
|
+
// `orders.write` - is covered the day it is declared instead of the day someone remembers to
|
|
61
|
+
// widen this rule. A server function is a public POST endpoint like any other, so this is what
|
|
62
|
+
// stops one shipping unauthenticated.
|
|
63
|
+
{
|
|
64
|
+
name: "authenticated-write",
|
|
65
|
+
match: { access: "write", zone: "domain" },
|
|
66
|
+
require: [NIFRA_ASSURANCE.AUTHENTICATED],
|
|
67
|
+
},
|
|
68
|
+
// Anything that changes state must validate its input at the boundary. This demo is read-only,
|
|
69
|
+
// so the rule classifies nothing yet - which is the point: the first POST added to `page.ts`
|
|
70
|
+
// fails the check unless it bounds its body.
|
|
71
|
+
{
|
|
72
|
+
name: "mutation",
|
|
73
|
+
match: { methods: ["POST", "PUT", "PATCH", "DELETE"] },
|
|
74
|
+
require: [NIFRA_ASSURANCE.BODY_BOUNDED],
|
|
75
|
+
},
|
|
76
|
+
// Reads carry no requirement yet. When some become private, split this into a public rule and
|
|
77
|
+
// an authenticated one rather than weakening the whole class.
|
|
78
|
+
{ name: "read", match: { methods: ["GET"] }, require: [] },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
})
|
|
@@ -6,19 +6,20 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "bun run build.ts",
|
|
8
8
|
"dev": "bunx wrangler dev",
|
|
9
|
-
"dev:bun": "bun run server.ts"
|
|
9
|
+
"dev:bun": "bun run server.ts",
|
|
10
|
+
"check": "nifra check && nifra assure"
|
|
10
11
|
},
|
|
11
12
|
"dependencies": {
|
|
12
|
-
"@nifrajs/client": "^2.
|
|
13
|
-
"@nifrajs/core": "^2.
|
|
14
|
-
"@nifrajs/schema": "^2.
|
|
15
|
-
"@nifrajs/web": "^2.
|
|
16
|
-
"@nifrajs/web-react": "^2.
|
|
13
|
+
"@nifrajs/client": "^2.3.0",
|
|
14
|
+
"@nifrajs/core": "^2.3.0",
|
|
15
|
+
"@nifrajs/schema": "^2.3.0",
|
|
16
|
+
"@nifrajs/web": "^2.3.0",
|
|
17
|
+
"@nifrajs/web-react": "^2.3.0",
|
|
17
18
|
"react": "^19.0.0",
|
|
18
19
|
"react-dom": "^19.0.0"
|
|
19
20
|
},
|
|
20
21
|
"devDependencies": {
|
|
21
|
-
"@nifrajs/cli": "^2.
|
|
22
|
+
"@nifrajs/cli": "^2.3.0",
|
|
22
23
|
"@types/react": "^19.0.0",
|
|
23
24
|
"@types/react-dom": "^19.0.0",
|
|
24
25
|
"typescript": "^6.0.3"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { server } from "@nifrajs/core/server"
|
|
2
|
+
import { t } from "@nifrajs/schema"
|
|
3
|
+
|
|
4
|
+
// This demo counts server renders so the ISR cache is observable: a cache HIT serves stored bytes (the
|
|
5
|
+
// loader 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 page = server().get("/page", { response: t.object({ renders: t.number() }) }, () => {
|
|
10
|
+
renders += 1
|
|
11
|
+
return { renders }
|
|
12
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from "react"
|
|
2
2
|
|
|
3
|
-
/** Root layout
|
|
3
|
+
/** Root layout - rendered inside the document shell `renderPage` emits. */
|
|
4
4
|
export default function Layout(props: { children?: ReactNode }) {
|
|
5
5
|
return (
|
|
6
6
|
<main
|
|
@@ -7,7 +7,7 @@ export const meta = {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
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
|
|
10
|
+
// stale-while-revalidate. `revalidate` is the freshness window in SECONDS - nifra emits it as the
|
|
11
11
|
// `x-nifra-isr-revalidate` header, which the wrapper reads to set this page's TTL. Loader/meta are
|
|
12
12
|
// pure annotated exports, so they tree-shake out of the client bundle.
|
|
13
13
|
export const revalidate = 10
|
|
@@ -24,7 +24,7 @@ export default function Home(props: { data: LoaderData<typeof loader> }) {
|
|
|
24
24
|
server renders: <b>{props.data.renders}</b>
|
|
25
25
|
</p>
|
|
26
26
|
<p>
|
|
27
|
-
Reload within {revalidate}s and this holds
|
|
27
|
+
Reload within {revalidate}s and this holds - you're served the cached page (response header{" "}
|
|
28
28
|
<code>x-nifra-isr: hit</code>). After the window, the next request gets the stale page
|
|
29
29
|
instantly (<code>stale</code>) while a fresh copy regenerates behind it. Edit{" "}
|
|
30
30
|
<code>routes/index.tsx</code> to begin.
|
package/template-isr/server.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local Bun dev server (no wrangler)
|
|
2
|
+
* Local Bun dev server (no wrangler) - the same app with an in-memory ISR store. `bun run dev:bun`
|
|
3
3
|
* after `bun run build`. Production uses Workers KV (worker.ts); the only line that changes is the
|
|
4
4
|
* store. Watch the `x-nifra-isr` response header: miss → hit → stale.
|
|
5
5
|
*/
|