@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
@@ -0,0 +1,982 @@
1
+ /**
2
+ * `nifra migrate layout` - move an app onto the frontend/backend split.
3
+ *
4
+ * What it does, in order:
5
+ * 1. splits every route file: the exports only the server reads (`loader`, `action`, ...) and the code
6
+ * only they use move to the route's `x.backend.ts`; the frontend file keeps the rest, plus an
7
+ * `import type` for any moved name it still mentions in a type;
8
+ * 2. folds each `_middleware.ts` into its directory's `_layout.backend.ts` as `middleware`;
9
+ * 3. moves `backend.ts` to `backend/app.ts` and `framework.ts` to `backend/framework.ts`;
10
+ * 4. moves each `x.server.ts` under `backend/` without the retired suffix, and zones every other module
11
+ * by who imports it at runtime: only route frontends -> `frontend/`, only the server side ->
12
+ * `backend/`, both -> `shared/`;
13
+ * 5. rewrites every relative import in the app to the new locations, `@nifrajs/web/server-only` to
14
+ * `@nifrajs/web/backend-only`, and `ServerOnly` to `BackendOnly`.
15
+ *
16
+ * It is a dry run unless `write` is set, and it reports what it could not decide (a frontend that uses
17
+ * a moved name at runtime, a helper both halves need, a loader with no output schema) instead of
18
+ * guessing.
19
+ */
20
+ import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
21
+ import { dirname, extname, join, posix, relative, resolve } from "node:path";
22
+ import { BACKEND_ROUTE_EXPORTS } from "@nifrajs/web/route-manifest";
23
+ import { codeUnitOrder } from "./internal/code-unit-order.js";
24
+ import { importTypeScript } from "./internal/typescript-import.js";
25
+ import { missingOutputSchemas } from "./rules/data-guard.js";
26
+ /** Exports only the server or the build reads: `@nifrajs/web`'s route-pair contract. */
27
+ const BACKEND_EXPORTS = BACKEND_ROUTE_EXPORTS;
28
+ const ROUTE_FILE = /\.(?:tsx|jsx|svelte|vue|mdx)$/;
29
+ const SOURCE_FILE = /\.(?:[cm]?[jt]sx?|svelte|vue|mdx)$/;
30
+ const RESOLVE_EXTENSIONS = [
31
+ ".ts",
32
+ ".tsx",
33
+ ".js",
34
+ ".jsx",
35
+ ".mjs",
36
+ ".mts",
37
+ ".svelte",
38
+ ".vue",
39
+ ".mdx",
40
+ ".css",
41
+ ".json",
42
+ ];
43
+ const LEGACY_SERVER = /\.server(\.(?:[cm]?[jt]sx?|svelte|vue|mdx))$/;
44
+ const RETIRED_SPECIFIERS = {
45
+ "@nifrajs/web/server-only": "@nifrajs/web/backend-only",
46
+ };
47
+ const SKIP_DIRS = new Set(["node_modules", "dist", "build", "coverage", ".git", ".vite", "public"]);
48
+ const ZONE_DIRS = new Set(["frontend", "backend", "shared", "routes", "public"]);
49
+ const toPosix = (path) => path.replaceAll("\\", "/");
50
+ /** Every first-party file under `root`, app-relative and posix. */
51
+ function listFiles(root) {
52
+ const out = [];
53
+ const walk = (dir) => {
54
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
55
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name))
56
+ continue;
57
+ const full = join(dir, entry.name);
58
+ if (entry.isDirectory())
59
+ walk(full);
60
+ else if (entry.isFile())
61
+ out.push(toPosix(relative(root, full)));
62
+ }
63
+ };
64
+ walk(root);
65
+ return out.sort();
66
+ }
67
+ function bindingNames(ts, name, into) {
68
+ if (ts.isIdentifier(name))
69
+ into.add(name.text);
70
+ else
71
+ for (const element of name.elements)
72
+ if (!ts.isOmittedExpression(element))
73
+ bindingNames(ts, element.name, into);
74
+ }
75
+ const hasModifier = (ts, node, kind) => (ts.canHaveModifiers(node) ? (ts.getModifiers(node) ?? []) : []).some((m) => m.kind === kind);
76
+ /** Identifiers a statement reads, split into value and type positions. Over-approximates on purpose:
77
+ * an extra name only keeps a declaration in a half that did not need it. */
78
+ function references(ts, node) {
79
+ const value = new Set();
80
+ const type = new Set();
81
+ const visit = (current, inType) => {
82
+ const typed = inType ||
83
+ ts.isTypeNode(current) ||
84
+ // @nifra-gate-reviewed: `token` is the heritage clause's syntax kind, not a credential.
85
+ (ts.isHeritageClause(current) && current.token === ts.SyntaxKind.ImplementsKeyword);
86
+ if (ts.isIdentifier(current)) {
87
+ const parent = current.parent;
88
+ const isName = parent !== undefined &&
89
+ ((ts.isPropertyAccessExpression(parent) && parent.name === current) ||
90
+ (ts.isPropertyAssignment(parent) && parent.name === current) ||
91
+ (ts.isPropertySignature(parent) && parent.name === current) ||
92
+ (ts.isPropertyDeclaration(parent) && parent.name === current) ||
93
+ (ts.isMethodDeclaration(parent) && parent.name === current) ||
94
+ (ts.isMethodSignature(parent) && parent.name === current) ||
95
+ (ts.isQualifiedName(parent) && parent.right === current) ||
96
+ (ts.isJsxAttribute(parent) && parent.name === current) ||
97
+ (ts.isBindingElement(parent) && parent.propertyName === current) ||
98
+ ts.isImportSpecifier(parent) ||
99
+ ts.isExportSpecifier(parent) ||
100
+ ts.isImportClause(parent) ||
101
+ ts.isNamespaceImport(parent));
102
+ if (!isName)
103
+ (typed ? type : value).add(current.text);
104
+ return;
105
+ }
106
+ if (ts.isShorthandPropertyAssignment(current))
107
+ value.add(current.name.text);
108
+ ts.forEachChild(current, (child) => visit(child, typed));
109
+ };
110
+ visit(node, false);
111
+ return { value, type };
112
+ }
113
+ function describeStatements(ts, sf) {
114
+ return sf.statements.map((node) => {
115
+ const declares = new Set();
116
+ const exports = new Map();
117
+ const exported = hasModifier(ts, node, ts.SyntaxKind.ExportKeyword);
118
+ const isDefault = hasModifier(ts, node, ts.SyntaxKind.DefaultKeyword);
119
+ const addExport = (local, as) => {
120
+ exports.set(local, [...(exports.get(local) ?? []), as]);
121
+ };
122
+ if (ts.isImportDeclaration(node)) {
123
+ const clause = node.importClause;
124
+ if (clause?.name)
125
+ declares.add(clause.name.text);
126
+ const bindings = clause?.namedBindings;
127
+ if (bindings && ts.isNamespaceImport(bindings))
128
+ declares.add(bindings.name.text);
129
+ if (bindings && ts.isNamedImports(bindings))
130
+ for (const el of bindings.elements)
131
+ declares.add(el.name.text);
132
+ }
133
+ else if (ts.isVariableStatement(node)) {
134
+ for (const decl of node.declarationList.declarations) {
135
+ const names = new Set();
136
+ bindingNames(ts, decl.name, names);
137
+ for (const name of names) {
138
+ declares.add(name);
139
+ if (exported)
140
+ addExport(name, name);
141
+ }
142
+ }
143
+ }
144
+ else if (ts.isFunctionDeclaration(node) ||
145
+ ts.isClassDeclaration(node) ||
146
+ ts.isInterfaceDeclaration(node) ||
147
+ ts.isTypeAliasDeclaration(node) ||
148
+ ts.isEnumDeclaration(node)) {
149
+ const name = node.name?.text;
150
+ if (name !== undefined)
151
+ declares.add(name);
152
+ if (exported)
153
+ addExport(name ?? "default", isDefault ? "default" : (name ?? "default"));
154
+ }
155
+ else if (ts.isExportAssignment(node)) {
156
+ addExport("default", "default");
157
+ }
158
+ else if (ts.isExportDeclaration(node) &&
159
+ node.exportClause &&
160
+ ts.isNamedExports(node.exportClause)) {
161
+ for (const el of node.exportClause.elements) {
162
+ addExport(node.moduleSpecifier ? `\0${el.name.text}` : (el.propertyName ?? el.name).text, el.name.text);
163
+ }
164
+ }
165
+ const refs = ts.isImportDeclaration(node)
166
+ ? { value: new Set(), type: new Set() }
167
+ : references(ts, node);
168
+ for (const name of declares) {
169
+ refs.value.delete(name);
170
+ refs.type.delete(name);
171
+ }
172
+ return {
173
+ node,
174
+ text: node.getFullText(sf),
175
+ declares,
176
+ exports,
177
+ valueRefs: refs.value,
178
+ typeRefs: refs.type,
179
+ };
180
+ });
181
+ }
182
+ /** Rebuild an import keeping only the bindings `keep` names. `undefined` when none remain. */
183
+ function filterImport(ts, sf, node, keep) {
184
+ const clause = node.importClause;
185
+ const spec = node.moduleSpecifier.getText(sf);
186
+ const attributes = node.attributes ? ` ${node.attributes.getText(sf)}` : "";
187
+ if (clause === undefined)
188
+ return node.getFullText(sf);
189
+ const parts = [];
190
+ if (clause.name && keep(clause.name.text))
191
+ parts.push(clause.name.text);
192
+ const bindings = clause.namedBindings;
193
+ if (bindings && ts.isNamespaceImport(bindings) && keep(bindings.name.text))
194
+ parts.push(`* as ${bindings.name.text}`);
195
+ if (bindings && ts.isNamedImports(bindings)) {
196
+ const named = bindings.elements.filter((el) => keep(el.name.text)).map((el) => el.getText(sf));
197
+ if (named.length > 0)
198
+ parts.push(`{ ${named.join(", ")} }`);
199
+ }
200
+ if (parts.length === 0)
201
+ return undefined;
202
+ const typeOnly = clause.isTypeOnly ? "type " : "";
203
+ const leading = node.getFullText(sf).slice(0, node.getStart(sf) - node.getFullStart());
204
+ return `${leading}import ${typeOnly}${parts.join(", ")} from ${spec}${attributes}`;
205
+ }
206
+ /** Rebuild `export { a, b as c }` keeping only the exported names `keep` accepts. */
207
+ function filterExportList(ts, sf, node, keep) {
208
+ const clause = node.exportClause;
209
+ if (!clause || !ts.isNamedExports(clause))
210
+ return node.getFullText(sf);
211
+ const kept = clause.elements.filter((el) => keep(el.name.text)).map((el) => el.getText(sf));
212
+ if (kept.length === 0)
213
+ return undefined;
214
+ const from = node.moduleSpecifier ? ` from ${node.moduleSpecifier.getText(sf)}` : "";
215
+ const leading = node.getFullText(sf).slice(0, node.getStart(sf) - node.getFullStart());
216
+ return `${leading}export ${node.isTypeOnly ? "type " : ""}{ ${kept.join(", ")} }${from}`;
217
+ }
218
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
219
+ const joinStatements = (texts) => {
220
+ const body = texts.join("").replace(/^\s*\n/, "");
221
+ return body.endsWith("\n") ? body : `${body}\n`;
222
+ };
223
+ /**
224
+ * Split one route module's source into its frontend and backend halves. `backendSpecifier` is how the
225
+ * frontend refers to the backend half in an `import type` (`./index.backend.ts`).
226
+ */
227
+ export function splitRouteSource(ts, source, fileName, backendSpecifier) {
228
+ const kind = fileName.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
229
+ const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, kind);
230
+ const statements = describeStatements(ts, sf);
231
+ const issues = [];
232
+ const exportsOf = (info) => [...info.exports.values()].flat();
233
+ const isBackendStatement = (info) => exportsOf(info).some((name) => BACKEND_EXPORTS.has(name));
234
+ const moved = statements.flatMap(exportsOf).filter((name) => BACKEND_EXPORTS.has(name));
235
+ if (moved.length === 0)
236
+ return { frontend: source, backend: undefined, moved: [], issues: [] };
237
+ const declaredBy = new Map();
238
+ for (const info of statements) {
239
+ if (ts.isImportDeclaration(info.node))
240
+ continue;
241
+ for (const name of info.declares)
242
+ declaredBy.set(name, info);
243
+ }
244
+ // The backend half: its export statements plus every top-level declaration they reach.
245
+ const backendSet = new Set();
246
+ const queue = statements.filter(isBackendStatement);
247
+ for (const info of queue)
248
+ backendSet.add(info);
249
+ while (queue.length > 0) {
250
+ const info = queue.pop();
251
+ for (const name of [...info.valueRefs, ...info.typeRefs]) {
252
+ const target = declaredBy.get(name);
253
+ if (target !== undefined && !backendSet.has(target)) {
254
+ backendSet.add(target);
255
+ queue.push(target);
256
+ }
257
+ }
258
+ // An exported-name statement (`export { loader }`) reaches the declaration it names.
259
+ for (const local of info.exports.keys()) {
260
+ const target = declaredBy.get(local);
261
+ if (target !== undefined && !backendSet.has(target)) {
262
+ backendSet.add(target);
263
+ queue.push(target);
264
+ }
265
+ }
266
+ }
267
+ // The frontend half: everything that is not a backend export statement, minus declarations that only
268
+ // the backend half needs.
269
+ const movedLocals = new Set();
270
+ for (const info of statements) {
271
+ for (const [local, names] of info.exports)
272
+ if (names.some((name) => BACKEND_EXPORTS.has(name)))
273
+ movedLocals.add(local);
274
+ }
275
+ const frontendCandidates = statements.filter((info) => !ts.isImportDeclaration(info.node));
276
+ const frontendKeep = new Set();
277
+ const seed = frontendCandidates.filter((info) => !backendSet.has(info) ||
278
+ (!isBackendStatement(info) && exportsOf(info).length > 0) ||
279
+ // a statement with no declaration (a side effect) stays where it ran
280
+ (info.declares.size === 0 && info.exports.size === 0));
281
+ const fqueue = [...seed];
282
+ for (const info of fqueue)
283
+ frontendKeep.add(info);
284
+ while (fqueue.length > 0) {
285
+ const info = fqueue.pop();
286
+ for (const name of info.valueRefs) {
287
+ if (movedLocals.has(name) && !info.declares.has(name))
288
+ continue;
289
+ const target = declaredBy.get(name);
290
+ if (target !== undefined && !frontendKeep.has(target) && !isBackendStatement(target)) {
291
+ frontendKeep.add(target);
292
+ fqueue.push(target);
293
+ }
294
+ }
295
+ for (const name of info.typeRefs) {
296
+ if (movedLocals.has(name))
297
+ continue;
298
+ const target = declaredBy.get(name);
299
+ if (target !== undefined && !frontendKeep.has(target) && !isBackendStatement(target)) {
300
+ frontendKeep.add(target);
301
+ fqueue.push(target);
302
+ }
303
+ }
304
+ }
305
+ // A moved name the frontend still mentions: in a type it becomes `import type`; at runtime it is an
306
+ // error the author has to resolve.
307
+ const typeImports = new Set();
308
+ for (const info of frontendKeep) {
309
+ if (isBackendStatement(info))
310
+ continue;
311
+ for (const name of movedLocals) {
312
+ if (info.declares.has(name))
313
+ continue;
314
+ if (info.valueRefs.has(name))
315
+ issues.push(`the frontend uses "${name}" at runtime, but it runs on the server only`);
316
+ if (info.typeRefs.has(name) || info.valueRefs.has(name))
317
+ typeImports.add(name);
318
+ }
319
+ }
320
+ for (const info of frontendKeep) {
321
+ if (!backendSet.has(info) || isBackendStatement(info) || info.declares.size === 0)
322
+ continue;
323
+ const values = [...info.declares].filter(() => !ts.isInterfaceDeclaration(info.node) && !ts.isTypeAliasDeclaration(info.node));
324
+ if (values.length > 0)
325
+ issues.push(`"${values.join(", ")}" is needed by both halves and was copied into each; consider moving it to shared/`);
326
+ }
327
+ const used = (infos) => {
328
+ const names = new Set();
329
+ for (const info of infos)
330
+ for (const name of [...info.valueRefs, ...info.typeRefs])
331
+ names.add(name);
332
+ return names;
333
+ };
334
+ const backendUses = used(backendSet);
335
+ const frontendUses = used([...frontendKeep].filter((info) => !isBackendStatement(info)));
336
+ const backendTexts = [];
337
+ const frontendTexts = [];
338
+ let afterImports = 0;
339
+ for (const info of statements) {
340
+ const node = info.node;
341
+ if (ts.isImportDeclaration(node)) {
342
+ const back = filterImport(ts, sf, node, (name) => backendUses.has(name));
343
+ if (back !== undefined)
344
+ backendTexts.push(node.importClause ? back : "");
345
+ const front = filterImport(ts, sf, node, (name) => frontendUses.has(name));
346
+ if (front !== undefined)
347
+ frontendTexts.push(front);
348
+ afterImports = frontendTexts.length;
349
+ continue;
350
+ }
351
+ if (isBackendStatement(info)) {
352
+ if (ts.isExportDeclaration(node) &&
353
+ exportsOf(info).some((name) => !BACKEND_EXPORTS.has(name))) {
354
+ backendTexts.push(filterExportList(ts, sf, node, (name) => BACKEND_EXPORTS.has(name)) ?? "");
355
+ frontendTexts.push(filterExportList(ts, sf, node, (name) => !BACKEND_EXPORTS.has(name)) ?? "");
356
+ continue;
357
+ }
358
+ if (exportsOf(info).some((name) => !BACKEND_EXPORTS.has(name))) {
359
+ issues.push(`one statement exports both "${exportsOf(info).join('", "')}"; split it by hand`);
360
+ }
361
+ backendTexts.push(info.text);
362
+ continue;
363
+ }
364
+ if (backendSet.has(info))
365
+ backendTexts.push(stripExport(ts, sf, info));
366
+ if (frontendKeep.has(info))
367
+ frontendTexts.push(info.text);
368
+ }
369
+ // After the other imports: the nearest relative path sorts last.
370
+ if (typeImports.size > 0) {
371
+ frontendTexts.splice(afterImports, 0, `\nimport type { ${[...typeImports].sort().join(", ")} } from "${backendSpecifier}"`);
372
+ }
373
+ // A JSX pragma counts only in the file's leading comments, and the backend half has no JSX.
374
+ const pragmas = (ts.getLeadingCommentRanges(source, 0) ?? [])
375
+ .map((range) => source.slice(range.pos, range.end))
376
+ .filter((comment) => /@jsx/.test(comment));
377
+ const lift = (text) => pragmas.reduce((out, pragma) => out.replace(new RegExp(`${escapeRegExp(pragma)}[ \\t]*\\n?`), ""), text);
378
+ const frontend = lift(joinStatements(frontendTexts));
379
+ return {
380
+ frontend: pragmas.length > 0 ? `${pragmas.join("\n")}\n${frontend}` : frontend,
381
+ backend: lift(joinStatements(backendTexts)),
382
+ moved: [...new Set(moved)].sort(),
383
+ issues,
384
+ };
385
+ }
386
+ /** A helper the backend half copies keeps its declaration; an `export` on it would add a backend export. */
387
+ function stripExport(ts, sf, info) {
388
+ if (info.exports.size === 0)
389
+ return info.text;
390
+ const node = info.node;
391
+ const exportKeyword = (ts.canHaveModifiers(node) ? (ts.getModifiers(node) ?? []) : []).find((m) => m.kind === ts.SyntaxKind.ExportKeyword);
392
+ if (exportKeyword === undefined)
393
+ return info.text;
394
+ const start = exportKeyword.getStart(sf) - node.getFullStart();
395
+ const end = exportKeyword.getEnd() - node.getFullStart();
396
+ return info.text.slice(0, start) + info.text.slice(end).replace(/^\s+/, "");
397
+ }
398
+ /** `_middleware.ts`'s default export, as the `middleware` export of a `_layout.backend.ts`. */
399
+ export function middlewareSource(ts, source, fileName) {
400
+ const sf = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
401
+ let out = source;
402
+ // Edit from the end so earlier offsets stay valid.
403
+ for (const node of [...sf.statements].reverse()) {
404
+ if (ts.isExportAssignment(node) && !node.isExportEquals) {
405
+ out = `${out.slice(0, node.getStart(sf))}export const middleware = ${node.expression.getText(sf)}${out.slice(node.getEnd())}`;
406
+ }
407
+ else if (ts.isFunctionDeclaration(node) &&
408
+ hasModifier(ts, node, ts.SyntaxKind.DefaultKeyword)) {
409
+ const modifiers = (ts.getModifiers(node) ?? []).filter((m) => m.kind === ts.SyntaxKind.ExportKeyword || m.kind === ts.SyntaxKind.DefaultKeyword);
410
+ const first = modifiers[0];
411
+ const last = modifiers[modifiers.length - 1];
412
+ if (first === undefined || last === undefined)
413
+ continue;
414
+ if (node.name === undefined) {
415
+ const text = out
416
+ .slice(last.getEnd(), node.getEnd())
417
+ .replace(/^(\s*(?:async\s+)?function\s*\*?)\s*/, "$1 middleware");
418
+ out = `${out.slice(0, first.getStart(sf))}export${text}${out.slice(node.getEnd())}`;
419
+ }
420
+ else {
421
+ out = `${out.slice(0, first.getStart(sf))}${out.slice(last.getEnd()).replace(/^\s+/, "")}`;
422
+ out = `${out.trimEnd()}\nexport { ${node.name.text} as middleware }\n`;
423
+ }
424
+ }
425
+ }
426
+ return out;
427
+ }
428
+ /** Merge two backend modules: imports first (identical ones once), then the rest of each in order. */
429
+ function mergeModules(ts, first, second) {
430
+ if (first.trim() === "")
431
+ return second;
432
+ const split = (text) => {
433
+ const sf = ts.createSourceFile("merge.ts", text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
434
+ const imports = [];
435
+ const body = [];
436
+ for (const node of sf.statements) {
437
+ ;
438
+ (ts.isImportDeclaration(node) ? imports : body).push(node.getFullText(sf).replace(/^\s*\n/, ""));
439
+ }
440
+ return { imports, body };
441
+ };
442
+ const a = split(first);
443
+ const b = split(second);
444
+ const imports = [...new Set([...a.imports, ...b.imports].map((text) => text.trim()))];
445
+ const body = [...a.body, ...b.body].map((text) => text.trim()).join("\n\n");
446
+ return `${imports.join("\n")}${imports.length > 0 ? "\n\n" : ""}${body}\n`;
447
+ }
448
+ // ---------------------------------------------------------------------------------------------------
449
+ // SFC script blocks
450
+ // ---------------------------------------------------------------------------------------------------
451
+ /** The `<script>` blocks of a Svelte/Vue file: their content range and whether it is the module script. */
452
+ function scriptBlocks(source) {
453
+ const blocks = [];
454
+ // A closing tag ends at any space, slash or `>` after its name, as the compilers read it.
455
+ const pattern = /<script\b([^>]*)>([\s\S]*?)<\/script(?=[\s/>])[^>]*>/gi;
456
+ // A `<script>` written inside an HTML comment is prose, not a block; blank comments out, keeping offsets.
457
+ const visible = source.replace(/<!--[\s\S]*?-->/g, (comment) => comment.replace(/[^\n]/g, " "));
458
+ for (const match of visible.matchAll(pattern)) {
459
+ const attrs = match[1] ?? "";
460
+ const start = (match.index ?? 0) + match[0].indexOf(">") + 1;
461
+ blocks.push({
462
+ start,
463
+ end: start + (match[2] ?? "").length,
464
+ module: /\bmodule\b|context=["']module["']/.test(attrs),
465
+ setup: /\bsetup\b/.test(attrs),
466
+ });
467
+ }
468
+ return blocks;
469
+ }
470
+ function importsOf(ts, file, text) {
471
+ if (/\.(?:svelte|vue)$/.test(file)) {
472
+ return scriptBlocks(text).flatMap((block) => importsOf(ts, "block.ts", text.slice(block.start, block.end)));
473
+ }
474
+ if (file.endsWith(".css")) {
475
+ return [...text.matchAll(/@import\s+(?:url\()?["']([^"']+)["']/g)].map((m) => ({
476
+ specifier: m[1] ?? "",
477
+ typeOnly: false,
478
+ }));
479
+ }
480
+ if (!/\.(?:[cm]?[jt]sx?|mdx)$/.test(file))
481
+ return [];
482
+ const code = file.endsWith(".mdx")
483
+ ? text
484
+ .split("\n")
485
+ .filter((line) => /^(?:import|export)\s/.test(line))
486
+ .join("\n")
487
+ : text;
488
+ const sf = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
489
+ const refs = [];
490
+ const visit = (node) => {
491
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
492
+ const clause = node.importClause;
493
+ const named = clause?.namedBindings;
494
+ const typeOnly = clause !== undefined &&
495
+ (clause.isTypeOnly ||
496
+ (clause.name === undefined &&
497
+ named !== undefined &&
498
+ ts.isNamedImports(named) &&
499
+ named.elements.length > 0 &&
500
+ named.elements.every((el) => el.isTypeOnly)));
501
+ refs.push({ specifier: node.moduleSpecifier.text, typeOnly });
502
+ }
503
+ else if (ts.isExportDeclaration(node) &&
504
+ node.moduleSpecifier &&
505
+ ts.isStringLiteral(node.moduleSpecifier)) {
506
+ refs.push({ specifier: node.moduleSpecifier.text, typeOnly: node.isTypeOnly });
507
+ }
508
+ else if (ts.isImportTypeNode(node)) {
509
+ const arg = node.argument;
510
+ if (ts.isLiteralTypeNode(arg) && ts.isStringLiteral(arg.literal))
511
+ refs.push({ specifier: arg.literal.text, typeOnly: true });
512
+ }
513
+ else if (ts.isCallExpression(node) && node.arguments.length > 0) {
514
+ const arg = node.arguments[0];
515
+ const isImport = node.expression.kind === ts.SyntaxKind.ImportKeyword;
516
+ const isRequire = ts.isIdentifier(node.expression) && node.expression.text === "require";
517
+ if ((isImport || isRequire) && arg !== undefined && ts.isStringLiteralLike(arg))
518
+ refs.push({ specifier: arg.text, typeOnly: false });
519
+ }
520
+ else if (ts.isImportEqualsDeclaration(node) &&
521
+ ts.isExternalModuleReference(node.moduleReference)) {
522
+ const expr = node.moduleReference.expression;
523
+ if (ts.isStringLiteral(expr))
524
+ refs.push({ specifier: expr.text, typeOnly: node.isTypeOnly });
525
+ }
526
+ ts.forEachChild(node, visit);
527
+ };
528
+ visit(sf);
529
+ return refs;
530
+ }
531
+ /** Resolve a relative specifier against the file map, the way a bundler would. */
532
+ function resolveIn(files, from, specifier) {
533
+ if (!specifier.startsWith("."))
534
+ return undefined;
535
+ const clean = specifier.replace(/[?#].*$/, "");
536
+ const base = posix.normalize(posix.join(posix.dirname(from), clean));
537
+ const candidates = [
538
+ base,
539
+ ...RESOLVE_EXTENSIONS.map((ext) => base + ext),
540
+ ...RESOLVE_EXTENSIONS.map((ext) => `${base}/index${ext}`),
541
+ ];
542
+ // `./x.js` written for a `./x.ts` source.
543
+ const js = /\.(?:[cm]?js|jsx)$/.exec(base);
544
+ if (js !== null) {
545
+ const stem = base.slice(0, -js[0].length);
546
+ candidates.push(`${stem}.ts`, `${stem}.tsx`, `${stem}.mts`);
547
+ }
548
+ return candidates.find((candidate) => files.has(candidate));
549
+ }
550
+ /** Rewrite one relative specifier from `from`'s new location to `target`'s new location, keeping the
551
+ * author's style: extension kept when written, dropped when not, `/index` collapsed when it was. */
552
+ function respecify(original, oldTarget, newFrom, newTarget) {
553
+ const query = /[?#].*$/.exec(original)?.[0] ?? "";
554
+ const clean = original.slice(0, original.length - query.length);
555
+ let target = newTarget;
556
+ const writtenExt = extname(clean);
557
+ const targetExt = extname(oldTarget);
558
+ if (writtenExt === "" || (writtenExt !== targetExt && !RESOLVE_EXTENSIONS.includes(writtenExt))) {
559
+ target = target.slice(0, target.length - extname(target).length);
560
+ if (posix.basename(oldTarget).startsWith("index.") &&
561
+ !posix.basename(clean).startsWith("index")) {
562
+ target = posix.dirname(target);
563
+ }
564
+ }
565
+ else if (writtenExt !== targetExt) {
566
+ target = target.slice(0, target.length - targetExt.length) + writtenExt;
567
+ }
568
+ let rel = posix.relative(posix.dirname(newFrom), target);
569
+ if (!rel.startsWith("."))
570
+ rel = `./${rel}`;
571
+ return rel + query;
572
+ }
573
+ /** A relative specifier whose target does not move, rewritten for an importer that does. */
574
+ function rebase(specifier, from, newFrom) {
575
+ if (!specifier.startsWith(".") || from === newFrom)
576
+ return undefined;
577
+ const target = posix.join(posix.dirname(from), specifier);
578
+ const rel = posix.relative(posix.dirname(newFrom), target);
579
+ return rel.startsWith(".") ? rel : `./${rel}`;
580
+ }
581
+ function rewriteImports(ts, file, text, rewrite) {
582
+ const replace = (code, offset, out, kind) => {
583
+ const sf = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, kind);
584
+ const visit = (node) => {
585
+ let literal;
586
+ if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
587
+ node.moduleSpecifier &&
588
+ ts.isStringLiteral(node.moduleSpecifier))
589
+ literal = node.moduleSpecifier;
590
+ else if (ts.isImportTypeNode(node) &&
591
+ ts.isLiteralTypeNode(node.argument) &&
592
+ ts.isStringLiteral(node.argument.literal))
593
+ literal = node.argument.literal;
594
+ else if (ts.isCallExpression(node) &&
595
+ (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
596
+ (ts.isIdentifier(node.expression) && node.expression.text === "require"))) {
597
+ const arg = node.arguments[0];
598
+ if (arg !== undefined && ts.isStringLiteralLike(arg))
599
+ literal = arg;
600
+ }
601
+ if (literal !== undefined) {
602
+ const next = rewrite(literal.text);
603
+ if (next !== undefined && next !== literal.text) {
604
+ out.push({
605
+ start: offset + literal.getStart(sf) + 1,
606
+ end: offset + literal.getEnd() - 1,
607
+ text: next,
608
+ });
609
+ }
610
+ }
611
+ ts.forEachChild(node, visit);
612
+ };
613
+ visit(sf);
614
+ };
615
+ const edits = [];
616
+ if (/\.(?:svelte|vue)$/.test(file)) {
617
+ for (const block of scriptBlocks(text))
618
+ replace(text.slice(block.start, block.end), block.start, edits, ts.ScriptKind.TS);
619
+ }
620
+ else if (file.endsWith(".css")) {
621
+ for (const match of text.matchAll(/(@import\s+(?:url\()?["'])([^"']+)["']/g)) {
622
+ const next = rewrite(match[2] ?? "");
623
+ if (next !== undefined) {
624
+ const start = (match.index ?? 0) + (match[1] ?? "").length;
625
+ edits.push({ start, end: start + (match[2] ?? "").length, text: next });
626
+ }
627
+ }
628
+ }
629
+ else if (file.endsWith(".mdx")) {
630
+ for (const match of text.matchAll(/^((?:import|export)\s[^\n]*?from\s+["'])([^"']+)["']/gm)) {
631
+ const next = rewrite(match[2] ?? "");
632
+ if (next !== undefined) {
633
+ const start = (match.index ?? 0) + (match[1] ?? "").length;
634
+ edits.push({ start, end: start + (match[2] ?? "").length, text: next });
635
+ }
636
+ }
637
+ }
638
+ else if (/\.[cm]?[jt]sx?$/.test(file)) {
639
+ replace(text, 0, edits, file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
640
+ }
641
+ let out = text;
642
+ for (const edit of edits.sort((a, b) => b.start - a.start))
643
+ out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
644
+ return out;
645
+ }
646
+ /** `ServerOnly` imported from `@nifrajs/web` becomes `BackendOnly`, along with every use of it. */
647
+ function renameRetiredTypes(ts, file, text) {
648
+ if (!text.includes("ServerOnly"))
649
+ return text;
650
+ const edits = [];
651
+ const scan = (code, offset, kind) => {
652
+ const sf = ts.createSourceFile(file, code, ts.ScriptTarget.Latest, true, kind);
653
+ const rename = (node) => {
654
+ edits.push({
655
+ start: offset + node.getStart(sf),
656
+ end: offset + node.getEnd(),
657
+ text: "BackendOnly",
658
+ });
659
+ };
660
+ let unaliased = false;
661
+ for (const statement of sf.statements) {
662
+ if (!ts.isImportDeclaration(statement) ||
663
+ !ts.isStringLiteral(statement.moduleSpecifier) ||
664
+ statement.moduleSpecifier.text !== "@nifrajs/web")
665
+ continue;
666
+ const bindings = statement.importClause?.namedBindings;
667
+ if (bindings === undefined || !ts.isNamedImports(bindings))
668
+ continue;
669
+ for (const element of bindings.elements) {
670
+ if (element.propertyName?.text === "ServerOnly")
671
+ rename(element.propertyName);
672
+ else if (element.propertyName === undefined && element.name.text === "ServerOnly")
673
+ unaliased = true;
674
+ }
675
+ }
676
+ if (!unaliased)
677
+ return;
678
+ const visit = (node) => {
679
+ if (ts.isIdentifier(node) && node.text === "ServerOnly")
680
+ rename(node);
681
+ ts.forEachChild(node, visit);
682
+ };
683
+ visit(sf);
684
+ };
685
+ if (/\.(?:svelte|vue)$/.test(file)) {
686
+ for (const block of scriptBlocks(text))
687
+ scan(text.slice(block.start, block.end), block.start, ts.ScriptKind.TS);
688
+ }
689
+ else if (/\.[cm]?[jt]sx?$/.test(file)) {
690
+ scan(text, 0, file.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
691
+ }
692
+ let out = text;
693
+ for (const edit of edits.sort((a, b) => b.start - a.start))
694
+ out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
695
+ return out;
696
+ }
697
+ // ---------------------------------------------------------------------------------------------------
698
+ // The migration
699
+ // ---------------------------------------------------------------------------------------------------
700
+ const ROOT_MOVES = {
701
+ "backend.ts": "backend/app.ts",
702
+ "framework.ts": "backend/framework.ts",
703
+ };
704
+ /** Root files that stay where they are: the CLI's config, and scripts no build loads. */
705
+ const isRootScript = (file) => !file.includes("/") && SOURCE_FILE.test(file) && ROOT_MOVES[file] === undefined;
706
+ const isTestFile = (file) => /(?:^|\/)(?:test|tests|__tests__)\//.test(file) || /\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file);
707
+ export async function migrateLayout(appRoot, options = {}) {
708
+ const ts = options.typescript ?? (await importTypeScript());
709
+ if (ts === undefined)
710
+ throw new Error("[nifra] `nifra migrate layout` needs TypeScript: bun add -d typescript");
711
+ const root = resolve(appRoot);
712
+ if (!existsSync(join(root, "routes")))
713
+ throw new Error(`[nifra] no routes/ directory in ${root}`);
714
+ const original = new Map();
715
+ const everyFile = listFiles(root);
716
+ for (const file of everyFile) {
717
+ if (SOURCE_FILE.test(file) || file.endsWith(".css"))
718
+ original.set(file, readFileSync(join(root, file), "utf8"));
719
+ }
720
+ const issues = [];
721
+ const splits = [];
722
+ /** The app after the migration: new path -> text. */
723
+ const next = new Map(original);
724
+ /** Old path -> new path, for every file that moves. */
725
+ const moveMap = new Map();
726
+ // 1-2. Route halves and middleware.
727
+ const backendHalves = new Map();
728
+ for (const [file, text] of original) {
729
+ if (!file.startsWith("routes/") || file.includes(".backend."))
730
+ continue;
731
+ if (/(?:^|\/)_middleware\.[cm]?[jt]sx?$/.test(file)) {
732
+ const target = posix.join(posix.dirname(file), "_layout.backend.ts");
733
+ backendHalves.set(target, mergeModules(ts, backendHalves.get(target) ?? "", middlewareSource(ts, text, file)));
734
+ next.delete(file);
735
+ splits.push({ file, backend: target, moved: ["middleware"] });
736
+ continue;
737
+ }
738
+ if (!ROUTE_FILE.test(file))
739
+ continue;
740
+ const backendFile = file.replace(ROUTE_FILE, ".backend.ts");
741
+ const specifier = `./${posix.basename(backendFile)}`;
742
+ let result;
743
+ if (/\.(?:svelte|vue)$/.test(file)) {
744
+ const blocks = scriptBlocks(text);
745
+ const block = blocks.find((b) => b.module) ?? blocks.find((b) => !b.setup && file.endsWith(".vue"));
746
+ if (block === undefined)
747
+ continue;
748
+ const inner = splitRouteSource(ts, text.slice(block.start, block.end), "block.ts", specifier);
749
+ result = {
750
+ ...inner,
751
+ frontend: `${text.slice(0, block.start)}\n${inner.frontend}${text.slice(block.end)}`,
752
+ };
753
+ }
754
+ else if (file.endsWith(".mdx")) {
755
+ if (/^export\s+(?:async\s+)?(?:const|function|let)\s+(?:loader|action|getStaticPaths|prerender|revalidate)\b/m.test(text)) {
756
+ issues.push({
757
+ file,
758
+ reason: "an MDX route exports server code; move it to the route's .backend.ts by hand",
759
+ });
760
+ }
761
+ continue;
762
+ }
763
+ else {
764
+ result = splitRouteSource(ts, text, file, specifier);
765
+ }
766
+ if (result.backend === undefined)
767
+ continue;
768
+ next.set(file, result.frontend);
769
+ backendHalves.set(backendFile, mergeModules(ts, backendHalves.get(backendFile) ?? "", result.backend));
770
+ splits.push({ file, backend: backendFile, moved: [...result.moved] });
771
+ for (const reason of result.issues)
772
+ issues.push({ file, reason });
773
+ }
774
+ for (const [file, text] of backendHalves) {
775
+ const existing = next.get(file);
776
+ next.set(file, existing === undefined ? text : mergeModules(ts, existing, text));
777
+ }
778
+ // 3-4. Zoning by runtime reachability, on the post-split graph.
779
+ for (const [from, to] of Object.entries(ROOT_MOVES))
780
+ if (next.has(from))
781
+ moveMap.set(from, to);
782
+ // Assets (images, fonts) resolve too, so they move with the modules that import them.
783
+ const known = new Set([...next.keys(), ...everyFile]);
784
+ const edges = new Map();
785
+ for (const [file, text] of next) {
786
+ const refs = [];
787
+ for (const ref of importsOf(ts, file, text)) {
788
+ if (ref.specifier === "@nifrajs/web/plugins/vite-server-only") {
789
+ issues.push({
790
+ file,
791
+ reason: "imports the retired @nifrajs/web/plugins/vite-server-only; remove it: nifra's Vite builds and dev server enforce the zones themselves (a hand-built Vite config uses viteLeakGuard from @nifrajs/web/plugins/vite-leak-guard)",
792
+ });
793
+ }
794
+ const target = resolveIn(known, file, ref.specifier);
795
+ if (target !== undefined)
796
+ refs.push({ target, typeOnly: ref.typeOnly });
797
+ else if (/^[@~]\//.test(ref.specifier))
798
+ issues.push({
799
+ file,
800
+ reason: `the path alias "${ref.specifier}" is left as written; point it at the new location by hand`,
801
+ });
802
+ }
803
+ edges.set(file, refs);
804
+ }
805
+ const reach = (roots) => {
806
+ const seen = new Set();
807
+ const queue = [...roots];
808
+ while (queue.length > 0) {
809
+ const file = queue.pop();
810
+ if (seen.has(file))
811
+ continue;
812
+ seen.add(file);
813
+ for (const edge of edges.get(file) ?? [])
814
+ if (!edge.typeOnly)
815
+ queue.push(edge.target);
816
+ }
817
+ return seen;
818
+ };
819
+ const files = [...new Set([...next.keys(), ...everyFile.filter((f) => !original.has(f))])];
820
+ const frontendRoots = files.filter((f) => f.startsWith("routes/") && ROUTE_FILE.test(f) && !f.includes(".backend."));
821
+ const backendRoots = files.filter((f) => (f.startsWith("routes/") && f.includes(".backend.")) ||
822
+ f.startsWith("backend/") ||
823
+ ROOT_MOVES[f] !== undefined ||
824
+ (isRootScript(f) && f !== "nifra.config.ts" && !isTestFile(f)));
825
+ const browser = reach(frontendRoots);
826
+ const server = reach(backendRoots);
827
+ // What the app's own server modules reach, without the root scripts: a root file in here is a module
828
+ // the app imports, not an entry, so it moves into a zone.
829
+ const appServer = reach(backendRoots.filter((f) => f.includes("/") || ROOT_MOVES[f] !== undefined));
830
+ // A `.server` module ran empty in the browser, so only server code used it: it moves under backend/.
831
+ for (const file of files) {
832
+ if (!LEGACY_SERVER.test(file) || isTestFile(file))
833
+ continue;
834
+ const parts = file.replace(LEGACY_SERVER, "$1").split("/");
835
+ if (parts.length > 1 && ZONE_DIRS.has(parts[0] ?? ""))
836
+ parts.shift();
837
+ const to = `backend/${parts.join("/")}`;
838
+ moveMap.set(file, to);
839
+ if (browser.has(file)) {
840
+ issues.push({
841
+ file,
842
+ reason: `a route frontend still imports it at runtime, where it ran empty as a ".server" module; move that use into the route's backend half before it moves to ${to}`,
843
+ });
844
+ }
845
+ }
846
+ const zoneOf = (file) => browser.has(file) && server.has(file) ? "shared" : browser.has(file) ? "frontend" : "backend";
847
+ for (const file of files) {
848
+ if (moveMap.has(file) ||
849
+ isTestFile(file) ||
850
+ file.endsWith(".d.ts") ||
851
+ file === "nifra.config.ts")
852
+ continue;
853
+ if (/\.(?:frontend|backend|shared|fn)\.[cm]?[jt]sx?$/.test(file))
854
+ continue;
855
+ const top = file.split("/")[0] ?? "";
856
+ if (file.includes("/")) {
857
+ if (ZONE_DIRS.has(top) || (!browser.has(file) && !server.has(file)))
858
+ continue;
859
+ }
860
+ else if (!browser.has(file) && !appServer.has(file))
861
+ continue;
862
+ moveMap.set(file, `${zoneOf(file)}/${file}`);
863
+ }
864
+ const claimed = new Map();
865
+ for (const [from, to] of moveMap) {
866
+ const holder = claimed.get(to) ?? (known.has(to) && !moveMap.has(to) ? to : undefined);
867
+ if (holder === undefined) {
868
+ claimed.set(to, from);
869
+ continue;
870
+ }
871
+ issues.push({
872
+ file: from,
873
+ reason: `would move to "${to}", which ${holder === to ? "already exists" : `"${holder}" moves to as well`}; move it by hand`,
874
+ });
875
+ moveMap.delete(from);
876
+ }
877
+ for (const [from, to] of moveMap) {
878
+ if (to.startsWith("shared/") && /from\s+["'](?:node:|bun:)/.test(next.get(from) ?? "")) {
879
+ issues.push({
880
+ file: from,
881
+ reason: `both halves import it at runtime, but it uses a server built-in; split it before moving to ${to}`,
882
+ });
883
+ }
884
+ }
885
+ // 5. Rewrite imports to the new locations.
886
+ const newPath = (file) => moveMap.get(file) ?? file;
887
+ const final = new Map();
888
+ const rewritten = [];
889
+ for (const [file, text] of next) {
890
+ const imports = rewriteImports(ts, file, text, (specifier) => {
891
+ const retired = RETIRED_SPECIFIERS[specifier];
892
+ if (retired !== undefined)
893
+ return retired;
894
+ const target = resolveIn(known, file, specifier);
895
+ // Outside the app (or a skipped dir): the target stays put, so only a moved importer re-bases it.
896
+ if (target === undefined)
897
+ return rebase(specifier, file, newPath(file));
898
+ if (newPath(target) === target && newPath(file) === file)
899
+ return undefined;
900
+ return respecify(specifier, target, newPath(file), newPath(target));
901
+ });
902
+ const updated = renameRetiredTypes(ts, file, imports);
903
+ final.set(newPath(file), updated);
904
+ if (updated !== text && !moveMap.has(file) && original.get(file) === text)
905
+ rewritten.push(file);
906
+ }
907
+ // A path in a string or comment is not an import, so nothing above rewrote it: a build script that
908
+ // writes `data/x.json` would keep writing to the old place.
909
+ for (const [file, text] of final) {
910
+ for (const [from, to] of moveMap) {
911
+ // `shared/lib/x.ts` contains `lib/x.ts`; that is the new path, not a stale one.
912
+ const newPrefix = to.endsWith(from) ? to.slice(0, to.length - from.length) : undefined;
913
+ const stale = [
914
+ ...text.matchAll(new RegExp(`(?<![\\w.-])${escapeRegExp(from)}(?![\\w])`, "g")),
915
+ ].some((match) => newPrefix === undefined || !text.slice(0, match.index).endsWith(newPrefix));
916
+ if (stale)
917
+ issues.push({ file, reason: `mentions "${from}" by path; it moved to "${to}"` });
918
+ }
919
+ }
920
+ // The server refuses data a loader or action returns without an output schema; only the author
921
+ // knows which fields the browser may see.
922
+ for (const [file, text] of final) {
923
+ if (!file.startsWith("routes/") || !/\.backend\.[cm]?[jt]s$/.test(file))
924
+ continue;
925
+ for (const { name, schema } of missingOutputSchemas(text)) {
926
+ issues.push({
927
+ file,
928
+ reason: `exports ${name === "action" ? "an" : "a"} ${name} but no ${schema}, so data it returns fails the request. Declare what the browser may see: export const ${schema} = t.object({ ... })`,
929
+ });
930
+ }
931
+ }
932
+ const moves = [...moveMap]
933
+ .map(([from, to]) => ({ from, to }))
934
+ .sort((a, b) => codeUnitOrder(a.from, b.from));
935
+ if (options.write === true) {
936
+ for (const [file, text] of final) {
937
+ if (original.get(file) === text)
938
+ continue;
939
+ const path = join(root, file);
940
+ mkdirSync(dirname(path), { recursive: true });
941
+ writeFileSync(path, text);
942
+ }
943
+ for (const file of original.keys()) {
944
+ if (!final.has(file))
945
+ rmSync(join(root, file), { force: true });
946
+ }
947
+ for (const { from, to } of moves) {
948
+ if (original.has(from) || !existsSync(join(root, from)))
949
+ continue;
950
+ mkdirSync(dirname(join(root, to)), { recursive: true });
951
+ renameSync(join(root, from), join(root, to));
952
+ }
953
+ pruneEmptyDirs(root);
954
+ }
955
+ return {
956
+ ok: issues.length === 0,
957
+ write: options.write === true,
958
+ splits: splits.sort((a, b) => codeUnitOrder(a.file, b.file)),
959
+ moves,
960
+ rewritten: rewritten.sort(),
961
+ issues,
962
+ };
963
+ }
964
+ function pruneEmptyDirs(root) {
965
+ const prune = (dir) => {
966
+ let empty = true;
967
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
968
+ if (entry.name.startsWith(".") || SKIP_DIRS.has(entry.name)) {
969
+ empty = false;
970
+ continue;
971
+ }
972
+ const full = join(dir, entry.name);
973
+ if (entry.isDirectory() ? !prune(full) : true)
974
+ empty = false;
975
+ }
976
+ if (empty && dir !== root)
977
+ rmSync(dir, { recursive: true, force: true });
978
+ return empty;
979
+ };
980
+ prune(root);
981
+ }
982
+ //# sourceMappingURL=migrate-layout.js.map