@nifrajs/cli 4.0.2 → 5.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 +3 -0
- 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 +7 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +13 -3
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +20 -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 +75 -22
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1475 -310
- 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 +192 -65
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -5
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +18 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +49 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +16 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +133 -29
- 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/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.map +1 -1
- package/dist/fix-recipes.js +21 -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 +2 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +17 -11
- package/dist/init-agents.js.map +1 -1
- 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 +16 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +396 -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 +2 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -0
- 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 +5 -3
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +55 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +93 -15
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +31 -8
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +37 -7
- 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 +2 -2
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-exec.d.ts +12 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +50 -13
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +17 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +40 -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-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 +8 -2
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +7 -0
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +778 -64
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +94 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +864 -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/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/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 +38 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +10 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +10 -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 +7 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +319 -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 +158 -43
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +24 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +221 -3
- 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 +12 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +281 -68
- 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 +192 -40
- 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 +163 -7
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +12 -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 +69 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +420 -77
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +11 -0
- 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 +42 -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 +54 -6
- package/docs/llms-full.txt +1539 -138
- package/docs/types.json +1456 -206
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +15 -2
- package/src/capabilities-tool.ts +15 -3
- package/src/check-diagnostics.ts +73 -9
- package/src/check-scan.ts +1773 -381
- package/src/check.ts +237 -76
- package/src/cli.ts +141 -5
- package/src/client-module.ts +61 -0
- package/src/command-catalog.ts +166 -37
- 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/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 +25 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +17 -11
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +424 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +25 -0
- package/src/internal/typescript-import.ts +10 -3
- package/src/introspect.ts +65 -27
- package/src/levels-tool.ts +117 -20
- package/src/load.ts +78 -14
- package/src/mcp-context.ts +7 -7
- package/src/mcp-exec.ts +60 -17
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +41 -0
- package/src/mcp-isolate.ts +9 -2
- 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 +8 -3
- package/src/migrate-layout.ts +885 -63
- package/src/output-schemas.ts +1084 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +39 -10
- package/src/rules/codes.ts +10 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +351 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +184 -51
- package/src/rules/pages.ts +238 -3
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +352 -71
- package/src/rules/zones.ts +205 -38
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +134 -7
- package/src/test-tool.ts +18 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +487 -71
- package/src/verification-plan.ts +24 -0
- package/src/version-drift.ts +67 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/check.ts
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
* anything fails. Pure scanners (`scanFetchText`, `scanServerOnlyImports`) are unit-tested.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
20
|
-
import {
|
|
19
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync, rmdirSync, rmSync } from "node:fs"
|
|
20
|
+
import { tmpdir } from "node:os"
|
|
21
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
|
22
|
+
import { specifierPackage } from "@nifrajs/web/zones"
|
|
21
23
|
import { BACKEND_APP_FILE } from "./app-files.ts"
|
|
22
24
|
import {
|
|
23
25
|
type CheckAssuranceContext,
|
|
@@ -26,16 +28,22 @@ import {
|
|
|
26
28
|
type CheckResult,
|
|
27
29
|
collectCheckDiagnostics,
|
|
28
30
|
type RuleOverride,
|
|
31
|
+
safeIgnoreGlobMatch,
|
|
29
32
|
} from "./check-diagnostics.ts"
|
|
33
|
+
import { appDirs, clientModuleRoot } from "./client-module.ts"
|
|
30
34
|
import { checkContractsLock, DEFAULT_CONTRACTS_LOCK } from "./contracts.ts"
|
|
31
35
|
import { type Diagnostic, normalizeSeverity, toSarifLog } from "./diagnostics.ts"
|
|
36
|
+
import { createApiOnlyProbe, createNifraProjectProbe } from "./internal/nifra-project.ts"
|
|
37
|
+
import { createSpecifierResolver } from "./internal/project-specifiers.ts"
|
|
32
38
|
import { createSourceFacts } from "./internal/source-facts.ts"
|
|
33
39
|
import { loadProjectTypeScript, type TypeScriptApi } from "./internal/typescript-import.ts"
|
|
40
|
+
import { forwardTerminalSignals, OWN_PROCESS_GROUP, signalProcessGroup } from "./mcp-io.ts"
|
|
34
41
|
// Type-only: `pipeline-report.ts` imports this module's source scanners, so a value import here would
|
|
35
42
|
// close a cycle. Doctor is what actually runs the collector (see the `pipeline` rule below).
|
|
36
43
|
import type { ContractCheckFacts, ProjectFactsSeed } from "./project-facts.ts"
|
|
37
44
|
import { RULE_CODES } from "./rules/codes.ts"
|
|
38
45
|
import { sourceIndex } from "./rules/index.ts"
|
|
46
|
+
import { filesOutsideProgram, splitListedFiles } from "./typecheck-coverage.ts"
|
|
39
47
|
|
|
40
48
|
export type {
|
|
41
49
|
CheckAnalysisInput,
|
|
@@ -50,24 +58,31 @@ export type {
|
|
|
50
58
|
} from "./check-diagnostics.ts"
|
|
51
59
|
|
|
52
60
|
import {
|
|
61
|
+
COMPONENT_FILE,
|
|
62
|
+
componentScripts,
|
|
53
63
|
createProjectSqlImports,
|
|
64
|
+
createServerOnlyChains,
|
|
54
65
|
isBrowserSource,
|
|
55
66
|
type ModuleReader,
|
|
56
67
|
type ModuleResolver,
|
|
57
|
-
resolveServerOnlyChains,
|
|
58
68
|
type SourceFinding,
|
|
69
|
+
type SqlFinding,
|
|
59
70
|
type StaticRouteFinding,
|
|
60
71
|
scanFetchText,
|
|
61
|
-
scanInterpolatedSql,
|
|
62
72
|
scanRemovedImports,
|
|
63
73
|
scanResponseRoutes,
|
|
64
74
|
scanServerManifestDrift,
|
|
75
|
+
scanSqlSites,
|
|
65
76
|
scanStaticRouteText,
|
|
66
77
|
scanStreamText,
|
|
78
|
+
scanUnresolvedRoutePaths,
|
|
67
79
|
scanUntypedClient,
|
|
68
80
|
type TransitiveServerImportFinding,
|
|
81
|
+
type TypedClientFinding,
|
|
82
|
+
type UnresolvedRoutePathFinding,
|
|
69
83
|
walkSource,
|
|
70
84
|
} from "./check-scan.ts"
|
|
85
|
+
import { componentCheckReceipts } from "./component-check.ts"
|
|
71
86
|
|
|
72
87
|
export * from "./check-scan.ts"
|
|
73
88
|
|
|
@@ -80,6 +95,10 @@ interface TypecheckResult {
|
|
|
80
95
|
/** tsconfig.json exists but no `typescript` install was found walking up from cwd. The contract
|
|
81
96
|
* gate could not run - the caller surfaces this as a FAILING diagnostic, never a silent skip. */
|
|
82
97
|
readonly missingTypeScript?: boolean
|
|
98
|
+
/** Every file in the compiler's program, absolute. Absent when the compiler listed none. */
|
|
99
|
+
readonly programFiles?: readonly string[]
|
|
100
|
+
/** Zoned TypeScript files outside the program, project-relative: the typecheck never read them. */
|
|
101
|
+
readonly outsideProgram?: readonly string[]
|
|
83
102
|
}
|
|
84
103
|
|
|
85
104
|
/**
|
|
@@ -117,38 +136,70 @@ async function typecheck(cwd: string, signal?: AbortSignal): Promise<TypecheckRe
|
|
|
117
136
|
note: "typescript not installed (run: bun add -d typescript)",
|
|
118
137
|
}
|
|
119
138
|
}
|
|
139
|
+
const first = await runTsc(cwd, tscBin, tsconfig, true, signal)
|
|
140
|
+
// A compiler that rejects `--listFiles` still gets its typecheck; only the coverage check is lost.
|
|
141
|
+
if (!first.ok && !first.cancelled && /TS50(?:23|25)\b[^\n]*listFiles/.test(first.output ?? ""))
|
|
142
|
+
return runTsc(cwd, tscBin, tsconfig, false, signal)
|
|
143
|
+
return first
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function runTsc(
|
|
147
|
+
cwd: string,
|
|
148
|
+
tscBin: string,
|
|
149
|
+
tsconfig: string,
|
|
150
|
+
listFiles: boolean,
|
|
151
|
+
signal?: AbortSignal,
|
|
152
|
+
): Promise<TypecheckResult> {
|
|
120
153
|
if (signal?.aborted) return { ran: true, ok: false, cancelled: true, output: "cancelled" }
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
cancelled = true
|
|
129
|
-
proc.kill()
|
|
130
|
-
}
|
|
131
|
-
signal?.addEventListener("abort", abort, { once: true })
|
|
132
|
-
let out = ""
|
|
133
|
-
let err = ""
|
|
134
|
-
let code: number | null = null
|
|
154
|
+
const args = [process.execPath, tscBin, "--noEmit", "-p", tsconfig]
|
|
155
|
+
if (listFiles) args.push("--listFiles")
|
|
156
|
+
// tsc calls process.exit after printing. Bun can discard buffered pipe writes at that point,
|
|
157
|
+
// especially while the parent's other scans occupy the event loop. Files keep the full program.
|
|
158
|
+
const outputDirectory = mkdtempSync(join(tmpdir(), "nifra-tsc-"))
|
|
159
|
+
const stdout = Bun.file(join(outputDirectory, "stdout"))
|
|
160
|
+
const stderr = Bun.file(join(outputDirectory, "stderr"))
|
|
135
161
|
try {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
162
|
+
const proc = Bun.spawn(args, {
|
|
163
|
+
cwd,
|
|
164
|
+
stdout,
|
|
165
|
+
stderr,
|
|
166
|
+
// Its own group lets cancellation also stop a compiler process the tsc entry started.
|
|
167
|
+
detached: OWN_PROCESS_GROUP,
|
|
168
|
+
})
|
|
169
|
+
let cancelled = false
|
|
170
|
+
const stop = (): void => signalProcessGroup(proc, "SIGTERM")
|
|
171
|
+
const abort = (): void => {
|
|
172
|
+
cancelled = true
|
|
173
|
+
stop()
|
|
174
|
+
}
|
|
175
|
+
signal?.addEventListener("abort", abort, { once: true })
|
|
176
|
+
const releaseSignals = forwardTerminalSignals(stop)
|
|
177
|
+
let out = ""
|
|
178
|
+
let err = ""
|
|
179
|
+
let code: number | null = null
|
|
180
|
+
try {
|
|
181
|
+
code = await proc.exited
|
|
182
|
+
const result = await Promise.all([stdout.text(), stderr.text()])
|
|
183
|
+
out = result[0]
|
|
184
|
+
err = result[1]
|
|
185
|
+
} finally {
|
|
186
|
+
signal?.removeEventListener("abort", abort)
|
|
187
|
+
releaseSignals()
|
|
188
|
+
}
|
|
189
|
+
if (cancelled) return { ran: true, ok: false, output: "cancelled", cancelled: true }
|
|
190
|
+
const listed = listFiles ? splitListedFiles(out) : undefined
|
|
191
|
+
return {
|
|
192
|
+
ran: true,
|
|
193
|
+
ok: code === 0,
|
|
194
|
+
output: `${listed?.output ?? out}${err}`.trim(),
|
|
195
|
+
...(listed !== undefined && listed.programFiles.length > 0
|
|
196
|
+
? { programFiles: listed.programFiles }
|
|
197
|
+
: {}),
|
|
198
|
+
}
|
|
144
199
|
} finally {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
ran: true,
|
|
149
|
-
ok: code === 0 && !cancelled,
|
|
150
|
-
output: cancelled ? "cancelled" : `${out}${err}`.trim(),
|
|
151
|
-
...(cancelled ? { cancelled: true } : {}),
|
|
200
|
+
rmSync(join(outputDirectory, "stdout"), { force: true })
|
|
201
|
+
rmSync(join(outputDirectory, "stderr"), { force: true })
|
|
202
|
+
rmdirSync(outputDirectory)
|
|
152
203
|
}
|
|
153
204
|
}
|
|
154
205
|
|
|
@@ -244,12 +295,7 @@ async function collectContractFacts(cwd: string): Promise<ContractCheckFacts> {
|
|
|
244
295
|
const hasLock = existsSync(join(cwd, DEFAULT_CONTRACTS_LOCK))
|
|
245
296
|
if (!hasBackend && !hasLock) return { present: false, vacuous: false, diagnostics: [] }
|
|
246
297
|
try {
|
|
247
|
-
|
|
248
|
-
return {
|
|
249
|
-
present: result.present,
|
|
250
|
-
vacuous: result.vacuous,
|
|
251
|
-
diagnostics: result.diagnostics,
|
|
252
|
-
}
|
|
298
|
+
return await checkContractsLock(cwd)
|
|
253
299
|
} catch (error) {
|
|
254
300
|
if (error instanceof Error && error.message === "contract source not configured") {
|
|
255
301
|
return { present: false, vacuous: false, diagnostics: [] }
|
|
@@ -263,43 +309,80 @@ async function collectContractFacts(cwd: string): Promise<ContractCheckFacts> {
|
|
|
263
309
|
}
|
|
264
310
|
}
|
|
265
311
|
|
|
312
|
+
/** Record the zoned files the compiler's program left out, minus the typecheck rule's `ignore` globs. */
|
|
313
|
+
function withProgramCoverage(
|
|
314
|
+
cwd: string,
|
|
315
|
+
result: TypecheckResult,
|
|
316
|
+
sourceFiles: ReadonlyArray<{ readonly file: string }>,
|
|
317
|
+
checkConfig: CheckConfig,
|
|
318
|
+
): TypecheckResult {
|
|
319
|
+
const { programFiles, ...rest } = result
|
|
320
|
+
if (programFiles === undefined) return result
|
|
321
|
+
const ignore = (checkConfig.rules.typecheck ?? checkConfig.rules["NF-C001"])?.ignore
|
|
322
|
+
const outside = filesOutsideProgram(
|
|
323
|
+
cwd,
|
|
324
|
+
sourceFiles.map((source) => source.file),
|
|
325
|
+
programFiles,
|
|
326
|
+
).filter((file) => !ignore?.some((pattern) => safeIgnoreGlobMatch(pattern, file)))
|
|
327
|
+
return outside.length === 0 ? rest : { ...rest, outsideProgram: outside }
|
|
328
|
+
}
|
|
329
|
+
|
|
266
330
|
/** Build the rule input from one source walk and the other check-wide scans. */
|
|
267
331
|
async function buildProjectScan(
|
|
268
332
|
cwd: string,
|
|
269
333
|
opts: Pick<CheckCollectionOptions, "lintsOnly" | "signal" | "loadTypeScript">,
|
|
270
334
|
): Promise<ProjectScan> {
|
|
271
|
-
const fetches:
|
|
272
|
-
const streams:
|
|
335
|
+
const fetches: TypedClientFinding[] = []
|
|
336
|
+
const streams: TypedClientFinding[] = []
|
|
273
337
|
const staticRoutes: StaticRouteFinding[] = []
|
|
274
338
|
const untypedClients: SourceFinding[] = []
|
|
275
339
|
const removedImports: SourceFinding[] = []
|
|
276
340
|
const serverImports: TransitiveServerImportFinding[] = []
|
|
277
341
|
const responseRoutes: SourceFinding[] = []
|
|
278
|
-
const interpolatedSql:
|
|
342
|
+
const interpolatedSql: SqlFinding[] = []
|
|
343
|
+
const routePaths: UnresolvedRoutePathFinding[] = []
|
|
279
344
|
const browserModules: Array<{ rel: string; content: string }> = []
|
|
280
345
|
const sourceFiles: Array<{ file: string; content: string }> = []
|
|
346
|
+
const inNifraProject = createNifraProjectProbe(cwd)
|
|
347
|
+
// An API-only project has no browser bundle for server code to reach.
|
|
348
|
+
const apiOnly = createApiOnlyProbe(cwd)
|
|
281
349
|
const {
|
|
282
350
|
config: checkConfig,
|
|
283
351
|
error: checkConfigError,
|
|
284
352
|
warnings: checkConfigWarnings,
|
|
285
353
|
} = await loadCheckConfig(cwd)
|
|
286
|
-
const [
|
|
354
|
+
const [typecheckRun, _, doctor, manifestDrift, contracts] = await Promise.all([
|
|
287
355
|
opts.lintsOnly
|
|
288
356
|
? Promise.resolve<TypecheckResult>({ ran: false, ok: true, note: "lints-only mode" })
|
|
289
357
|
: typecheck(cwd, opts.signal),
|
|
290
|
-
walkSource(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
358
|
+
walkSource(
|
|
359
|
+
cwd,
|
|
360
|
+
(rel, content) => {
|
|
361
|
+
sourceFiles.push({ file: rel, content })
|
|
362
|
+
fetches.push(...scanFetchText(rel, content, checkConfig.externalMounts))
|
|
363
|
+
streams.push(...scanStreamText(rel, content, checkConfig.externalMounts))
|
|
364
|
+
untypedClients.push(...scanUntypedClient(rel, content))
|
|
365
|
+
removedImports.push(...scanRemovedImports(rel, content))
|
|
366
|
+
const dir = dirname(rel).replace(/^\.$/, "")
|
|
367
|
+
if (isBrowserSource(rel) && inNifraProject(dir) && !apiOnly(dir))
|
|
368
|
+
browserModules.push({ rel, content })
|
|
369
|
+
},
|
|
370
|
+
{ scripts: true },
|
|
371
|
+
),
|
|
298
372
|
import("./doctor.ts").then((m) => m.collectDoctorResult(cwd)),
|
|
299
373
|
scanServerManifestDrift(cwd),
|
|
300
374
|
collectContractFacts(cwd),
|
|
301
375
|
])
|
|
302
376
|
|
|
377
|
+
// tsc compiles TypeScript sources only; the JavaScript and component files the scan reads are no
|
|
378
|
+
// program's to cover.
|
|
379
|
+
const typecheckResult = withProgramCoverage(
|
|
380
|
+
cwd,
|
|
381
|
+
typecheckRun,
|
|
382
|
+
sourceFiles.filter(({ file }) => /\.[cm]?tsx?$/.test(file)),
|
|
383
|
+
checkConfig,
|
|
384
|
+
)
|
|
385
|
+
|
|
303
386
|
// TypeScript 7's unstable API materializes source files through one project session. Build the
|
|
304
387
|
// source index first so every syntax-backed scanner (including the independent rule registry)
|
|
305
388
|
// shares that session instead of spawning one compiler process per rule.
|
|
@@ -309,37 +392,95 @@ async function buildProjectScan(
|
|
|
309
392
|
const sqlCompiler = loadedTypeScript.compiler
|
|
310
393
|
const sourceFacts = sqlCompiler === undefined ? undefined : createSourceFacts(sqlCompiler)
|
|
311
394
|
const sqlImports = sqlCompiler === undefined ? undefined : createProjectSqlImports(cwd)
|
|
395
|
+
const project = new Map(
|
|
396
|
+
sourceFiles.map(({ file, content }) => [file.replaceAll("\\", "/"), content]),
|
|
397
|
+
)
|
|
312
398
|
for (const { file, content } of sourceFiles) {
|
|
313
|
-
staticRoutes.push(...scanStaticRouteText(file, content, sourceFacts))
|
|
399
|
+
staticRoutes.push(...scanStaticRouteText(file, content, sourceFacts, project))
|
|
400
|
+
routePaths.push(...scanUnresolvedRoutePaths(file, content, project, sourceFacts))
|
|
314
401
|
responseRoutes.push(...scanResponseRoutes(file, content, sourceFacts))
|
|
315
402
|
if (sqlCompiler !== undefined)
|
|
316
|
-
interpolatedSql.push(...
|
|
403
|
+
interpolatedSql.push(...scanSqlSites(file, content, sqlCompiler, sqlImports))
|
|
317
404
|
}
|
|
318
405
|
|
|
319
406
|
// Resolved modules stay project-relative, as the scanned files are: the zone of a path is read
|
|
320
407
|
// from its folders, and a checkout under a folder named `backend/` must not make it all backend.
|
|
321
|
-
|
|
408
|
+
// The resolver answers with real paths, so the project root is read as one too.
|
|
409
|
+
const realCwd = (() => {
|
|
322
410
|
try {
|
|
323
|
-
|
|
324
|
-
const resolved = Bun.resolveSync(specifier, dirname(fromAbs))
|
|
325
|
-
const rel = relative(cwd, resolved).replaceAll("\\", "/")
|
|
326
|
-
return rel.startsWith("../") || isAbsolute(rel) ? resolved : rel
|
|
411
|
+
return realpathSync(cwd)
|
|
327
412
|
} catch {
|
|
328
|
-
return
|
|
413
|
+
return cwd
|
|
329
414
|
}
|
|
415
|
+
})()
|
|
416
|
+
const resolveSpecifier = createSpecifierResolver()
|
|
417
|
+
const projectPath = (path: string): string => {
|
|
418
|
+
const rel = relative(realCwd, path).replaceAll("\\", "/")
|
|
419
|
+
return rel.startsWith("../") || isAbsolute(rel) ? path : rel
|
|
420
|
+
}
|
|
421
|
+
const resolveModule: ModuleResolver = (fromFile, specifier) => {
|
|
422
|
+
const resolved = resolveSpecifier(
|
|
423
|
+
isAbsolute(fromFile) ? fromFile : join(realCwd, fromFile),
|
|
424
|
+
specifier,
|
|
425
|
+
)
|
|
426
|
+
return resolved === undefined ? undefined : projectPath(resolved)
|
|
330
427
|
}
|
|
331
428
|
const readModule: ModuleReader = (path) => {
|
|
429
|
+
let text: string
|
|
332
430
|
try {
|
|
333
|
-
|
|
431
|
+
text = readFileSync(isAbsolute(path) ? path : join(cwd, path), "utf8")
|
|
334
432
|
} catch {
|
|
335
433
|
return undefined
|
|
336
434
|
}
|
|
435
|
+
return COMPONENT_FILE.test(path) ? componentScripts(text) : text
|
|
337
436
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
437
|
+
const components = await componentCheckReceipts(cwd, source)
|
|
438
|
+
const componentReceipts = components.receipts
|
|
439
|
+
// An app's `clientModule` is browser code wherever it lives: the generated client entry imports it.
|
|
440
|
+
const browserRoots = new Set<string>()
|
|
441
|
+
for (const app of appDirs(source.files)) {
|
|
442
|
+
const entry = clientModuleRoot(cwd, app, resolveSpecifier)
|
|
443
|
+
if (entry === undefined) continue
|
|
444
|
+
const rel = projectPath(entry.file)
|
|
445
|
+
if (isBrowserSource(rel) || browserRoots.has(rel)) continue
|
|
446
|
+
const content = readModule(rel)
|
|
447
|
+
if (content === undefined) continue
|
|
448
|
+
browserRoots.add(rel)
|
|
449
|
+
browserModules.push({ rel, content })
|
|
450
|
+
}
|
|
451
|
+
// Node's lookup: the nearest `node_modules/<name>` at or above the importing file.
|
|
452
|
+
const backendPackage = (fromFile: string, specifier: string): boolean => {
|
|
453
|
+
const name = specifierPackage(specifier)
|
|
454
|
+
if (name === undefined) return false
|
|
455
|
+
let dir = dirname(isAbsolute(fromFile) ? fromFile : join(realCwd, fromFile))
|
|
456
|
+
for (;;) {
|
|
457
|
+
const manifest = join(dir, "node_modules", name, "package.json")
|
|
458
|
+
if (existsSync(manifest)) {
|
|
459
|
+
try {
|
|
460
|
+
const nifra: unknown = JSON.parse(readFileSync(manifest, "utf8"))?.nifra
|
|
461
|
+
return (
|
|
462
|
+
typeof nifra === "object" &&
|
|
463
|
+
nifra !== null &&
|
|
464
|
+
Reflect.get(nifra, "environment") === "backend"
|
|
465
|
+
)
|
|
466
|
+
} catch {
|
|
467
|
+
return false
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const parent = dirname(dir)
|
|
471
|
+
if (parent === dir) return false
|
|
472
|
+
dir = parent
|
|
473
|
+
}
|
|
342
474
|
}
|
|
475
|
+
const serverOnlyChains = createServerOnlyChains({
|
|
476
|
+
resolve: resolveModule,
|
|
477
|
+
read: readModule,
|
|
478
|
+
facts: sourceFacts,
|
|
479
|
+
componentReceipts,
|
|
480
|
+
backendPackage,
|
|
481
|
+
})
|
|
482
|
+
for (const { rel, content } of browserModules)
|
|
483
|
+
serverImports.push(...serverOnlyChains(rel, content, browserRoots.has(rel)))
|
|
343
484
|
|
|
344
485
|
const facts: ProjectFactsSeed = {
|
|
345
486
|
source,
|
|
@@ -351,6 +492,10 @@ async function buildProjectScan(
|
|
|
351
492
|
check: {
|
|
352
493
|
typecheck: typecheckResult,
|
|
353
494
|
sqlCompilerAvailable: sqlCompiler !== undefined,
|
|
495
|
+
componentCheckUnsupported:
|
|
496
|
+
components.unsupported === undefined
|
|
497
|
+
? undefined
|
|
498
|
+
: { version: components.unsupported.version, message: components.unsupported.message },
|
|
354
499
|
...(loadedTypeScript.unsupported === undefined
|
|
355
500
|
? {}
|
|
356
501
|
: { unsupportedTypeScriptVersion: loadedTypeScript.unsupported.version }),
|
|
@@ -368,6 +513,7 @@ async function buildProjectScan(
|
|
|
368
513
|
removedImports,
|
|
369
514
|
responseRoutes,
|
|
370
515
|
interpolatedSql,
|
|
516
|
+
routePaths,
|
|
371
517
|
},
|
|
372
518
|
}
|
|
373
519
|
return { facts }
|
|
@@ -381,9 +527,11 @@ async function buildProjectScan(
|
|
|
381
527
|
* report, `--json`, and the MCP tool all render from. */
|
|
382
528
|
/** Run the scan, freeze its facts, and project them into the public diagnostic result. */
|
|
383
529
|
export async function collectCheckResult(
|
|
384
|
-
|
|
530
|
+
root: string,
|
|
385
531
|
opts: CheckCollectionOptions = {},
|
|
386
532
|
): Promise<CheckResult> {
|
|
533
|
+
// The zone classifier works on absolute paths; under a relative root every file read as unzoned.
|
|
534
|
+
const cwd = resolve(root)
|
|
387
535
|
const scan = await buildProjectScan(cwd, opts)
|
|
388
536
|
try {
|
|
389
537
|
return await collectCheckDiagnostics(cwd, scan, opts)
|
|
@@ -416,7 +564,7 @@ const REPORT_SECTIONS = [
|
|
|
416
564
|
/**
|
|
417
565
|
* Render the human-readable check report as lines. Pure (no I/O, no cwd) so tests can assert
|
|
418
566
|
* report/exit-code parity: every diagnostic in `result.diagnostics` appears in the output, and the
|
|
419
|
-
* trailer states the error
|
|
567
|
+
* trailer states the error, advisory and info counts that produced `ok`.
|
|
420
568
|
*/
|
|
421
569
|
export function renderCheckReport(result: CheckResult): string[] {
|
|
422
570
|
const lines: string[] = []
|
|
@@ -425,7 +573,7 @@ export function renderCheckReport(result: CheckResult): string[] {
|
|
|
425
573
|
result.typecheck === "pass"
|
|
426
574
|
? "✓ typecheck passed"
|
|
427
575
|
: result.typecheck === "fail"
|
|
428
|
-
?
|
|
576
|
+
? `✗ typecheck failed - ${result.typecheckNote ?? "the frontend/backend contract is broken"}`
|
|
429
577
|
: `⚠ typecheck SKIPPED - ${result.typecheckNote ?? "no tsconfig / typescript not installed"} (the contract gate did not run)`,
|
|
430
578
|
)
|
|
431
579
|
// Stated on every run, passing or not. "Which bundler is this app on" decides which plugin slot is
|
|
@@ -467,13 +615,17 @@ export function renderCheckReport(result: CheckResult): string[] {
|
|
|
467
615
|
// resolve condition the Bun dev bundler can't take does not), so the counts are split rather than
|
|
468
616
|
// rounded up to the worse of the two.
|
|
469
617
|
const errors = ds.filter((d) => d.severity === "error").length
|
|
470
|
-
const
|
|
618
|
+
const warnings = ds.filter((d) => d.severity === "warning").length
|
|
619
|
+
const infos = ds.length - errors - warnings
|
|
620
|
+
const rest = belowError(warnings, infos)
|
|
471
621
|
lines.push(
|
|
472
622
|
ds.length === 0
|
|
473
623
|
? `✓ ${label}: none`
|
|
474
|
-
: errors === 0
|
|
475
|
-
?
|
|
476
|
-
:
|
|
624
|
+
: errors + warnings === 0
|
|
625
|
+
? `• ${label}: ${ds.length} (info)`
|
|
626
|
+
: errors === 0
|
|
627
|
+
? `⚠ ${label}: ${warnings} (advisory)${infos > 0 ? ` (+${infos} info)` : ""}`
|
|
628
|
+
: `✗ ${label}: ${errors}${rest === "" ? "" : ` (+${rest})`}`,
|
|
477
629
|
)
|
|
478
630
|
}
|
|
479
631
|
for (const d of ds) {
|
|
@@ -520,18 +672,27 @@ export function renderCheckReport(result: CheckResult): string[] {
|
|
|
520
672
|
)
|
|
521
673
|
}
|
|
522
674
|
const errors = result.diagnostics.filter((d) => d.severity === "error").length
|
|
523
|
-
const
|
|
675
|
+
const warnings = result.diagnostics.filter((d) => d.severity === "warning").length
|
|
676
|
+
const rest = belowError(warnings, result.diagnostics.length - errors - warnings)
|
|
524
677
|
lines.push(
|
|
525
678
|
"",
|
|
526
679
|
result.ok
|
|
527
|
-
?
|
|
528
|
-
?
|
|
529
|
-
:
|
|
530
|
-
: `✗ check failed: ${errors} error${errors === 1 ? "" : "s"}${
|
|
680
|
+
? rest === ""
|
|
681
|
+
? "✓ check passed"
|
|
682
|
+
: `✓ check passed (${rest})`
|
|
683
|
+
: `✗ check failed: ${errors} error${errors === 1 ? "" : "s"}${rest === "" ? "" : ` (+${rest})`}`,
|
|
531
684
|
)
|
|
532
685
|
return lines
|
|
533
686
|
}
|
|
534
687
|
|
|
688
|
+
/** The findings that do not fail the check, named by severity: an info finding is not advisory. */
|
|
689
|
+
function belowError(warnings: number, infos: number): string {
|
|
690
|
+
const parts: string[] = []
|
|
691
|
+
if (warnings > 0) parts.push(`${warnings} advisory`)
|
|
692
|
+
if (infos > 0) parts.push(`${infos} info`)
|
|
693
|
+
return parts.join(", ")
|
|
694
|
+
}
|
|
695
|
+
|
|
535
696
|
/** Run the full check; print a report (`--json` for machine output) and return whether it passed. */
|
|
536
697
|
export async function runCheck(
|
|
537
698
|
cwd: string,
|