@nifrajs/cli 3.3.0 → 4.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.
Files changed (425) 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 +7 -15
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +162 -713
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +58 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +323 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts +1 -0
  30. package/dist/check.d.ts.map +1 -1
  31. package/dist/check.js +145 -54
  32. package/dist/check.js.map +1 -1
  33. package/dist/cli.d.ts +42 -16
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +306 -87
  36. package/dist/cli.js.map +1 -1
  37. package/dist/command-catalog.d.ts +60 -5
  38. package/dist/command-catalog.d.ts.map +1 -1
  39. package/dist/command-catalog.js +298 -33
  40. package/dist/command-catalog.js.map +1 -1
  41. package/dist/config-target.d.ts +8 -0
  42. package/dist/config-target.d.ts.map +1 -0
  43. package/dist/config-target.js +45 -0
  44. package/dist/config-target.js.map +1 -0
  45. package/dist/contracts.d.ts +3 -2
  46. package/dist/contracts.d.ts.map +1 -1
  47. package/dist/contracts.js +56 -16
  48. package/dist/contracts.js.map +1 -1
  49. package/dist/db-audit.d.ts +37 -0
  50. package/dist/db-audit.d.ts.map +1 -0
  51. package/dist/db-audit.js +99 -0
  52. package/dist/db-audit.js.map +1 -0
  53. package/dist/db-child.d.ts +64 -0
  54. package/dist/db-child.d.ts.map +1 -0
  55. package/dist/db-child.js +213 -0
  56. package/dist/db-child.js.map +1 -0
  57. package/dist/db-config.d.ts +60 -0
  58. package/dist/db-config.d.ts.map +1 -0
  59. package/dist/db-config.js +176 -0
  60. package/dist/db-config.js.map +1 -0
  61. package/dist/db-tool.d.ts +84 -0
  62. package/dist/db-tool.d.ts.map +1 -0
  63. package/dist/db-tool.js +537 -0
  64. package/dist/db-tool.js.map +1 -0
  65. package/dist/dev-bun-config.d.ts +20 -13
  66. package/dist/dev-bun-config.d.ts.map +1 -1
  67. package/dist/dev-bun-config.js +98 -41
  68. package/dist/dev-bun-config.js.map +1 -1
  69. package/dist/dev-feed-tool.d.ts +106 -0
  70. package/dist/dev-feed-tool.d.ts.map +1 -0
  71. package/dist/dev-feed-tool.js +713 -0
  72. package/dist/dev-feed-tool.js.map +1 -0
  73. package/dist/dev-server-client.d.ts +67 -0
  74. package/dist/dev-server-client.d.ts.map +1 -0
  75. package/dist/dev-server-client.js +195 -0
  76. package/dist/dev-server-client.js.map +1 -0
  77. package/dist/diagnostics.d.ts +86 -0
  78. package/dist/diagnostics.d.ts.map +1 -1
  79. package/dist/diagnostics.js +85 -0
  80. package/dist/diagnostics.js.map +1 -1
  81. package/dist/diff-tool.d.ts +3 -3
  82. package/dist/diff-tool.d.ts.map +1 -1
  83. package/dist/diff-tool.js +6 -5
  84. package/dist/diff-tool.js.map +1 -1
  85. package/dist/docs-search.d.ts.map +1 -1
  86. package/dist/docs-search.js +12 -4
  87. package/dist/docs-search.js.map +1 -1
  88. package/dist/doctor.d.ts +17 -0
  89. package/dist/doctor.d.ts.map +1 -1
  90. package/dist/doctor.js +76 -15
  91. package/dist/doctor.js.map +1 -1
  92. package/dist/env-file.d.ts +15 -0
  93. package/dist/env-file.d.ts.map +1 -1
  94. package/dist/env-file.js +73 -1
  95. package/dist/env-file.js.map +1 -1
  96. package/dist/fix-recipes.d.ts.map +1 -1
  97. package/dist/fix-recipes.js +41 -21
  98. package/dist/fix-recipes.js.map +1 -1
  99. package/dist/frontend-guidance.js +5 -5
  100. package/dist/frontend-guidance.js.map +1 -1
  101. package/dist/i18n-check.d.ts +16 -0
  102. package/dist/i18n-check.d.ts.map +1 -0
  103. package/dist/i18n-check.js +154 -0
  104. package/dist/i18n-check.js.map +1 -0
  105. package/dist/init-agents.d.ts +49 -13
  106. package/dist/init-agents.d.ts.map +1 -1
  107. package/dist/init-agents.js +239 -34
  108. package/dist/init-agents.js.map +1 -1
  109. package/dist/internal/code-unit-order.d.ts +4 -0
  110. package/dist/internal/code-unit-order.d.ts.map +1 -0
  111. package/dist/internal/code-unit-order.js +6 -0
  112. package/dist/internal/code-unit-order.js.map +1 -0
  113. package/dist/internal/script-kind.d.ts +7 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -0
  115. package/dist/internal/script-kind.js +12 -0
  116. package/dist/internal/script-kind.js.map +1 -0
  117. package/dist/internal/source-facts.d.ts +5 -0
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +61 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +49 -24
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +507 -67
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +6 -4
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +93 -74
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/learn.js +3 -3
  130. package/dist/learn.js.map +1 -1
  131. package/dist/levels-tool.d.ts +10 -0
  132. package/dist/levels-tool.d.ts.map +1 -1
  133. package/dist/levels-tool.js +33 -3
  134. package/dist/levels-tool.js.map +1 -1
  135. package/dist/load.d.ts +71 -16
  136. package/dist/load.d.ts.map +1 -1
  137. package/dist/load.js +78 -18
  138. package/dist/load.js.map +1 -1
  139. package/dist/manifest-tool.d.ts.map +1 -1
  140. package/dist/manifest-tool.js +3 -2
  141. package/dist/manifest-tool.js.map +1 -1
  142. package/dist/mcp-app-tool.d.ts.map +1 -1
  143. package/dist/mcp-app-tool.js +2 -0
  144. package/dist/mcp-app-tool.js.map +1 -1
  145. package/dist/mcp-context.d.ts.map +1 -1
  146. package/dist/mcp-context.js +5 -6
  147. package/dist/mcp-context.js.map +1 -1
  148. package/dist/mcp-delegate.d.ts +57 -0
  149. package/dist/mcp-delegate.d.ts.map +1 -0
  150. package/dist/mcp-delegate.js +130 -0
  151. package/dist/mcp-delegate.js.map +1 -0
  152. package/dist/mcp-docs-tools.d.ts.map +1 -1
  153. package/dist/mcp-docs-tools.js +23 -5
  154. package/dist/mcp-docs-tools.js.map +1 -1
  155. package/dist/mcp-exec.d.ts +3 -0
  156. package/dist/mcp-exec.d.ts.map +1 -1
  157. package/dist/mcp-exec.js +263 -131
  158. package/dist/mcp-exec.js.map +1 -1
  159. package/dist/mcp-http.d.ts.map +1 -1
  160. package/dist/mcp-http.js +20 -5
  161. package/dist/mcp-http.js.map +1 -1
  162. package/dist/mcp-io.d.ts +26 -0
  163. package/dist/mcp-io.d.ts.map +1 -1
  164. package/dist/mcp-io.js +113 -0
  165. package/dist/mcp-io.js.map +1 -1
  166. package/dist/mcp-isolate.d.ts +48 -0
  167. package/dist/mcp-isolate.d.ts.map +1 -0
  168. package/dist/mcp-isolate.js +222 -0
  169. package/dist/mcp-isolate.js.map +1 -0
  170. package/dist/mcp-path.d.ts +10 -0
  171. package/dist/mcp-path.d.ts.map +1 -0
  172. package/dist/mcp-path.js +51 -0
  173. package/dist/mcp-path.js.map +1 -0
  174. package/dist/mcp-project-child.d.ts +74 -0
  175. package/dist/mcp-project-child.d.ts.map +1 -0
  176. package/dist/mcp-project-child.js +130 -0
  177. package/dist/mcp-project-child.js.map +1 -0
  178. package/dist/mcp-reflect.d.ts.map +1 -1
  179. package/dist/mcp-reflect.js +2 -2
  180. package/dist/mcp-reflect.js.map +1 -1
  181. package/dist/mcp-render.d.ts.map +1 -1
  182. package/dist/mcp-render.js +64 -45
  183. package/dist/mcp-render.js.map +1 -1
  184. package/dist/mcp-root.d.ts +37 -9
  185. package/dist/mcp-root.d.ts.map +1 -1
  186. package/dist/mcp-root.js +124 -25
  187. package/dist/mcp-root.js.map +1 -1
  188. package/dist/mcp-run.d.ts +18 -1
  189. package/dist/mcp-run.d.ts.map +1 -1
  190. package/dist/mcp-run.js +159 -55
  191. package/dist/mcp-run.js.map +1 -1
  192. package/dist/mcp-ws.js +4 -1
  193. package/dist/mcp-ws.js.map +1 -1
  194. package/dist/mcp.d.ts +18 -5
  195. package/dist/mcp.d.ts.map +1 -1
  196. package/dist/mcp.js +165 -82
  197. package/dist/mcp.js.map +1 -1
  198. package/dist/migrate-layout.d.ts +44 -0
  199. package/dist/migrate-layout.d.ts.map +1 -0
  200. package/dist/migrate-layout.js +982 -0
  201. package/dist/migrate-layout.js.map +1 -0
  202. package/dist/openapi-tool.d.ts +2 -2
  203. package/dist/openapi-tool.d.ts.map +1 -1
  204. package/dist/openapi-tool.js.map +1 -1
  205. package/dist/openapi-types.d.ts +2 -1
  206. package/dist/openapi-types.d.ts.map +1 -1
  207. package/dist/openapi-types.js +240 -12
  208. package/dist/openapi-types.js.map +1 -1
  209. package/dist/pipeline-report.d.ts +3 -2
  210. package/dist/pipeline-report.d.ts.map +1 -1
  211. package/dist/pipeline-report.js +19 -16
  212. package/dist/pipeline-report.js.map +1 -1
  213. package/dist/port.d.ts +5 -4
  214. package/dist/port.d.ts.map +1 -1
  215. package/dist/port.js +46 -29
  216. package/dist/port.js.map +1 -1
  217. package/dist/project-facts.d.ts +28 -4
  218. package/dist/project-facts.d.ts.map +1 -1
  219. package/dist/project-facts.js +12 -2
  220. package/dist/project-facts.js.map +1 -1
  221. package/dist/recipes/4.0.0.d.ts +9 -0
  222. package/dist/recipes/4.0.0.d.ts.map +1 -0
  223. package/dist/recipes/4.0.0.js +34 -0
  224. package/dist/recipes/4.0.0.js.map +1 -0
  225. package/dist/recipes/index.d.ts.map +1 -1
  226. package/dist/recipes/index.js +2 -0
  227. package/dist/recipes/index.js.map +1 -1
  228. package/dist/release-verification.d.ts +11 -1
  229. package/dist/release-verification.d.ts.map +1 -1
  230. package/dist/release-verification.js +47 -4
  231. package/dist/release-verification.js.map +1 -1
  232. package/dist/review.d.ts +21 -0
  233. package/dist/review.d.ts.map +1 -0
  234. package/dist/review.js +823 -0
  235. package/dist/review.js.map +1 -0
  236. package/dist/route-types.d.ts +28 -0
  237. package/dist/route-types.d.ts.map +1 -0
  238. package/dist/route-types.js +106 -0
  239. package/dist/route-types.js.map +1 -0
  240. package/dist/rules/codes.d.ts +9 -0
  241. package/dist/rules/codes.d.ts.map +1 -1
  242. package/dist/rules/codes.js +9 -0
  243. package/dist/rules/codes.js.map +1 -1
  244. package/dist/rules/data-guard.d.ts +12 -0
  245. package/dist/rules/data-guard.d.ts.map +1 -0
  246. package/dist/rules/data-guard.js +213 -0
  247. package/dist/rules/data-guard.js.map +1 -0
  248. package/dist/rules/index.d.ts +3 -0
  249. package/dist/rules/index.d.ts.map +1 -1
  250. package/dist/rules/index.js +17 -5
  251. package/dist/rules/index.js.map +1 -1
  252. package/dist/rules/islands.d.ts.map +1 -1
  253. package/dist/rules/islands.js +7 -4
  254. package/dist/rules/islands.js.map +1 -1
  255. package/dist/rules/legacy.d.ts +3 -1
  256. package/dist/rules/legacy.d.ts.map +1 -1
  257. package/dist/rules/legacy.js +829 -34
  258. package/dist/rules/legacy.js.map +1 -1
  259. package/dist/rules/nano.d.ts.map +1 -1
  260. package/dist/rules/nano.js +15 -6
  261. package/dist/rules/nano.js.map +1 -1
  262. package/dist/rules/pages.d.ts +15 -0
  263. package/dist/rules/pages.d.ts.map +1 -0
  264. package/dist/rules/pages.js +88 -0
  265. package/dist/rules/pages.js.map +1 -0
  266. package/dist/rules/routes.d.ts +11 -0
  267. package/dist/rules/routes.d.ts.map +1 -1
  268. package/dist/rules/routes.js +180 -2
  269. package/dist/rules/routes.js.map +1 -1
  270. package/dist/rules/secrets.d.ts +10 -0
  271. package/dist/rules/secrets.d.ts.map +1 -0
  272. package/dist/rules/secrets.js +169 -0
  273. package/dist/rules/secrets.js.map +1 -0
  274. package/dist/rules/security.d.ts +7 -0
  275. package/dist/rules/security.d.ts.map +1 -1
  276. package/dist/rules/security.js +171 -65
  277. package/dist/rules/security.js.map +1 -1
  278. package/dist/rules/typescript.d.ts +16 -0
  279. package/dist/rules/typescript.d.ts.map +1 -0
  280. package/dist/rules/typescript.js +59 -0
  281. package/dist/rules/typescript.js.map +1 -0
  282. package/dist/rules/zones.d.ts +17 -0
  283. package/dist/rules/zones.d.ts.map +1 -0
  284. package/dist/rules/zones.js +174 -0
  285. package/dist/rules/zones.js.map +1 -0
  286. package/dist/scaffold.d.ts +25 -12
  287. package/dist/scaffold.d.ts.map +1 -1
  288. package/dist/scaffold.js +154 -90
  289. package/dist/scaffold.js.map +1 -1
  290. package/dist/sdk.d.ts +17 -3
  291. package/dist/sdk.d.ts.map +1 -1
  292. package/dist/sdk.js +941 -153
  293. package/dist/sdk.js.map +1 -1
  294. package/dist/search-terms.d.ts +2 -0
  295. package/dist/search-terms.d.ts.map +1 -1
  296. package/dist/search-terms.js +23 -8
  297. package/dist/search-terms.js.map +1 -1
  298. package/dist/smoke.d.ts +79 -0
  299. package/dist/smoke.d.ts.map +1 -0
  300. package/dist/smoke.js +494 -0
  301. package/dist/smoke.js.map +1 -0
  302. package/dist/sync-manifest.d.ts.map +1 -1
  303. package/dist/sync-manifest.js +2 -1
  304. package/dist/sync-manifest.js.map +1 -1
  305. package/dist/sync-routes.d.ts +4 -3
  306. package/dist/sync-routes.d.ts.map +1 -1
  307. package/dist/sync-routes.js +4 -3
  308. package/dist/sync-routes.js.map +1 -1
  309. package/dist/test-tool.d.ts.map +1 -1
  310. package/dist/test-tool.js +30 -11
  311. package/dist/test-tool.js.map +1 -1
  312. package/dist/types-search.d.ts.map +1 -1
  313. package/dist/types-search.js +11 -5
  314. package/dist/types-search.js.map +1 -1
  315. package/dist/upgrade.d.ts +36 -6
  316. package/dist/upgrade.d.ts.map +1 -1
  317. package/dist/upgrade.js +167 -29
  318. package/dist/upgrade.js.map +1 -1
  319. package/dist/verification-plan.d.ts.map +1 -1
  320. package/dist/verification-plan.js +8 -0
  321. package/dist/verification-plan.js.map +1 -1
  322. package/dist/verification.d.ts.map +1 -1
  323. package/dist/verification.js +45 -12
  324. package/dist/verification.js.map +1 -1
  325. package/dist/web-app-options.d.ts +19 -0
  326. package/dist/web-app-options.d.ts.map +1 -0
  327. package/dist/web-app-options.js +49 -0
  328. package/dist/web-app-options.js.map +1 -0
  329. package/dist/work-graph.d.ts +3 -0
  330. package/dist/work-graph.d.ts.map +1 -1
  331. package/dist/work-graph.js +33 -16
  332. package/dist/work-graph.js.map +1 -1
  333. package/docs/examples.json +185 -29
  334. package/docs/llms-full.txt +4123 -548
  335. package/docs/types.json +14391 -10060
  336. package/package.json +19 -11
  337. package/src/app-files.ts +40 -0
  338. package/src/assure-hydration.ts +139 -46
  339. package/src/assure.ts +17 -2
  340. package/src/capabilities-tool.ts +35 -9
  341. package/src/cdn-check.ts +268 -0
  342. package/src/check-diagnostics.ts +199 -797
  343. package/src/check-scan.ts +394 -63
  344. package/src/check.ts +146 -58
  345. package/src/cli.ts +347 -92
  346. package/src/command-catalog.ts +407 -45
  347. package/src/config-target.ts +47 -0
  348. package/src/contracts.ts +66 -15
  349. package/src/db-audit.ts +128 -0
  350. package/src/db-child.ts +285 -0
  351. package/src/db-config.ts +224 -0
  352. package/src/db-tool.ts +690 -0
  353. package/src/dev-bun-config.ts +116 -40
  354. package/src/dev-feed-tool.ts +912 -0
  355. package/src/dev-server-client.ts +267 -0
  356. package/src/diagnostics.ts +179 -0
  357. package/src/diff-tool.ts +6 -5
  358. package/src/docs-search.ts +10 -3
  359. package/src/doctor.ts +98 -15
  360. package/src/env-file.ts +78 -1
  361. package/src/fix-recipes.ts +44 -31
  362. package/src/frontend-guidance.ts +5 -5
  363. package/src/i18n-check.ts +179 -0
  364. package/src/init-agents.ts +301 -38
  365. package/src/internal/code-unit-order.ts +5 -0
  366. package/src/internal/script-kind.ts +11 -0
  367. package/src/internal/source-facts.ts +96 -22
  368. package/src/internal/typescript-import.ts +663 -68
  369. package/src/introspect.ts +93 -82
  370. package/src/learn.ts +3 -3
  371. package/src/levels-tool.ts +47 -2
  372. package/src/load.ts +144 -32
  373. package/src/manifest-tool.ts +3 -2
  374. package/src/mcp-app-tool.ts +2 -0
  375. package/src/mcp-context.ts +5 -7
  376. package/src/mcp-delegate.ts +162 -0
  377. package/src/mcp-docs-tools.ts +23 -5
  378. package/src/mcp-exec.ts +301 -161
  379. package/src/mcp-http.ts +22 -5
  380. package/src/mcp-io.ts +130 -0
  381. package/src/mcp-isolate.ts +287 -0
  382. package/src/mcp-path.ts +50 -0
  383. package/src/mcp-project-child.ts +182 -0
  384. package/src/mcp-reflect.ts +3 -2
  385. package/src/mcp-render.ts +72 -42
  386. package/src/mcp-root.ts +130 -24
  387. package/src/mcp-run.ts +190 -56
  388. package/src/mcp-ws.ts +4 -1
  389. package/src/mcp.ts +193 -91
  390. package/src/migrate-layout.ts +1121 -0
  391. package/src/openapi-tool.ts +7 -3
  392. package/src/openapi-types.ts +315 -13
  393. package/src/pipeline-report.ts +22 -18
  394. package/src/port.ts +53 -30
  395. package/src/project-facts.ts +42 -8
  396. package/src/recipes/4.0.0.ts +35 -0
  397. package/src/recipes/index.ts +2 -0
  398. package/src/release-verification.ts +74 -5
  399. package/src/review.ts +988 -0
  400. package/src/route-types.ts +133 -0
  401. package/src/rules/codes.ts +9 -0
  402. package/src/rules/data-guard.ts +230 -0
  403. package/src/rules/index.ts +18 -5
  404. package/src/rules/islands.ts +12 -4
  405. package/src/rules/legacy.ts +947 -35
  406. package/src/rules/nano.ts +18 -6
  407. package/src/rules/pages.ts +99 -0
  408. package/src/rules/routes.ts +181 -2
  409. package/src/rules/secrets.ts +174 -0
  410. package/src/rules/security.ts +198 -70
  411. package/src/rules/typescript.ts +77 -0
  412. package/src/rules/zones.ts +195 -0
  413. package/src/scaffold.ts +175 -98
  414. package/src/sdk.ts +1215 -158
  415. package/src/search-terms.ts +24 -7
  416. package/src/smoke.ts +607 -0
  417. package/src/sync-manifest.ts +2 -1
  418. package/src/sync-routes.ts +4 -3
  419. package/src/test-tool.ts +32 -11
  420. package/src/types-search.ts +11 -4
  421. package/src/upgrade.ts +205 -25
  422. package/src/verification-plan.ts +27 -0
  423. package/src/verification.ts +50 -12
  424. package/src/web-app-options.ts +57 -0
  425. package/src/work-graph.ts +40 -16
