@nifrajs/cli 3.5.0 → 4.0.2

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 (404) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +4 -0
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +13 -0
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +49 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +291 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +99 -63
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +42 -16
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +262 -82
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +62 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +275 -35
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/config-target.d.ts +8 -0
  41. package/dist/config-target.d.ts.map +1 -0
  42. package/dist/config-target.js +45 -0
  43. package/dist/config-target.js.map +1 -0
  44. package/dist/contracts.d.ts.map +1 -1
  45. package/dist/contracts.js +52 -13
  46. package/dist/contracts.js.map +1 -1
  47. package/dist/db-audit.d.ts +37 -0
  48. package/dist/db-audit.d.ts.map +1 -0
  49. package/dist/db-audit.js +99 -0
  50. package/dist/db-audit.js.map +1 -0
  51. package/dist/db-child.d.ts +64 -0
  52. package/dist/db-child.d.ts.map +1 -0
  53. package/dist/db-child.js +213 -0
  54. package/dist/db-child.js.map +1 -0
  55. package/dist/db-config.d.ts +60 -0
  56. package/dist/db-config.d.ts.map +1 -0
  57. package/dist/db-config.js +176 -0
  58. package/dist/db-config.js.map +1 -0
  59. package/dist/db-tool.d.ts +84 -0
  60. package/dist/db-tool.d.ts.map +1 -0
  61. package/dist/db-tool.js +537 -0
  62. package/dist/db-tool.js.map +1 -0
  63. package/dist/dev-bun-config.d.ts +20 -13
  64. package/dist/dev-bun-config.d.ts.map +1 -1
  65. package/dist/dev-bun-config.js +98 -41
  66. package/dist/dev-bun-config.js.map +1 -1
  67. package/dist/dev-feed-tool.d.ts +106 -0
  68. package/dist/dev-feed-tool.d.ts.map +1 -0
  69. package/dist/dev-feed-tool.js +713 -0
  70. package/dist/dev-feed-tool.js.map +1 -0
  71. package/dist/dev-server-client.d.ts +67 -0
  72. package/dist/dev-server-client.d.ts.map +1 -0
  73. package/dist/dev-server-client.js +195 -0
  74. package/dist/dev-server-client.js.map +1 -0
  75. package/dist/diff-tool.d.ts +3 -3
  76. package/dist/diff-tool.d.ts.map +1 -1
  77. package/dist/diff-tool.js +6 -5
  78. package/dist/diff-tool.js.map +1 -1
  79. package/dist/docs-search.d.ts.map +1 -1
  80. package/dist/docs-search.js +12 -4
  81. package/dist/docs-search.js.map +1 -1
  82. package/dist/doctor.d.ts +17 -0
  83. package/dist/doctor.d.ts.map +1 -1
  84. package/dist/doctor.js +76 -15
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/env-file.d.ts +15 -0
  87. package/dist/env-file.d.ts.map +1 -1
  88. package/dist/env-file.js +73 -1
  89. package/dist/env-file.js.map +1 -1
  90. package/dist/fix-recipes.d.ts.map +1 -1
  91. package/dist/fix-recipes.js +48 -22
  92. package/dist/fix-recipes.js.map +1 -1
  93. package/dist/frontend-guidance.js +5 -5
  94. package/dist/frontend-guidance.js.map +1 -1
  95. package/dist/i18n-check.d.ts +16 -0
  96. package/dist/i18n-check.d.ts.map +1 -0
  97. package/dist/i18n-check.js +154 -0
  98. package/dist/i18n-check.js.map +1 -0
  99. package/dist/init-agents.d.ts +49 -13
  100. package/dist/init-agents.d.ts.map +1 -1
  101. package/dist/init-agents.js +239 -34
  102. package/dist/init-agents.js.map +1 -1
  103. package/dist/internal/code-unit-order.d.ts +4 -0
  104. package/dist/internal/code-unit-order.d.ts.map +1 -0
  105. package/dist/internal/code-unit-order.js +6 -0
  106. package/dist/internal/code-unit-order.js.map +1 -0
  107. package/dist/internal/script-kind.d.ts +7 -0
  108. package/dist/internal/script-kind.d.ts.map +1 -0
  109. package/dist/internal/script-kind.js +12 -0
  110. package/dist/internal/script-kind.js.map +1 -0
  111. package/dist/internal/source-facts.d.ts +5 -0
  112. package/dist/internal/source-facts.d.ts.map +1 -1
  113. package/dist/internal/source-facts.js +61 -5
  114. package/dist/internal/source-facts.js.map +1 -1
  115. package/dist/internal/typescript-import.d.ts +49 -24
  116. package/dist/internal/typescript-import.d.ts.map +1 -1
  117. package/dist/internal/typescript-import.js +507 -67
  118. package/dist/internal/typescript-import.js.map +1 -1
  119. package/dist/introspect.d.ts +5 -1
  120. package/dist/introspect.d.ts.map +1 -1
  121. package/dist/introspect.js +92 -27
  122. package/dist/introspect.js.map +1 -1
  123. package/dist/learn.js +3 -3
  124. package/dist/learn.js.map +1 -1
  125. package/dist/levels-tool.d.ts +10 -0
  126. package/dist/levels-tool.d.ts.map +1 -1
  127. package/dist/levels-tool.js +33 -3
  128. package/dist/levels-tool.js.map +1 -1
  129. package/dist/load.d.ts +67 -17
  130. package/dist/load.d.ts.map +1 -1
  131. package/dist/load.js +78 -18
  132. package/dist/load.js.map +1 -1
  133. package/dist/manifest-tool.d.ts.map +1 -1
  134. package/dist/manifest-tool.js +3 -2
  135. package/dist/manifest-tool.js.map +1 -1
  136. package/dist/mcp-app-tool.d.ts.map +1 -1
  137. package/dist/mcp-app-tool.js +2 -0
  138. package/dist/mcp-app-tool.js.map +1 -1
  139. package/dist/mcp-context.js +1 -1
  140. package/dist/mcp-context.js.map +1 -1
  141. package/dist/mcp-delegate.d.ts +57 -0
  142. package/dist/mcp-delegate.d.ts.map +1 -0
  143. package/dist/mcp-delegate.js +130 -0
  144. package/dist/mcp-delegate.js.map +1 -0
  145. package/dist/mcp-docs-tools.d.ts.map +1 -1
  146. package/dist/mcp-docs-tools.js +23 -5
  147. package/dist/mcp-docs-tools.js.map +1 -1
  148. package/dist/mcp-exec.d.ts +2 -0
  149. package/dist/mcp-exec.d.ts.map +1 -1
  150. package/dist/mcp-exec.js +132 -108
  151. package/dist/mcp-exec.js.map +1 -1
  152. package/dist/mcp-http.d.ts.map +1 -1
  153. package/dist/mcp-http.js +17 -5
  154. package/dist/mcp-http.js.map +1 -1
  155. package/dist/mcp-io.d.ts +14 -0
  156. package/dist/mcp-io.d.ts.map +1 -1
  157. package/dist/mcp-io.js +45 -0
  158. package/dist/mcp-io.js.map +1 -1
  159. package/dist/mcp-isolate.d.ts +48 -0
  160. package/dist/mcp-isolate.d.ts.map +1 -0
  161. package/dist/mcp-isolate.js +222 -0
  162. package/dist/mcp-isolate.js.map +1 -0
  163. package/dist/mcp-project-child.d.ts +74 -0
  164. package/dist/mcp-project-child.d.ts.map +1 -0
  165. package/dist/mcp-project-child.js +130 -0
  166. package/dist/mcp-project-child.js.map +1 -0
  167. package/dist/mcp-render.js +2 -2
  168. package/dist/mcp-render.js.map +1 -1
  169. package/dist/mcp-root.d.ts +37 -9
  170. package/dist/mcp-root.d.ts.map +1 -1
  171. package/dist/mcp-root.js +110 -23
  172. package/dist/mcp-root.js.map +1 -1
  173. package/dist/mcp-run.d.ts +18 -1
  174. package/dist/mcp-run.d.ts.map +1 -1
  175. package/dist/mcp-run.js +82 -9
  176. package/dist/mcp-run.js.map +1 -1
  177. package/dist/mcp-ws.js +4 -1
  178. package/dist/mcp-ws.js.map +1 -1
  179. package/dist/mcp.d.ts +16 -3
  180. package/dist/mcp.d.ts.map +1 -1
  181. package/dist/mcp.js +81 -53
  182. package/dist/mcp.js.map +1 -1
  183. package/dist/migrate-layout.d.ts +44 -0
  184. package/dist/migrate-layout.d.ts.map +1 -0
  185. package/dist/migrate-layout.js +1155 -0
  186. package/dist/migrate-layout.js.map +1 -0
  187. package/dist/openapi-tool.d.ts +2 -2
  188. package/dist/openapi-tool.d.ts.map +1 -1
  189. package/dist/openapi-tool.js.map +1 -1
  190. package/dist/openapi-types.d.ts +2 -1
  191. package/dist/openapi-types.d.ts.map +1 -1
  192. package/dist/openapi-types.js +240 -12
  193. package/dist/openapi-types.js.map +1 -1
  194. package/dist/pipeline-report.d.ts +3 -2
  195. package/dist/pipeline-report.d.ts.map +1 -1
  196. package/dist/pipeline-report.js +19 -16
  197. package/dist/pipeline-report.js.map +1 -1
  198. package/dist/port.d.ts +5 -4
  199. package/dist/port.d.ts.map +1 -1
  200. package/dist/port.js +46 -29
  201. package/dist/port.js.map +1 -1
  202. package/dist/project-facts.d.ts +5 -0
  203. package/dist/project-facts.d.ts.map +1 -1
  204. package/dist/project-facts.js.map +1 -1
  205. package/dist/recipes/4.0.0.d.ts +9 -0
  206. package/dist/recipes/4.0.0.d.ts.map +1 -0
  207. package/dist/recipes/4.0.0.js +36 -0
  208. package/dist/recipes/4.0.0.js.map +1 -0
  209. package/dist/recipes/index.d.ts.map +1 -1
  210. package/dist/recipes/index.js +2 -0
  211. package/dist/recipes/index.js.map +1 -1
  212. package/dist/release-verification.d.ts +11 -1
  213. package/dist/release-verification.d.ts.map +1 -1
  214. package/dist/release-verification.js +47 -4
  215. package/dist/release-verification.js.map +1 -1
  216. package/dist/review.d.ts.map +1 -1
  217. package/dist/review.js +12 -8
  218. package/dist/review.js.map +1 -1
  219. package/dist/route-types.d.ts +28 -0
  220. package/dist/route-types.d.ts.map +1 -0
  221. package/dist/route-types.js +106 -0
  222. package/dist/route-types.js.map +1 -0
  223. package/dist/rules/codes.d.ts +9 -0
  224. package/dist/rules/codes.d.ts.map +1 -1
  225. package/dist/rules/codes.js +9 -0
  226. package/dist/rules/codes.js.map +1 -1
  227. package/dist/rules/data-guard.d.ts +12 -0
  228. package/dist/rules/data-guard.d.ts.map +1 -0
  229. package/dist/rules/data-guard.js +213 -0
  230. package/dist/rules/data-guard.js.map +1 -0
  231. package/dist/rules/index.d.ts +3 -0
  232. package/dist/rules/index.d.ts.map +1 -1
  233. package/dist/rules/index.js +17 -5
  234. package/dist/rules/index.js.map +1 -1
  235. package/dist/rules/islands.d.ts.map +1 -1
  236. package/dist/rules/islands.js +7 -4
  237. package/dist/rules/islands.js.map +1 -1
  238. package/dist/rules/legacy.d.ts.map +1 -1
  239. package/dist/rules/legacy.js +71 -38
  240. package/dist/rules/legacy.js.map +1 -1
  241. package/dist/rules/nano.d.ts.map +1 -1
  242. package/dist/rules/nano.js +15 -6
  243. package/dist/rules/nano.js.map +1 -1
  244. package/dist/rules/pages.d.ts +15 -0
  245. package/dist/rules/pages.d.ts.map +1 -0
  246. package/dist/rules/pages.js +88 -0
  247. package/dist/rules/pages.js.map +1 -0
  248. package/dist/rules/routes.d.ts +11 -0
  249. package/dist/rules/routes.d.ts.map +1 -1
  250. package/dist/rules/routes.js +180 -2
  251. package/dist/rules/routes.js.map +1 -1
  252. package/dist/rules/secrets.d.ts +10 -0
  253. package/dist/rules/secrets.d.ts.map +1 -0
  254. package/dist/rules/secrets.js +169 -0
  255. package/dist/rules/secrets.js.map +1 -0
  256. package/dist/rules/security.d.ts +7 -0
  257. package/dist/rules/security.d.ts.map +1 -1
  258. package/dist/rules/security.js +171 -65
  259. package/dist/rules/security.js.map +1 -1
  260. package/dist/rules/typescript.d.ts +16 -0
  261. package/dist/rules/typescript.d.ts.map +1 -0
  262. package/dist/rules/typescript.js +59 -0
  263. package/dist/rules/typescript.js.map +1 -0
  264. package/dist/rules/zones.d.ts +17 -0
  265. package/dist/rules/zones.d.ts.map +1 -0
  266. package/dist/rules/zones.js +174 -0
  267. package/dist/rules/zones.js.map +1 -0
  268. package/dist/scaffold.d.ts +25 -12
  269. package/dist/scaffold.d.ts.map +1 -1
  270. package/dist/scaffold.js +132 -87
  271. package/dist/scaffold.js.map +1 -1
  272. package/dist/sdk.d.ts +17 -3
  273. package/dist/sdk.d.ts.map +1 -1
  274. package/dist/sdk.js +941 -153
  275. package/dist/sdk.js.map +1 -1
  276. package/dist/search-terms.d.ts +2 -0
  277. package/dist/search-terms.d.ts.map +1 -1
  278. package/dist/search-terms.js +23 -8
  279. package/dist/search-terms.js.map +1 -1
  280. package/dist/smoke.d.ts +79 -0
  281. package/dist/smoke.d.ts.map +1 -0
  282. package/dist/smoke.js +494 -0
  283. package/dist/smoke.js.map +1 -0
  284. package/dist/sync-manifest.d.ts +10 -2
  285. package/dist/sync-manifest.d.ts.map +1 -1
  286. package/dist/sync-manifest.js +44 -19
  287. package/dist/sync-manifest.js.map +1 -1
  288. package/dist/sync-routes.d.ts +4 -3
  289. package/dist/sync-routes.d.ts.map +1 -1
  290. package/dist/sync-routes.js +4 -3
  291. package/dist/sync-routes.js.map +1 -1
  292. package/dist/test-tool.d.ts.map +1 -1
  293. package/dist/test-tool.js +30 -11
  294. package/dist/test-tool.js.map +1 -1
  295. package/dist/types-search.d.ts.map +1 -1
  296. package/dist/types-search.js +11 -5
  297. package/dist/types-search.js.map +1 -1
  298. package/dist/upgrade.d.ts +36 -6
  299. package/dist/upgrade.d.ts.map +1 -1
  300. package/dist/upgrade.js +167 -29
  301. package/dist/upgrade.js.map +1 -1
  302. package/dist/verification-plan.d.ts.map +1 -1
  303. package/dist/verification-plan.js +4 -0
  304. package/dist/verification-plan.js.map +1 -1
  305. package/dist/verification.d.ts.map +1 -1
  306. package/dist/verification.js +45 -12
  307. package/dist/verification.js.map +1 -1
  308. package/dist/web-app-options.d.ts +19 -0
  309. package/dist/web-app-options.d.ts.map +1 -0
  310. package/dist/web-app-options.js +49 -0
  311. package/dist/web-app-options.js.map +1 -0
  312. package/dist/work-graph.d.ts.map +1 -1
  313. package/dist/work-graph.js +29 -13
  314. package/dist/work-graph.js.map +1 -1
  315. package/docs/examples.json +185 -29
  316. package/docs/llms-full.txt +3807 -580
  317. package/docs/types.json +14229 -10939
  318. package/package.json +19 -12
  319. package/src/app-files.ts +40 -0
  320. package/src/assure-hydration.ts +139 -46
  321. package/src/assure.ts +17 -2
  322. package/src/capabilities-tool.ts +35 -9
  323. package/src/cdn-check.ts +268 -0
  324. package/src/check-diagnostics.ts +17 -0
  325. package/src/check-scan.ts +358 -63
  326. package/src/check.ts +97 -62
  327. package/src/cli.ts +296 -87
  328. package/src/command-catalog.ts +389 -48
  329. package/src/config-target.ts +47 -0
  330. package/src/contracts.ts +54 -12
  331. package/src/db-audit.ts +128 -0
  332. package/src/db-child.ts +285 -0
  333. package/src/db-config.ts +224 -0
  334. package/src/db-tool.ts +690 -0
  335. package/src/dev-bun-config.ts +116 -40
  336. package/src/dev-feed-tool.ts +912 -0
  337. package/src/dev-server-client.ts +267 -0
  338. package/src/diff-tool.ts +6 -5
  339. package/src/docs-search.ts +10 -3
  340. package/src/doctor.ts +98 -15
  341. package/src/env-file.ts +78 -1
  342. package/src/fix-recipes.ts +53 -32
  343. package/src/frontend-guidance.ts +5 -5
  344. package/src/i18n-check.ts +179 -0
  345. package/src/init-agents.ts +301 -38
  346. package/src/internal/code-unit-order.ts +5 -0
  347. package/src/internal/script-kind.ts +11 -0
  348. package/src/internal/source-facts.ts +96 -22
  349. package/src/internal/typescript-import.ts +663 -68
  350. package/src/introspect.ts +90 -27
  351. package/src/learn.ts +3 -3
  352. package/src/levels-tool.ts +47 -2
  353. package/src/load.ts +140 -33
  354. package/src/manifest-tool.ts +3 -2
  355. package/src/mcp-app-tool.ts +2 -0
  356. package/src/mcp-context.ts +1 -1
  357. package/src/mcp-delegate.ts +162 -0
  358. package/src/mcp-docs-tools.ts +23 -5
  359. package/src/mcp-exec.ts +160 -132
  360. package/src/mcp-http.ts +19 -5
  361. package/src/mcp-io.ts +57 -0
  362. package/src/mcp-isolate.ts +287 -0
  363. package/src/mcp-project-child.ts +182 -0
  364. package/src/mcp-render.ts +2 -2
  365. package/src/mcp-root.ts +117 -22
  366. package/src/mcp-run.ts +104 -10
  367. package/src/mcp-ws.ts +4 -1
  368. package/src/mcp.ts +106 -64
  369. package/src/migrate-layout.ts +1338 -0
  370. package/src/openapi-tool.ts +7 -3
  371. package/src/openapi-types.ts +315 -13
  372. package/src/pipeline-report.ts +22 -18
  373. package/src/port.ts +53 -30
  374. package/src/project-facts.ts +5 -0
  375. package/src/recipes/4.0.0.ts +37 -0
  376. package/src/recipes/index.ts +2 -0
  377. package/src/release-verification.ts +74 -5
  378. package/src/review.ts +13 -8
  379. package/src/route-types.ts +133 -0
  380. package/src/rules/codes.ts +9 -0
  381. package/src/rules/data-guard.ts +230 -0
  382. package/src/rules/index.ts +18 -5
  383. package/src/rules/islands.ts +12 -4
  384. package/src/rules/legacy.ts +71 -39
  385. package/src/rules/nano.ts +18 -6
  386. package/src/rules/pages.ts +99 -0
  387. package/src/rules/routes.ts +181 -2
  388. package/src/rules/secrets.ts +174 -0
  389. package/src/rules/security.ts +198 -70
  390. package/src/rules/typescript.ts +77 -0
  391. package/src/rules/zones.ts +195 -0
  392. package/src/scaffold.ts +147 -95
  393. package/src/sdk.ts +1215 -158
  394. package/src/search-terms.ts +24 -7
  395. package/src/smoke.ts +607 -0
  396. package/src/sync-manifest.ts +50 -19
  397. package/src/sync-routes.ts +4 -3
  398. package/src/test-tool.ts +32 -11
  399. package/src/types-search.ts +11 -4
  400. package/src/upgrade.ts +205 -25
  401. package/src/verification-plan.ts +13 -0
  402. package/src/verification.ts +50 -12
  403. package/src/web-app-options.ts +57 -0
  404. package/src/work-graph.ts +27 -13
