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