package/src/rules/nano.ts CHANGED
@@ -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
  * The nano lane lints (`@nifrajs/web/nano`). nano is deliberately explicit - every reactive edge is a
@@ -38,8 +39,13 @@ function parsedTree(
38
39
  files.set(file, null)
39
40
  return undefined
40
41
  }
41
- const kind = /\.tsx?$/.test(file) ? ts.ScriptKind.TSX : ts.ScriptKind.JS
42
- const tree = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, kind)
42
+ const tree = ts.createSourceFile(
43
+ file,
44
+ source,
45
+ ts.ScriptTarget.Latest,
46
+ true,
47
+ scriptKindOf(ts, file),
48
+ )
43
49
  files.set(file, { tree })
44
50
  return tree
45
51
  }
@@ -165,7 +171,9 @@ export const nanoBindCleanupRule: CheckRule = {
165
171
  code: "NF-C021",
166
172
  title: "nano binding cleanup check",
167
173
  async scan(ctx) {
168
- const ts = await importProjectTypeScript(ctx.root)
174
+ const loaded = await loadRuleTypeScript(ctx)
175
+ if (loaded.diagnostics !== undefined) return loaded.diagnostics
176
+ const ts = loaded.compiler
169
177
  if (ts === undefined) return [toolchainMissing("NF-C021")]
170
178
  const findings: Diagnostic[] = []
171
179
  for (const file of ctx.project.source.files) {
@@ -205,7 +213,9 @@ export const nanoListKeyRule: CheckRule = {
205
213
  code: "NF-C022",
206
214
  title: "nano bindList key check",
207
215
  async scan(ctx) {
208
- const ts = await importProjectTypeScript(ctx.root)
216
+ const loaded = await loadRuleTypeScript(ctx)
217
+ if (loaded.diagnostics !== undefined) return loaded.diagnostics
218
+ const ts = loaded.compiler
209
219
  if (ts === undefined) return [toolchainMissing("NF-C022")]
210
220
  const findings: Diagnostic[] = []
211
221
  for (const file of ctx.project.source.files) {
@@ -246,7 +256,9 @@ export const nanoComputedDepsRule: CheckRule = {
246
256
  code: "NF-C023",
247
257
  title: "nano computed deps check",
248
258
  async scan(ctx) {
249
- const ts = await importProjectTypeScript(ctx.root)
259
+ const loaded = await loadRuleTypeScript(ctx)
260
+ if (loaded.diagnostics !== undefined) return loaded.diagnostics
261
+ const ts = loaded.compiler
250
262
  if (ts === undefined) return [toolchainMissing("NF-C023")]
251
263
  const findings: Diagnostic[] = []
252
264
  for (const file of ctx.project.source.files) {
@@ -0,0 +1,99 @@
1
+ /**
2
+ * NF-C027: a page file whose URL sits under the backend's mount path.
3
+ *
4
+ * `backend/app.ts`'s backend is mounted at `apiPrefix` (default `/api`) ahead of page routing, and
5
+ * its 404 is final, so `routes/api/report.tsx` never renders - every request for it gets the
6
+ * backend's 404. `createWebApp` refuses such an app at startup and `nifra build` refuses to build
7
+ * it; this reports it earlier, without running app code (check's pre-`loadApp` invariant): the
8
+ * routes come from a scan of `routes/`, and `apiPrefix` is read only when backend/framework.ts
9
+ * declares it as a string literal.
10
+ *
11
+ * Mounts other than the backend's hold `app` objects no static read can see, so they are left to the
12
+ * startup and build checks.
13
+ */
14
+ import { existsSync, readFileSync } from "node:fs"
15
+ import { join } from "node:path"
16
+ import { discoverRoutes } from "@nifrajs/web/fs"
17
+ import { shadowedPages } from "@nifrajs/web/route-manifest"
18
+ import { BACKEND_APP_FILE, CONFIG_FILE, FRAMEWORK_FILE } from "../app-files.ts"
19
+ import { stripComments } from "../check-scan.ts"
20
+ import { type Diagnostic, diagnostic } from "../diagnostics.ts"
21
+ import type { CheckRule } from "./index.ts"
22
+
23
+ /** What a static read of a framework config says about `apiPrefix`. */
24
+ export type StaticApiPrefix =
25
+ | { readonly kind: "default" }
26
+ | { readonly kind: "literal"; readonly value: string }
27
+ | { readonly kind: "unreadable" }
28
+
29
+ const LITERAL_EXPORT =
30
+ /\bexport\s+const\s+apiPrefix\s*(?::\s*string\s*)?=\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')\s*(?:as\s+const\s*)?(?:;|\r?\n|$)/
31
+ const MENTION = /\bapiPrefix\b/
32
+ const BACKEND_EXPORT =
33
+ /\bexport\s+(?:const|let|var|function|class)\s+backend\b|\bexport\s*\{[^}]*\bbackend\b[^}]*\}/
34
+
35
+ /** Read `apiPrefix` from a framework config's source without running it. Pure. */
36
+ export function readStaticApiPrefix(source: string): StaticApiPrefix {
37
+ const code = stripComments(source)
38
+ const literal = LITERAL_EXPORT.exec(code)
39
+ if (literal !== null) return { kind: "literal", value: literal[1] ?? literal[2] ?? "" }
40
+ return MENTION.test(code) ? { kind: "unreadable" } : { kind: "default" }
41
+ }
42
+
43
+ const CONFIG_FILES = [FRAMEWORK_FILE, CONFIG_FILE] as const
44
+
45
+ export const shadowedPageRule: CheckRule = {
46
+ code: "NF-C027",
47
+ title: "Page route under the backend mount",
48
+ async scan(ctx) {
49
+ const backendFile = join(ctx.root, BACKEND_APP_FILE)
50
+ const routesDir = join(ctx.root, "routes")
51
+ if (!existsSync(backendFile) || !existsSync(routesDir)) return []
52
+ if (!BACKEND_EXPORT.test(stripComments(readFileSync(backendFile, "utf8")))) return []
53
+
54
+ let apiPrefix = "/api"
55
+ // backend/framework.ts first: the generated server entry imports from it, and the build refuses
56
+ // a nifra.config.ts whose forwarded fields differ from it.
57
+ const configFile = CONFIG_FILES.find((file) => existsSync(join(ctx.root, file)))
58
+ if (configFile !== undefined) {
59
+ const read = readStaticApiPrefix(readFileSync(join(ctx.root, configFile), "utf8"))
60
+ if (read.kind === "unreadable") {
61
+ return [
62
+ diagnostic({
63
+ code: "NF-C027",
64
+ severity: "info",
65
+ file: configFile,
66
+ message: `apiPrefix in ${configFile} is not a string literal, so check cannot tell which page routes sit under the backend mount; createWebApp still refuses a page under it at startup, and nifra build at build time`,
67
+ verify: "nifra build",
68
+ }),
69
+ ]
70
+ }
71
+ if (read.kind === "literal") apiPrefix = read.value
72
+ }
73
+ if (apiPrefix === "") return []
74
+
75
+ let manifest: ReturnType<typeof discoverRoutes>
76
+ try {
77
+ manifest = discoverRoutes(routesDir)
78
+ } catch {
79
+ // A routes/ tree the manifest refuses fails dev and build with its own error.
80
+ return []
81
+ }
82
+ const findings: Diagnostic[] = []
83
+ for (const page of shadowedPages(manifest, [apiPrefix])) {
84
+ findings.push(
85
+ diagnostic({
86
+ code: "NF-C027",
87
+ severity: "error",
88
+ file: `routes/${page.file}`,
89
+ message: `routes/${page.file} serves ${page.pattern}, under the backend mounted at ${page.mount} - the backend answers every request there before page routing and its 404 is final, so this page can never render. Move the file out of ${page.mount}, serve the URL from backend/app.ts, or change the mount path with \`export const apiPrefix = "/backend"\` in backend/framework.ts`,
90
+ evidence: [`page: ${page.pattern}`, `mount: ${page.mount}`],
91
+ verify: "nifra check",
92
+ }),
93
+ )
94
+ }
95
+ return findings
96
+ },
97
+ }
98
+
99
+ export const pageRules = Object.freeze([shadowedPageRule])
@@ -1,4 +1,6 @@
1
1
  import { RESERVED_KEY_READOUT, reservedKeyFor } from "@nifrajs/client"
2
+ import { RoutePatternOverlapLimitError, routePatternOverlap } from "@nifrajs/core"
3
+ import { expandOptionalParams, paramConstraint } from "@nifrajs/core/pattern"
2
4
  import { type Diagnostic, diagnostic } from "../diagnostics.ts"
3
5
  import { commentBlockHasMarker } from "./comment-markers.ts"
4
6
  import type { CheckRule, RuleContext } from "./index.ts"
@@ -23,6 +25,8 @@ import type { CheckRule, RuleContext } from "./index.ts"
23
25
 
24
26
  /** Opt-out pragma for a route deliberately served only to NON-typed-client consumers. */
25
27
  const RESERVED_SEGMENT_PRAGMA = "nifra-expect reserved-segment"
28
+ const ROUTE_OVERLAP_PRAGMA = "nifra-expect route-overlap"
29
+ const PARAM_MODIFIER_PRAGMA = "nifra-expect param-modifier"
26
30
 
27
31
  interface StaticRouteFact {
28
32
  readonly file: string
@@ -50,6 +54,17 @@ function routeFacts(ctx: RuleContext): StaticRouteFact[] {
50
54
  return out
51
55
  }
52
56
 
57
+ /** One fact per registration site, so `all()` or `method([...])` reports its path once, not per method. */
58
+ function pathSites(routes: readonly StaticRouteFact[]): StaticRouteFact[] {
59
+ const seen = new Set<string>()
60
+ return routes.filter((route) => {
61
+ const site = `${route.file}\n${route.line}\n${route.path}`
62
+ if (seen.has(site)) return false
63
+ seen.add(site)
64
+ return true
65
+ })
66
+ }
67
+
53
68
  /**
54
69
  * The typed escape spelling for a colliding segment, e.g. `/api/delete` + `delete` →
55
70
  * `api("delete").post()` shown as the chain up to the collision. Best-effort readability: earlier
@@ -67,7 +82,7 @@ export const reservedSegmentRule: CheckRule = {
67
82
  async scan(ctx) {
68
83
  const findings: Diagnostic[] = []
69
84
  const linesByFile = new Map<string, readonly string[]>()
70
- for (const route of routeFacts(ctx)) {
85
+ for (const route of pathSites(routeFacts(ctx))) {
71
86
  for (const segment of route.path.split("/")) {
72
87
  if (segment === "") continue
73
88
  const collision = reservedKeyFor(segment)
@@ -112,6 +127,9 @@ export const duplicateRouteRule: CheckRule = {
112
127
  async scan(ctx) {
113
128
  const findings: Diagnostic[] = []
114
129
  const byFile = new Map<string, Map<string, StaticRouteFact>>()
130
+ // Two calls that each register several methods collide once per shared method; that is one
131
+ // pair of lines to fix, so it is reported once.
132
+ const reported = new Set<string>()
115
133
  for (const route of routeFacts(ctx)) {
116
134
  let seen = byFile.get(route.file)
117
135
  if (seen === undefined) {
@@ -124,6 +142,9 @@ export const duplicateRouteRule: CheckRule = {
124
142
  seen.set(key, route)
125
143
  continue
126
144
  }
145
+ const pair = `${route.file}\n${route.line}\n${first.line}\n${route.path}`
146
+ if (reported.has(pair)) continue
147
+ reported.add(pair)
127
148
  findings.push(
128
149
  diagnostic({
129
150
  code: "NF-C019",
@@ -140,4 +161,162 @@ export const duplicateRouteRule: CheckRule = {
140
161
  },
141
162
  }
142
163
 
143
- export const routeRules = Object.freeze([reservedSegmentRule, duplicateRouteRule])
164
+ /**
165
+ * NF-C024: two different same-method route patterns in one source file accept at least one of the
166
+ * same paths. This is deliberately file-scoped, like NF-C019: the static scanner cannot prove
167
+ * which application instance owns routes collected from different files in a monorepo.
168
+ */
169
+ export const overlappingRouteRule: CheckRule = {
170
+ code: "NF-C024",
171
+ title: "Overlapping route registration",
172
+ async scan(ctx) {
173
+ const findings: Diagnostic[] = []
174
+ const linesByFile = new Map<string, readonly string[]>()
175
+ const byFile = new Map<string, StaticRouteFact[]>()
176
+ for (const route of routeFacts(ctx)) {
177
+ const routes = byFile.get(route.file)
178
+ if (routes === undefined) byFile.set(route.file, [route])
179
+ else routes.push(route)
180
+ }
181
+
182
+ // As in NF-C019: two multi-method calls overlap once per shared method, reported once.
183
+ const reported = new Set<string>()
184
+ for (const [file, routes] of byFile) {
185
+ const lines = (): readonly string[] => {
186
+ let value = linesByFile.get(file)
187
+ if (value === undefined) {
188
+ value = (ctx.project.source.read(file) ?? "").split("\n")
189
+ linesByFile.set(file, value)
190
+ }
191
+ return value
192
+ }
193
+
194
+ for (let laterIndex = 0; laterIndex < routes.length; laterIndex += 1) {
195
+ const later = routes[laterIndex]!
196
+ const laterKey = `${later.method} ${later.path}`
197
+ for (let earlierIndex = 0; earlierIndex < laterIndex; earlierIndex += 1) {
198
+ const earlier = routes[earlierIndex]!
199
+ if (earlier.method !== later.method || `${earlier.method} ${earlier.path}` === laterKey)
200
+ continue
201
+ if (commentBlockHasMarker(lines(), later.line, ROUTE_OVERLAP_PRAGMA)) continue
202
+ if (commentBlockHasMarker(lines(), earlier.line, ROUTE_OVERLAP_PRAGMA)) continue
203
+
204
+ let witness: string | undefined
205
+ try {
206
+ witness = routePatternOverlap(earlier.path, later.path)
207
+ } catch (error) {
208
+ if (error instanceof RoutePatternOverlapLimitError) {
209
+ findings.push(
210
+ diagnostic({
211
+ code: "NF-C025",
212
+ severity: "error",
213
+ file: later.file,
214
+ line: later.line,
215
+ message: `${laterKey} could not be proven disjoint from ${earlier.method} ${earlier.path}; bounded route-overlap analysis exceeded its safety budget - simplify the patterns or add the ${ROUTE_OVERLAP_PRAGMA} comment only after manual review`,
216
+ evidence: [
217
+ `${earlier.method} ${earlier.path}`,
218
+ `${later.method} ${later.path}`,
219
+ `first registration: line ${earlier.line}`,
220
+ ],
221
+ verify: "nifra check --lints-only",
222
+ }),
223
+ )
224
+ }
225
+ // The primary route parser diagnostic owns malformed route patterns. A secondary lint
226
+ // must remain total and never turn an invalid route into a checker crash.
227
+ continue
228
+ }
229
+ if (witness === undefined) continue
230
+ const pair = `${file}\n${later.line}\n${later.path}\n${earlier.line}\n${earlier.path}`
231
+ if (reported.has(pair)) break
232
+ reported.add(pair)
233
+
234
+ findings.push(
235
+ diagnostic({
236
+ code: "NF-C024",
237
+ severity: "error",
238
+ file: later.file,
239
+ line: later.line,
240
+ message: `${laterKey} overlaps ${earlier.method} ${earlier.path} (first at line ${earlier.line}); witness path: ${witness} - make the patterns disjoint, order them intentionally with a pragma, or add the ${ROUTE_OVERLAP_PRAGMA} comment above either registration`,
241
+ evidence: [
242
+ `${earlier.method} ${earlier.path}`,
243
+ `${later.method} ${later.path}`,
244
+ `witness: ${witness}`,
245
+ `first registration: line ${earlier.line}`,
246
+ ],
247
+ verify: "nifra check --lints-only",
248
+ }),
249
+ )
250
+ break
251
+ }
252
+ }
253
+ }
254
+ return findings
255
+ },
256
+ }
257
+
258
+ /** A param name directly followed by a character other routers read as a modifier. */
259
+ const PARAM_MODIFIER = /:[A-Za-z_][A-Za-z0-9_]*[?*+{(<]/g
260
+
261
+ /**
262
+ * The first param in `path` whose modifier the router reads as literal text. A `{...}` group the
263
+ * router reads as a constraint (`:id{[0-9]+}`, `:ext{png|jpg}`) is syntax, so it is passed over.
264
+ */
265
+ function literalModifier(path: string): string | undefined {
266
+ PARAM_MODIFIER.lastIndex = 0
267
+ for (let found = PARAM_MODIFIER.exec(path); found !== null; found = PARAM_MODIFIER.exec(path)) {
268
+ const text = found[0]
269
+ if (!text.endsWith("{")) return text
270
+ const constraint = paramConstraint(path.slice(PARAM_MODIFIER.lastIndex - 1))
271
+ if (constraint === undefined) return text
272
+ const end = PARAM_MODIFIER.lastIndex + constraint.source.length + 1
273
+ // A modifier after the constraint is literal text like any other (`:id{[0-9]+}?/posts`).
274
+ if ("?*+{(<".includes(path[end] ?? "/")) return path.slice(found.index, end + 1)
275
+ PARAM_MODIFIER.lastIndex = end
276
+ }
277
+ return undefined
278
+ }
279
+
280
+ /**
281
+ * NF-C026: a param followed by `?`, `*`, `+`, `{`, `(` or `<` that the router reads as literal text
282
+ * (`/users/:id?/posts`, `/users/:id{int}`). Only a trailing `?` run and a valid `{...}` are syntax.
283
+ */
284
+ export const paramModifierRule: CheckRule = {
285
+ code: "NF-C026",
286
+ title: "Route param followed by an unsupported modifier",
287
+ async scan(ctx) {
288
+ const findings: Diagnostic[] = []
289
+ const linesByFile = new Map<string, readonly string[]>()
290
+ for (const route of pathSites(routeFacts(ctx))) {
291
+ const forms = expandOptionalParams(route.path)
292
+ const text = literalModifier(forms[forms.length - 1] ?? route.path)
293
+ if (text === undefined) continue
294
+ let lines = linesByFile.get(route.file)
295
+ if (lines === undefined) {
296
+ lines = (ctx.project.source.read(route.file) ?? "").split("\n")
297
+ linesByFile.set(route.file, lines)
298
+ }
299
+ if (commentBlockHasMarker(lines, route.line, PARAM_MODIFIER_PRAGMA)) continue
300
+ const character = text[text.length - 1]!
301
+ findings.push(
302
+ diagnostic({
303
+ code: "NF-C026",
304
+ severity: "warn",
305
+ file: route.file,
306
+ line: route.line,
307
+ message: `${route.method} ${route.path} - '${character}' after '${text.slice(0, -1)}' is matched as literal text, not as a param modifier${character === "?" ? "; a request path never contains '?', so this route cannot be reached" : ""}. Optional params are supported as the trailing whole segments of a path (\`/users/:id?\`, \`/d/:year?/:month?\`), and a constraint as one character class with an optional count (\`:id{[0-9]+}\`, \`:code{[A-Z]{2}}\`) or a list of two or more values (\`:ext{png|jpg}\`); for anything else register each path, or mark a deliberate literal with \`// ${PARAM_MODIFIER_PRAGMA}\` above the registration`,
308
+ evidence: [`${route.method} ${route.path}`, `literal: ${text}`],
309
+ verify: "nifra check --lints-only",
310
+ }),
311
+ )
312
+ }
313
+ return findings
314
+ },
315
+ }
316
+
317
+ export const routeRules = Object.freeze([
318
+ reservedSegmentRule,
319
+ duplicateRouteRule,
320
+ overlappingRouteRule,
321
+ paramModifierRule,
322
+ ])
@@ -0,0 +1,174 @@
1
+ /**
2
+ * NF-C032: what looks like a credential in browser code or in `public/`, checked from source with the
3
+ * scanner every client build runs (`@nifrajs/web/zones`). The build also matches the values of the
4
+ * build environment's private variables in its output; that needs the build's environment, so it
5
+ * stays the build's.
6
+ */
7
+ import { existsSync, readFileSync } from "node:fs"
8
+ import { join } from "node:path"
9
+ import {
10
+ publicScanFiles,
11
+ type SecretExemption,
12
+ type SecretScanFile,
13
+ scanForSecrets,
14
+ } from "@nifrajs/web/zones"
15
+ import { CONFIG_FILE } from "../app-files.ts"
16
+ import { stripComments } from "../check-scan.ts"
17
+ import { type Diagnostic, diagnostic } from "../diagnostics.ts"
18
+ import { codeUnitOrder } from "../internal/code-unit-order.ts"
19
+ import type { CheckRule } from "./index.ts"
20
+ import { zonedFiles } from "./zones.ts"
21
+
22
+ const BROWSER_CODE = new Set(["route-frontend", "frontend", "shared"])
23
+
24
+ const EXEMPTIONS_START = /\bexport\s+const\s+secretExemptions\b[^=]*=\s*\[/
25
+
26
+ type Token = { readonly kind: "string" | "word" | "punct"; readonly value: string }
27
+
28
+ /**
29
+ * The tokens of an array literal, from just after its `[` to the `]` that closes it. A string is one
30
+ * token, so a bracket or a colon inside one (`"routes/[lang]/index.tsx"`) is text, never syntax.
31
+ */
32
+ function arrayTokens(src: string, from: number): Token[] {
33
+ const tokens: Token[] = []
34
+ let depth = 0
35
+ let i = from
36
+ while (i < src.length) {
37
+ const c = src[i] as string
38
+ if (c === '"' || c === "'" || c === "`") {
39
+ let value = ""
40
+ let j = i + 1
41
+ while (j < src.length && src[j] !== c) {
42
+ if (src[j] === "\\") {
43
+ value += src[j + 1] ?? ""
44
+ j += 2
45
+ } else value += src[j++]
46
+ }
47
+ tokens.push({ kind: "string", value })
48
+ i = j + 1
49
+ } else if (/[\w$]/.test(c)) {
50
+ let j = i
51
+ while (j < src.length && /[\w$]/.test(src[j] as string)) j++
52
+ tokens.push({ kind: "word", value: src.slice(i, j) })
53
+ i = j
54
+ } else {
55
+ if (c === "]" || c === "}" || c === ")") {
56
+ if (depth === 0) break
57
+ depth--
58
+ } else if (c === "[" || c === "{" || c === "(") depth++
59
+ if (!/\s/.test(c)) tokens.push({ kind: "punct", value: c })
60
+ i++
61
+ }
62
+ }
63
+ return tokens
64
+ }
65
+
66
+ const isPunct = (token: Token | undefined, value: string): boolean =>
67
+ token?.kind === "punct" && token.value === value
68
+
69
+ /**
70
+ * The app's `secretExemptions`, read statically from `nifra.config.ts` (check never imports app code):
71
+ * an array literal of object literals with string fields, the form the build error suggests.
72
+ */
73
+ export function readSecretExemptions(appRoot: string): SecretExemption[] {
74
+ const path = join(appRoot, CONFIG_FILE)
75
+ if (!existsSync(path)) return []
76
+ const text = stripComments(readFileSync(path, "utf8"))
77
+ const opened = EXEMPTIONS_START.exec(text)
78
+ if (opened === null) return []
79
+ const tokens = arrayTokens(text, opened.index + opened[0].length)
80
+ const exemptions: SecretExemption[] = []
81
+ let depth = 0
82
+ let fields: Record<string, string> | undefined
83
+ for (let i = 0; i < tokens.length; i++) {
84
+ const token = tokens[i] as Token
85
+ if (token.kind === "punct" && "[{(".includes(token.value)) {
86
+ depth++
87
+ if (depth === 1 && token.value === "{") fields = {}
88
+ } else if (token.kind === "punct" && "]})".includes(token.value)) {
89
+ if (depth === 1 && fields !== undefined) {
90
+ const { rule, reason, file, env } = fields
91
+ if (rule !== undefined && reason !== undefined) {
92
+ exemptions.push({
93
+ rule: rule as SecretExemption["rule"],
94
+ reason,
95
+ ...(file === undefined ? {} : { file }),
96
+ ...(env === undefined ? {} : { env }),
97
+ })
98
+ }
99
+ fields = undefined
100
+ }
101
+ depth--
102
+ } else if (
103
+ depth === 1 &&
104
+ fields !== undefined &&
105
+ token.kind !== "punct" &&
106
+ isPunct(tokens[i + 1], ":") &&
107
+ tokens[i + 2]?.kind === "string" &&
108
+ (isPunct(tokens[i + 3], ",") || isPunct(tokens[i + 3], "}"))
109
+ ) {
110
+ fields[token.value] ??= (tokens[i + 2] as Token).value
111
+ i += 2
112
+ }
113
+ }
114
+ return exemptions
115
+ }
116
+
117
+ export const secretSourceRule: CheckRule = {
118
+ code: "NF-C032",
119
+ title: "Credential in browser code",
120
+ async scan(ctx) {
121
+ const { files } = zonedFiles(ctx)
122
+ const apps = new Map<string, SecretScanFile[]>()
123
+ for (const { file, app, classification } of files) {
124
+ if (!apps.has(app)) apps.set(app, [])
125
+ if (!BROWSER_CODE.has(classification.zone)) continue
126
+ const text = ctx.sources.read(file)
127
+ if (text === undefined) continue
128
+ const name = app === "" ? file : file.slice(app.length + 1)
129
+ apps.get(app)?.push({ name, text, firstParty: true })
130
+ }
131
+ const findings: Diagnostic[] = []
132
+ for (const [app, sources] of apps) {
133
+ const appRoot = join(ctx.root, app)
134
+ const publicDir = join(appRoot, "public")
135
+ try {
136
+ const found = scanForSecrets({
137
+ sources,
138
+ artifacts: existsSync(publicDir) ? publicScanFiles(publicDir, "public") : [],
139
+ exemptions: readSecretExemptions(appRoot),
140
+ })
141
+ for (const finding of found) {
142
+ const file = app === "" ? finding.file : `${app}/${finding.file}`
143
+ findings.push(
144
+ diagnostic({
145
+ code: "NF-C032",
146
+ severity: "error",
147
+ file,
148
+ line: finding.line,
149
+ message: `${file} carries what looks like a credential (${finding.what}), and it reaches the browser. Read it from the environment in a route's backend half or under backend/, and rotate it if it was ever committed`,
150
+ evidence: [`rule: ${finding.rule}`, `match: ${finding.preview}`],
151
+ verify: "nifra check",
152
+ }),
153
+ )
154
+ }
155
+ } catch (error) {
156
+ // A malformed exemption: the build refuses it with the same message.
157
+ findings.push(
158
+ diagnostic({
159
+ code: "NF-C032",
160
+ severity: "error",
161
+ file: join(app, CONFIG_FILE),
162
+ message: error instanceof Error ? error.message : String(error),
163
+ verify: "nifra check",
164
+ }),
165
+ )
166
+ }
167
+ }
168
+ return findings.sort(
169
+ (a, b) => codeUnitOrder(a.file ?? "", b.file ?? "") || (a.line ?? 0) - (b.line ?? 0),
170
+ )
171
+ },
172
+ }
173
+
174
+ export const secretRules = Object.freeze([secretSourceRule])