@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
@@ -31,35 +31,27 @@ registerFixRecipe({
31
31
  description: "Replace a direct secret comparison with a length check and timing-safe comparison.",
32
32
  verify: "nifra check --lints-only",
33
33
  async apply(root, diagnostic) {
34
- if (diagnostic.file === undefined || diagnostic.line === undefined) return []
34
+ if (diagnostic.file === undefined) return []
35
35
  const path = await resolveInsideProject(root, diagnostic.file)
36
36
  if (path === undefined) return []
37
- const lines = (await readFile(path, "utf8")).split("\n")
38
- const index = diagnostic.line - 1
39
- const original = lines[index]
40
- if (original === undefined || original.includes("@nifra-gate-reviewed")) return []
41
- const comparison = /\b([A-Za-z_$][\w$]*)\s*(===|!==|==|!=)\s*([A-Za-z_$][\w$]*)/.exec(original)
42
- if (comparison === null) return []
43
- const left = comparison[1] as string
44
- const operator = comparison[2] as string
45
- const right = comparison[3] as string
46
- const expression = `nifraTimingSafeEqual(${left}, ${right})`
47
- const replacement = operator === "!==" || operator === "!=" ? `!${expression}` : expression
48
- lines[index] = original.replace(comparison[0], replacement)
49
- if (!lines.some((line) => line.includes("function nifraTimingSafeEqual"))) {
50
- lines.unshift(
51
- 'import { timingSafeEqual } from "node:crypto"',
52
- "",
53
- "function nifraTimingSafeEqual(left: string, right: string): boolean {",
54
- " const leftBytes = Buffer.from(left)",
55
- " const rightBytes = Buffer.from(right)",
56
- " return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes)",
57
- "}",
58
- "",
37
+ const { loadProjectTypeScript } = await import("./internal/typescript-import.ts")
38
+ const loaded = await loadProjectTypeScript(root)
39
+ if (loaded.unsupported !== undefined) throw loaded.unsupported
40
+ if (loaded.compiler === undefined)
41
+ throw new Error(
42
+ "[nifra] the timing-safe fix finds each comparison in the syntax tree, so it needs TypeScript - run `bun add -d typescript`, then rerun the fix",
59
43
  )
44
+ try {
45
+ const { rewriteSecretComparisons } = await import("./rules/security.ts")
46
+ const source = await readFile(path, "utf8")
47
+ // Every site in the file at once, so the rest of that file's diagnostics find nothing left.
48
+ const rewritten = rewriteSecretComparisons(loaded.compiler, diagnostic.file, source)
49
+ if (rewritten === source) return []
50
+ await writeFile(path, rewritten, "utf8")
51
+ return [diagnostic.file]
52
+ } finally {
53
+ await loaded.session?.close()
60
54
  }
61
- await writeFile(path, lines.join("\n"), "utf8")
62
- return [diagnostic.file]
63
55
  },
64
56
  })
65
57
 
@@ -70,7 +62,15 @@ registerFixRecipe({
70
62
  async apply(root) {
71
63
  const { syncServerManifests } = await import("./sync-manifest.ts")
72
64
  const results = await syncServerManifests(root)
73
- return results.filter((result) => result.changed).map((result) => result.file)
65
+ const changed = results.filter((result) => result.changed).map((result) => result.file)
66
+ const failed = results.filter((result) => result.error !== undefined)
67
+ if (failed.length > 0) {
68
+ const synced = changed.length === 0 ? "" : `synced ${changed.join(", ")}; `
69
+ throw new Error(
70
+ `[nifra] ${synced}cannot sync ${failed.map((result) => `${result.file}: ${result.error}`).join("; ")}`,
71
+ )
72
+ }
73
+ return changed
74
74
  },
75
75
  })
76
76
 
@@ -101,7 +101,7 @@ registerFixRecipe({
101
101
  // Refusing is a result the caller has to see. Returning "changed nothing" made `nifra fix
102
102
  // --code NF-C010` a silent no-op on the only findings that produce it.
103
103
  if (!linked.ok) throw new Error(`[nifra] cannot rebuild ${packageName}: ${linked.reason}`)
104
- const proc = Bun.spawn(["bun", "run", linked.buildScript], {
104
+ const proc = Bun.spawn([process.execPath, "run", linked.buildScript], {
105
105
  cwd: linked.dir,
106
106
  stdout: "ignore",
107
107
  stderr: "pipe",
@@ -117,6 +117,24 @@ registerFixRecipe({
117
117
  },
118
118
  })
119
119
 
120
+ registerFixRecipe({
121
+ id: "imports.moved-export",
122
+ description: "Import a name that moved to a subpath from the module that now exports it.",
123
+ verify: "nifra check --lints-only",
124
+ async apply(root, diagnostic) {
125
+ if (diagnostic.file === undefined) return []
126
+ const path = await resolveInsideProject(root, diagnostic.file)
127
+ if (path === undefined) return []
128
+ const { rewriteMovedExports } = await import("./check-scan.ts")
129
+ const source = await readFile(path, "utf8")
130
+ // Rewrites every site in the file at once, so the rest of that file's diagnostics find nothing left.
131
+ const rewritten = rewriteMovedExports(source)
132
+ if (rewritten === source) return []
133
+ await writeFile(path, rewritten, "utf8")
134
+ return [diagnostic.file]
135
+ },
136
+ })
137
+
120
138
  /**
121
139
  * How many typecheck passes the reserved-segment codemod will run. A nested collision
122
140
  * (`/api/delete/then`) only reveals its second site once the first is rewritten, so one pass is not
@@ -147,11 +165,14 @@ registerFixRecipe({
147
165
  const changed = new Set<string>()
148
166
  const skipped: string[] = []
149
167
  for (let pass = 0; pass < RESERVED_SEGMENT_PASSES; pass += 1) {
150
- const proc = Bun.spawn(["bun", tscBin, "--noEmit", "--pretty", "false", "-p", tsconfig], {
151
- cwd: root,
152
- stdout: "pipe",
153
- stderr: "pipe",
154
- })
168
+ const proc = Bun.spawn(
169
+ [process.execPath, tscBin, "--noEmit", "--pretty", "false", "-p", tsconfig],
170
+ {
171
+ cwd: root,
172
+ stdout: "pipe",
173
+ stderr: "pipe",
174
+ },
175
+ )
155
176
  const [stdout, stderr] = await Promise.all([
156
177
  new Response(proc.stdout).text(),
157
178
  new Response(proc.stderr).text(),
@@ -52,11 +52,11 @@ export const FRONTEND_GUIDANCE: readonly GuidanceEntry[] = [
52
52
  symptom:
53
53
  "A client component crashes at build or runtime with a missing Node built-in, a leaked secret, or 'module not found in the browser'.",
54
54
  cause:
55
- "A server-only module (the DB, a secret, `node:*`, the backend file) is imported at the top level of a component, so the bundler tries to ship it to the browser.",
56
- fix: "Move the access into a server-only `loader`/`action` and read the result in the component. Reach the backend through the typed `api` argument, never a top-level server-only import.",
55
+ "Browser code (a page, `frontend/` or `shared/`) imports backend code - `backend/`, a route's `.backend.ts` half, the DB, a secret, `node:*` - so the browser build refuses it.",
56
+ fix: "Move the access into the route's `.backend.ts` half (`loader`/`action` with `loaderOutput`/`actionOutput`) and read the result as `data` in the page. Reach the backend through the typed `api` argument; put code both sides need in `shared/`.",
57
57
  verify:
58
58
  "nifra_check - its transitive server-import scan flags the exact import chain into a client module.",
59
- seeAlso: "loader action server-only import boundary",
59
+ seeAlso: "project structure route backend half import rules",
60
60
  },
61
61
  {
62
62
  id: "hydration-mismatch",
@@ -88,10 +88,10 @@ export const FRONTEND_GUIDANCE: readonly GuidanceEntry[] = [
88
88
  "Loader data is typed `any` in the page, or a hand-written interface silently drifts from what the loader actually returns.",
89
89
  cause:
90
90
  "The component types its props by hand instead of inferring them from the loader, so a change to the loader is not a type error at the use site.",
91
- fix: "Type the page from the loader: `props: { data: LoaderData<typeof loader> }`. Call the backend through the typed `api` so the request/response shape is inferred, not restated.",
91
+ fix: 'Type the page from its generated route types: `import type { Route } from "./+types/<name>"` and `props: Route.ComponentProps`. Its `data` is the `loaderOutput` schema\'s type - what actually reaches the browser - while `LoaderData<typeof loader>` also types fields the schema strips. Call the backend through the typed `api` (`Route.LoaderArgs`) so the request/response shape is inferred, not restated.',
92
92
  verify:
93
93
  "nifra_context prints the exact loader shape and typed call form; nifra_check fails on drift.",
94
- seeAlso: "LoaderData typed client loader inference",
94
+ seeAlso: "route types Route.ComponentProps loaderOutput typed client",
95
95
  },
96
96
  {
97
97
  id: "list-key",
@@ -0,0 +1,179 @@
1
+ /**
2
+ * `nifra i18n check [entry]` - runs `checkCatalogs()` from `@nifrajs/i18n/check` over an app's locale
3
+ * registry and catalogs. Unlike `nifra check`, this IMPORTS app code: the entry module, which exports
4
+ * `locales` (from `defineLocales`) and `catalogs` (by locale key: a catalog, or a function returning
5
+ * one or a promise of one, such as `() => import("./messages/fr.json")`), plus an optional `ignore`.
6
+ * The default entry is the first of {@link DEFAULT_ENTRIES} that exists.
7
+ */
8
+ import { existsSync } from "node:fs"
9
+ import { relative, resolve } from "node:path"
10
+ import { pathToFileURL } from "node:url"
11
+ import type { MessageTree } from "@nifrajs/i18n"
12
+ import type { CatalogCheckOptions, CatalogCheckResult, CatalogFinding } from "@nifrajs/i18n/check"
13
+
14
+ export const DEFAULT_ENTRIES = [
15
+ "shared/i18n.ts",
16
+ "i18n.ts",
17
+ "lib/i18n.ts",
18
+ "src/i18n.ts",
19
+ "src/lib/i18n.ts",
20
+ "app/i18n.ts",
21
+ ] as const
22
+
23
+ export interface I18nCheckOutput {
24
+ /** The entry module, relative to the project directory. */
25
+ readonly entry: string
26
+ readonly result: CatalogCheckResult
27
+ }
28
+
29
+ const isObject = (value: unknown): value is Record<string, unknown> =>
30
+ value !== null && typeof value === "object"
31
+
32
+ /** A loaded module's default export when the value is a module namespace (`import("./fr.json")`). */
33
+ const unwrapModule = (value: unknown): unknown =>
34
+ isObject(value) && Object.prototype.toString.call(value) === "[object Module]"
35
+ ? value.default
36
+ : value
37
+
38
+ const isRegistry = (value: unknown): value is CatalogCheckOptions["locales"] =>
39
+ isObject(value) &&
40
+ typeof value.default === "string" &&
41
+ Array.isArray(value.all) &&
42
+ typeof value.get === "function" &&
43
+ typeof value.chain === "function"
44
+
45
+ function resolveEntry(cwd: string, entry: string | undefined): string {
46
+ if (entry !== undefined) {
47
+ const path = resolve(cwd, entry)
48
+ if (!existsSync(path)) throw new Error(`i18n check: ${entry} does not exist`)
49
+ return path
50
+ }
51
+ for (const candidate of DEFAULT_ENTRIES) {
52
+ const path = resolve(cwd, candidate)
53
+ if (existsSync(path)) return path
54
+ }
55
+ throw new Error(
56
+ `i18n check: no entry module found (looked for ${DEFAULT_ENTRIES.join(", ")}). Pass one: nifra i18n check <entry>, a module exporting \`locales\` and \`catalogs\`.`,
57
+ )
58
+ }
59
+
60
+ /** Import the entry, load every catalog it names, and check them. */
61
+ export async function runI18nCheck(
62
+ cwd: string,
63
+ options: { readonly entry?: string | undefined } = {},
64
+ ): Promise<I18nCheckOutput> {
65
+ const path = resolveEntry(cwd, options.entry)
66
+ const mod = (await import(pathToFileURL(path).href)) as Record<string, unknown>
67
+ const source = isObject(mod.default) && mod.locales === undefined ? mod.default : mod
68
+ const { locales, catalogs, ignore } = source
69
+ const entry = relative(cwd, path).replaceAll("\\", "/") || path
70
+ if (!isRegistry(locales)) {
71
+ throw new Error(
72
+ `i18n check: ${entry} must export \`locales\`, the registry defineLocales() returns`,
73
+ )
74
+ }
75
+ if (!isObject(catalogs)) {
76
+ throw new Error(
77
+ `i18n check: ${entry} must export \`catalogs\`, an object of catalogs by locale key`,
78
+ )
79
+ }
80
+ if (ignore !== undefined && !isObject(ignore)) {
81
+ throw new Error(`i18n check: \`ignore\` in ${entry} must be an object of key patterns by check`)
82
+ }
83
+ const loaded: Record<string, MessageTree | undefined> = {}
84
+ for (const key of Object.keys(catalogs)) {
85
+ let value = catalogs[key]
86
+ if (typeof value === "function") value = (value as () => unknown)()
87
+ loaded[key] = unwrapModule(await value) as MessageTree | undefined
88
+ }
89
+ // Loaded on use: the command catalog imports this module on every CLI start.
90
+ const { checkCatalogs } = await import("@nifrajs/i18n/check")
91
+ return {
92
+ entry,
93
+ result: checkCatalogs({
94
+ locales,
95
+ catalogs: loaded,
96
+ ignore: ignore as CatalogCheckOptions["ignore"],
97
+ }),
98
+ }
99
+ }
100
+
101
+ const MARK = { error: "✖", warning: "⚠", info: "•" } as const
102
+ // Per-key findings that are lists rather than problems one by one: grouped per locale in the text.
103
+ const GROUPED = new Set(["missing-key", "unused-key", "untranslated"])
104
+ const SHOWN_KEYS = 6
105
+
106
+ // Catalog keys and text reach the terminal: control and bidi-override characters print as escapes, so
107
+ // a key can neither drive the terminal nor reorder what the line appears to say.
108
+ const unprintable = (code: number): boolean =>
109
+ code <= 0x1f ||
110
+ (code >= 0x7f && code <= 0x9f) ||
111
+ code === 0x200e ||
112
+ code === 0x200f ||
113
+ (code >= 0x202a && code <= 0x202e) ||
114
+ (code >= 0x2066 && code <= 0x2069)
115
+ const printable = (line: string): string => {
116
+ let out = ""
117
+ for (const char of line) {
118
+ const code = char.codePointAt(0) ?? 0
119
+ out += unprintable(code) ? `\\u${code.toString(16).padStart(4, "0")}` : char
120
+ }
121
+ return out
122
+ }
123
+
124
+ const percent = (share: number): string => `${(share * 100).toFixed(share === 1 ? 0 : 1)}%`
125
+
126
+ /** The human report: a coverage table, then findings, grouped where they are key lists. */
127
+ export function renderI18nCheck(out: I18nCheckOutput): readonly string[] {
128
+ const { result } = out
129
+ const lines = [`nifra i18n check - ${out.entry}`, ""]
130
+ const width = Math.max(6, ...result.coverage.map((row) => row.locale.length))
131
+ for (const row of result.coverage) {
132
+ const label = row.default ? "default" : row.draft ? "draft" : ""
133
+ const counts = row.default
134
+ ? `${row.total} messages`
135
+ : `${row.translated}/${row.total} (${percent(row.coverage)})${row.own === row.translated ? "" : `, ${row.own} own`}`
136
+ lines.push(` ${row.locale.padEnd(width)} ${label.padEnd(7)} ${counts}`)
137
+ }
138
+ if (result.coverage.length > 0) lines.push("")
139
+
140
+ const groups = new Map<string, CatalogFinding[]>()
141
+ for (const finding of result.findings) {
142
+ if (!GROUPED.has(finding.code) || finding.key === undefined) {
143
+ lines.push(`${MARK[finding.severity]} ${finding.locale} ${finding.code}: ${finding.message}`)
144
+ continue
145
+ }
146
+ const id = `${finding.severity}\u0000${finding.locale}\u0000${finding.code}`
147
+ const group = groups.get(id)
148
+ if (group === undefined) groups.set(id, [finding])
149
+ else group.push(finding)
150
+ }
151
+ for (const group of groups.values()) {
152
+ const first = group[0] as CatalogFinding
153
+ const keys = group.map((finding) => finding.key as string)
154
+ const shown = keys.slice(0, SHOWN_KEYS).join(", ")
155
+ const more = keys.length > SHOWN_KEYS ? `, +${keys.length - SHOWN_KEYS} more` : ""
156
+ lines.push(
157
+ `${MARK[first.severity]} ${first.locale} ${first.code} (${keys.length}): ${shown}${more}`,
158
+ )
159
+ }
160
+
161
+ const count = (severity: CatalogFinding["severity"]): number =>
162
+ result.findings.filter((finding) => finding.severity === severity).length
163
+ const errors = count("error")
164
+ const warnings = count("warning")
165
+ if (result.findings.length > 0) lines.push("")
166
+ lines.push(
167
+ errors === 0 && warnings === 0
168
+ ? "✓ catalogs are complete and consistent"
169
+ : `${errors} error${errors === 1 ? "" : "s"}, ${warnings} warning${warnings === 1 ? "" : "s"}${errors === 0 ? " - pass --strict to fail on warnings" : ""}`,
170
+ )
171
+ return lines.map(printable)
172
+ }
173
+
174
+ /** Errors fail the run; with `strict`, warnings do too. Info never does. */
175
+ export function i18nCheckPassed(out: I18nCheckOutput, strict: boolean): boolean {
176
+ return out.result.findings.every(
177
+ (finding) => finding.severity === "info" || (finding.severity === "warning" && !strict),
178
+ )
179
+ }