@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/legacy.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { AssuranceConfig, AssuranceReport } from "@nifrajs/core/assurance"
|
|
|
5
5
|
import { type ProjectEvidenceSnapshot, snapshotProjectEvidence } from "@nifrajs/core/evidence"
|
|
6
6
|
import { SINGLE_COPY_REGISTER_SPECIFIER } from "@nifrajs/core/single-copy"
|
|
7
7
|
import type { CapabilityProjectReport } from "../capabilities-tool.ts"
|
|
8
|
-
import type { SourceFinding, StaticRouteFinding } from "../check-scan.ts"
|
|
8
|
+
import type { SourceFinding, StaticRouteFinding, TypedClientFinding } from "../check-scan.ts"
|
|
9
9
|
import {
|
|
10
10
|
IDENT,
|
|
11
11
|
movedExportSites,
|
|
@@ -22,7 +22,11 @@ import {
|
|
|
22
22
|
} from "../diagnostics.ts"
|
|
23
23
|
import type { DuplicateInstallFinding } from "../doctor.ts"
|
|
24
24
|
import { codeUnitOrder } from "../internal/code-unit-order.ts"
|
|
25
|
+
import { createWebAppProbe, withApiKindHint } from "../internal/nifra-project.ts"
|
|
25
26
|
import { unsupportedTypeScriptDiagnostic } from "../internal/typescript-import.ts"
|
|
27
|
+
import { zoneFolderOf } from "../typecheck-coverage.ts"
|
|
28
|
+
import { versionDriftMessage } from "../version-drift.ts"
|
|
29
|
+
import { MISSING_EXPECTATION_REASON } from "./comment-markers.ts"
|
|
26
30
|
import type { CheckRule, RuleContext } from "./index.ts"
|
|
27
31
|
|
|
28
32
|
/** Legacy rule name → stable NF- code. Exported so `nifra.check.json` rule overrides can be keyed
|
|
@@ -89,13 +93,13 @@ export const LEGACY_RULE_ORDER: readonly string[] = Object.freeze([
|
|
|
89
93
|
])
|
|
90
94
|
|
|
91
95
|
const FETCH_HINT =
|
|
92
|
-
"hand-rolled fetch() to your own API - call it through client<typeof app> (from @nifrajs/client) so the compiler catches drift"
|
|
96
|
+
"hand-rolled fetch() to your own API - call it through client<typeof app> (from @nifrajs/client) so the compiler catches drift; for a call no typed client covers (a foreign handler, a static file), add a `// nifra-expect NF-C002: <reason>` comment"
|
|
93
97
|
const STREAM_HINT =
|
|
94
|
-
"hand-rolled EventSource/WebSocket to your own API - subscribe through client<typeof app> (`.subscribe()` for `app.sse()` routes, `.ws()` for `app.ws()` routes) so the compiler catches drift"
|
|
98
|
+
"hand-rolled EventSource/WebSocket to your own API - subscribe through client<typeof app> (`.subscribe()` for `app.sse()` routes, `.ws()` for `app.ws()` routes) so the compiler catches drift; for a stream no typed client covers, add a `// nifra-expect NF-C002: <reason>` comment"
|
|
95
99
|
const SERVER_IMPORT_HINT =
|
|
96
100
|
"server-only code in browser code (a route's frontend half, frontend/ or shared/) - reach it from the route's backend half (x.backend.ts) or a *.fn.ts server function; a dynamic import() is bundled too"
|
|
97
101
|
const RESPONSE_ROUTE_HINT =
|
|
98
|
-
"route handler returns a raw Response - the typed client infers `data: never`, so drift detection is lost for this route. Return a plain object (it's serialized for you); for a stream use a typed SSE route (`app.sse(...)`), which keeps typed events; or, if a raw Response is intended (file/redirect), add `{ response: t.… }` or a `// nifra-expect
|
|
102
|
+
"route handler returns a raw Response - the typed client infers `data: never`, so drift detection is lost for this route. Return a plain object (it's serialized for you); for a stream use a typed SSE route (`app.sse(...)`), which keeps typed events; or, if a raw Response is intended (file/redirect), add `{ response: t.… }` or a `// nifra-expect NF-C007: <reason>` comment to mark it and silence this"
|
|
99
103
|
const PIPELINE_DOC_HINT =
|
|
100
104
|
"nifra runs one bundler per phase - `vitePlugins` feed Vite, `clientPlugins`/`serverPlugins` feed Bun, and the file `nifra build` imports the adapter from is bundled into the server. See the Gotchas section of the Dev & HMR guide."
|
|
101
105
|
const UNDECLARED_DEP_HINT =
|
|
@@ -124,6 +128,7 @@ interface LegacyFields {
|
|
|
124
128
|
readonly message: string
|
|
125
129
|
readonly evidence?: readonly string[]
|
|
126
130
|
readonly chain?: readonly string[]
|
|
131
|
+
readonly modules?: readonly string[]
|
|
127
132
|
readonly fix?: string
|
|
128
133
|
/** A recipe for this one finding, when the rule's findings are not all mechanically fixable. */
|
|
129
134
|
readonly recipe?: Diagnostic["fix"]
|
|
@@ -163,6 +168,7 @@ function legacyDiagnostic(rule: string, fields: LegacyFields): Diagnostic {
|
|
|
163
168
|
...(fields.fix === undefined ? {} : { fix: fields.fix }),
|
|
164
169
|
...(fields.suggestion === undefined ? {} : { suggestion: fields.suggestion }),
|
|
165
170
|
...(fields.chain === undefined ? {} : { chain: fields.chain }),
|
|
171
|
+
...(fields.modules === undefined ? {} : { modules: fields.modules }),
|
|
166
172
|
...(fields.includeCode === true ? { includeCode: true } : {}),
|
|
167
173
|
}
|
|
168
174
|
return diagnosticWithCompatibility(canonical, compatibility)
|
|
@@ -312,7 +318,7 @@ function responseRouteSuggestion(): DiagnosticSuggestion {
|
|
|
312
318
|
steps: [
|
|
313
319
|
"Prefer returning a plain object from JSON routes; nifra serializes it for you.",
|
|
314
320
|
"For a stream, use a typed SSE route - `app.sse(...)` (or `sse(c, run)` from `@nifrajs/core/server`) - which keeps typed events instead of collapsing the client to `data: never`.",
|
|
315
|
-
"If this route must return a raw Response (redirect, file), declare an explicit response schema, or add a `// nifra-expect
|
|
321
|
+
"If this route must return a raw Response (redirect, file), declare an explicit response schema, or add a `// nifra-expect NF-C007: <reason>` comment above the return to mark it intentional and silence this advisory.",
|
|
316
322
|
],
|
|
317
323
|
}
|
|
318
324
|
}
|
|
@@ -339,6 +345,25 @@ const typecheckRule: CheckRule = {
|
|
|
339
345
|
}),
|
|
340
346
|
)
|
|
341
347
|
}
|
|
348
|
+
if (tc.sourcesWithoutTsconfig !== undefined) {
|
|
349
|
+
findings.push(
|
|
350
|
+
legacyDiagnostic("typecheck", {
|
|
351
|
+
severity: "error",
|
|
352
|
+
file: "tsconfig.json",
|
|
353
|
+
message: `${tc.sourcesWithoutTsconfig} TypeScript file(s) but no tsconfig.json in this directory - the typecheck gate did NOT run`,
|
|
354
|
+
fix: "add a tsconfig.json that includes the project's sources",
|
|
355
|
+
suggestion: {
|
|
356
|
+
kind: "manual",
|
|
357
|
+
title: "Add a tsconfig.json so the contract gate can run",
|
|
358
|
+
steps: [
|
|
359
|
+
"Create tsconfig.json in the directory `nifra check` runs from, with `include` covering the project's TypeScript sources.",
|
|
360
|
+
"In a monorepo, run `nifra check` from each app that has its own tsconfig.json.",
|
|
361
|
+
"Run `nifra_check` again: the typecheck now runs `tsc --noEmit -p tsconfig.json`.",
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
}),
|
|
365
|
+
)
|
|
366
|
+
}
|
|
342
367
|
if (tc.ran && !tc.ok) {
|
|
343
368
|
const lines = (tc.output ?? "").split("\n")
|
|
344
369
|
let matched = false
|
|
@@ -378,35 +403,93 @@ const typecheckRule: CheckRule = {
|
|
|
378
403
|
)
|
|
379
404
|
}
|
|
380
405
|
}
|
|
406
|
+
findings.push(...outsideProgramFindings(tc.outsideProgram ?? []))
|
|
381
407
|
return findings
|
|
382
408
|
},
|
|
383
409
|
}
|
|
384
410
|
|
|
411
|
+
/** One finding per zone folder: the fix (tsconfig `include`) is per folder, not per file. */
|
|
412
|
+
function outsideProgramFindings(files: readonly string[]): Diagnostic[] {
|
|
413
|
+
const byFolder = new Map<string, { readonly first: string; readonly group: string[] }>()
|
|
414
|
+
for (const file of files) {
|
|
415
|
+
const folder = zoneFolderOf(file) ?? file
|
|
416
|
+
const entry = byFolder.get(folder)
|
|
417
|
+
if (entry === undefined) byFolder.set(folder, { first: file, group: [file] })
|
|
418
|
+
else entry.group.push(file)
|
|
419
|
+
}
|
|
420
|
+
return [...byFolder].map(([folder, { first, group }]) => {
|
|
421
|
+
const shown = group.slice(0, 3).join(", ")
|
|
422
|
+
const more = group.length > 3 ? ` and ${group.length - 3} more` : ""
|
|
423
|
+
return legacyDiagnostic("typecheck", {
|
|
424
|
+
severity: "error",
|
|
425
|
+
file: first,
|
|
426
|
+
message: `${group.length} file(s) under ${folder}/ are outside tsc's program, so the typecheck passed without reading them: ${shown}${more}`,
|
|
427
|
+
evidence: group,
|
|
428
|
+
fix: `Add "${folder}" to tsconfig.json "include" (or list the files), then run \`nifra check\` again`,
|
|
429
|
+
suggestion: {
|
|
430
|
+
kind: "manual",
|
|
431
|
+
title: "Bring the folder into the TypeScript program",
|
|
432
|
+
steps: [
|
|
433
|
+
`Add "${folder}" to the "include" array in tsconfig.json.`,
|
|
434
|
+
'Files kept out on purpose: name them in the tsconfig "exclude", or under "typecheck": { "ignore": [...] } in nifra.check.json.',
|
|
435
|
+
"Run `nifra check` again; the newly checked files may report type errors.",
|
|
436
|
+
],
|
|
437
|
+
},
|
|
438
|
+
})
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* One NF-C002 finding. A `// nifra-expect NF-C002: <reason>` comment keeps it in the report as `info`,
|
|
444
|
+
* carrying the comment as evidence; the same comment without a reason silences nothing.
|
|
445
|
+
*/
|
|
446
|
+
function typedClientDiagnostic(
|
|
447
|
+
finding: TypedClientFinding,
|
|
448
|
+
hint: string,
|
|
449
|
+
kind: string,
|
|
450
|
+
suggestion: DiagnosticSuggestion,
|
|
451
|
+
): Diagnostic {
|
|
452
|
+
const reason = finding.expected?.reason
|
|
453
|
+
if (reason !== undefined) {
|
|
454
|
+
return legacyDiagnostic("typed-client", {
|
|
455
|
+
severity: "info",
|
|
456
|
+
file: finding.file,
|
|
457
|
+
line: finding.line,
|
|
458
|
+
message: `${finding.snippet} - ${kind} is expected here`,
|
|
459
|
+
evidence: [`nifra-expect NF-C002: ${reason}`],
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
const unreasoned = finding.expected === undefined ? "" : `; ${MISSING_EXPECTATION_REASON}`
|
|
463
|
+
return legacyDiagnostic("typed-client", {
|
|
464
|
+
severity: "error",
|
|
465
|
+
file: finding.file,
|
|
466
|
+
line: finding.line,
|
|
467
|
+
message: `${finding.snippet} - ${hint}${unreasoned}`,
|
|
468
|
+
fix: hint,
|
|
469
|
+
suggestion,
|
|
470
|
+
})
|
|
471
|
+
}
|
|
472
|
+
|
|
385
473
|
const typedClientRule: CheckRule = {
|
|
386
474
|
code: LEGACY_RULE_CODES["typed-client"]!,
|
|
387
475
|
title: TITLES["typed-client"]!,
|
|
388
476
|
async scan(ctx) {
|
|
389
477
|
const routes = staticRouteMap(ctx.project.routes)
|
|
390
|
-
const fetchFindings = [...ctx.project.sourceFindings.fetches]
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const streamFindings = [...ctx.project.sourceFindings.streams]
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
message: `${finding.snippet} - ${STREAM_HINT}`,
|
|
406
|
-
fix: STREAM_HINT,
|
|
407
|
-
suggestion: ownStreamSuggestion(),
|
|
408
|
-
}),
|
|
409
|
-
)
|
|
478
|
+
const fetchFindings = [...ctx.project.sourceFindings.fetches]
|
|
479
|
+
.sort(bySite)
|
|
480
|
+
.map((finding) =>
|
|
481
|
+
typedClientDiagnostic(
|
|
482
|
+
finding,
|
|
483
|
+
FETCH_HINT,
|
|
484
|
+
"own-API fetch",
|
|
485
|
+
ownFetchSuggestion(finding, routes),
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
const streamFindings = [...ctx.project.sourceFindings.streams]
|
|
489
|
+
.sort(bySite)
|
|
490
|
+
.map((finding) =>
|
|
491
|
+
typedClientDiagnostic(finding, STREAM_HINT, "own-API stream", ownStreamSuggestion()),
|
|
492
|
+
)
|
|
410
493
|
return [...fetchFindings, ...streamFindings].sort(
|
|
411
494
|
(a, b) => codeUnitOrder(a.file ?? "", b.file ?? "") || (a.line ?? 0) - (b.line ?? 0),
|
|
412
495
|
)
|
|
@@ -468,19 +551,31 @@ const serverOnlyImportRule: CheckRule = {
|
|
|
468
551
|
code: LEGACY_RULE_CODES["server-only-import"]!,
|
|
469
552
|
title: TITLES["server-only-import"]!,
|
|
470
553
|
async scan(ctx) {
|
|
471
|
-
|
|
554
|
+
const inWebApp = createWebAppProbe(ctx.root)
|
|
555
|
+
const findings: Diagnostic[] = []
|
|
556
|
+
// No component module got a receipt, so each is judged as an ordinary import: say why.
|
|
557
|
+
const components = ctx.project.check.componentCheckUnsupported
|
|
558
|
+
if (components !== undefined)
|
|
559
|
+
findings.push(unsupportedTypeScriptDiagnostic(components.version, components.message))
|
|
560
|
+
for (const finding of [...ctx.project.importGraph].sort(bySite)) {
|
|
472
561
|
const chain = finding.chain
|
|
473
562
|
const sink = chain[chain.length - 1] ?? finding.specifier
|
|
474
|
-
|
|
563
|
+
const dir = finding.file.slice(0, Math.max(finding.file.lastIndexOf("/"), 0))
|
|
564
|
+
const found = legacyDiagnostic("server-only-import", {
|
|
475
565
|
severity: "error",
|
|
476
566
|
file: finding.file,
|
|
477
567
|
line: finding.line,
|
|
478
|
-
message:
|
|
568
|
+
message: (inWebApp(dir) ? (text: string) => text : withApiKindHint)(
|
|
569
|
+
`${finding.snippet} - server-only "${sink}" reaches the browser bundle via ${chain.join(" → ")}${finding.fallback ? " (direct edge - couldn't resolve the transitive chain precisely)" : ""}; ${SERVER_IMPORT_HINT}`,
|
|
570
|
+
),
|
|
479
571
|
fix: SERVER_IMPORT_HINT,
|
|
480
572
|
chain,
|
|
573
|
+
modules: finding.modules,
|
|
481
574
|
suggestion: serverImportSuggestion(finding.specifier, chain, finding.fallback),
|
|
482
575
|
})
|
|
483
|
-
|
|
576
|
+
findings.push(found)
|
|
577
|
+
}
|
|
578
|
+
return findings
|
|
484
579
|
},
|
|
485
580
|
}
|
|
486
581
|
|
|
@@ -507,6 +602,18 @@ const interpolatedSqlRule: CheckRule = {
|
|
|
507
602
|
)
|
|
508
603
|
}
|
|
509
604
|
for (const finding of [...ctx.project.sourceFindings.interpolatedSql].sort(bySite)) {
|
|
605
|
+
if (finding.expected !== undefined) {
|
|
606
|
+
findings.push(
|
|
607
|
+
legacyDiagnostic("interpolated-sql", {
|
|
608
|
+
severity: "info",
|
|
609
|
+
file: finding.file,
|
|
610
|
+
line: finding.line,
|
|
611
|
+
message: `${finding.snippet} - interpolated SQL is expected here`,
|
|
612
|
+
evidence: [`nifra-expect NF-C006: ${finding.expected}`],
|
|
613
|
+
}),
|
|
614
|
+
)
|
|
615
|
+
continue
|
|
616
|
+
}
|
|
510
617
|
findings.push(
|
|
511
618
|
legacyDiagnostic("interpolated-sql", {
|
|
512
619
|
severity: "error",
|
|
@@ -522,7 +629,7 @@ const interpolatedSqlRule: CheckRule = {
|
|
|
522
629
|
"Pass the value as an argument alongside the statement, so the driver binds it.",
|
|
523
630
|
`Or switch to the driver's tagged template (sql\`… ${SQL_INTERPOLATION_EXAMPLE} …\`): the tag receives substitutions separately and binds them.`,
|
|
524
631
|
"An identifier that genuinely cannot be bound (a table or column name) must be checked against an allowlist you control, never taken from the request.",
|
|
525
|
-
"If the statement is dynamic but every value is already bound (generating `($1),($2),…` for a batch insert), mark it with a `// nifra-expect
|
|
632
|
+
"If the statement is dynamic but every value is already bound (generating `($1),($2),…` for a batch insert), mark it with a `// nifra-expect NF-C006: <reason>` comment on the line above - the reason is required.",
|
|
526
633
|
],
|
|
527
634
|
},
|
|
528
635
|
}),
|
|
@@ -556,31 +663,61 @@ function copyLines(finding: DuplicateInstallFinding): string[] {
|
|
|
556
663
|
)
|
|
557
664
|
}
|
|
558
665
|
|
|
559
|
-
|
|
560
|
-
|
|
666
|
+
/** One fix for duplicates that share a cause and topology, so a linked sibling repository that
|
|
667
|
+
* duplicates every package reads as one finding instead of one paragraph per package. */
|
|
668
|
+
function duplicateSuggestion(group: readonly DuplicateInstallFinding[]): DiagnosticSuggestion {
|
|
669
|
+
const names = group.map((finding) => `"${finding.package}"`).join(", ")
|
|
561
670
|
return {
|
|
562
671
|
kind: "manual",
|
|
563
|
-
title:
|
|
672
|
+
title:
|
|
673
|
+
group.length === 1
|
|
674
|
+
? `Resolve ${group[0]?.package} to one loaded copy`
|
|
675
|
+
: `Resolve each of ${group.length} packages to one loaded copy`,
|
|
564
676
|
diff: [
|
|
565
677
|
"package.json:",
|
|
566
|
-
` "nifra": { "singleCopy": [
|
|
678
|
+
` "nifra": { "singleCopy": [${names}] }`,
|
|
567
679
|
"bunfig.toml:",
|
|
568
680
|
` preload = ["${SINGLE_COPY_REGISTER_SPECIFIER}"]`,
|
|
569
681
|
" [test]",
|
|
570
682
|
` preload = ["${SINGLE_COPY_REGISTER_SPECIFIER}"]`,
|
|
571
683
|
].join("\n"),
|
|
572
684
|
steps: [
|
|
573
|
-
...
|
|
574
|
-
...(finding
|
|
575
|
-
|
|
576
|
-
|
|
685
|
+
...group.flatMap(copyLines).map((line) => `Copy: ${line}`),
|
|
686
|
+
...group.flatMap((finding) =>
|
|
687
|
+
finding.provenance === undefined
|
|
688
|
+
? []
|
|
689
|
+
: [
|
|
690
|
+
`Planted links${group.length === 1 ? "" : ` (${finding.package})`}: ${finding.provenance} Try this before the fixes below.`,
|
|
691
|
+
],
|
|
692
|
+
),
|
|
577
693
|
"Fix 1 - deduplicate: align workspace dependency and peer ranges on one compatible version, remove stale nested installs, and reinstall from the workspace root so every importer resolves one physical copy.",
|
|
578
|
-
`Fix 2 - declare single-copy: apply the package.json and bunfig.toml config printed above; nifra then rewrites every duplicate to this app's copy.${finding.cause === "version-skew" ? " A declaration only covers same-version duplicates, so fix 1's range alignment must land first." : ""}`,
|
|
694
|
+
`Fix 2 - declare single-copy: apply the package.json and bunfig.toml config printed above; nifra then rewrites every duplicate to this app's copy.${group.some((finding) => finding.cause === "version-skew") ? " A declaration only covers same-version duplicates, so fix 1's range alignment must land first." : ""}`,
|
|
579
695
|
"Re-run `nifra check`; the gate stays failing until one fix lands.",
|
|
580
696
|
],
|
|
581
697
|
}
|
|
582
698
|
}
|
|
583
699
|
|
|
700
|
+
function duplicateDiagnostic(group: readonly DuplicateInstallFinding[]): Diagnostic | undefined {
|
|
701
|
+
const first = group[0]
|
|
702
|
+
if (first === undefined) return undefined
|
|
703
|
+
const context = `${first.topology === undefined ? "" : `. Topology: ${first.topology}`}${first.scope === undefined ? "" : `. Scope: ${first.scope}`}`
|
|
704
|
+
const skew = first.cause === "version-skew"
|
|
705
|
+
const names = group.map((finding) => finding.package)
|
|
706
|
+
return legacyDiagnostic("duplicate-install", {
|
|
707
|
+
severity: "error",
|
|
708
|
+
message:
|
|
709
|
+
group.length === 1
|
|
710
|
+
? `${first.package} identity preflight found ${first.cause} - ${first.explanation}${context}`
|
|
711
|
+
: `${group.length} packages share one identity preflight finding, ${first.cause} - ${names.join(", ")} ${skew ? "each resolve to multiple versions and physical paths" : "are each loaded from more than one physical path"}, so module state and symbols are not shared${context}`,
|
|
712
|
+
fix:
|
|
713
|
+
group.length === 1 || skew
|
|
714
|
+
? first.remediation
|
|
715
|
+
: `Deduplicate so each resolves to a single physical path, or declare them single-copy: add "nifra": { "singleCopy": [${names.map((name) => `"${name}"`).join(", ")}] } to package.json and preload "${SINGLE_COPY_REGISTER_SPECIFIER}" from bunfig.toml.`,
|
|
716
|
+
evidence: group.flatMap(copyLines),
|
|
717
|
+
suggestion: duplicateSuggestion(group),
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
|
|
584
721
|
const doctorRules: readonly CheckRule[] = Object.freeze([
|
|
585
722
|
{
|
|
586
723
|
code: LEGACY_RULE_CODES["undeclared-dependency"]!,
|
|
@@ -610,20 +747,14 @@ const doctorRules: readonly CheckRule[] = Object.freeze([
|
|
|
610
747
|
async scan(ctx: RuleContext) {
|
|
611
748
|
const doctor = ctx.project.packages.doctor
|
|
612
749
|
if (!doctor.ran) return []
|
|
613
|
-
|
|
750
|
+
// Duplicates with one cause and one layout (every package a linked sibling repo installs again)
|
|
751
|
+
// have one fix, so they are one finding; `identityPreflight` in --json keeps each package.
|
|
752
|
+
const groups = new Map<string, DuplicateInstallFinding[]>()
|
|
614
753
|
for (const finding of doctor.duplicateInstalls) {
|
|
615
|
-
const
|
|
616
|
-
|
|
617
|
-
legacyDiagnostic("duplicate-install", {
|
|
618
|
-
severity: "error",
|
|
619
|
-
message: `${finding.package} identity preflight found ${finding.cause} - ${finding.explanation}${finding.topology === undefined ? "" : `. Topology: ${finding.topology}`}${finding.scope === undefined ? "" : `. Scope: ${finding.scope}`}`,
|
|
620
|
-
fix: finding.remediation,
|
|
621
|
-
evidence: copies,
|
|
622
|
-
suggestion: duplicateSuggestion(finding),
|
|
623
|
-
}),
|
|
624
|
-
)
|
|
754
|
+
const key = [finding.cause, finding.topology ?? "", finding.scope ?? ""].join("\0")
|
|
755
|
+
groups.set(key, [...(groups.get(key) ?? []), finding])
|
|
625
756
|
}
|
|
626
|
-
return
|
|
757
|
+
return [...groups.values()].flatMap((group) => duplicateDiagnostic(group) ?? [])
|
|
627
758
|
},
|
|
628
759
|
},
|
|
629
760
|
{
|
|
@@ -658,6 +789,26 @@ const doctorRules: readonly CheckRule[] = Object.freeze([
|
|
|
658
789
|
},
|
|
659
790
|
])
|
|
660
791
|
|
|
792
|
+
/** Read from the doctor result check already collected; it spawns nothing and edits nothing. */
|
|
793
|
+
const versionDriftRule: CheckRule = {
|
|
794
|
+
code: "NF-D002",
|
|
795
|
+
title: "Mixed nifra versions",
|
|
796
|
+
async scan(ctx) {
|
|
797
|
+
const drift = ctx.project.packages.doctor.versionDrift
|
|
798
|
+
if (drift === undefined) return []
|
|
799
|
+
return [
|
|
800
|
+
diagnostic({
|
|
801
|
+
code: "NF-D002",
|
|
802
|
+
severity: "info",
|
|
803
|
+
message: versionDriftMessage(drift),
|
|
804
|
+
evidence: drift.versions.map(({ version }) => version),
|
|
805
|
+
fix: { recipe: "upgrade.align-versions", command: `nifra upgrade ${drift.highest}` },
|
|
806
|
+
verify: "nifra doctor",
|
|
807
|
+
}),
|
|
808
|
+
]
|
|
809
|
+
},
|
|
810
|
+
}
|
|
811
|
+
|
|
661
812
|
const pipelineRule: CheckRule = {
|
|
662
813
|
code: LEGACY_RULE_CODES.pipeline!,
|
|
663
814
|
title: TITLES.pipeline!,
|
|
@@ -951,7 +1102,9 @@ const contractRule: CheckRule = {
|
|
|
951
1102
|
}),
|
|
952
1103
|
]
|
|
953
1104
|
}
|
|
954
|
-
|
|
1105
|
+
// `vacuous` describes the routes as they are now, so it only stands in for drift when the lock has
|
|
1106
|
+
// none: a lock whose schemas were all deleted is drift, not an empty contract.
|
|
1107
|
+
if (contract.vacuous && contract.error === undefined && contract.diagnostics.length === 0) {
|
|
955
1108
|
return [
|
|
956
1109
|
diagnostic({
|
|
957
1110
|
code: LEGACY_RULE_CODES["contract-drift"]!,
|
|
@@ -995,6 +1148,7 @@ export const legacyRules: readonly CheckRule[] = Object.freeze([
|
|
|
995
1148
|
interpolatedSqlRule,
|
|
996
1149
|
responseRouteRule,
|
|
997
1150
|
...doctorRules,
|
|
1151
|
+
versionDriftRule,
|
|
998
1152
|
pipelineRule,
|
|
999
1153
|
serverManifestRule,
|
|
1000
1154
|
manifestRule,
|