@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,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `nifra types`: the generated `./+types/<route>` modules (see `@nifrajs/web/route-types`), plus the one
|
|
3
|
+
* tsconfig line that lets routes import them. `nifra dev`, `nifra build` and `nifra check` refresh them
|
|
4
|
+
* too, so a fresh checkout type-checks without running this by hand.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync, watch } from "node:fs"
|
|
7
|
+
import { dirname, join, relative, resolve } from "node:path"
|
|
8
|
+
import { ROUTE_TYPES_DIR, staleRouteTypes, writeRouteTypes } from "@nifrajs/web/route-types"
|
|
9
|
+
import { stripComments } from "./check-scan.ts"
|
|
10
|
+
|
|
11
|
+
export interface RouteTypesReport {
|
|
12
|
+
readonly ok: boolean
|
|
13
|
+
readonly check: boolean
|
|
14
|
+
/** Files written (or, with `check`, that would be). */
|
|
15
|
+
readonly written: readonly string[]
|
|
16
|
+
readonly removed: readonly string[]
|
|
17
|
+
/** With `check`: every generated file missing, out of date, or no longer generated. */
|
|
18
|
+
readonly stale: readonly string[]
|
|
19
|
+
/** Set when tsconfig does not merge `.nifra/types` in through `rootDirs`. */
|
|
20
|
+
readonly tsconfig?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TSCONFIG_HINT = `add "rootDirs": [".", "./${ROUTE_TYPES_DIR}"] to compilerOptions in tsconfig.json, so a route can import ./+types/<name>`
|
|
24
|
+
|
|
25
|
+
/** A tsconfig's JSONC as an object, following relative `extends`, or `undefined` when unreadable. */
|
|
26
|
+
function readTsconfig(
|
|
27
|
+
path: string,
|
|
28
|
+
depth = 0,
|
|
29
|
+
): { compilerOptions?: Record<string, unknown> } | undefined {
|
|
30
|
+
if (depth > 8 || !existsSync(path)) return undefined
|
|
31
|
+
try {
|
|
32
|
+
const text = stripComments(readFileSync(path, "utf8")).replace(/,(\s*[}\]])/g, "$1")
|
|
33
|
+
const config = JSON.parse(text) as {
|
|
34
|
+
extends?: unknown
|
|
35
|
+
compilerOptions?: Record<string, unknown>
|
|
36
|
+
}
|
|
37
|
+
if (config.compilerOptions?.rootDirs !== undefined || typeof config.extends !== "string") {
|
|
38
|
+
return config
|
|
39
|
+
}
|
|
40
|
+
if (!config.extends.startsWith(".")) return config
|
|
41
|
+
const base = config.extends.endsWith(".json") ? config.extends : `${config.extends}.json`
|
|
42
|
+
const parent = readTsconfig(resolve(dirname(path), base), depth + 1)
|
|
43
|
+
return { compilerOptions: { ...parent?.compilerOptions, ...config.compilerOptions } }
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Why the app's tsconfig cannot resolve `./+types/<name>`, or `undefined` when it can. */
|
|
50
|
+
export function routeTypesTsconfigIssue(appRoot: string): string | undefined {
|
|
51
|
+
const tsconfig = join(appRoot, "tsconfig.json")
|
|
52
|
+
if (!existsSync(tsconfig)) return undefined
|
|
53
|
+
const rootDirs = readTsconfig(tsconfig)?.compilerOptions?.rootDirs
|
|
54
|
+
const merged =
|
|
55
|
+
Array.isArray(rootDirs) &&
|
|
56
|
+
rootDirs.some(
|
|
57
|
+
(dir) =>
|
|
58
|
+
typeof dir === "string" && resolve(appRoot, dir) === resolve(appRoot, ROUTE_TYPES_DIR),
|
|
59
|
+
)
|
|
60
|
+
return merged ? undefined : TSCONFIG_HINT
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const appRelative = (appRoot: string, paths: readonly string[]): string[] =>
|
|
64
|
+
paths.map((path) => relative(appRoot, path).replaceAll("\\", "/"))
|
|
65
|
+
|
|
66
|
+
/** Generate (or, with `check`, verify) an app's route types. */
|
|
67
|
+
export function routeTypes(
|
|
68
|
+
appRoot: string,
|
|
69
|
+
options: { readonly check?: boolean } = {},
|
|
70
|
+
): RouteTypesReport {
|
|
71
|
+
const tsconfig = routeTypesTsconfigIssue(appRoot)
|
|
72
|
+
const extra = tsconfig === undefined ? {} : { tsconfig }
|
|
73
|
+
if (options.check === true) {
|
|
74
|
+
const stale = appRelative(appRoot, staleRouteTypes({ appRoot }))
|
|
75
|
+
return { ok: stale.length === 0, check: true, written: [], removed: [], stale, ...extra }
|
|
76
|
+
}
|
|
77
|
+
const { written, removed } = writeRouteTypes({ appRoot })
|
|
78
|
+
return {
|
|
79
|
+
ok: true,
|
|
80
|
+
check: false,
|
|
81
|
+
written: appRelative(appRoot, written),
|
|
82
|
+
removed: appRelative(appRoot, removed),
|
|
83
|
+
stale: [],
|
|
84
|
+
...extra,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Refresh route types before a command that type-checks or bundles; quiet unless something changed.
|
|
90
|
+
* Never throws: a routes tree the generator cannot read is the command's own error to report.
|
|
91
|
+
*/
|
|
92
|
+
export function refreshRouteTypes(
|
|
93
|
+
appRoot: string,
|
|
94
|
+
log: (line: string) => void = console.log,
|
|
95
|
+
): void {
|
|
96
|
+
if (!existsSync(join(appRoot, "routes"))) return
|
|
97
|
+
let report: RouteTypesReport
|
|
98
|
+
try {
|
|
99
|
+
report = routeTypes(appRoot)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
log(`[nifra] route types: ${error instanceof Error ? error.message : String(error)}`)
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
const { written, removed, tsconfig } = report
|
|
105
|
+
if (written.length + removed.length > 0) {
|
|
106
|
+
log(
|
|
107
|
+
`[nifra] route types: ${written.length} written, ${removed.length} removed in ${ROUTE_TYPES_DIR}`,
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
if (tsconfig !== undefined) log(`[nifra] route types: ${tsconfig}`)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Keep route types current while `nifra dev` runs: they depend only on which route files exist, so a
|
|
115
|
+
* route added or removed (or `backend/app.ts` appearing) regenerates them. Returns the stop function.
|
|
116
|
+
*/
|
|
117
|
+
export function watchRouteTypes(appRoot: string): () => void {
|
|
118
|
+
const routes = join(appRoot, "routes")
|
|
119
|
+
if (!existsSync(routes)) return () => {}
|
|
120
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
121
|
+
const schedule = (): void => {
|
|
122
|
+
clearTimeout(timer)
|
|
123
|
+
timer = setTimeout(() => refreshRouteTypes(appRoot), 50)
|
|
124
|
+
}
|
|
125
|
+
// Not persistent: the dev server's own shutdown must not wait on this watcher.
|
|
126
|
+
const watchers = [watch(routes, { recursive: true, persistent: false }, schedule)]
|
|
127
|
+
const backend = join(appRoot, "backend")
|
|
128
|
+
if (existsSync(backend)) watchers.push(watch(backend, { persistent: false }, schedule))
|
|
129
|
+
return () => {
|
|
130
|
+
clearTimeout(timer)
|
|
131
|
+
for (const watcher of watchers) watcher.close()
|
|
132
|
+
}
|
|
133
|
+
}
|
package/src/rules/codes.ts
CHANGED
|
@@ -20,6 +20,15 @@ export const RULE_CODES = Object.freeze({
|
|
|
20
20
|
"NF-C017": "Rule pack validation",
|
|
21
21
|
"NF-C018": "Reserved client segment check",
|
|
22
22
|
"NF-C019": "Duplicate route registration check",
|
|
23
|
+
"NF-C024": "Overlapping route registration check",
|
|
24
|
+
"NF-C025": "Route overlap analysis budget check",
|
|
25
|
+
"NF-C026": "Route param modifier check",
|
|
26
|
+
"NF-C027": "Page route under the backend mount",
|
|
27
|
+
"NF-C028": "Zone import check",
|
|
28
|
+
"NF-C029": "Private environment read in browser code",
|
|
29
|
+
"NF-C030": "Route data without an output schema",
|
|
30
|
+
"NF-C031": "Sensitive field in an output schema",
|
|
31
|
+
"NF-C032": "Credential in browser code",
|
|
23
32
|
"NF-C020": "Island enhancer cleanup check",
|
|
24
33
|
"NF-C021": "nano binding cleanup check",
|
|
25
34
|
"NF-C022": "nano bindList key check",
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NF-C030 and NF-C031: the data guard, checked from source.
|
|
3
|
+
*
|
|
4
|
+
* The server enforces both at runtime, on every request: data a loader or action returns without an
|
|
5
|
+
* output schema fails the request, and an output schema naming a sensitive field without
|
|
6
|
+
* `t.declassified` fails the route when it loads. These report what source alone shows, before a
|
|
7
|
+
* request ever does. NF-C030 is a warning because only running the loader proves it returns data.
|
|
8
|
+
*/
|
|
9
|
+
import { isSensitiveFieldName } from "@nifrajs/web/zones"
|
|
10
|
+
import { codePositionMask } from "../check-scan.ts"
|
|
11
|
+
import { type Diagnostic, diagnostic } from "../diagnostics.ts"
|
|
12
|
+
import { codeUnitOrder } from "../internal/code-unit-order.ts"
|
|
13
|
+
import type { CheckRule } from "./index.ts"
|
|
14
|
+
import { zonedFiles } from "./zones.ts"
|
|
15
|
+
|
|
16
|
+
const lineAt = (text: string, index: number): number => text.slice(0, index).split("\n").length
|
|
17
|
+
|
|
18
|
+
const byPlace = (a: Diagnostic, b: Diagnostic): number =>
|
|
19
|
+
codeUnitOrder(a.file ?? "", b.file ?? "") || (a.line ?? 0) - (b.line ?? 0)
|
|
20
|
+
|
|
21
|
+
const exported = (mask: string, name: string): RegExpExecArray | null =>
|
|
22
|
+
new RegExp(
|
|
23
|
+
`\\bexport\\s+(?:(?:async\\s+)?function\\s*\\*?\\s*|(?:const|let|var)\\s+)${name}\\b|\\bexport\\s*\\{[^}]*\\b${name}\\b[^}]*\\}`,
|
|
24
|
+
).exec(mask)
|
|
25
|
+
|
|
26
|
+
/** A return that hands the browser nothing: a bare return, a redirect, a status, a thrown signal. */
|
|
27
|
+
const CONTROL_FLOW =
|
|
28
|
+
/^(?:;|$|null\b|undefined\b|redirect\s*\(|notFound\s*\(|gone\s*\(|statusPage\s*\(|new\s+Response\s*\(|Response\s*\.)/
|
|
29
|
+
|
|
30
|
+
const closing = (mask: string, open: number): number => {
|
|
31
|
+
let depth = 0
|
|
32
|
+
for (let i = open; i < mask.length; i++) {
|
|
33
|
+
const c = mask[i]
|
|
34
|
+
if (c === "(" || c === "{" || c === "[") depth++
|
|
35
|
+
else if (c === ")" || c === "}" || c === "]") {
|
|
36
|
+
depth--
|
|
37
|
+
if (depth === 0) return i
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return mask.length
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether the function exported as `name` may return data, read from its type-stripped source: an
|
|
45
|
+
* arrow's expression body, or a `return <value>` that is not a redirect or a status.
|
|
46
|
+
*/
|
|
47
|
+
function mayReturnData(js: string, name: string): boolean {
|
|
48
|
+
const mask = codePositionMask(js)
|
|
49
|
+
const found = exported(mask, name)
|
|
50
|
+
if (found === null || found[0].startsWith("export {") || found[0].startsWith("export{")) {
|
|
51
|
+
return true // declared elsewhere in the file and re-exported: assume it does
|
|
52
|
+
}
|
|
53
|
+
const params = mask.indexOf("(", found.index + found[0].length)
|
|
54
|
+
if (params === -1) return true
|
|
55
|
+
let at = closing(mask, params) + 1
|
|
56
|
+
while (/\s/.test(mask[at] ?? "")) at++
|
|
57
|
+
if (mask.startsWith("=>", at)) {
|
|
58
|
+
at += 2
|
|
59
|
+
while (/\s/.test(mask[at] ?? "")) at++
|
|
60
|
+
if (mask[at] !== "{") return !CONTROL_FLOW.test(js.slice(at, at + 40))
|
|
61
|
+
}
|
|
62
|
+
if (mask[at] !== "{") return true
|
|
63
|
+
const body = mask.slice(at, closing(mask, at))
|
|
64
|
+
for (const match of body.matchAll(/\breturn\b/g)) {
|
|
65
|
+
const from = at + match.index + "return".length
|
|
66
|
+
const rest = js.slice(from, from + 40).replace(/^[ \t]+/, "")
|
|
67
|
+
if (!CONTROL_FLOW.test(rest)) return true
|
|
68
|
+
}
|
|
69
|
+
return false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Where an expression starting at `start` ends: a `,`, `;` or closer at its own depth. */
|
|
73
|
+
function expressionEnd(mask: string, start: number): number {
|
|
74
|
+
let depth = 0
|
|
75
|
+
for (let i = start; i < mask.length; i++) {
|
|
76
|
+
const c = mask[i]
|
|
77
|
+
if (c === "(" || c === "{" || c === "[") depth++
|
|
78
|
+
else if (c === ")" || c === "}" || c === "]") {
|
|
79
|
+
if (depth === 0) return i
|
|
80
|
+
depth--
|
|
81
|
+
} else if (depth === 0 && (c === "," || c === ";")) return i
|
|
82
|
+
else if (depth === 0 && c === "\n") {
|
|
83
|
+
const next = /\S/.exec(mask.slice(i))?.[0]
|
|
84
|
+
const prev = /\S(?=\s*$)/.exec(mask.slice(start, i))?.[0]
|
|
85
|
+
if (
|
|
86
|
+
next !== undefined &&
|
|
87
|
+
!".?:+-*/&|".includes(next) &&
|
|
88
|
+
!"=(,[{:?+-*/&|".includes(prev ?? "")
|
|
89
|
+
) {
|
|
90
|
+
return i
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return mask.length
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Object keys inside `[start, end)` whose name is sensitive and whose value is not declassified. */
|
|
98
|
+
function sensitiveKeys(
|
|
99
|
+
src: string,
|
|
100
|
+
mask: string,
|
|
101
|
+
start: number,
|
|
102
|
+
end: number,
|
|
103
|
+
): Array<{ readonly key: string; readonly index: number }> {
|
|
104
|
+
const found: Array<{ key: string; index: number }> = []
|
|
105
|
+
for (
|
|
106
|
+
let colon = mask.indexOf(":", start);
|
|
107
|
+
colon !== -1 && colon < end;
|
|
108
|
+
colon = mask.indexOf(":", colon + 1)
|
|
109
|
+
) {
|
|
110
|
+
let k = colon - 1
|
|
111
|
+
while (k >= start && /\s/.test(src[k] ?? "")) k--
|
|
112
|
+
let key: string
|
|
113
|
+
let keyStart: number
|
|
114
|
+
if (/[\w$]/.test(mask[k] ?? "")) {
|
|
115
|
+
keyStart = k
|
|
116
|
+
while (keyStart > start && /[\w$]/.test(mask[keyStart - 1] ?? "")) keyStart--
|
|
117
|
+
key = mask.slice(keyStart, k + 1)
|
|
118
|
+
} else if (src[k] === '"' || src[k] === "'") {
|
|
119
|
+
keyStart = src.lastIndexOf(src[k] as string, k - 1)
|
|
120
|
+
if (keyStart < start) continue
|
|
121
|
+
key = src.slice(keyStart + 1, k)
|
|
122
|
+
} else continue
|
|
123
|
+
let p = keyStart - 1
|
|
124
|
+
while (p >= start && /\s/.test(mask[p] ?? "")) p--
|
|
125
|
+
if (mask[p] !== "{" && mask[p] !== ",") continue
|
|
126
|
+
if (!isSensitiveFieldName(key)) continue
|
|
127
|
+
const value = src.slice(colon + 1, colon + 80).trimStart()
|
|
128
|
+
if (/^[\w$.]*declassified\s*\(/.test(value)) continue
|
|
129
|
+
found.push({ key, index: keyStart })
|
|
130
|
+
}
|
|
131
|
+
return found
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface MissingOutputSchema {
|
|
135
|
+
readonly name: "loader" | "action"
|
|
136
|
+
readonly schema: "loaderOutput" | "actionOutput"
|
|
137
|
+
readonly line: number
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let transpiler: Bun.Transpiler | undefined
|
|
141
|
+
|
|
142
|
+
/** The loader and action a route's backend half exports, may return data from, and gives no schema. */
|
|
143
|
+
export function missingOutputSchemas(content: string): MissingOutputSchema[] {
|
|
144
|
+
const mask = codePositionMask(content)
|
|
145
|
+
const missing: MissingOutputSchema[] = []
|
|
146
|
+
let js: string | undefined
|
|
147
|
+
for (const [name, schema] of [
|
|
148
|
+
["loader", "loaderOutput"],
|
|
149
|
+
["action", "actionOutput"],
|
|
150
|
+
] as const) {
|
|
151
|
+
const fn = exported(mask, name)
|
|
152
|
+
if (fn === null || exported(mask, schema) !== null) continue
|
|
153
|
+
try {
|
|
154
|
+
transpiler ??= new Bun.Transpiler({ loader: "ts" })
|
|
155
|
+
js ??= transpiler.transformSync(content)
|
|
156
|
+
} catch {
|
|
157
|
+
js = content
|
|
158
|
+
}
|
|
159
|
+
if (mayReturnData(js, name)) missing.push({ name, schema, line: lineAt(content, fn.index) })
|
|
160
|
+
}
|
|
161
|
+
return missing
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export const missingOutputRule: CheckRule = {
|
|
165
|
+
code: "NF-C030",
|
|
166
|
+
title: "Route data without an output schema",
|
|
167
|
+
async scan(ctx) {
|
|
168
|
+
const findings: Diagnostic[] = []
|
|
169
|
+
for (const { file, classification } of zonedFiles(ctx).files) {
|
|
170
|
+
if (classification.zone !== "route-backend") continue
|
|
171
|
+
const content = ctx.sources.read(file)
|
|
172
|
+
if (content === undefined) continue
|
|
173
|
+
for (const { name, schema, line } of missingOutputSchemas(content)) {
|
|
174
|
+
findings.push(
|
|
175
|
+
diagnostic({
|
|
176
|
+
code: "NF-C030",
|
|
177
|
+
severity: "warn",
|
|
178
|
+
file,
|
|
179
|
+
line,
|
|
180
|
+
message: `${file} exports a ${name} but no ${schema}. Data it returns fails the request: everything sent to the browser needs an output schema. Export one: export const ${schema} = t.object({ ... })`,
|
|
181
|
+
evidence: [`export: ${name}`, `missing: ${schema}`],
|
|
182
|
+
verify: "nifra check",
|
|
183
|
+
}),
|
|
184
|
+
)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return findings.sort(byPlace)
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const OUTPUT_DECLARATION =
|
|
192
|
+
/\bexport\s+(?:const|let|var)\s+(?:loaderOutput|actionOutput)\b[^=]*=|\boutput\s*:/g
|
|
193
|
+
|
|
194
|
+
export const sensitiveOutputRule: CheckRule = {
|
|
195
|
+
code: "NF-C031",
|
|
196
|
+
title: "Sensitive field in an output schema",
|
|
197
|
+
async scan(ctx) {
|
|
198
|
+
const findings: Diagnostic[] = []
|
|
199
|
+
for (const { file, classification } of zonedFiles(ctx).files) {
|
|
200
|
+
if (classification.zone !== "route-backend" && classification.zone !== "fn") continue
|
|
201
|
+
const content = ctx.sources.read(file)
|
|
202
|
+
if (content === undefined) continue
|
|
203
|
+
const mask = codePositionMask(content)
|
|
204
|
+
for (const declaration of mask.matchAll(OUTPUT_DECLARATION)) {
|
|
205
|
+
const start = declaration.index + declaration[0].length
|
|
206
|
+
for (const { key, index } of sensitiveKeys(
|
|
207
|
+
content,
|
|
208
|
+
mask,
|
|
209
|
+
start,
|
|
210
|
+
expressionEnd(mask, start),
|
|
211
|
+
)) {
|
|
212
|
+
findings.push(
|
|
213
|
+
diagnostic({
|
|
214
|
+
code: "NF-C031",
|
|
215
|
+
severity: "error",
|
|
216
|
+
file,
|
|
217
|
+
line: lineAt(content, index),
|
|
218
|
+
message: `${file} declares "${key}" in an output schema, a field name that usually holds a credential or personal identifier. Remove it, or wrap its schema in t.declassified("why it may reach the browser", ...)`,
|
|
219
|
+
evidence: [`field: ${key}`],
|
|
220
|
+
verify: "nifra check",
|
|
221
|
+
}),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return findings.sort(byPlace)
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const dataGuardRules = Object.freeze([missingOutputRule, sensitiveOutputRule])
|
package/src/rules/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Diagnostic } from "../diagnostics.ts"
|
|
2
|
+
import type { TypeScriptSession } from "../internal/typescript-import.ts"
|
|
2
3
|
import type { ProjectFacts } from "../project-facts.ts"
|
|
3
4
|
import { isBuiltInCode } from "./codes.ts"
|
|
5
|
+
import { releaseRuleTypeScript, retainRuleTypeScript } from "./typescript.ts"
|
|
4
6
|
|
|
5
7
|
export type { ProjectFacts } from "../project-facts.ts"
|
|
6
8
|
|
|
@@ -18,6 +20,8 @@ export interface RuleContext {
|
|
|
18
20
|
readonly root: string
|
|
19
21
|
readonly sources: SourceIndex
|
|
20
22
|
readonly project: ProjectFacts
|
|
23
|
+
/** One compiler session shared by every AST-backed rule in a check. */
|
|
24
|
+
readonly typescriptSession?: TypeScriptSession
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
export interface CheckRule {
|
|
@@ -155,12 +159,21 @@ export async function runRuleRegistry(
|
|
|
155
159
|
): Promise<Diagnostic[]> {
|
|
156
160
|
const rules = [...builtIns, ...validateRulePacks(packs).flatMap((pack) => pack.rules)]
|
|
157
161
|
const out: Diagnostic[] = []
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
const ownsTypeScriptSession =
|
|
163
|
+
ctx.typescriptSession === undefined &&
|
|
164
|
+
ctx.project.check.unsupportedTypeScriptVersion === undefined &&
|
|
165
|
+
rules.some((rule) => isBuiltInCode(rule.code) || rule.code.startsWith("NF-"))
|
|
166
|
+
if (ownsTypeScriptSession) retainRuleTypeScript(ctx)
|
|
167
|
+
try {
|
|
168
|
+
for (const rule of rules) {
|
|
169
|
+
if (isBuiltInCode(rule.code) === false && rule.code.startsWith("NF-")) {
|
|
170
|
+
throw new Error(`application rule pack code ${rule.code} uses the reserved NF- prefix`)
|
|
171
|
+
}
|
|
172
|
+
const findings = await rule.scan(ctx)
|
|
173
|
+
out.push(...findings)
|
|
161
174
|
}
|
|
162
|
-
|
|
163
|
-
|
|
175
|
+
} finally {
|
|
176
|
+
if (ownsTypeScriptSession) await releaseRuleTypeScript(ctx)
|
|
164
177
|
}
|
|
165
178
|
return out
|
|
166
179
|
}
|
package/src/rules/islands.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type * as TSApi from "typescript"
|
|
2
2
|
import { type Diagnostic, diagnostic } from "../diagnostics.ts"
|
|
3
|
-
import {
|
|
3
|
+
import { scriptKindOf } from "../internal/script-kind.ts"
|
|
4
4
|
import type { CheckRule, SourceIndex } from "./index.ts"
|
|
5
|
+
import { loadRuleTypeScript } from "./typescript.ts"
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* NF-C020 - the island cleanup lint. An island enhancer (`@nifrajs/web/islands`) runs once and must
|
|
@@ -43,8 +44,13 @@ function parsedTree(
|
|
|
43
44
|
files.set(file, null)
|
|
44
45
|
return undefined
|
|
45
46
|
}
|
|
46
|
-
const
|
|
47
|
-
|
|
47
|
+
const tree = ts.createSourceFile(
|
|
48
|
+
file,
|
|
49
|
+
source,
|
|
50
|
+
ts.ScriptTarget.Latest,
|
|
51
|
+
true,
|
|
52
|
+
scriptKindOf(ts, file),
|
|
53
|
+
)
|
|
48
54
|
files.set(file, { tree })
|
|
49
55
|
return tree
|
|
50
56
|
}
|
|
@@ -126,7 +132,9 @@ export const islandCleanupRule: CheckRule = {
|
|
|
126
132
|
code: "NF-C020",
|
|
127
133
|
title: "Island enhancer cleanup check",
|
|
128
134
|
async scan(ctx) {
|
|
129
|
-
const
|
|
135
|
+
const loaded = await loadRuleTypeScript(ctx)
|
|
136
|
+
if (loaded.diagnostics !== undefined) return loaded.diagnostics
|
|
137
|
+
const ts = loaded.compiler
|
|
130
138
|
if (ts === undefined) {
|
|
131
139
|
return [
|
|
132
140
|
diagnostic({
|