@nifrajs/cli 3.5.0 → 4.0.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 +42 -3
- package/dist/app-files.d.ts +11 -0
- package/dist/app-files.d.ts.map +1 -0
- package/dist/app-files.js +34 -0
- package/dist/app-files.js.map +1 -0
- package/dist/assure-hydration.d.ts +7 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +128 -43
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +16 -2
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +5 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +32 -10
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/cdn-check.d.ts +41 -0
- package/dist/cdn-check.d.ts.map +1 -0
- package/dist/cdn-check.js +207 -0
- package/dist/cdn-check.js.map +1 -0
- package/dist/check-diagnostics.d.ts +4 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +13 -0
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +49 -7
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +291 -62
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +99 -63
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +42 -16
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -82
- package/dist/cli.js.map +1 -1
- package/dist/command-catalog.d.ts +57 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +270 -26
- package/dist/command-catalog.js.map +1 -1
- package/dist/config-target.d.ts +8 -0
- package/dist/config-target.d.ts.map +1 -0
- package/dist/config-target.js +45 -0
- package/dist/config-target.js.map +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +52 -13
- package/dist/contracts.js.map +1 -1
- package/dist/db-audit.d.ts +37 -0
- package/dist/db-audit.d.ts.map +1 -0
- package/dist/db-audit.js +99 -0
- package/dist/db-audit.js.map +1 -0
- package/dist/db-child.d.ts +64 -0
- package/dist/db-child.d.ts.map +1 -0
- package/dist/db-child.js +213 -0
- package/dist/db-child.js.map +1 -0
- package/dist/db-config.d.ts +60 -0
- package/dist/db-config.d.ts.map +1 -0
- package/dist/db-config.js +176 -0
- package/dist/db-config.js.map +1 -0
- package/dist/db-tool.d.ts +84 -0
- package/dist/db-tool.d.ts.map +1 -0
- package/dist/db-tool.js +537 -0
- package/dist/db-tool.js.map +1 -0
- package/dist/dev-bun-config.d.ts +20 -13
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +98 -41
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +106 -0
- package/dist/dev-feed-tool.d.ts.map +1 -0
- package/dist/dev-feed-tool.js +713 -0
- package/dist/dev-feed-tool.js.map +1 -0
- package/dist/dev-server-client.d.ts +67 -0
- package/dist/dev-server-client.d.ts.map +1 -0
- package/dist/dev-server-client.js +195 -0
- package/dist/dev-server-client.js.map +1 -0
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +6 -5
- package/dist/diff-tool.js.map +1 -1
- package/dist/docs-search.d.ts.map +1 -1
- package/dist/docs-search.js +12 -4
- package/dist/docs-search.js.map +1 -1
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +76 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +15 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +73 -1
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +41 -21
- package/dist/fix-recipes.js.map +1 -1
- package/dist/frontend-guidance.js +5 -5
- package/dist/frontend-guidance.js.map +1 -1
- package/dist/i18n-check.d.ts +16 -0
- package/dist/i18n-check.d.ts.map +1 -0
- package/dist/i18n-check.js +154 -0
- package/dist/i18n-check.js.map +1 -0
- package/dist/init-agents.d.ts +49 -13
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +239 -34
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/code-unit-order.d.ts +4 -0
- package/dist/internal/code-unit-order.d.ts.map +1 -0
- package/dist/internal/code-unit-order.js +6 -0
- package/dist/internal/code-unit-order.js.map +1 -0
- package/dist/internal/script-kind.d.ts +7 -0
- package/dist/internal/script-kind.d.ts.map +1 -0
- package/dist/internal/script-kind.js +12 -0
- package/dist/internal/script-kind.js.map +1 -0
- package/dist/internal/source-facts.d.ts +5 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +61 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +49 -24
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +507 -67
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +5 -1
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +92 -27
- package/dist/introspect.js.map +1 -1
- package/dist/learn.js +3 -3
- package/dist/learn.js.map +1 -1
- package/dist/levels-tool.d.ts +10 -0
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -3
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +67 -17
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +78 -18
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +3 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-app-tool.d.ts.map +1 -1
- package/dist/mcp-app-tool.js +2 -0
- package/dist/mcp-app-tool.js.map +1 -1
- package/dist/mcp-context.js +1 -1
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +57 -0
- package/dist/mcp-delegate.d.ts.map +1 -0
- package/dist/mcp-delegate.js +130 -0
- package/dist/mcp-delegate.js.map +1 -0
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +23 -5
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-exec.d.ts +2 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +132 -108
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.d.ts.map +1 -1
- package/dist/mcp-http.js +17 -5
- package/dist/mcp-http.js.map +1 -1
- package/dist/mcp-io.d.ts +14 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +45 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts +48 -0
- package/dist/mcp-isolate.d.ts.map +1 -0
- package/dist/mcp-isolate.js +222 -0
- package/dist/mcp-isolate.js.map +1 -0
- package/dist/mcp-project-child.d.ts +74 -0
- package/dist/mcp-project-child.d.ts.map +1 -0
- package/dist/mcp-project-child.js +130 -0
- package/dist/mcp-project-child.js.map +1 -0
- package/dist/mcp-render.js +2 -2
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-root.d.ts +37 -9
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +110 -23
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-run.d.ts +18 -1
- package/dist/mcp-run.d.ts.map +1 -1
- package/dist/mcp-run.js +82 -9
- package/dist/mcp-run.js.map +1 -1
- package/dist/mcp-ws.js +4 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts +16 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +81 -53
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +44 -0
- package/dist/migrate-layout.d.ts.map +1 -0
- package/dist/migrate-layout.js +982 -0
- package/dist/migrate-layout.js.map +1 -0
- package/dist/openapi-tool.d.ts +2 -2
- package/dist/openapi-tool.d.ts.map +1 -1
- package/dist/openapi-tool.js.map +1 -1
- package/dist/openapi-types.d.ts +2 -1
- package/dist/openapi-types.d.ts.map +1 -1
- package/dist/openapi-types.js +240 -12
- package/dist/openapi-types.js.map +1 -1
- package/dist/pipeline-report.d.ts +3 -2
- package/dist/pipeline-report.d.ts.map +1 -1
- package/dist/pipeline-report.js +19 -16
- package/dist/pipeline-report.js.map +1 -1
- package/dist/port.d.ts +5 -4
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +46 -29
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +5 -0
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js.map +1 -1
- package/dist/recipes/4.0.0.d.ts +9 -0
- package/dist/recipes/4.0.0.d.ts.map +1 -0
- package/dist/recipes/4.0.0.js +34 -0
- package/dist/recipes/4.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +11 -1
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +47 -4
- package/dist/release-verification.js.map +1 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +12 -8
- package/dist/review.js.map +1 -1
- package/dist/route-types.d.ts +28 -0
- package/dist/route-types.d.ts.map +1 -0
- package/dist/route-types.js +106 -0
- package/dist/route-types.js.map +1 -0
- package/dist/rules/codes.d.ts +9 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +9 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +12 -0
- package/dist/rules/data-guard.d.ts.map +1 -0
- package/dist/rules/data-guard.js +213 -0
- package/dist/rules/data-guard.js.map +1 -0
- package/dist/rules/index.d.ts +3 -0
- package/dist/rules/index.d.ts.map +1 -1
- package/dist/rules/index.js +17 -5
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/islands.d.ts.map +1 -1
- package/dist/rules/islands.js +7 -4
- package/dist/rules/islands.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +71 -38
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/nano.d.ts.map +1 -1
- package/dist/rules/nano.js +15 -6
- package/dist/rules/nano.js.map +1 -1
- package/dist/rules/pages.d.ts +15 -0
- package/dist/rules/pages.d.ts.map +1 -0
- package/dist/rules/pages.js +88 -0
- package/dist/rules/pages.js.map +1 -0
- package/dist/rules/routes.d.ts +11 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +180 -2
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.d.ts +10 -0
- package/dist/rules/secrets.d.ts.map +1 -0
- package/dist/rules/secrets.js +169 -0
- package/dist/rules/secrets.js.map +1 -0
- package/dist/rules/security.d.ts +7 -0
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +171 -65
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/typescript.d.ts +16 -0
- package/dist/rules/typescript.d.ts.map +1 -0
- package/dist/rules/typescript.js +59 -0
- package/dist/rules/typescript.js.map +1 -0
- package/dist/rules/zones.d.ts +17 -0
- package/dist/rules/zones.d.ts.map +1 -0
- package/dist/rules/zones.js +174 -0
- package/dist/rules/zones.js.map +1 -0
- package/dist/scaffold.d.ts +25 -12
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +132 -87
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts +17 -3
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +941 -153
- package/dist/sdk.js.map +1 -1
- package/dist/search-terms.d.ts +2 -0
- package/dist/search-terms.d.ts.map +1 -1
- package/dist/search-terms.js +23 -8
- package/dist/search-terms.js.map +1 -1
- package/dist/smoke.d.ts +79 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +494 -0
- package/dist/smoke.js.map +1 -0
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +2 -1
- package/dist/sync-manifest.js.map +1 -1
- package/dist/sync-routes.d.ts +4 -3
- package/dist/sync-routes.d.ts.map +1 -1
- package/dist/sync-routes.js +4 -3
- package/dist/sync-routes.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +30 -11
- package/dist/test-tool.js.map +1 -1
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +11 -5
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +36 -6
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +167 -29
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +4 -0
- package/dist/verification-plan.js.map +1 -1
- package/dist/verification.d.ts.map +1 -1
- package/dist/verification.js +45 -12
- package/dist/verification.js.map +1 -1
- package/dist/web-app-options.d.ts +19 -0
- package/dist/web-app-options.d.ts.map +1 -0
- package/dist/web-app-options.js +49 -0
- package/dist/web-app-options.js.map +1 -0
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +29 -13
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +185 -29
- package/docs/llms-full.txt +3807 -580
- package/docs/types.json +14229 -10939
- package/package.json +19 -12
- package/src/app-files.ts +40 -0
- package/src/assure-hydration.ts +139 -46
- package/src/assure.ts +17 -2
- package/src/capabilities-tool.ts +35 -9
- package/src/cdn-check.ts +268 -0
- package/src/check-diagnostics.ts +17 -0
- package/src/check-scan.ts +358 -63
- package/src/check.ts +97 -62
- package/src/cli.ts +296 -87
- package/src/command-catalog.ts +374 -38
- package/src/config-target.ts +47 -0
- package/src/contracts.ts +54 -12
- package/src/db-audit.ts +128 -0
- package/src/db-child.ts +285 -0
- package/src/db-config.ts +224 -0
- package/src/db-tool.ts +690 -0
- package/src/dev-bun-config.ts +116 -40
- package/src/dev-feed-tool.ts +912 -0
- package/src/dev-server-client.ts +267 -0
- package/src/diff-tool.ts +6 -5
- package/src/docs-search.ts +10 -3
- package/src/doctor.ts +98 -15
- package/src/env-file.ts +78 -1
- package/src/fix-recipes.ts +44 -31
- package/src/frontend-guidance.ts +5 -5
- package/src/i18n-check.ts +179 -0
- package/src/init-agents.ts +301 -38
- package/src/internal/code-unit-order.ts +5 -0
- package/src/internal/script-kind.ts +11 -0
- package/src/internal/source-facts.ts +96 -22
- package/src/internal/typescript-import.ts +663 -68
- package/src/introspect.ts +90 -27
- package/src/learn.ts +3 -3
- package/src/levels-tool.ts +47 -2
- package/src/load.ts +140 -33
- package/src/manifest-tool.ts +3 -2
- package/src/mcp-app-tool.ts +2 -0
- package/src/mcp-context.ts +1 -1
- package/src/mcp-delegate.ts +162 -0
- package/src/mcp-docs-tools.ts +23 -5
- package/src/mcp-exec.ts +160 -132
- package/src/mcp-http.ts +19 -5
- package/src/mcp-io.ts +57 -0
- package/src/mcp-isolate.ts +287 -0
- package/src/mcp-project-child.ts +182 -0
- package/src/mcp-render.ts +2 -2
- package/src/mcp-root.ts +117 -22
- package/src/mcp-run.ts +104 -10
- package/src/mcp-ws.ts +4 -1
- package/src/mcp.ts +106 -64
- package/src/migrate-layout.ts +1121 -0
- package/src/openapi-tool.ts +7 -3
- package/src/openapi-types.ts +315 -13
- package/src/pipeline-report.ts +22 -18
- package/src/port.ts +53 -30
- package/src/project-facts.ts +5 -0
- package/src/recipes/4.0.0.ts +35 -0
- package/src/recipes/index.ts +2 -0
- package/src/release-verification.ts +74 -5
- package/src/review.ts +13 -8
- package/src/route-types.ts +133 -0
- package/src/rules/codes.ts +9 -0
- package/src/rules/data-guard.ts +230 -0
- package/src/rules/index.ts +18 -5
- package/src/rules/islands.ts +12 -4
- package/src/rules/legacy.ts +71 -39
- package/src/rules/nano.ts +18 -6
- package/src/rules/pages.ts +99 -0
- package/src/rules/routes.ts +181 -2
- package/src/rules/secrets.ts +174 -0
- package/src/rules/security.ts +198 -70
- package/src/rules/typescript.ts +77 -0
- package/src/rules/zones.ts +195 -0
- package/src/scaffold.ts +147 -95
- package/src/sdk.ts +1215 -158
- package/src/search-terms.ts +24 -7
- package/src/smoke.ts +607 -0
- package/src/sync-manifest.ts +2 -1
- package/src/sync-routes.ts +4 -3
- package/src/test-tool.ts +32 -11
- package/src/types-search.ts +11 -4
- package/src/upgrade.ts +205 -25
- package/src/verification-plan.ts +13 -0
- package/src/verification.ts +50 -12
- package/src/web-app-options.ts +57 -0
- package/src/work-graph.ts +27 -13
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NF-C028 and NF-C029: the frontend/backend zone rules, checked from source.
|
|
3
|
+
*
|
|
4
|
+
* Every browser build and dev server enforces these already; this reports them without building,
|
|
5
|
+
* with the same classifier (`@nifrajs/web/zones`) so the three can never disagree. NF-C004 owns
|
|
6
|
+
* browser code reaching server code (with the chain); NF-C028 owns the other directions: a zoned
|
|
7
|
+
* file importing one in no zone, backend code importing frontend code, and shared code importing
|
|
8
|
+
* anything but shared code. NF-C029 owns private environment reads in browser code.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
11
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
12
|
+
import { createZoneClassifier, importAllowed, importRuleMessage, privateEnvReads, privateEnvReason, } from "@nifrajs/web/zones";
|
|
13
|
+
import { CONFIG_FILE, FRAMEWORK_FILE } from "../app-files.js";
|
|
14
|
+
import { importSites, stripComments } from "../check-scan.js";
|
|
15
|
+
import { diagnostic } from "../diagnostics.js";
|
|
16
|
+
import { codeUnitOrder } from "../internal/code-unit-order.js";
|
|
17
|
+
const ZONE_FOLDER = new Set(["routes", "frontend", "backend", "shared"]);
|
|
18
|
+
const FRONTEND = new Set(["route-frontend", "frontend"]);
|
|
19
|
+
const SERVER = new Set(["backend", "route-backend", "fn"]);
|
|
20
|
+
const BACKEND = new Set(["backend", "route-backend"]);
|
|
21
|
+
const BROWSER_CODE = new Set(["route-frontend", "frontend", "shared"]);
|
|
22
|
+
const CODE_FILE = /\.(?:[cm]?[jt]sx?|svelte|vue|mdx)$/;
|
|
23
|
+
/** The app a project file belongs to: the path before its first zone folder, or `undefined`. */
|
|
24
|
+
function appOf(file) {
|
|
25
|
+
const parts = file.split("/");
|
|
26
|
+
const at = parts.findIndex((part, i) => i < parts.length - 1 && ZONE_FOLDER.has(part));
|
|
27
|
+
return at === -1 ? undefined : parts.slice(0, at).join("/");
|
|
28
|
+
}
|
|
29
|
+
/** Every scanned source file that belongs to an app, classified once per app classifier. */
|
|
30
|
+
export function zonedFiles(ctx) {
|
|
31
|
+
const classifiers = new Map();
|
|
32
|
+
const classifierFor = (app) => {
|
|
33
|
+
let classifier = classifiers.get(app);
|
|
34
|
+
if (classifier === undefined) {
|
|
35
|
+
const appRoot = join(ctx.root, app);
|
|
36
|
+
classifier = createZoneClassifier({
|
|
37
|
+
appRoot,
|
|
38
|
+
generatedFiles: [join(appRoot, "server-manifest.ts")],
|
|
39
|
+
});
|
|
40
|
+
classifiers.set(app, classifier);
|
|
41
|
+
}
|
|
42
|
+
return classifier;
|
|
43
|
+
};
|
|
44
|
+
const files = [];
|
|
45
|
+
for (const file of ctx.sources.files) {
|
|
46
|
+
if (!CODE_FILE.test(file))
|
|
47
|
+
continue;
|
|
48
|
+
const app = appOf(file);
|
|
49
|
+
if (app === undefined)
|
|
50
|
+
continue;
|
|
51
|
+
files.push({ file, app, classification: classifierFor(app).classify(join(ctx.root, file)) });
|
|
52
|
+
}
|
|
53
|
+
return { files, classifierFor };
|
|
54
|
+
}
|
|
55
|
+
const real = (path) => {
|
|
56
|
+
try {
|
|
57
|
+
return realpathSync(path);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return path;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/** Where a resolved import lands, as the project names it. Both sides are real paths: a checkout
|
|
64
|
+
* reached through a symlink (`/var` -> `/private/var` on macOS) still names files project-relative. */
|
|
65
|
+
function display(root, file) {
|
|
66
|
+
const rel = relative(real(root), real(file)).replaceAll("\\", "/");
|
|
67
|
+
return rel.startsWith("../") || isAbsolute(rel) ? file : rel;
|
|
68
|
+
}
|
|
69
|
+
/** A relative import resolved against the importing file, or `undefined` when it does not resolve. */
|
|
70
|
+
function resolveRelative(root, from, specifier) {
|
|
71
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../"))
|
|
72
|
+
return undefined;
|
|
73
|
+
try {
|
|
74
|
+
// Relative specifiers only: a bare one would let the resolver reach for a global install.
|
|
75
|
+
return Bun.resolveSync(specifier, dirname(join(root, from)));
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export const zoneImportRule = {
|
|
82
|
+
code: "NF-C028",
|
|
83
|
+
title: "Zone import check",
|
|
84
|
+
async scan(ctx) {
|
|
85
|
+
const { files, classifierFor } = zonedFiles(ctx);
|
|
86
|
+
const findings = [];
|
|
87
|
+
for (const { file, app, classification: from } of files) {
|
|
88
|
+
if (from.zone === "error" || from.zone === "library" || from.zone === "generated")
|
|
89
|
+
continue;
|
|
90
|
+
const content = ctx.sources.read(file);
|
|
91
|
+
if (content === undefined)
|
|
92
|
+
continue;
|
|
93
|
+
for (const { specifier, line } of importSites(content, file)) {
|
|
94
|
+
const target = resolveRelative(ctx.root, file, specifier);
|
|
95
|
+
if (target === undefined)
|
|
96
|
+
continue;
|
|
97
|
+
const to = classifierFor(app).classify(target);
|
|
98
|
+
const targetName = display(ctx.root, target);
|
|
99
|
+
let message;
|
|
100
|
+
if (to.zone === "error")
|
|
101
|
+
message = `${file} imports ${targetName}: ${to.reason}`;
|
|
102
|
+
else if ((SERVER.has(from.zone) && FRONTEND.has(to.zone)) ||
|
|
103
|
+
// Shared code reaching backend code is NF-C004's, reported there with the chain.
|
|
104
|
+
(from.zone === "shared" && !importAllowed(from.zone, to.zone) && !BACKEND.has(to.zone))) {
|
|
105
|
+
message = importRuleMessage(file, from.zone, targetName, to.zone);
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
continue;
|
|
109
|
+
findings.push(diagnostic({
|
|
110
|
+
code: "NF-C028",
|
|
111
|
+
severity: "error",
|
|
112
|
+
file,
|
|
113
|
+
line,
|
|
114
|
+
message,
|
|
115
|
+
evidence: [`import: ${specifier}`, `from zone: ${from.zone}`, `to zone: ${to.zone}`],
|
|
116
|
+
verify: "nifra check",
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return findings.sort((a, b) => codeUnitOrder(a.file ?? "", b.file ?? "") || (a.line ?? 0) - (b.line ?? 0));
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
const PREFIX_EXPORT = /\bexport\s+const\s+publicEnvPrefix\s*(?::\s*string\s*)?=\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')/;
|
|
124
|
+
/** The app's public-env prefix when its config declares it as a string literal, else the default. */
|
|
125
|
+
function publicPrefix(root, app) {
|
|
126
|
+
for (const config of [FRAMEWORK_FILE, CONFIG_FILE]) {
|
|
127
|
+
const path = join(root, app, config);
|
|
128
|
+
if (!existsSync(path))
|
|
129
|
+
continue;
|
|
130
|
+
const match = PREFIX_EXPORT.exec(stripComments(readFileSync(path, "utf8")));
|
|
131
|
+
if (match !== null)
|
|
132
|
+
return match[1] ?? match[2] ?? "PUBLIC_";
|
|
133
|
+
}
|
|
134
|
+
return "PUBLIC_";
|
|
135
|
+
}
|
|
136
|
+
export const privateEnvRule = {
|
|
137
|
+
code: "NF-C029",
|
|
138
|
+
title: "Private environment read in browser code",
|
|
139
|
+
async scan(ctx) {
|
|
140
|
+
const { files } = zonedFiles(ctx);
|
|
141
|
+
const prefixes = new Map();
|
|
142
|
+
const findings = [];
|
|
143
|
+
for (const { file, app, classification } of files) {
|
|
144
|
+
if (!BROWSER_CODE.has(classification.zone))
|
|
145
|
+
continue;
|
|
146
|
+
const content = ctx.sources.read(file);
|
|
147
|
+
if (content === undefined)
|
|
148
|
+
continue;
|
|
149
|
+
let prefix = prefixes.get(app);
|
|
150
|
+
if (prefix === undefined) {
|
|
151
|
+
prefix = publicPrefix(ctx.root, app);
|
|
152
|
+
prefixes.set(app, prefix);
|
|
153
|
+
}
|
|
154
|
+
const reads = privateEnvReads(file, content, prefix);
|
|
155
|
+
if (reads.length === 0)
|
|
156
|
+
continue;
|
|
157
|
+
const first = reads[0] ?? "";
|
|
158
|
+
const name = /[.[(]["']?([A-Za-z_$][\w$]*)["']?[)\]]?$/.exec(first)?.[1];
|
|
159
|
+
const index = name === undefined ? -1 : content.indexOf(name);
|
|
160
|
+
findings.push(diagnostic({
|
|
161
|
+
code: "NF-C029",
|
|
162
|
+
severity: "error",
|
|
163
|
+
file,
|
|
164
|
+
...(index === -1 ? {} : { line: content.slice(0, index).split("\n").length }),
|
|
165
|
+
message: `${file} ${privateEnvReason(reads, prefix)}`,
|
|
166
|
+
evidence: reads.map((read) => `read: ${read}`),
|
|
167
|
+
verify: "nifra check",
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
return findings;
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
export const zoneRules = Object.freeze([zoneImportRule, privateEnvRule]);
|
|
174
|
+
//# sourceMappingURL=zones.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zones.js","sourceRoot":"","sources":["../../src/rules/zones.ts"],"names":[],"mappings":"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"}
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra_scaffold` - turn a URL path into the correct `routes/`
|
|
3
|
-
* gets wrong) + a minimal, contract-correct
|
|
4
|
-
*
|
|
5
|
-
* `/` → `
|
|
2
|
+
* `nifra_scaffold` - turn a URL path into the correct `routes/` files (the convention an agent most often
|
|
3
|
+
* gets wrong) + a minimal, contract-correct route pair: the page, and its `.backend.ts` half holding the
|
|
4
|
+
* loader and its output schema. The mapping is the inverse of @nifrajs/web's `filePathToPatterns`:
|
|
5
|
+
* `:id`/`[id]` → `[id]`, `*rest`/`[...rest]` → `[...rest]`, `[[lang]]` optional, `/` → `index`. The
|
|
6
|
+
* framework (→ file extension) comes from the project's `clientModule`.
|
|
6
7
|
*
|
|
7
8
|
* Page stubs are emitted for the JSX family (react/preact/solid - one shared, verified shape) and
|
|
8
9
|
* for vanilla (a zero-runtime `html` page carrying the golden island pattern); for vue/svelte we
|
|
9
|
-
* return the correct
|
|
10
|
-
* rather than hand-writing an SFC we can't typecheck here.
|
|
10
|
+
* return the correct PATHS, the backend half and the route contract, and point at `nifra_example` for
|
|
11
|
+
* the page body, rather than hand-writing an SFC we can't typecheck here.
|
|
11
12
|
*/
|
|
12
13
|
export type Framework = "react" | "preact" | "solid" | "vue" | "svelte" | "vanilla";
|
|
13
14
|
/** A scaffold flavour on top of the framework. `default` is the plain page stub; `stateful` only
|
|
@@ -20,22 +21,34 @@ export declare function frameworkFromClientModule(clientModule: string | undefin
|
|
|
20
21
|
/** Map a URL path to its `routes/` file path (relative to `routes/`, without extension prefix dir).
|
|
21
22
|
* `/` → `index`; `/users/:id` → `users/[id]`; `/blog/*slug` → `blog/[...slug]`. */
|
|
22
23
|
export declare function routePathToFile(urlPath: string, ext: string): string;
|
|
24
|
+
/** `routes/users/[id].tsx` → `routes/users/[id].backend.ts`. */
|
|
25
|
+
export declare const backendFileOf: (file: string) => string;
|
|
23
26
|
export interface ScaffoldResult {
|
|
27
|
+
/** The page file. */
|
|
24
28
|
readonly file: string;
|
|
29
|
+
/** The page stub, when there is a verified one for the framework. */
|
|
25
30
|
readonly content?: string;
|
|
31
|
+
/** The page's backend half - framework-independent, so always present. */
|
|
32
|
+
readonly backend: {
|
|
33
|
+
readonly file: string;
|
|
34
|
+
readonly content: string;
|
|
35
|
+
};
|
|
26
36
|
readonly note: string;
|
|
27
37
|
}
|
|
28
38
|
export interface ScaffoldWriteResult extends ScaffoldResult {
|
|
29
39
|
readonly written: boolean;
|
|
30
40
|
readonly reason?: string;
|
|
31
41
|
}
|
|
32
|
-
/** Scaffold a page route for `urlPath` under the project's `framework`. Returns the correct file
|
|
33
|
-
* always; a ready-to-write stub for the JSX family, contract
|
|
42
|
+
/** Scaffold a page route for `urlPath` under the project's `framework`. Returns the correct file paths
|
|
43
|
+
* and the backend half always; a ready-to-write page stub for the JSX family and vanilla, contract
|
|
44
|
+
* guidance otherwise. */
|
|
34
45
|
export declare function scaffoldRoute(urlPath: string, framework: Framework, variant?: ScaffoldVariant): ScaffoldResult;
|
|
35
|
-
/** Write a scaffolded route
|
|
36
|
-
* intentionally conservative: it refuses
|
|
37
|
-
* uses `wx`, so an agent
|
|
46
|
+
/** Write a scaffolded route pair when the framework has a verified ready-to-write page body. The write
|
|
47
|
+
* is intentionally conservative: it refuses frameworks where we only return contract guidance (a
|
|
48
|
+
* backend half alone is a build error), refuses when either file exists, and uses `wx`, so an agent
|
|
49
|
+
* cannot overwrite user work by accident. It then generates the route's `./+types` module, so both
|
|
50
|
+
* halves typecheck at once. */
|
|
38
51
|
export declare function writeScaffoldRoute(cwd: string, urlPath: string, framework: Framework, variant?: ScaffoldVariant): Promise<ScaffoldWriteResult>;
|
|
39
|
-
/** Render the tool result as markdown - the file
|
|
52
|
+
/** Render the tool result as markdown - the file paths, the stubs (if any), and the contract note. */
|
|
40
53
|
export declare function renderScaffold(urlPath: string, framework: Framework, variant?: ScaffoldVariant): string;
|
|
41
54
|
//# sourceMappingURL=scaffold.d.ts.map
|
package/dist/scaffold.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAMH,MAAM,MAAM,SAAS,GAAG,OAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,CAAA;AAEnF;;;yFAGyF;AACzF,MAAM,MAAM,eAAe,GAAG,SAAS,GAAG,UAAU,CAAA;AAWpD,sGAAsG;AACtG,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CAGrF;AAwCD;mFACmF;AACnF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAWpE;AAuBD,gEAAgE;AAChE,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,KAAG,MAAkD,CAAA;AAwJ/F,MAAM,WAAW,cAAc;IAC7B,qBAAqB;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACrE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CACtB;AAED,MAAM,WAAW,mBAAoB,SAAQ,cAAc;IACzD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED;;yBAEyB;AACzB,wBAAgB,aAAa,CAC3B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,eAA2B,GACnC,cAAc,CAuChB;AA2CD;;;;+BAI+B;AAC/B,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,eAA2B,GACnC,OAAO,CAAC,mBAAmB,CAAC,CAmC9B;AAED,sGAAsG;AACtG,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,SAAS,EACpB,OAAO,GAAE,eAA2B,GACnC,MAAM,CAUR"}
|
package/dist/scaffold.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `nifra_scaffold` - turn a URL path into the correct `routes/`
|
|
3
|
-
* gets wrong) + a minimal, contract-correct
|
|
4
|
-
*
|
|
5
|
-
* `/` → `
|
|
2
|
+
* `nifra_scaffold` - turn a URL path into the correct `routes/` files (the convention an agent most often
|
|
3
|
+
* gets wrong) + a minimal, contract-correct route pair: the page, and its `.backend.ts` half holding the
|
|
4
|
+
* loader and its output schema. The mapping is the inverse of @nifrajs/web's `filePathToPatterns`:
|
|
5
|
+
* `:id`/`[id]` → `[id]`, `*rest`/`[...rest]` → `[...rest]`, `[[lang]]` optional, `/` → `index`. The
|
|
6
|
+
* framework (→ file extension) comes from the project's `clientModule`.
|
|
6
7
|
*
|
|
7
8
|
* Page stubs are emitted for the JSX family (react/preact/solid - one shared, verified shape) and
|
|
8
9
|
* for vanilla (a zero-runtime `html` page carrying the golden island pattern); for vue/svelte we
|
|
9
|
-
* return the correct
|
|
10
|
-
* rather than hand-writing an SFC we can't typecheck here.
|
|
10
|
+
* return the correct PATHS, the backend half and the route contract, and point at `nifra_example` for
|
|
11
|
+
* the page body, rather than hand-writing an SFC we can't typecheck here.
|
|
11
12
|
*/
|
|
12
13
|
import { lstat, mkdir, realpath, writeFile } from "node:fs/promises";
|
|
13
|
-
import { dirname, resolve, sep } from "node:path";
|
|
14
|
+
import { basename, dirname, resolve, sep } from "node:path";
|
|
15
|
+
import { paramConstraint } from "@nifrajs/core/pattern";
|
|
14
16
|
const EXT = {
|
|
15
17
|
react: "tsx",
|
|
16
18
|
preact: "tsx",
|
|
@@ -40,6 +42,10 @@ function assertSafeRouteSegment(segment, urlPath) {
|
|
|
40
42
|
if (segment === "." || segment === ".." || segment.includes("\0") || segment.includes("\\")) {
|
|
41
43
|
throw new Error(`invalid route segment in ${JSON.stringify(urlPath)}: ${JSON.stringify(segment)}`);
|
|
42
44
|
}
|
|
45
|
+
const brace = segment.indexOf("{");
|
|
46
|
+
if (segment.startsWith(":") && brace > 0 && paramConstraint(segment.slice(brace)) !== undefined) {
|
|
47
|
+
throw new Error(`a param constraint cannot be written in a route file name: ${JSON.stringify(segment)} in ${JSON.stringify(urlPath)}. Scaffold the page without it and check the value in the route's loader.`);
|
|
48
|
+
}
|
|
43
49
|
const fileSegment = segmentToFile(segment);
|
|
44
50
|
if (fileSegment === "." ||
|
|
45
51
|
fileSegment === ".." ||
|
|
@@ -67,22 +73,58 @@ export function routePathToFile(urlPath, ext) {
|
|
|
67
73
|
}
|
|
68
74
|
return `routes/${segments.map(segmentToFile).join("/")}.${ext}`;
|
|
69
75
|
}
|
|
70
|
-
const ROUTE_CONTRACT = `A route
|
|
71
|
-
- \`export default function Page(
|
|
72
|
-
- \`export async function loader({ params,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
export
|
|
85
|
-
|
|
76
|
+
const ROUTE_CONTRACT = `A route is two files:
|
|
77
|
+
- The page, \`routes/x.<ext>\` - ships to the browser: \`export default function Page({ data }: Route.ComponentProps)\` and \`export const meta = { title, meta: […] }\`. It never imports backend code.
|
|
78
|
+
- Its backend half, \`routes/x.backend.ts\` - never reaches the browser: \`export const loaderOutput = t.object({…})\` with \`export async function loader({ params, api }: Route.LoaderArgs)\` (data for SSR - only what the schema declares is sent), \`actionOutput\` with \`action\` (the form POST), and \`hydrate\`, \`islandScripts\`, \`revalidate\`, \`middleware\`. Reach the backend through the typed \`api\`.
|
|
79
|
+
Both import \`type { Route } from "./+types/<name>"\`, which \`nifra dev\`, \`nifra build\`, \`nifra check\` and \`nifra types\` generate. Path params are typed on \`params\`.`;
|
|
80
|
+
/** Param names a route file declares, for the stubs. */
|
|
81
|
+
function paramsOf(file) {
|
|
82
|
+
const out = [];
|
|
83
|
+
for (const m of file.matchAll(/(\[)?\[(?:\.\.\.)?([A-Za-z_][A-Za-z0-9_]*)\]/g))
|
|
84
|
+
out.push({ name: m[2], optional: m[1] !== undefined });
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** `./+types/<name>` for a route file: the generated module both halves import `Route` from. */
|
|
88
|
+
const typesOf = (file) => `./+types/${basename(file).replace(/\.[^.]+$/, "")}`;
|
|
89
|
+
/** `routes/users/[id].tsx` → `routes/users/[id].backend.ts`. */
|
|
90
|
+
export const backendFileOf = (file) => file.replace(/\.[^./]+$/, ".backend.ts");
|
|
91
|
+
/** The page's backend half: its loader, and the schema that bounds what the loader sends. */
|
|
92
|
+
function backendStub(file, params, vanilla) {
|
|
93
|
+
const last = params.at(-1);
|
|
94
|
+
const loader = last === undefined
|
|
95
|
+
? `export async function loader(_args: Route.LoaderArgs) {
|
|
96
|
+
// Load through the typed backend client, \`api\` in these args; see nifra_example("loader").
|
|
97
|
+
return { title: "TODO" }
|
|
98
|
+
}`
|
|
99
|
+
: `export async function loader({ params }: Route.LoaderArgs) {
|
|
100
|
+
// Load by params.${last.name} through the typed backend client, \`api\` in these args; see nifra_example("loader").
|
|
101
|
+
return { title: \`TODO: \${params.${last.name}${last.optional ? ' ?? ""' : ""}}\` }
|
|
102
|
+
}`;
|
|
103
|
+
const vanillaExports = vanilla
|
|
104
|
+
? `
|
|
105
|
+
|
|
106
|
+
// No client framework to hydrate with - vanilla routes are documents, not hydrated apps.
|
|
107
|
+
export const hydrate = false
|
|
108
|
+
|
|
109
|
+
// The built URL of each island enhancer this page renders a marker for (./<name>.client.ts).
|
|
110
|
+
// export const islandScripts = []`
|
|
111
|
+
: "";
|
|
112
|
+
return `// ${backendFileOf(file)} - the server half of ${file}; it never reaches the browser.
|
|
113
|
+
import { t } from "@nifrajs/schema"
|
|
114
|
+
import type { Route } from "${typesOf(file)}"
|
|
115
|
+
|
|
116
|
+
// Only what this schema declares is sent to the page.
|
|
117
|
+
export const loaderOutput = t.object({ title: t.string() })
|
|
118
|
+
|
|
119
|
+
${loader}${vanillaExports}
|
|
120
|
+
`;
|
|
121
|
+
}
|
|
122
|
+
function jsxStub(file) {
|
|
123
|
+
return `// ${file} - the page; it ships to the browser. Its data comes from ${basename(backendFileOf(file))}.
|
|
124
|
+
import type { Route } from "${typesOf(file)}"
|
|
125
|
+
|
|
126
|
+
export default function Page({ data }: Route.ComponentProps) {
|
|
127
|
+
return <main>{data.title}</main>
|
|
86
128
|
}
|
|
87
129
|
`;
|
|
88
130
|
}
|
|
@@ -93,27 +135,20 @@ export default function Page() {
|
|
|
93
135
|
* returns its cleanup (the one thing NF-C020 checks), wired through `mountIslands`. Interactivity is
|
|
94
136
|
* added by uncommenting and writing the companion `<name>.client.ts`, never by turning on hydration.
|
|
95
137
|
*/
|
|
96
|
-
function vanillaStub(file
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
? `// export async function loader({ params, api }: LoaderArgs<typeof backend>) { return { /* fetch by ${params[0]} */ } }`
|
|
100
|
-
: `// export async function loader({ api }: LoaderArgs<typeof backend>) { return {} }`;
|
|
101
|
-
return `// ${file} - @nifrajs/web-vanilla route. Server-rendered HTML, ZERO framework runtime.
|
|
102
|
-
// Available here: ${paramsNote}. Fetch data in a loader via the typed \`api\`; see nifra_example("loader").
|
|
138
|
+
function vanillaStub(file) {
|
|
139
|
+
return `// ${file} - @nifrajs/web-vanilla page. Server-rendered HTML, ZERO framework runtime; its data,
|
|
140
|
+
// and \`hydrate = false\`, live in ${basename(backendFileOf(file))}.
|
|
103
141
|
import { html } from "@nifrajs/web-vanilla"
|
|
142
|
+
import type { Route } from "${typesOf(file)}"
|
|
104
143
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
${loaderLine}
|
|
109
|
-
|
|
110
|
-
export default function Page() {
|
|
111
|
-
return html\`<main><h1>TODO: ${file}</h1></main>\`
|
|
144
|
+
export default function Page({ data }: Route.ComponentProps) {
|
|
145
|
+
return html\`<main><h1>\${data.title}</h1></main>\`
|
|
112
146
|
}
|
|
113
147
|
|
|
114
148
|
// --- Add interactivity the AI-safe way (islands), NOT hydration ---------------------------------
|
|
115
149
|
// 1. Render a marker in the page above: html\`<nifra-island data-id="counter" data-props=\${JSON.stringify({ start: 0 })}></nifra-island>\`
|
|
116
|
-
// 2. Wire the route to its enhancer bundle
|
|
150
|
+
// 2. Wire the route to its enhancer bundle, in ${basename(backendFileOf(file))}:
|
|
151
|
+
// export const islandScripts = [/* built URL of ./counter.client.ts */]
|
|
117
152
|
// 3. Write ./counter.client.ts as an imperative enhancer that ALWAYS returns its cleanup:
|
|
118
153
|
//
|
|
119
154
|
// import { defineIsland, mountIslands } from "@nifrajs/web/islands"
|
|
@@ -141,31 +176,21 @@ export default function Page() {
|
|
|
141
176
|
* The page body is a valid, typecheckable static document; interactivity is added by writing the
|
|
142
177
|
* companion client shown, never by turning on hydration.
|
|
143
178
|
*/
|
|
144
|
-
function nanoStub(file
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
: `// export async function loader({ api }: LoaderArgs<typeof backend>) { return {} }`;
|
|
149
|
-
return `// ${file} - @nifrajs/web-vanilla route + a nano island (explicit reactivity, zero VDOM).
|
|
150
|
-
// Available here: ${paramsNote}. Seed data in a loader via the typed \`api\`; see nifra_example("loader").
|
|
179
|
+
function nanoStub(file) {
|
|
180
|
+
return `// ${file} - @nifrajs/web-vanilla page + a nano island (explicit reactivity, zero VDOM). The
|
|
181
|
+
// document is static and the nano client below owns all interactivity; \`hydrate = false\` and the
|
|
182
|
+
// island's \`islandScripts\` URL live in ${basename(backendFileOf(file))}.
|
|
151
183
|
import { html } from "@nifrajs/web-vanilla"
|
|
184
|
+
import type { Route } from "${typesOf(file)}"
|
|
152
185
|
|
|
153
|
-
|
|
154
|
-
export const hydrate = false
|
|
155
|
-
|
|
156
|
-
${loaderLine}
|
|
157
|
-
|
|
158
|
-
export default function Page() {
|
|
186
|
+
export default function Page({ data }: Route.ComponentProps) {
|
|
159
187
|
// The island marker: data-props is the initial state the client reads on mount.
|
|
160
188
|
return html\`<main>
|
|
161
|
-
<h1
|
|
189
|
+
<h1>\${data.title}</h1>
|
|
162
190
|
<nifra-island data-id="todos" data-props=\${JSON.stringify({ items: [] })}></nifra-island>
|
|
163
191
|
</main>\`
|
|
164
192
|
}
|
|
165
193
|
|
|
166
|
-
// Wire the route to the built URL of ./todos.client.ts (the nano enhancer below).
|
|
167
|
-
// export const islandScripts = [/* built URL of ./todos.client.ts */]
|
|
168
|
-
|
|
169
194
|
// --- ./todos.client.ts - the golden nano pattern (write this as a companion file) -----------------
|
|
170
195
|
//
|
|
171
196
|
// import { defineIsland, mountIslands } from "@nifrajs/web/islands"
|
|
@@ -208,45 +233,47 @@ export default function Page() {
|
|
|
208
233
|
// has the full cookbook; \`nifra check\` runs NF-C021/C022/C023 over the client you write.
|
|
209
234
|
`;
|
|
210
235
|
}
|
|
211
|
-
/**
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
for (const m of file.matchAll(/\[(?:\.\.\.)?([A-Za-z_][A-Za-z0-9_]*)\]/g))
|
|
215
|
-
out.push(m[1]);
|
|
216
|
-
return out;
|
|
217
|
-
}
|
|
218
|
-
/** Scaffold a page route for `urlPath` under the project's `framework`. Returns the correct file path
|
|
219
|
-
* always; a ready-to-write stub for the JSX family, contract guidance otherwise. */
|
|
236
|
+
/** Scaffold a page route for `urlPath` under the project's `framework`. Returns the correct file paths
|
|
237
|
+
* and the backend half always; a ready-to-write page stub for the JSX family and vanilla, contract
|
|
238
|
+
* guidance otherwise. */
|
|
220
239
|
export function scaffoldRoute(urlPath, framework, variant = "default") {
|
|
221
240
|
const ext = EXT[framework];
|
|
222
241
|
const file = routePathToFile(urlPath, ext);
|
|
223
242
|
const params = paramsOf(file);
|
|
243
|
+
const backend = {
|
|
244
|
+
file: backendFileOf(file),
|
|
245
|
+
content: backendStub(file, params, framework === "vanilla"),
|
|
246
|
+
};
|
|
224
247
|
// `stateful` is vanilla-only: it swaps the static island stub for the nano golden pattern. Asking
|
|
225
248
|
// for it on a JSX/SFC framework is a no-op on the flavour (those lanes have their own reactivity).
|
|
226
249
|
if (framework === "vanilla" && variant === "stateful") {
|
|
227
250
|
return {
|
|
228
251
|
file,
|
|
229
|
-
content: nanoStub(file
|
|
230
|
-
|
|
252
|
+
content: nanoStub(file),
|
|
253
|
+
backend,
|
|
254
|
+
note: `Create ${file} and ${backend.file} as a zero-runtime @nifrajs/web-vanilla route with a nano island. ${ROUTE_CONTRACT}\nThe stub embeds the golden nano client (signal + computed(fn,[deps]) + keyed bindList + collected cleanups); write it as the companion .client.ts. \`nifra check\` runs NF-C021/C022/C023 over it; nifra_docs("nano") is the full cookbook.`,
|
|
231
255
|
};
|
|
232
256
|
}
|
|
233
257
|
if (framework === "react" || framework === "preact" || framework === "solid") {
|
|
234
258
|
return {
|
|
235
259
|
file,
|
|
236
|
-
content: jsxStub(file
|
|
237
|
-
|
|
260
|
+
content: jsxStub(file),
|
|
261
|
+
backend,
|
|
262
|
+
note: `Create ${file} and ${backend.file}. ${ROUTE_CONTRACT}\nIf the rendered page misbehaves (hydration warning, a value that stops updating), call nifra_frontend { adapter: "${framework}" } for the cause + fix.`,
|
|
238
263
|
};
|
|
239
264
|
}
|
|
240
265
|
if (framework === "vanilla") {
|
|
241
266
|
return {
|
|
242
267
|
file,
|
|
243
|
-
content: vanillaStub(file
|
|
244
|
-
|
|
268
|
+
content: vanillaStub(file),
|
|
269
|
+
backend,
|
|
270
|
+
note: `Create ${file} and ${backend.file} as a zero-runtime @nifrajs/web-vanilla route. ${ROUTE_CONTRACT}\nInteractivity comes from islands (imperative enhancers), never hydration - the stub embeds the golden pattern; nifra_example("islands") has the full cookbook. For explicit local state (a list a human edits), scaffold with variant "stateful" to get the nano pattern instead.`,
|
|
245
271
|
};
|
|
246
272
|
}
|
|
247
273
|
return {
|
|
248
274
|
file,
|
|
249
|
-
|
|
275
|
+
backend,
|
|
276
|
+
note: `Create ${file} as a ${framework} route module, and ${backend.file} from the stub below. ${ROUTE_CONTRACT}\nFor the ${framework} page body, call nifra_example (it ships verified ${framework} snippets) rather than guessing the SFC shape. If a value stops updating the template, call nifra_frontend { adapter: "${framework}" } for the ${framework} reactivity-loss fix.`,
|
|
250
277
|
};
|
|
251
278
|
}
|
|
252
279
|
function resolveInsideRoutes(cwd, relativeFile) {
|
|
@@ -289,9 +316,11 @@ async function assertNoSymlinkedAncestors(root, target) {
|
|
|
289
316
|
current = dirname(current);
|
|
290
317
|
}
|
|
291
318
|
}
|
|
292
|
-
/** Write a scaffolded route
|
|
293
|
-
* intentionally conservative: it refuses
|
|
294
|
-
* uses `wx`, so an agent
|
|
319
|
+
/** Write a scaffolded route pair when the framework has a verified ready-to-write page body. The write
|
|
320
|
+
* is intentionally conservative: it refuses frameworks where we only return contract guidance (a
|
|
321
|
+
* backend half alone is a build error), refuses when either file exists, and uses `wx`, so an agent
|
|
322
|
+
* cannot overwrite user work by accident. It then generates the route's `./+types` module, so both
|
|
323
|
+
* halves typecheck at once. */
|
|
295
324
|
export async function writeScaffoldRoute(cwd, urlPath, framework, variant = "default") {
|
|
296
325
|
const result = scaffoldRoute(urlPath, framework, variant);
|
|
297
326
|
if (result.content === undefined) {
|
|
@@ -301,22 +330,37 @@ export async function writeScaffoldRoute(cwd, urlPath, framework, variant = "def
|
|
|
301
330
|
reason: "no verified ready-to-write stub for this framework; use nifra_example for the body",
|
|
302
331
|
};
|
|
303
332
|
}
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
333
|
+
const files = [
|
|
334
|
+
{ file: result.file, content: result.content },
|
|
335
|
+
{ file: result.backend.file, content: result.backend.content },
|
|
336
|
+
];
|
|
337
|
+
const targets = files.map(({ file }) => resolveInsideRoutes(cwd, file));
|
|
338
|
+
for (const target of targets)
|
|
339
|
+
await assertNoSymlinkedAncestors(cwd, target);
|
|
340
|
+
for (const [i, target] of targets.entries()) {
|
|
341
|
+
if ((await lstat(target).catch(() => undefined)) !== undefined) {
|
|
342
|
+
return { ...result, written: false, reason: `file already exists: ${files[i]?.file}` };
|
|
343
|
+
}
|
|
310
344
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
345
|
+
await mkdir(dirname(targets[0]), { recursive: true });
|
|
346
|
+
for (const target of targets)
|
|
347
|
+
await assertNoSymlinkedAncestors(cwd, target);
|
|
348
|
+
for (const [i, target] of targets.entries()) {
|
|
349
|
+
try {
|
|
350
|
+
await writeFile(target, files[i]?.content ?? "", { flag: "wx" });
|
|
351
|
+
}
|
|
352
|
+
catch (err) {
|
|
353
|
+
if (err && typeof err === "object" && err.code === "EEXIST") {
|
|
354
|
+
return { ...result, written: false, reason: `file already exists: ${files[i]?.file}` };
|
|
355
|
+
}
|
|
356
|
+
throw err;
|
|
314
357
|
}
|
|
315
|
-
throw err;
|
|
316
358
|
}
|
|
359
|
+
const { refreshRouteTypes } = await import("./route-types.js");
|
|
360
|
+
refreshRouteTypes(cwd, () => { });
|
|
317
361
|
return { ...result, written: true };
|
|
318
362
|
}
|
|
319
|
-
/** Render the tool result as markdown - the file
|
|
363
|
+
/** Render the tool result as markdown - the file paths, the stubs (if any), and the contract note. */
|
|
320
364
|
export function renderScaffold(urlPath, framework, variant = "default") {
|
|
321
365
|
let r;
|
|
322
366
|
try {
|
|
@@ -325,7 +369,8 @@ export function renderScaffold(urlPath, framework, variant = "default") {
|
|
|
325
369
|
catch (err) {
|
|
326
370
|
return `Cannot scaffold ${JSON.stringify(urlPath)}: ${err instanceof Error ? err.message : String(err)}`;
|
|
327
371
|
}
|
|
328
|
-
const
|
|
329
|
-
|
|
372
|
+
const page = r.content ? `\n\n\`${r.file}\`:\n\n\`\`\`${EXT[framework]}\n${r.content}\`\`\`` : "";
|
|
373
|
+
const backend = `\n\n\`${r.backend.file}\`:\n\n\`\`\`ts\n${r.backend.content}\`\`\``;
|
|
374
|
+
return `# Scaffold route \`${urlPath}\` (${framework})\n\n**Files:** \`${r.file}\` + \`${r.backend.file}\`\n\n${r.note}${page}${backend}`;
|
|
330
375
|
}
|
|
331
376
|
//# sourceMappingURL=scaffold.js.map
|