@nifrajs/cli 4.0.2 → 5.1.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 +10 -2
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +9 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +8 -23
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -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 +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +218 -44
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +94 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/dev-feed-tool.ts +112 -1
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/rules/data-guard.ts
CHANGED
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
* The server enforces both at runtime, on every request: data a loader or action returns without an
|
|
5
5
|
* output schema fails the request, and an output schema naming a sensitive field without
|
|
6
6
|
* `t.declassified` fails the route when it loads. These report what source alone shows, before a
|
|
7
|
-
* request ever does. NF-C030 is
|
|
7
|
+
* request ever does. NF-C030 is an error when the loader's own body returns a literal the runtime
|
|
8
|
+
* refuses (an object, array, string, number or boolean) and a warning otherwise, since only running
|
|
9
|
+
* the loader proves what an expression evaluates to.
|
|
8
10
|
*/
|
|
11
|
+
import { realpathSync } from "node:fs"
|
|
12
|
+
import { isAbsolute, join, relative } from "node:path"
|
|
9
13
|
import { isSensitiveFieldName } from "@nifrajs/web/zones"
|
|
10
14
|
import { codePositionMask } from "../check-scan.ts"
|
|
11
15
|
import { type Diagnostic, diagnostic } from "../diagnostics.ts"
|
|
12
16
|
import { codeUnitOrder } from "../internal/code-unit-order.ts"
|
|
17
|
+
import { createSpecifierResolver } from "../internal/project-specifiers.ts"
|
|
13
18
|
import type { CheckRule } from "./index.ts"
|
|
14
19
|
import { zonedFiles } from "./zones.ts"
|
|
15
20
|
|
|
@@ -18,7 +23,8 @@ const lineAt = (text: string, index: number): number => text.slice(0, index).spl
|
|
|
18
23
|
const byPlace = (a: Diagnostic, b: Diagnostic): number =>
|
|
19
24
|
codeUnitOrder(a.file ?? "", b.file ?? "") || (a.line ?? 0) - (b.line ?? 0)
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
/** The export statement of `name` in a code-position mask: a declaration or an `export { }` list. */
|
|
27
|
+
export const exported = (mask: string, name: string): RegExpExecArray | null =>
|
|
22
28
|
new RegExp(
|
|
23
29
|
`\\bexport\\s+(?:(?:async\\s+)?function\\s*\\*?\\s*|(?:const|let|var)\\s+)${name}\\b|\\bexport\\s*\\{[^}]*\\b${name}\\b[^}]*\\}`,
|
|
24
30
|
).exec(mask)
|
|
@@ -40,37 +46,129 @@ const closing = (mask: string, open: number): number => {
|
|
|
40
46
|
return mask.length
|
|
41
47
|
}
|
|
42
48
|
|
|
49
|
+
const opening = (mask: string, close: number): number => {
|
|
50
|
+
let depth = 0
|
|
51
|
+
for (let i = close; i >= 0; i--) {
|
|
52
|
+
const c = mask[i]
|
|
53
|
+
if (c === ")" || c === "}" || c === "]") depth++
|
|
54
|
+
else if (c === "(" || c === "{" || c === "[") {
|
|
55
|
+
depth--
|
|
56
|
+
if (depth === 0) return i
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const CONTROL_HEAD = /(?:^|[^\w$.])(?:if|for(?:\s+await)?|while|catch|switch|with)\s*$/
|
|
63
|
+
const BLOCK_WORD = /(?:^|[^\w$.])(?:else|try|finally|do)$/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether the `{` at `open` starts a statement block of the enclosing function: after a control
|
|
67
|
+
* head, `else`/`try`/`finally`/`do`, or a statement boundary. A function, method, class, object or
|
|
68
|
+
* `case` body is not one, so a `return` inside it is never counted as the outer function's.
|
|
69
|
+
*/
|
|
70
|
+
function isStatementBlock(mask: string, open: number): boolean {
|
|
71
|
+
let p = open - 1
|
|
72
|
+
while (p >= 0 && /\s/.test(mask[p] ?? "")) p--
|
|
73
|
+
const prev = mask[p]
|
|
74
|
+
if (prev === undefined || prev === ";" || prev === "{" || prev === "}") return true
|
|
75
|
+
if (prev === ")") {
|
|
76
|
+
const head = opening(mask, p)
|
|
77
|
+
return CONTROL_HEAD.test(mask.slice(Math.max(0, head - 16), head))
|
|
78
|
+
}
|
|
79
|
+
return BLOCK_WORD.test(mask.slice(Math.max(0, p - 8), p + 1))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Offsets of the `return`s that belong to the block at `open` itself, not to a nested function. */
|
|
83
|
+
function ownReturns(mask: string, open: number, close: number): number[] {
|
|
84
|
+
const own: boolean[] = []
|
|
85
|
+
const found: number[] = []
|
|
86
|
+
for (let i = open; i < close; i++) {
|
|
87
|
+
const c = mask[i]
|
|
88
|
+
if (c === "{") own.push(i === open || (own.at(-1) === true && isStatementBlock(mask, i)))
|
|
89
|
+
else if (c === "}") own.pop()
|
|
90
|
+
else if (
|
|
91
|
+
c === "r" &&
|
|
92
|
+
own.at(-1) === true &&
|
|
93
|
+
mask.startsWith("return", i) &&
|
|
94
|
+
!/[\w$.]/.test(mask[i - 1] ?? "") &&
|
|
95
|
+
!/[\w$]/.test(mask[i + 6] ?? "")
|
|
96
|
+
) {
|
|
97
|
+
found.push(i)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return found
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const NUMBER = /^-?(?:0[xob][\da-f_]+|(?:\d[\d_]*\.?[\d_]*|\.\d[\d_]*)(?:e[+-]?\d+)?)n?$/i
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether the expression at `start` is one literal the runtime refuses without an output schema: an
|
|
107
|
+
* object, array, string, template, number or boolean, alone, not part of a larger expression.
|
|
108
|
+
*/
|
|
109
|
+
function isDataLiteral(js: string, mask: string, start: number): boolean {
|
|
110
|
+
let from = start
|
|
111
|
+
let to = expressionEnd(mask, start)
|
|
112
|
+
for (;;) {
|
|
113
|
+
// Skip on the source: the mask blanks a string literal, quotes included.
|
|
114
|
+
while (from < to && /\s/.test(js[from] ?? "")) from++
|
|
115
|
+
while (to > from && /\s/.test(js[to - 1] ?? "")) to--
|
|
116
|
+
if (from >= to) return false
|
|
117
|
+
if (mask[from] !== "(" || closing(mask, from) !== to - 1) break
|
|
118
|
+
from++
|
|
119
|
+
to--
|
|
120
|
+
}
|
|
121
|
+
const first = mask[from]
|
|
122
|
+
if (first === "{" || first === "[") return closing(mask, from) === to - 1
|
|
123
|
+
const text = mask.slice(from, to)
|
|
124
|
+
if (text.trim() === "") {
|
|
125
|
+
const quote = js[from] ?? ""
|
|
126
|
+
return `"'\``.includes(quote) && js[to - 1] === quote
|
|
127
|
+
}
|
|
128
|
+
return NUMBER.test(text) || text === "true" || text === "false"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
type DataReturn = "none" | "possible" | "proven"
|
|
132
|
+
|
|
43
133
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
134
|
+
* What the function exported as `name` returns, read from its type-stripped source. `proven` when
|
|
135
|
+
* its expression body, or a `return` of its own body, is a data literal; `possible` when an
|
|
136
|
+
* expression body or some `return <value>` is not a redirect or a status; `none` otherwise.
|
|
46
137
|
*/
|
|
47
|
-
function
|
|
138
|
+
function dataReturn(js: string, name: string): DataReturn {
|
|
48
139
|
const mask = codePositionMask(js)
|
|
49
140
|
const found = exported(mask, name)
|
|
50
141
|
if (found === null || found[0].startsWith("export {") || found[0].startsWith("export{")) {
|
|
51
|
-
return
|
|
142
|
+
return "possible" // declared elsewhere in the file and re-exported: assume it does
|
|
52
143
|
}
|
|
53
144
|
const params = mask.indexOf("(", found.index + found[0].length)
|
|
54
|
-
if (params === -1) return
|
|
145
|
+
if (params === -1) return "possible"
|
|
55
146
|
let at = closing(mask, params) + 1
|
|
56
147
|
while (/\s/.test(mask[at] ?? "")) at++
|
|
57
148
|
if (mask.startsWith("=>", at)) {
|
|
58
149
|
at += 2
|
|
59
|
-
while (/\s/.test(
|
|
60
|
-
if (mask[at] !== "{")
|
|
150
|
+
while (/\s/.test(js[at] ?? "")) at++
|
|
151
|
+
if (mask[at] !== "{") {
|
|
152
|
+
if (CONTROL_FLOW.test(js.slice(at, at + 40))) return "none"
|
|
153
|
+
return isDataLiteral(js, mask, at) ? "proven" : "possible"
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (mask[at] !== "{") return "possible"
|
|
157
|
+
const end = closing(mask, at)
|
|
158
|
+
for (const index of ownReturns(mask, at, end)) {
|
|
159
|
+
if (isDataLiteral(js, mask, index + "return".length)) return "proven"
|
|
61
160
|
}
|
|
62
|
-
|
|
63
|
-
const body = mask.slice(at, closing(mask, at))
|
|
161
|
+
const body = mask.slice(at, end)
|
|
64
162
|
for (const match of body.matchAll(/\breturn\b/g)) {
|
|
65
163
|
const from = at + match.index + "return".length
|
|
66
164
|
const rest = js.slice(from, from + 40).replace(/^[ \t]+/, "")
|
|
67
|
-
if (!CONTROL_FLOW.test(rest)) return
|
|
165
|
+
if (!CONTROL_FLOW.test(rest)) return "possible"
|
|
68
166
|
}
|
|
69
|
-
return
|
|
167
|
+
return "none"
|
|
70
168
|
}
|
|
71
169
|
|
|
72
170
|
/** Where an expression starting at `start` ends: a `,`, `;` or closer at its own depth. */
|
|
73
|
-
function expressionEnd(mask: string, start: number): number {
|
|
171
|
+
export function expressionEnd(mask: string, start: number): number {
|
|
74
172
|
let depth = 0
|
|
75
173
|
for (let i = start; i < mask.length; i++) {
|
|
76
174
|
const c = mask[i]
|
|
@@ -135,6 +233,8 @@ export interface MissingOutputSchema {
|
|
|
135
233
|
readonly name: "loader" | "action"
|
|
136
234
|
readonly schema: "loaderOutput" | "actionOutput"
|
|
137
235
|
readonly line: number
|
|
236
|
+
/** Its own body returns a data literal, so every request that reaches that return fails. */
|
|
237
|
+
readonly proven: boolean
|
|
138
238
|
}
|
|
139
239
|
|
|
140
240
|
let transpiler: Bun.Transpiler | undefined
|
|
@@ -156,11 +256,40 @@ export function missingOutputSchemas(content: string): MissingOutputSchema[] {
|
|
|
156
256
|
} catch {
|
|
157
257
|
js = content
|
|
158
258
|
}
|
|
159
|
-
|
|
259
|
+
const returns = dataReturn(js, name)
|
|
260
|
+
if (returns !== "none") {
|
|
261
|
+
missing.push({ name, schema, line: lineAt(content, fn.index), proven: returns === "proven" })
|
|
262
|
+
}
|
|
160
263
|
}
|
|
161
264
|
return missing
|
|
162
265
|
}
|
|
163
266
|
|
|
267
|
+
const GENERATED_LINE = /^[ \t]*\/\/ nifra:generated [0-9a-f]{8}\b/
|
|
268
|
+
const OPEN_DRAFT = /\bt\.json\(\)\s*\/\*\s*(any|unknown) in the source type\s*\*\//g
|
|
269
|
+
|
|
270
|
+
/** Where a generated output schema (the export under a `nifra:generated` line) admits any value: the
|
|
271
|
+
* note `nifra schemas` writes beside a `t.json()` drafted from `any` or `unknown`. */
|
|
272
|
+
export function openDraftLines(content: string): Array<{ line: number; type: string }> {
|
|
273
|
+
const found: Array<{ line: number; type: string }> = []
|
|
274
|
+
let marked = false
|
|
275
|
+
let generated = false
|
|
276
|
+
for (const [index, text] of content.split("\n").entries()) {
|
|
277
|
+
if (GENERATED_LINE.test(text)) {
|
|
278
|
+
marked = true
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
if (/^export\s/.test(text)) {
|
|
282
|
+
generated = marked
|
|
283
|
+
marked = false
|
|
284
|
+
}
|
|
285
|
+
if (!generated) continue
|
|
286
|
+
for (const match of text.matchAll(OPEN_DRAFT)) {
|
|
287
|
+
found.push({ line: index + 1, type: match[1] ?? "any" })
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return found
|
|
291
|
+
}
|
|
292
|
+
|
|
164
293
|
export const missingOutputRule: CheckRule = {
|
|
165
294
|
code: "NF-C030",
|
|
166
295
|
title: "Route data without an output schema",
|
|
@@ -170,19 +299,40 @@ export const missingOutputRule: CheckRule = {
|
|
|
170
299
|
if (classification.zone !== "route-backend") continue
|
|
171
300
|
const content = ctx.sources.read(file)
|
|
172
301
|
if (content === undefined) continue
|
|
173
|
-
for (const { name, schema, line } of missingOutputSchemas(content)) {
|
|
302
|
+
for (const { name, schema, line, proven } of missingOutputSchemas(content)) {
|
|
303
|
+
const returns = proven
|
|
304
|
+
? "It returns a data literal, so every request that reaches that return fails"
|
|
305
|
+
: "Data it returns fails the request"
|
|
174
306
|
findings.push(
|
|
175
307
|
diagnostic({
|
|
176
308
|
code: "NF-C030",
|
|
177
|
-
severity: "warn",
|
|
309
|
+
severity: proven ? "error" : "warn",
|
|
178
310
|
file,
|
|
179
311
|
line,
|
|
180
|
-
message: `${file} exports a ${name} but no ${schema}.
|
|
181
|
-
evidence: [
|
|
312
|
+
message: `${file} exports a ${name} but no ${schema}. ${returns}: everything sent to the browser needs an output schema. Export one: export const ${schema} = t.object({ ... })`,
|
|
313
|
+
evidence: [
|
|
314
|
+
`export: ${name}`,
|
|
315
|
+
`missing: ${schema}`,
|
|
316
|
+
...(proven ? ["returns: a data literal"] : []),
|
|
317
|
+
],
|
|
318
|
+
fix: { recipe: "schemas.draft", command: "nifra fix --code NF-C030" },
|
|
182
319
|
verify: "nifra check",
|
|
183
320
|
}),
|
|
184
321
|
)
|
|
185
322
|
}
|
|
323
|
+
for (const { line, type } of openDraftLines(content)) {
|
|
324
|
+
findings.push(
|
|
325
|
+
diagnostic({
|
|
326
|
+
code: "NF-C030",
|
|
327
|
+
severity: "warn",
|
|
328
|
+
file,
|
|
329
|
+
line,
|
|
330
|
+
message: `${file}'s generated output schema admits any value here, because the type it was drafted from is ${type} at this point: the output guard drops nothing below it. Give that value a type, then run \`nifra schemas --write\`.`,
|
|
331
|
+
evidence: [`draft: t.json() from ${type}`],
|
|
332
|
+
verify: "nifra schemas --check",
|
|
333
|
+
}),
|
|
334
|
+
)
|
|
335
|
+
}
|
|
186
336
|
}
|
|
187
337
|
return findings.sort(byPlace)
|
|
188
338
|
},
|
|
@@ -191,35 +341,170 @@ export const missingOutputRule: CheckRule = {
|
|
|
191
341
|
const OUTPUT_DECLARATION =
|
|
192
342
|
/\bexport\s+(?:const|let|var)\s+(?:loaderOutput|actionOutput)\b[^=]*=|\boutput\s*:/g
|
|
193
343
|
|
|
344
|
+
/** The initializer of the top-level `const`/`let`/`var` named `name`, as an offset just past its `=`. */
|
|
345
|
+
function declarationOf(mask: string, name: string): number | undefined {
|
|
346
|
+
const declaration = new RegExp(
|
|
347
|
+
`(?:^|\\n)(?:export\\s+)?(?:const|let|var)\\s+${name.replaceAll("$", "\\$")}(?![\\w$])[^=]*=`,
|
|
348
|
+
).exec(mask)
|
|
349
|
+
return declaration === null ? undefined : declaration.index + declaration[0].length
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Where the schema an output declaration names is written: the expression itself, or, when it is a
|
|
354
|
+
* bare name, the initializer of the top-level declaration of that name in the same file, followed
|
|
355
|
+
* through aliases.
|
|
356
|
+
*/
|
|
357
|
+
function schemaExpression(mask: string, start: number): { start: number; end: number } {
|
|
358
|
+
let range = { start, end: expressionEnd(mask, start) }
|
|
359
|
+
const seen = new Set<string>()
|
|
360
|
+
for (;;) {
|
|
361
|
+
const name = mask.slice(range.start, range.end).trim()
|
|
362
|
+
if (!/^[A-Za-z_$][\w$]*$/.test(name) || seen.has(name)) return range
|
|
363
|
+
seen.add(name)
|
|
364
|
+
const from = declarationOf(mask, name)
|
|
365
|
+
if (from === undefined) return range
|
|
366
|
+
range = { start: from, end: expressionEnd(mask, from) }
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** A name used as a value: not an object key, a member, a callee, or the object of a member. */
|
|
371
|
+
const VALUE_NAME = /(?<![\w$.])[A-Za-z_$][\w$]*(?![\w$])(?!\s*[:(.])/g
|
|
372
|
+
|
|
373
|
+
/** The module `name` is imported from as a named import, and its name there. */
|
|
374
|
+
function importOf(
|
|
375
|
+
source: SchemaSource,
|
|
376
|
+
name: string,
|
|
377
|
+
): { readonly specifier: string; readonly name: string } | undefined {
|
|
378
|
+
// The mask blanks the specifier string, so it is read from the source past `from`.
|
|
379
|
+
for (const clause of source.mask.matchAll(/\bimport\s+(?:type\s+)?\{([^}]*)\}\s*from\b/g)) {
|
|
380
|
+
const quoted = /^\s*(["'])([^"']+)\1/.exec(
|
|
381
|
+
source.content.slice(clause.index + clause[0].length),
|
|
382
|
+
)
|
|
383
|
+
if (quoted === null) continue
|
|
384
|
+
for (const element of (clause[1] ?? "").split(",")) {
|
|
385
|
+
const [imported, local = imported] = element
|
|
386
|
+
.trim()
|
|
387
|
+
.replace(/^type\s+/, "")
|
|
388
|
+
.split(/\s+as\s+/)
|
|
389
|
+
if (local === name && imported !== undefined)
|
|
390
|
+
return { specifier: quoted[2] ?? "", name: imported }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return undefined
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
interface SchemaSource {
|
|
397
|
+
readonly file: string
|
|
398
|
+
readonly content: string
|
|
399
|
+
readonly mask: string
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
interface SchemaRange {
|
|
403
|
+
readonly source: SchemaSource
|
|
404
|
+
readonly start: number
|
|
405
|
+
readonly end: number
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Every place an output schema is written: its own expression, then each schema it names inside it
|
|
410
|
+
* (`t.object({ user: userSchema })`), followed to this file's top-level declaration of that name or
|
|
411
|
+
* to the project file it is imported from. A schema under `declassified(...)` is not followed.
|
|
412
|
+
*/
|
|
413
|
+
function schemaRanges(
|
|
414
|
+
origin: SchemaSource,
|
|
415
|
+
start: number,
|
|
416
|
+
load: (from: string, specifier: string) => SchemaSource | undefined,
|
|
417
|
+
): SchemaRange[] {
|
|
418
|
+
const ranges: SchemaRange[] = [{ source: origin, ...schemaExpression(origin.mask, start) }]
|
|
419
|
+
const seen = new Set<string>()
|
|
420
|
+
// An array iterator reads the length each step, so ranges pushed below are walked too.
|
|
421
|
+
for (const { source, start: from, end } of ranges) {
|
|
422
|
+
const declassified: Array<readonly [number, number]> = []
|
|
423
|
+
for (const call of source.mask.slice(from, end).matchAll(/\bdeclassified\s*\(/g)) {
|
|
424
|
+
const open = from + call.index + call[0].length - 1
|
|
425
|
+
declassified.push([open, closing(source.mask, open)])
|
|
426
|
+
}
|
|
427
|
+
for (const reference of source.mask.slice(from, end).matchAll(VALUE_NAME)) {
|
|
428
|
+
const at = from + reference.index
|
|
429
|
+
if (declassified.some(([open, close]) => at > open && at < close)) continue
|
|
430
|
+
const name = reference[0]
|
|
431
|
+
if (seen.has(`${source.file}#${name}`)) continue
|
|
432
|
+
seen.add(`${source.file}#${name}`)
|
|
433
|
+
const local = declarationOf(source.mask, name)
|
|
434
|
+
if (local !== undefined) {
|
|
435
|
+
ranges.push({ source, start: local, end: expressionEnd(source.mask, local) })
|
|
436
|
+
continue
|
|
437
|
+
}
|
|
438
|
+
const imported = importOf(source, name)
|
|
439
|
+
const target = imported === undefined ? undefined : load(source.file, imported.specifier)
|
|
440
|
+
const there =
|
|
441
|
+
target === undefined ? undefined : declarationOf(target.mask, imported?.name ?? "")
|
|
442
|
+
if (target !== undefined && there !== undefined) {
|
|
443
|
+
ranges.push({ source: target, start: there, end: expressionEnd(target.mask, there) })
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return ranges
|
|
448
|
+
}
|
|
449
|
+
|
|
194
450
|
export const sensitiveOutputRule: CheckRule = {
|
|
195
451
|
code: "NF-C031",
|
|
196
452
|
title: "Sensitive field in an output schema",
|
|
197
453
|
async scan(ctx) {
|
|
198
454
|
const findings: Diagnostic[] = []
|
|
455
|
+
const resolve = createSpecifierResolver()
|
|
456
|
+
const realRoot = (() => {
|
|
457
|
+
try {
|
|
458
|
+
return realpathSync(ctx.root)
|
|
459
|
+
} catch {
|
|
460
|
+
return ctx.root
|
|
461
|
+
}
|
|
462
|
+
})()
|
|
463
|
+
const sources = new Map<string, SchemaSource | undefined>()
|
|
464
|
+
const sourceOf = (file: string): SchemaSource | undefined => {
|
|
465
|
+
if (!sources.has(file)) {
|
|
466
|
+
const content = ctx.sources.read(file)
|
|
467
|
+
sources.set(
|
|
468
|
+
file,
|
|
469
|
+
content === undefined ? undefined : { file, content, mask: codePositionMask(content) },
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
return sources.get(file)
|
|
473
|
+
}
|
|
474
|
+
// Only project source: an installed package's schemas are not the app's to rename.
|
|
475
|
+
const load = (from: string, specifier: string): SchemaSource | undefined => {
|
|
476
|
+
const target = resolve(join(ctx.root, from), specifier)
|
|
477
|
+
if (target === undefined) return undefined
|
|
478
|
+
const rel = relative(realRoot, target).replaceAll("\\", "/")
|
|
479
|
+
if (rel.startsWith("../") || isAbsolute(rel) || rel.split("/").includes("node_modules")) {
|
|
480
|
+
return undefined
|
|
481
|
+
}
|
|
482
|
+
return sourceOf(rel)
|
|
483
|
+
}
|
|
484
|
+
// A schema several declarations name, or several routes import, is reported once.
|
|
485
|
+
const reported = new Set<string>()
|
|
199
486
|
for (const { file, classification } of zonedFiles(ctx).files) {
|
|
200
487
|
if (classification.zone !== "route-backend" && classification.zone !== "fn") continue
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
const start
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
}),
|
|
222
|
-
)
|
|
488
|
+
const origin = sourceOf(file)
|
|
489
|
+
if (origin === undefined) continue
|
|
490
|
+
for (const declaration of origin.mask.matchAll(OUTPUT_DECLARATION)) {
|
|
491
|
+
const ranges = schemaRanges(origin, declaration.index + declaration[0].length, load)
|
|
492
|
+
for (const { source, start, end } of ranges) {
|
|
493
|
+
for (const { key, index } of sensitiveKeys(source.content, source.mask, start, end)) {
|
|
494
|
+
if (reported.has(`${source.file}:${index}`)) continue
|
|
495
|
+
reported.add(`${source.file}:${index}`)
|
|
496
|
+
findings.push(
|
|
497
|
+
diagnostic({
|
|
498
|
+
code: "NF-C031",
|
|
499
|
+
severity: "error",
|
|
500
|
+
file: source.file,
|
|
501
|
+
line: lineAt(source.content, index),
|
|
502
|
+
message: `${source.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", ...)`,
|
|
503
|
+
evidence: [`field: ${key}`],
|
|
504
|
+
verify: "nifra check",
|
|
505
|
+
}),
|
|
506
|
+
)
|
|
507
|
+
}
|
|
223
508
|
}
|
|
224
509
|
}
|
|
225
510
|
}
|
|
@@ -227,4 +512,68 @@ export const sensitiveOutputRule: CheckRule = {
|
|
|
227
512
|
},
|
|
228
513
|
}
|
|
229
514
|
|
|
230
|
-
export const
|
|
515
|
+
export const staleGeneratedOutputRule: CheckRule = {
|
|
516
|
+
code: "NF-C033",
|
|
517
|
+
title: "Generated output schema out of date",
|
|
518
|
+
async scan(ctx) {
|
|
519
|
+
const files = zonedFiles(ctx)
|
|
520
|
+
.files.filter((entry) => entry.classification.zone === "route-backend")
|
|
521
|
+
.map((entry) => entry.file)
|
|
522
|
+
.filter((file) => ctx.sources.read(file)?.includes("nifra:generated") === true)
|
|
523
|
+
if (files.length === 0) return []
|
|
524
|
+
const { staleGeneratedSchemas } = await import("../output-schemas.ts")
|
|
525
|
+
const { stale, unchecked, warnings } = await staleGeneratedSchemas(ctx.root, files, (file) =>
|
|
526
|
+
ctx.sources.read(file),
|
|
527
|
+
)
|
|
528
|
+
// A generated schema that could not be compared is reported, never passed silently.
|
|
529
|
+
const notChecked = [
|
|
530
|
+
...warnings.map((warning) =>
|
|
531
|
+
diagnostic({
|
|
532
|
+
code: "NF-C033",
|
|
533
|
+
severity: "warn",
|
|
534
|
+
message: `${files.length} generated output schema(s) were not checked: ${warning}`,
|
|
535
|
+
evidence: files.map((file) => `file: ${file}`),
|
|
536
|
+
verify: "nifra schemas",
|
|
537
|
+
}),
|
|
538
|
+
),
|
|
539
|
+
...unchecked.map((entry) =>
|
|
540
|
+
diagnostic({
|
|
541
|
+
code: "NF-C033",
|
|
542
|
+
severity: "warn",
|
|
543
|
+
file: entry.file,
|
|
544
|
+
line: entry.line,
|
|
545
|
+
message: `${entry.file}'s generated ${entry.schema} was not checked: ${entry.reason ?? "its type could not be read"}`,
|
|
546
|
+
evidence: [`schema: ${entry.schema}`],
|
|
547
|
+
verify: "nifra schemas",
|
|
548
|
+
}),
|
|
549
|
+
),
|
|
550
|
+
]
|
|
551
|
+
return [
|
|
552
|
+
...notChecked,
|
|
553
|
+
...stale.map((entry) => {
|
|
554
|
+
const fn = entry.schema === "loaderOutput" ? "loader" : "action"
|
|
555
|
+
const change =
|
|
556
|
+
entry.edited === true
|
|
557
|
+
? "it was edited by hand since"
|
|
558
|
+
: entry.dropped.length > 0
|
|
559
|
+
? `it now returns ${entry.dropped.join(", ")}, which the schema drops`
|
|
560
|
+
: "what it returns has changed"
|
|
561
|
+
return diagnostic({
|
|
562
|
+
code: "NF-C033",
|
|
563
|
+
severity: "error",
|
|
564
|
+
file: entry.file,
|
|
565
|
+
line: entry.line,
|
|
566
|
+
message: `${entry.file}'s ${entry.schema} was generated from the ${fn}'s return type, and ${change}. Run \`nifra schemas --write\` to regenerate it, or delete its "nifra:generated" line to own it by hand.`,
|
|
567
|
+
evidence: [`schema: ${entry.schema}`, ...entry.dropped.map((path) => `dropped: ${path}`)],
|
|
568
|
+
verify: "nifra schemas",
|
|
569
|
+
})
|
|
570
|
+
}),
|
|
571
|
+
]
|
|
572
|
+
},
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
export const dataGuardRules = Object.freeze([
|
|
576
|
+
missingOutputRule,
|
|
577
|
+
sensitiveOutputRule,
|
|
578
|
+
staleGeneratedOutputRule,
|
|
579
|
+
])
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { retiredRootFiles } from "../app-files.ts"
|
|
2
|
+
import { diagnostic } from "../diagnostics.ts"
|
|
3
|
+
import type { CheckRule } from "./index.ts"
|
|
4
|
+
|
|
5
|
+
/** Every gate that reads backend/app.ts sees nothing in a retired root file, so the check fails here. */
|
|
6
|
+
export const retiredLayoutRule: CheckRule = {
|
|
7
|
+
code: "NF-C038",
|
|
8
|
+
title: "Retired root layout",
|
|
9
|
+
async scan(ctx) {
|
|
10
|
+
return retiredRootFiles(ctx.root).map(([from, to]) =>
|
|
11
|
+
diagnostic({
|
|
12
|
+
code: "NF-C038",
|
|
13
|
+
severity: "error",
|
|
14
|
+
file: from,
|
|
15
|
+
line: 1,
|
|
16
|
+
message: `${from} is the retired root layout; nifra reads it at ${to}. Until it moves, nifra check runs no contract gate on this app, and the commands that load it refuse it. \`nifra migrate layout\` shows the move; \`--write\` makes it`,
|
|
17
|
+
fix: { recipe: "layout.migrate", command: "nifra migrate layout --write" },
|
|
18
|
+
verify: "nifra check --lints-only",
|
|
19
|
+
}),
|
|
20
|
+
)
|
|
21
|
+
},
|
|
22
|
+
}
|