@@ -0,0 +1,133 @@
1
+ /**
2
+ * `nifra types`: the generated `./+types/<route>` modules (see `@nifrajs/web/route-types`), plus the one
3
+ * tsconfig line that lets routes import them. `nifra dev`, `nifra build` and `nifra check` refresh them
4
+ * too, so a fresh checkout type-checks without running this by hand.
5
+ */
6
+ import { existsSync, readFileSync, watch } from "node:fs"
7
+ import { dirname, join, relative, resolve } from "node:path"
8
+ import { ROUTE_TYPES_DIR, staleRouteTypes, writeRouteTypes } from "@nifrajs/web/route-types"
9
+ import { stripComments } from "./check-scan.ts"
10
+
11
+ export interface RouteTypesReport {
12
+ readonly ok: boolean
13
+ readonly check: boolean
14
+ /** Files written (or, with `check`, that would be). */
15
+ readonly written: readonly string[]
16
+ readonly removed: readonly string[]
17
+ /** With `check`: every generated file missing, out of date, or no longer generated. */
18
+ readonly stale: readonly string[]
19
+ /** Set when tsconfig does not merge `.nifra/types` in through `rootDirs`. */
20
+ readonly tsconfig?: string
21
+ }
22
+
23
+ const TSCONFIG_HINT = `add "rootDirs": [".", "./${ROUTE_TYPES_DIR}"] to compilerOptions in tsconfig.json, so a route can import ./+types/<name>`
24
+
25
+ /** A tsconfig's JSONC as an object, following relative `extends`, or `undefined` when unreadable. */
26
+ function readTsconfig(
27
+ path: string,
28
+ depth = 0,
29
+ ): { compilerOptions?: Record<string, unknown> } | undefined {
30
+ if (depth > 8 || !existsSync(path)) return undefined
31
+ try {
32
+ const text = stripComments(readFileSync(path, "utf8")).replace(/,(\s*[}\]])/g, "$1")
33
+ const config = JSON.parse(text) as {
34
+ extends?: unknown
35
+ compilerOptions?: Record<string, unknown>
36
+ }
37
+ if (config.compilerOptions?.rootDirs !== undefined || typeof config.extends !== "string") {
38
+ return config
39
+ }
40
+ if (!config.extends.startsWith(".")) return config
41
+ const base = config.extends.endsWith(".json") ? config.extends : `${config.extends}.json`
42
+ const parent = readTsconfig(resolve(dirname(path), base), depth + 1)
43
+ return { compilerOptions: { ...parent?.compilerOptions, ...config.compilerOptions } }
44
+ } catch {
45
+ return undefined
46
+ }
47
+ }
48
+
49
+ /** Why the app's tsconfig cannot resolve `./+types/<name>`, or `undefined` when it can. */
50
+ export function routeTypesTsconfigIssue(appRoot: string): string | undefined {
51
+ const tsconfig = join(appRoot, "tsconfig.json")
52
+ if (!existsSync(tsconfig)) return undefined
53
+ const rootDirs = readTsconfig(tsconfig)?.compilerOptions?.rootDirs
54
+ const merged =
55
+ Array.isArray(rootDirs) &&
56
+ rootDirs.some(
57
+ (dir) =>
58
+ typeof dir === "string" && resolve(appRoot, dir) === resolve(appRoot, ROUTE_TYPES_DIR),
59
+ )
60
+ return merged ? undefined : TSCONFIG_HINT
61
+ }
62
+
63
+ const appRelative = (appRoot: string, paths: readonly string[]): string[] =>
64
+ paths.map((path) => relative(appRoot, path).replaceAll("\\", "/"))
65
+
66
+ /** Generate (or, with `check`, verify) an app's route types. */
67
+ export function routeTypes(
68
+ appRoot: string,
69
+ options: { readonly check?: boolean } = {},
70
+ ): RouteTypesReport {
71
+ const tsconfig = routeTypesTsconfigIssue(appRoot)
72
+ const extra = tsconfig === undefined ? {} : { tsconfig }
73
+ if (options.check === true) {
74
+ const stale = appRelative(appRoot, staleRouteTypes({ appRoot }))
75
+ return { ok: stale.length === 0, check: true, written: [], removed: [], stale, ...extra }
76
+ }
77
+ const { written, removed } = writeRouteTypes({ appRoot })
78
+ return {
79
+ ok: true,
80
+ check: false,
81
+ written: appRelative(appRoot, written),
82
+ removed: appRelative(appRoot, removed),
83
+ stale: [],
84
+ ...extra,
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Refresh route types before a command that type-checks or bundles; quiet unless something changed.
90
+ * Never throws: a routes tree the generator cannot read is the command's own error to report.
91
+ */
92
+ export function refreshRouteTypes(
93
+ appRoot: string,
94
+ log: (line: string) => void = console.log,
95
+ ): void {
96
+ if (!existsSync(join(appRoot, "routes"))) return
97
+ let report: RouteTypesReport
98
+ try {
99
+ report = routeTypes(appRoot)
100
+ } catch (error) {
101
+ log(`[nifra] route types: ${error instanceof Error ? error.message : String(error)}`)
102
+ return
103
+ }
104
+ const { written, removed, tsconfig } = report
105
+ if (written.length + removed.length > 0) {
106
+ log(
107
+ `[nifra] route types: ${written.length} written, ${removed.length} removed in ${ROUTE_TYPES_DIR}`,
108
+ )
109
+ }
110
+ if (tsconfig !== undefined) log(`[nifra] route types: ${tsconfig}`)
111
+ }
112
+
113
+ /**
114
+ * Keep route types current while `nifra dev` runs: they depend only on which route files exist, so a
115
+ * route added or removed (or `backend/app.ts` appearing) regenerates them. Returns the stop function.
116
+ */
117
+ export function watchRouteTypes(appRoot: string): () => void {
118
+ const routes = join(appRoot, "routes")
119
+ if (!existsSync(routes)) return () => {}
120
+ let timer: ReturnType<typeof setTimeout> | undefined
121
+ const schedule = (): void => {
122
+ clearTimeout(timer)
123
+ timer = setTimeout(() => refreshRouteTypes(appRoot), 50)
124
+ }
125
+ // Not persistent: the dev server's own shutdown must not wait on this watcher.
126
+ const watchers = [watch(routes, { recursive: true, persistent: false }, schedule)]
127
+ const backend = join(appRoot, "backend")
128
+ if (existsSync(backend)) watchers.push(watch(backend, { persistent: false }, schedule))
129
+ return () => {
130
+ clearTimeout(timer)
131
+ for (const watcher of watchers) watcher.close()
132
+ }
133
+ }
@@ -20,6 +20,15 @@ export const RULE_CODES = Object.freeze({
20
20
  "NF-C017": "Rule pack validation",
21
21
  "NF-C018": "Reserved client segment check",
22
22
  "NF-C019": "Duplicate route registration check",
23
+ "NF-C024": "Overlapping route registration check",
24
+ "NF-C025": "Route overlap analysis budget check",
25
+ "NF-C026": "Route param modifier check",
26
+ "NF-C027": "Page route under the backend mount",
27
+ "NF-C028": "Zone import check",
28
+ "NF-C029": "Private environment read in browser code",
29
+ "NF-C030": "Route data without an output schema",
30
+ "NF-C031": "Sensitive field in an output schema",
31
+ "NF-C032": "Credential in browser code",
23
32
  "NF-C020": "Island enhancer cleanup check",
24
33
  "NF-C021": "nano binding cleanup check",
25
34
  "NF-C022": "nano bindList key check",
@@ -0,0 +1,230 @@
1
+ /**
2
+ * NF-C030 and NF-C031: the data guard, checked from source.
3
+ *
4
+ * The server enforces both at runtime, on every request: data a loader or action returns without an
5
+ * output schema fails the request, and an output schema naming a sensitive field without
6
+ * `t.declassified` fails the route when it loads. These report what source alone shows, before a
7
+ * request ever does. NF-C030 is a warning because only running the loader proves it returns data.
8
+ */
9
+ import { isSensitiveFieldName } from "@nifrajs/web/zones"
10
+ import { codePositionMask } from "../check-scan.ts"
11
+ import { type Diagnostic, diagnostic } from "../diagnostics.ts"
12
+ import { codeUnitOrder } from "../internal/code-unit-order.ts"
13
+ import type { CheckRule } from "./index.ts"
14
+ import { zonedFiles } from "./zones.ts"
15
+
16
+ const lineAt = (text: string, index: number): number => text.slice(0, index).split("\n").length
17
+
18
+ const byPlace = (a: Diagnostic, b: Diagnostic): number =>
19
+ codeUnitOrder(a.file ?? "", b.file ?? "") || (a.line ?? 0) - (b.line ?? 0)
20
+
21
+ const exported = (mask: string, name: string): RegExpExecArray | null =>
22
+ new RegExp(
23
+ `\\bexport\\s+(?:(?:async\\s+)?function\\s*\\*?\\s*|(?:const|let|var)\\s+)${name}\\b|\\bexport\\s*\\{[^}]*\\b${name}\\b[^}]*\\}`,
24
+ ).exec(mask)
25
+
26
+ /** A return that hands the browser nothing: a bare return, a redirect, a status, a thrown signal. */
27
+ const CONTROL_FLOW =
28
+ /^(?:;|$|null\b|undefined\b|redirect\s*\(|notFound\s*\(|gone\s*\(|statusPage\s*\(|new\s+Response\s*\(|Response\s*\.)/
29
+
30
+ const closing = (mask: string, open: number): number => {
31
+ let depth = 0
32
+ for (let i = open; i < mask.length; i++) {
33
+ const c = mask[i]
34
+ if (c === "(" || c === "{" || c === "[") depth++
35
+ else if (c === ")" || c === "}" || c === "]") {
36
+ depth--
37
+ if (depth === 0) return i
38
+ }
39
+ }
40
+ return mask.length
41
+ }
42
+
43
+ /**
44
+ * Whether the function exported as `name` may return data, read from its type-stripped source: an
45
+ * arrow's expression body, or a `return <value>` that is not a redirect or a status.
46
+ */
47
+ function mayReturnData(js: string, name: string): boolean {
48
+ const mask = codePositionMask(js)
49
+ const found = exported(mask, name)
50
+ if (found === null || found[0].startsWith("export {") || found[0].startsWith("export{")) {
51
+ return true // declared elsewhere in the file and re-exported: assume it does
52
+ }
53
+ const params = mask.indexOf("(", found.index + found[0].length)
54
+ if (params === -1) return true
55
+ let at = closing(mask, params) + 1
56
+ while (/\s/.test(mask[at] ?? "")) at++
57
+ if (mask.startsWith("=>", at)) {
58
+ at += 2
59
+ while (/\s/.test(mask[at] ?? "")) at++
60
+ if (mask[at] !== "{") return !CONTROL_FLOW.test(js.slice(at, at + 40))
61
+ }
62
+ if (mask[at] !== "{") return true
63
+ const body = mask.slice(at, closing(mask, at))
64
+ for (const match of body.matchAll(/\breturn\b/g)) {
65
+ const from = at + match.index + "return".length
66
+ const rest = js.slice(from, from + 40).replace(/^[ \t]+/, "")
67
+ if (!CONTROL_FLOW.test(rest)) return true
68
+ }
69
+ return false
70
+ }
71
+
72
+ /** Where an expression starting at `start` ends: a `,`, `;` or closer at its own depth. */
73
+ function expressionEnd(mask: string, start: number): number {
74
+ let depth = 0
75
+ for (let i = start; i < mask.length; i++) {
76
+ const c = mask[i]
77
+ if (c === "(" || c === "{" || c === "[") depth++
78
+ else if (c === ")" || c === "}" || c === "]") {
79
+ if (depth === 0) return i
80
+ depth--
81
+ } else if (depth === 0 && (c === "," || c === ";")) return i
82
+ else if (depth === 0 && c === "\n") {
83
+ const next = /\S/.exec(mask.slice(i))?.[0]
84
+ const prev = /\S(?=\s*$)/.exec(mask.slice(start, i))?.[0]
85
+ if (
86
+ next !== undefined &&
87
+ !".?:+-*/&|".includes(next) &&
88
+ !"=(,[{:?+-*/&|".includes(prev ?? "")
89
+ ) {
90
+ return i
91
+ }
92
+ }
93
+ }
94
+ return mask.length
95
+ }
96
+
97
+ /** Object keys inside `[start, end)` whose name is sensitive and whose value is not declassified. */
98
+ function sensitiveKeys(
99
+ src: string,
100
+ mask: string,
101
+ start: number,
102
+ end: number,
103
+ ): Array<{ readonly key: string; readonly index: number }> {
104
+ const found: Array<{ key: string; index: number }> = []
105
+ for (
106
+ let colon = mask.indexOf(":", start);
107
+ colon !== -1 && colon < end;
108
+ colon = mask.indexOf(":", colon + 1)
109
+ ) {
110
+ let k = colon - 1
111
+ while (k >= start && /\s/.test(src[k] ?? "")) k--
112
+ let key: string
113
+ let keyStart: number
114
+ if (/[\w$]/.test(mask[k] ?? "")) {
115
+ keyStart = k
116
+ while (keyStart > start && /[\w$]/.test(mask[keyStart - 1] ?? "")) keyStart--
117
+ key = mask.slice(keyStart, k + 1)
118
+ } else if (src[k] === '"' || src[k] === "'") {
119
+ keyStart = src.lastIndexOf(src[k] as string, k - 1)
120
+ if (keyStart < start) continue
121
+ key = src.slice(keyStart + 1, k)
122
+ } else continue
123
+ let p = keyStart - 1
124
+ while (p >= start && /\s/.test(mask[p] ?? "")) p--
125
+ if (mask[p] !== "{" && mask[p] !== ",") continue
126
+ if (!isSensitiveFieldName(key)) continue
127
+ const value = src.slice(colon + 1, colon + 80).trimStart()
128
+ if (/^[\w$.]*declassified\s*\(/.test(value)) continue
129
+ found.push({ key, index: keyStart })
130
+ }
131
+ return found
132
+ }
133
+
134
+ export interface MissingOutputSchema {
135
+ readonly name: "loader" | "action"
136
+ readonly schema: "loaderOutput" | "actionOutput"
137
+ readonly line: number
138
+ }
139
+
140
+ let transpiler: Bun.Transpiler | undefined
141
+
142
+ /** The loader and action a route's backend half exports, may return data from, and gives no schema. */
143
+ export function missingOutputSchemas(content: string): MissingOutputSchema[] {
144
+ const mask = codePositionMask(content)
145
+ const missing: MissingOutputSchema[] = []
146
+ let js: string | undefined
147
+ for (const [name, schema] of [
148
+ ["loader", "loaderOutput"],
149
+ ["action", "actionOutput"],
150
+ ] as const) {
151
+ const fn = exported(mask, name)
152
+ if (fn === null || exported(mask, schema) !== null) continue
153
+ try {
154
+ transpiler ??= new Bun.Transpiler({ loader: "ts" })
155
+ js ??= transpiler.transformSync(content)
156
+ } catch {
157
+ js = content
158
+ }
159
+ if (mayReturnData(js, name)) missing.push({ name, schema, line: lineAt(content, fn.index) })
160
+ }
161
+ return missing
162
+ }
163
+
164
+ export const missingOutputRule: CheckRule = {
165
+ code: "NF-C030",
166
+ title: "Route data without an output schema",
167
+ async scan(ctx) {
168
+ const findings: Diagnostic[] = []
169
+ for (const { file, classification } of zonedFiles(ctx).files) {
170
+ if (classification.zone !== "route-backend") continue
171
+ const content = ctx.sources.read(file)
172
+ if (content === undefined) continue
173
+ for (const { name, schema, line } of missingOutputSchemas(content)) {
174
+ findings.push(
175
+ diagnostic({
176
+ code: "NF-C030",
177
+ severity: "warn",
178
+ file,
179
+ line,
180
+ message: `${file} exports a ${name} but no ${schema}. Data it returns fails the request: everything sent to the browser needs an output schema. Export one: export const ${schema} = t.object({ ... })`,
181
+ evidence: [`export: ${name}`, `missing: ${schema}`],
182
+ verify: "nifra check",
183
+ }),
184
+ )
185
+ }
186
+ }
187
+ return findings.sort(byPlace)
188
+ },
189
+ }
190
+
191
+ const OUTPUT_DECLARATION =
192
+ /\bexport\s+(?:const|let|var)\s+(?:loaderOutput|actionOutput)\b[^=]*=|\boutput\s*:/g
193
+
194
+ export const sensitiveOutputRule: CheckRule = {
195
+ code: "NF-C031",
196
+ title: "Sensitive field in an output schema",
197
+ async scan(ctx) {
198
+ const findings: Diagnostic[] = []
199
+ for (const { file, classification } of zonedFiles(ctx).files) {
200
+ if (classification.zone !== "route-backend" && classification.zone !== "fn") continue
201
+ const content = ctx.sources.read(file)
202
+ if (content === undefined) continue
203
+ const mask = codePositionMask(content)
204
+ for (const declaration of mask.matchAll(OUTPUT_DECLARATION)) {
205
+ const start = declaration.index + declaration[0].length
206
+ for (const { key, index } of sensitiveKeys(
207
+ content,
208
+ mask,
209
+ start,
210
+ expressionEnd(mask, start),
211
+ )) {
212
+ findings.push(
213
+ diagnostic({
214
+ code: "NF-C031",
215
+ severity: "error",
216
+ file,
217
+ line: lineAt(content, index),
218
+ message: `${file} declares "${key}" in an output schema, a field name that usually holds a credential or personal identifier. Remove it, or wrap its schema in t.declassified("why it may reach the browser", ...)`,
219
+ evidence: [`field: ${key}`],
220
+ verify: "nifra check",
221
+ }),
222
+ )
223
+ }
224
+ }
225
+ }
226
+ return findings.sort(byPlace)
227
+ },
228
+ }
229
+
230
+ export const dataGuardRules = Object.freeze([missingOutputRule, sensitiveOutputRule])
@@ -1,6 +1,8 @@
1
1
  import type { Diagnostic } from "../diagnostics.ts"
2
+ import type { TypeScriptSession } from "../internal/typescript-import.ts"
2
3
  import type { ProjectFacts } from "../project-facts.ts"
3
4
  import { isBuiltInCode } from "./codes.ts"
5
+ import { releaseRuleTypeScript, retainRuleTypeScript } from "./typescript.ts"
4
6
 
5
7
  export type { ProjectFacts } from "../project-facts.ts"
6
8
 
@@ -18,6 +20,8 @@ export interface RuleContext {
18
20
  readonly root: string
19
21
  readonly sources: SourceIndex
20
22
  readonly project: ProjectFacts
23
+ /** One compiler session shared by every AST-backed rule in a check. */
24
+ readonly typescriptSession?: TypeScriptSession
21
25
  }
22
26
 
23
27
  export interface CheckRule {
@@ -155,12 +159,21 @@ export async function runRuleRegistry(
155
159
  ): Promise<Diagnostic[]> {
156
160
  const rules = [...builtIns, ...validateRulePacks(packs).flatMap((pack) => pack.rules)]
157
161
  const out: Diagnostic[] = []
158
- for (const rule of rules) {
159
- if (isBuiltInCode(rule.code) === false && rule.code.startsWith("NF-")) {
160
- throw new Error(`application rule pack code ${rule.code} uses the reserved NF- prefix`)
162
+ const ownsTypeScriptSession =
163
+ ctx.typescriptSession === undefined &&
164
+ ctx.project.check.unsupportedTypeScriptVersion === undefined &&
165
+ rules.some((rule) => isBuiltInCode(rule.code) || rule.code.startsWith("NF-"))
166
+ if (ownsTypeScriptSession) retainRuleTypeScript(ctx)
167
+ try {
168
+ for (const rule of rules) {
169
+ if (isBuiltInCode(rule.code) === false && rule.code.startsWith("NF-")) {
170
+ throw new Error(`application rule pack code ${rule.code} uses the reserved NF- prefix`)
171
+ }
172
+ const findings = await rule.scan(ctx)
173
+ out.push(...findings)
161
174
  }
162
- const findings = await rule.scan(ctx)
163
- out.push(...findings)
175
+ } finally {
176
+ if (ownsTypeScriptSession) await releaseRuleTypeScript(ctx)
164
177
  }
165
178
  return out
166
179
  }
@@ -1,7 +1,8 @@
1
1
  import type * as TSApi from "typescript"
2
2
  import { type Diagnostic, diagnostic } from "../diagnostics.ts"
3
- import { importProjectTypeScript } from "../internal/typescript-import.ts"
3
+ import { scriptKindOf } from "../internal/script-kind.ts"
4
4
  import type { CheckRule, SourceIndex } from "./index.ts"
5
+ import { loadRuleTypeScript } from "./typescript.ts"
5
6
 
6
7
  /**
7
8
  * NF-C020 - the island cleanup lint. An island enhancer (`@nifrajs/web/islands`) runs once and must
@@ -43,8 +44,13 @@ function parsedTree(
43
44
  files.set(file, null)
44
45
  return undefined
45
46
  }
46
- const kind = /\.tsx?$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.JS
47
- const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind)
47
+ const tree = ts.createSourceFile(
48
+ file,
49
+ source,
50
+ ts.ScriptTarget.Latest,
51
+ true,
52
+ scriptKindOf(ts, file),
53
+ )
48
54
  files.set(file, { tree })
49
55
  return tree
50
56
  }
@@ -126,7 +132,9 @@ export const islandCleanupRule: CheckRule = {
126
132
  code: "NF-C020",
127
133
  title: "Island enhancer cleanup check",
128
134
  async scan(ctx) {
129
- const ts = await importProjectTypeScript(ctx.root)
135
+ const loaded = await loadRuleTypeScript(ctx)
136
+ if (loaded.diagnostics !== undefined) return loaded.diagnostics
137
+ const ts = loaded.compiler
130
138
  if (ts === undefined) {
131
139
  return [
132
140
  diagnostic({