@nifrajs/web 3.5.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 (390) hide show
  1. package/dist/backend-only.d.ts +15 -0
  2. package/dist/backend-only.d.ts.map +1 -0
  3. package/dist/backend-only.js +16 -0
  4. package/dist/backend-only.js.map +1 -0
  5. package/dist/boundary.d.ts +5 -0
  6. package/dist/boundary.d.ts.map +1 -1
  7. package/dist/boundary.js +11 -2
  8. package/dist/boundary.js.map +1 -1
  9. package/dist/build-plan.d.ts +18 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +82 -34
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +4 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +68 -13
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +57 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +356 -85
  20. package/dist/build.js.map +1 -1
  21. package/dist/bun-dev-entry.d.ts +1 -1
  22. package/dist/bun-dev-entry.js +1 -1
  23. package/dist/cdn.d.ts +74 -0
  24. package/dist/cdn.d.ts.map +1 -0
  25. package/dist/cdn.js +132 -0
  26. package/dist/cdn.js.map +1 -0
  27. package/dist/client.d.ts +3 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +111 -15
  30. package/dist/client.js.map +1 -1
  31. package/dist/conformance.d.ts +4 -3
  32. package/dist/conformance.d.ts.map +1 -1
  33. package/dist/conformance.js +12 -3
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/csp.d.ts +93 -0
  36. package/dist/csp.d.ts.map +1 -0
  37. package/dist/csp.js +122 -0
  38. package/dist/csp.js.map +1 -0
  39. package/dist/dev-client.d.ts +76 -0
  40. package/dist/dev-client.d.ts.map +1 -0
  41. package/dist/dev-client.js +280 -0
  42. package/dist/dev-client.js.map +1 -0
  43. package/dist/dev-diagnostics.d.ts +26 -2
  44. package/dist/dev-diagnostics.d.ts.map +1 -1
  45. package/dist/dev-diagnostics.js +38 -7
  46. package/dist/dev-diagnostics.js.map +1 -1
  47. package/dist/dev-error.d.ts +4 -13
  48. package/dist/dev-error.d.ts.map +1 -1
  49. package/dist/dev-error.js +46 -4
  50. package/dist/dev-error.js.map +1 -1
  51. package/dist/dev-feed.d.ts +298 -0
  52. package/dist/dev-feed.d.ts.map +1 -0
  53. package/dist/dev-feed.js +797 -0
  54. package/dist/dev-feed.js.map +1 -0
  55. package/dist/dev-indicator.d.ts +38 -0
  56. package/dist/dev-indicator.d.ts.map +1 -0
  57. package/dist/dev-indicator.js +93 -0
  58. package/dist/dev-indicator.js.map +1 -0
  59. package/dist/dev-session.d.ts +77 -0
  60. package/dist/dev-session.d.ts.map +1 -0
  61. package/dist/dev-session.js +580 -0
  62. package/dist/dev-session.js.map +1 -0
  63. package/dist/dev-sourcemap.d.ts +27 -0
  64. package/dist/dev-sourcemap.d.ts.map +1 -0
  65. package/dist/dev-sourcemap.js +235 -0
  66. package/dist/dev-sourcemap.js.map +1 -0
  67. package/dist/dev-ssr-graph.d.ts.map +1 -1
  68. package/dist/dev-ssr-graph.js +13 -5
  69. package/dist/dev-ssr-graph.js.map +1 -1
  70. package/dist/dev.d.ts +15 -2
  71. package/dist/dev.d.ts.map +1 -1
  72. package/dist/dev.js +69 -44
  73. package/dist/dev.js.map +1 -1
  74. package/dist/diagnostic-catalog.d.ts +35 -0
  75. package/dist/diagnostic-catalog.d.ts.map +1 -0
  76. package/dist/diagnostic-catalog.js +203 -0
  77. package/dist/diagnostic-catalog.js.map +1 -0
  78. package/dist/diagnostic-prompt.d.ts +49 -0
  79. package/dist/diagnostic-prompt.d.ts.map +1 -0
  80. package/dist/diagnostic-prompt.js +303 -0
  81. package/dist/diagnostic-prompt.js.map +1 -0
  82. package/dist/diagnostic.d.ts +17 -23
  83. package/dist/diagnostic.d.ts.map +1 -1
  84. package/dist/diagnostic.js +31 -39
  85. package/dist/diagnostic.js.map +1 -1
  86. package/dist/draft.d.ts.map +1 -1
  87. package/dist/draft.js +4 -1
  88. package/dist/draft.js.map +1 -1
  89. package/dist/fn.d.ts +5 -0
  90. package/dist/fn.d.ts.map +1 -1
  91. package/dist/fn.js +13 -3
  92. package/dist/fn.js.map +1 -1
  93. package/dist/fs.d.ts.map +1 -1
  94. package/dist/fs.js +4 -2
  95. package/dist/fs.js.map +1 -1
  96. package/dist/index.d.ts +13 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +11 -12
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  101. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  102. package/dist/internal/cdn/cloudflare.js +94 -0
  103. package/dist/internal/cdn/cloudflare.js.map +1 -0
  104. package/dist/internal/cdn/core.d.ts +91 -0
  105. package/dist/internal/cdn/core.d.ts.map +1 -0
  106. package/dist/internal/cdn/core.js +220 -0
  107. package/dist/internal/cdn/core.js.map +1 -0
  108. package/dist/internal/cdn/fastly.d.ts +17 -0
  109. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  110. package/dist/internal/cdn/fastly.js +43 -0
  111. package/dist/internal/cdn/fastly.js.map +1 -0
  112. package/dist/internal/cdn/vercel.d.ts +31 -0
  113. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  114. package/dist/internal/cdn/vercel.js +56 -0
  115. package/dist/internal/cdn/vercel.js.map +1 -0
  116. package/dist/internal/code-unit-order.d.ts +4 -0
  117. package/dist/internal/code-unit-order.d.ts.map +1 -0
  118. package/dist/internal/code-unit-order.js +6 -0
  119. package/dist/internal/code-unit-order.js.map +1 -0
  120. package/dist/internal/codegen.d.ts.map +1 -1
  121. package/dist/internal/codegen.js +141 -25
  122. package/dist/internal/codegen.js.map +1 -1
  123. package/dist/internal/dev-csp.d.ts +24 -0
  124. package/dist/internal/dev-csp.d.ts.map +1 -0
  125. package/dist/internal/dev-csp.js +160 -0
  126. package/dist/internal/dev-csp.js.map +1 -0
  127. package/dist/internal/dev-reserved.d.ts +25 -0
  128. package/dist/internal/dev-reserved.d.ts.map +1 -0
  129. package/dist/internal/dev-reserved.js +39 -0
  130. package/dist/internal/dev-reserved.js.map +1 -0
  131. package/dist/internal/html-spans.d.ts +10 -0
  132. package/dist/internal/html-spans.d.ts.map +1 -0
  133. package/dist/internal/html-spans.js +46 -0
  134. package/dist/internal/html-spans.js.map +1 -0
  135. package/dist/internal/loading-runtime.d.ts +44 -0
  136. package/dist/internal/loading-runtime.d.ts.map +1 -0
  137. package/dist/internal/loading-runtime.js +206 -0
  138. package/dist/internal/loading-runtime.js.map +1 -0
  139. package/dist/internal/matches-runtime.d.ts +8 -0
  140. package/dist/internal/matches-runtime.d.ts.map +1 -0
  141. package/dist/internal/matches-runtime.js +61 -0
  142. package/dist/internal/matches-runtime.js.map +1 -0
  143. package/dist/internal/mount-shadow.d.ts +36 -0
  144. package/dist/internal/mount-shadow.d.ts.map +1 -0
  145. package/dist/internal/mount-shadow.js +65 -0
  146. package/dist/internal/mount-shadow.js.map +1 -0
  147. package/dist/internal/node-builtins.d.ts +3 -0
  148. package/dist/internal/node-builtins.d.ts.map +1 -0
  149. package/dist/internal/node-builtins.js +79 -0
  150. package/dist/internal/node-builtins.js.map +1 -0
  151. package/dist/internal/output-guard.d.ts +65 -0
  152. package/dist/internal/output-guard.d.ts.map +1 -0
  153. package/dist/internal/output-guard.js +630 -0
  154. package/dist/internal/output-guard.js.map +1 -0
  155. package/dist/internal/page-execution.d.ts +20 -8
  156. package/dist/internal/page-execution.d.ts.map +1 -1
  157. package/dist/internal/page-execution.js +571 -105
  158. package/dist/internal/page-execution.js.map +1 -1
  159. package/dist/internal/parity.d.ts +21 -0
  160. package/dist/internal/parity.d.ts.map +1 -1
  161. package/dist/internal/parity.js +104 -22
  162. package/dist/internal/parity.js.map +1 -1
  163. package/dist/internal/private-env.d.ts +12 -0
  164. package/dist/internal/private-env.d.ts.map +1 -0
  165. package/dist/internal/private-env.js +335 -0
  166. package/dist/internal/private-env.js.map +1 -0
  167. package/dist/internal/public-url.d.ts +3 -0
  168. package/dist/internal/public-url.d.ts.map +1 -0
  169. package/dist/internal/public-url.js +18 -0
  170. package/dist/internal/public-url.js.map +1 -0
  171. package/dist/internal/render-document.d.ts +25 -19
  172. package/dist/internal/render-document.d.ts.map +1 -1
  173. package/dist/internal/render-document.js +77 -61
  174. package/dist/internal/render-document.js.map +1 -1
  175. package/dist/internal/response-controls.d.ts +75 -0
  176. package/dist/internal/response-controls.d.ts.map +1 -0
  177. package/dist/internal/response-controls.js +256 -0
  178. package/dist/internal/response-controls.js.map +1 -0
  179. package/dist/internal/runtime-contract.d.ts +11 -16
  180. package/dist/internal/runtime-contract.d.ts.map +1 -1
  181. package/dist/internal/runtime-contract.js.map +1 -1
  182. package/dist/internal/secret-scan.d.ts +104 -0
  183. package/dist/internal/secret-scan.d.ts.map +1 -0
  184. package/dist/internal/secret-scan.js +485 -0
  185. package/dist/internal/secret-scan.js.map +1 -0
  186. package/dist/internal/server-boundary.d.ts +6 -29
  187. package/dist/internal/server-boundary.d.ts.map +1 -1
  188. package/dist/internal/server-boundary.js +6 -66
  189. package/dist/internal/server-boundary.js.map +1 -1
  190. package/dist/internal/source-map.d.ts +47 -0
  191. package/dist/internal/source-map.d.ts.map +1 -0
  192. package/dist/internal/source-map.js +225 -0
  193. package/dist/internal/source-map.js.map +1 -0
  194. package/dist/internal/target-compat.d.ts +27 -0
  195. package/dist/internal/target-compat.d.ts.map +1 -0
  196. package/dist/internal/target-compat.js +77 -0
  197. package/dist/internal/target-compat.js.map +1 -0
  198. package/dist/internal/timing-safe-equal.d.ts +6 -0
  199. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  200. package/dist/internal/timing-safe-equal.js +10 -0
  201. package/dist/internal/timing-safe-equal.js.map +1 -1
  202. package/dist/internal/vitals-runtime.d.ts +18 -0
  203. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  204. package/dist/internal/vitals-runtime.js +38 -0
  205. package/dist/internal/vitals-runtime.js.map +1 -0
  206. package/dist/internal/web-app.d.ts +38 -9
  207. package/dist/internal/web-app.d.ts.map +1 -1
  208. package/dist/internal/web-app.js +199 -59
  209. package/dist/internal/web-app.js.map +1 -1
  210. package/dist/internal/zone-graph.d.ts +89 -0
  211. package/dist/internal/zone-graph.d.ts.map +1 -0
  212. package/dist/internal/zone-graph.js +492 -0
  213. package/dist/internal/zone-graph.js.map +1 -0
  214. package/dist/isr.d.ts +83 -6
  215. package/dist/isr.d.ts.map +1 -1
  216. package/dist/isr.js +263 -36
  217. package/dist/isr.js.map +1 -1
  218. package/dist/llms-txt.d.ts.map +1 -1
  219. package/dist/llms-txt.js +31 -6
  220. package/dist/llms-txt.js.map +1 -1
  221. package/dist/manifest.d.ts +220 -13
  222. package/dist/manifest.d.ts.map +1 -1
  223. package/dist/manifest.js +386 -9
  224. package/dist/manifest.js.map +1 -1
  225. package/dist/module-graph.d.ts +26 -5
  226. package/dist/module-graph.d.ts.map +1 -1
  227. package/dist/module-graph.js +17 -7
  228. package/dist/module-graph.js.map +1 -1
  229. package/dist/navigation.d.ts +17 -3
  230. package/dist/navigation.d.ts.map +1 -1
  231. package/dist/navigation.js +11 -5
  232. package/dist/navigation.js.map +1 -1
  233. package/dist/nonce.d.ts +47 -0
  234. package/dist/nonce.d.ts.map +1 -0
  235. package/dist/nonce.js +63 -0
  236. package/dist/nonce.js.map +1 -0
  237. package/dist/plugins/css-modules.d.ts.map +1 -1
  238. package/dist/plugins/css-modules.js +4 -3
  239. package/dist/plugins/css-modules.js.map +1 -1
  240. package/dist/plugins/kit.d.ts +9 -0
  241. package/dist/plugins/kit.d.ts.map +1 -1
  242. package/dist/plugins/kit.js +18 -0
  243. package/dist/plugins/kit.js.map +1 -1
  244. package/dist/plugins/svg.d.ts +22 -3
  245. package/dist/plugins/svg.d.ts.map +1 -1
  246. package/dist/plugins/svg.js +112 -1
  247. package/dist/plugins/svg.js.map +1 -1
  248. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  249. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  250. package/dist/plugins/vite-leak-guard.js +378 -17
  251. package/dist/plugins/vite-leak-guard.js.map +1 -1
  252. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  253. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  254. package/dist/plugins/vite-zone-guard.js +208 -0
  255. package/dist/plugins/vite-zone-guard.js.map +1 -0
  256. package/dist/plugins/zone-guard.d.ts +27 -0
  257. package/dist/plugins/zone-guard.d.ts.map +1 -0
  258. package/dist/plugins/zone-guard.js +72 -0
  259. package/dist/plugins/zone-guard.js.map +1 -0
  260. package/dist/prerender.d.ts +10 -0
  261. package/dist/prerender.d.ts.map +1 -1
  262. package/dist/prerender.js +35 -3
  263. package/dist/prerender.js.map +1 -1
  264. package/dist/public-dir.d.ts +9 -0
  265. package/dist/public-dir.d.ts.map +1 -1
  266. package/dist/public-dir.js +67 -9
  267. package/dist/public-dir.js.map +1 -1
  268. package/dist/pwa-manifest.d.ts +68 -0
  269. package/dist/pwa-manifest.d.ts.map +1 -0
  270. package/dist/pwa-manifest.js +178 -0
  271. package/dist/pwa-manifest.js.map +1 -0
  272. package/dist/query.js +4 -4
  273. package/dist/query.js.map +1 -1
  274. package/dist/render-seam.d.ts +42 -2
  275. package/dist/render-seam.d.ts.map +1 -1
  276. package/dist/render-seam.js +6 -0
  277. package/dist/render-seam.js.map +1 -1
  278. package/dist/route-manifest.d.ts +3 -0
  279. package/dist/route-manifest.d.ts.map +1 -1
  280. package/dist/route-manifest.js +26 -3
  281. package/dist/route-manifest.js.map +1 -1
  282. package/dist/route-types.d.ts +24 -0
  283. package/dist/route-types.d.ts.map +1 -0
  284. package/dist/route-types.js +195 -0
  285. package/dist/route-types.js.map +1 -0
  286. package/dist/router.d.ts +28 -6
  287. package/dist/router.d.ts.map +1 -1
  288. package/dist/router.js +129 -40
  289. package/dist/router.js.map +1 -1
  290. package/dist/vitals.d.ts +33 -0
  291. package/dist/vitals.d.ts.map +1 -0
  292. package/dist/vitals.js +16 -0
  293. package/dist/vitals.js.map +1 -0
  294. package/dist/vite.d.ts +16 -1
  295. package/dist/vite.d.ts.map +1 -1
  296. package/dist/vite.js +187 -36
  297. package/dist/vite.js.map +1 -1
  298. package/dist/zones.d.ts +70 -0
  299. package/dist/zones.d.ts.map +1 -0
  300. package/dist/zones.js +365 -0
  301. package/dist/zones.js.map +1 -0
  302. package/package.json +67 -14
  303. package/src/backend-only.ts +16 -0
  304. package/src/boundary.ts +18 -5
  305. package/src/build-plan.ts +90 -35
  306. package/src/build-vite.ts +77 -11
  307. package/src/build.ts +466 -96
  308. package/src/bun-dev-entry.ts +1 -1
  309. package/src/cdn.ts +216 -0
  310. package/src/client.ts +111 -18
  311. package/src/conformance.ts +16 -3
  312. package/src/csp.ts +194 -0
  313. package/src/dev-client.ts +349 -0
  314. package/src/dev-diagnostics.ts +60 -7
  315. package/src/dev-error.ts +49 -5
  316. package/src/dev-feed.ts +1140 -0
  317. package/src/dev-indicator.ts +113 -0
  318. package/src/dev-session.ts +729 -0
  319. package/src/dev-sourcemap.ts +281 -0
  320. package/src/dev-ssr-graph.ts +16 -5
  321. package/src/dev.ts +86 -43
  322. package/src/diagnostic-catalog.ts +243 -0
  323. package/src/diagnostic-prompt.ts +355 -0
  324. package/src/diagnostic.ts +63 -66
  325. package/src/draft.ts +4 -1
  326. package/src/fn.ts +19 -3
  327. package/src/fs.ts +13 -2
  328. package/src/index.ts +42 -10
  329. package/src/internal/cdn/cloudflare.ts +146 -0
  330. package/src/internal/cdn/core.ts +316 -0
  331. package/src/internal/cdn/fastly.ts +62 -0
  332. package/src/internal/cdn/vercel.ts +95 -0
  333. package/src/internal/code-unit-order.ts +5 -0
  334. package/src/internal/codegen.ts +161 -24
  335. package/src/internal/dev-csp.ts +176 -0
  336. package/src/internal/dev-reserved.ts +52 -0
  337. package/src/internal/html-spans.ts +50 -0
  338. package/src/internal/loading-runtime.ts +244 -0
  339. package/src/internal/matches-runtime.ts +62 -0
  340. package/src/internal/mount-shadow.ts +88 -0
  341. package/src/internal/node-builtins.ts +79 -0
  342. package/src/internal/output-guard.ts +701 -0
  343. package/src/internal/page-execution.ts +795 -185
  344. package/src/internal/parity.ts +150 -36
  345. package/src/internal/private-env.ts +334 -0
  346. package/src/internal/public-url.ts +22 -0
  347. package/src/internal/render-document.ts +114 -67
  348. package/src/internal/response-controls.ts +284 -0
  349. package/src/internal/runtime-contract.ts +10 -15
  350. package/src/internal/secret-scan.ts +623 -0
  351. package/src/internal/server-boundary.ts +6 -69
  352. package/src/internal/source-map.ts +256 -0
  353. package/src/internal/target-compat.ts +92 -0
  354. package/src/internal/timing-safe-equal.ts +13 -0
  355. package/src/internal/vitals-runtime.ts +53 -0
  356. package/src/internal/web-app.ts +262 -84
  357. package/src/internal/zone-graph.ts +593 -0
  358. package/src/isr.ts +325 -37
  359. package/src/llms-txt.ts +28 -4
  360. package/src/manifest.ts +670 -16
  361. package/src/module-graph.ts +53 -9
  362. package/src/navigation.ts +29 -6
  363. package/src/nonce.ts +104 -0
  364. package/src/plugins/css-modules.ts +5 -3
  365. package/src/plugins/kit.ts +24 -0
  366. package/src/plugins/svg.ts +135 -4
  367. package/src/plugins/vite-leak-guard.ts +529 -16
  368. package/src/plugins/vite-zone-guard.ts +263 -0
  369. package/src/plugins/zone-guard.ts +110 -0
  370. package/src/prerender.ts +49 -3
  371. package/src/public-dir.ts +69 -8
  372. package/src/pwa-manifest.ts +226 -0
  373. package/src/query.ts +4 -4
  374. package/src/render-seam.ts +46 -1
  375. package/src/route-manifest.ts +15 -3
  376. package/src/route-types.ts +264 -0
  377. package/src/router.ts +163 -63
  378. package/src/vitals.ts +44 -0
  379. package/src/vite.ts +207 -36
  380. package/src/zones.ts +430 -0
  381. package/dist/plugins/vite-server-only.d.ts +0 -49
  382. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  383. package/dist/plugins/vite-server-only.js +0 -57
  384. package/dist/plugins/vite-server-only.js.map +0 -1
  385. package/dist/server-only.d.ts +0 -21
  386. package/dist/server-only.d.ts.map +0 -1
  387. package/dist/server-only.js +0 -25
  388. package/dist/server-only.js.map +0 -1
  389. package/src/plugins/vite-server-only.ts +0 -75
  390. package/src/server-only.ts +0 -25
@@ -0,0 +1,593 @@
1
+ /**
2
+ * Proof that a client build holds browser code only.
3
+ *
4
+ * Runs on the finished module graph of either bundler, after the build and before anything is
5
+ * written. It fails closed: a module it cannot place, an import edge it cannot follow, an emitted file
6
+ * it cannot trace back to the graph or an import the bundle left external is an error, not a pass.
7
+ */
8
+ import { existsSync, realpathSync } from "node:fs"
9
+ import { basename, dirname, extname, isAbsolute, posix, relative, resolve } from "node:path"
10
+ import { BACKEND_ROUTE_EXPORTS, backendFileFor } from "../manifest.ts"
11
+ import type { ClientModuleGraph, GraphImport } from "../module-graph.ts"
12
+ import {
13
+ BACKEND_ONLY_MARKER,
14
+ browserDenial,
15
+ type Classification,
16
+ importAllowed,
17
+ importRuleMessage,
18
+ type ZoneClassifier,
19
+ } from "../zones.ts"
20
+ import { codeUnitOrder } from "./code-unit-order.ts"
21
+ import { isBareNodeBuiltin } from "./node-builtins.ts"
22
+
23
+ /** What a graph module id names. */
24
+ export type ModuleSource =
25
+ | { readonly kind: "file"; readonly file: string }
26
+ /** `node:`/`bun:` builtins; the node-builtin guard reports these with its own message. */
27
+ | { readonly kind: "builtin" }
28
+ /** A plugin's virtual module, with the file it derives from when it names one. */
29
+ | { readonly kind: "virtual"; readonly file?: string }
30
+
31
+ /** A namespace prefix needs two characters, so a Windows drive letter still reads as a path. */
32
+ const NAMESPACED = /^([a-z][\w.+-]+):(.*)$/i
33
+ const stripQuery = (id: string): string => id.replace(/[?#].*$/, "")
34
+ const virtualOf = (rest: string): ModuleSource => {
35
+ const file = stripQuery(rest)
36
+ return isAbsolute(file) && existsSync(file) ? { kind: "virtual", file } : { kind: "virtual" }
37
+ }
38
+
39
+ /** Bun metafile ids: cwd-relative paths, `node:` builtins, `namespace:path` for plugin modules. */
40
+ export function bunModuleSource(cwd: string): (id: string) => ModuleSource {
41
+ return (id) => {
42
+ if (id.startsWith("node:") || id.startsWith("bun:")) return { kind: "builtin" }
43
+ const namespaced = NAMESPACED.exec(id)
44
+ if (namespaced !== null && namespaced[1] !== "file") return virtualOf(namespaced[2] ?? "")
45
+ return { kind: "file", file: resolve(cwd, stripQuery(namespaced?.[2] ?? id)) }
46
+ }
47
+ }
48
+
49
+ /** Rollup/Vite ids: absolute paths, `\0`-prefixed virtual modules, Vite's browser-external shim. */
50
+ export function rollupModuleSource(id: string): ModuleSource {
51
+ if (id.startsWith("node:") || id.includes("__vite-browser-external")) return { kind: "builtin" }
52
+ if (id.startsWith("\0")) return virtualOf(id.slice(1).replace(/^[\w-]+:/, ""))
53
+ const file = stripQuery(id)
54
+ return isAbsolute(file) ? { kind: "file", file } : { kind: "virtual" }
55
+ }
56
+
57
+ export interface ZoneLeak {
58
+ /** The refused module, as the report names it. */
59
+ readonly module: string
60
+ readonly reason: string
61
+ /** Shortest import path from an entry: `[entry, ...as-written specifiers]`. */
62
+ readonly chain: readonly string[]
63
+ }
64
+
65
+ export interface ClientGraphVerdict {
66
+ readonly leaks: readonly ZoneLeak[]
67
+ /** Evidence the graph does not account for. Any entry fails the build. */
68
+ readonly gaps: readonly string[]
69
+ /** Route frontend files whose compiled entry exports a backend-only name. */
70
+ readonly misplacedExports: readonly string[]
71
+ }
72
+
73
+ export interface VerifyClientGraphOptions {
74
+ readonly classifier: ZoneClassifier
75
+ readonly sourceOf: (id: string) => ModuleSource
76
+ /** The private-env denial for a browser-code file (see `privateEnvCheck`); unchecked when absent. */
77
+ readonly privateEnv?: (file: string) => string | undefined
78
+ /** Files the zone guard refused while the bundler loaded them. Bun keeps the edge to a module
79
+ * tree-shaking removed but drops the module, so such an edge still names a refusal. */
80
+ readonly refused?: ReadonlySet<string>
81
+ }
82
+
83
+ const realOrSelf = (file: string): string => {
84
+ try {
85
+ return realpathSync(file)
86
+ } catch {
87
+ return file
88
+ }
89
+ }
90
+
91
+ /** Zones whose code runs in a browser, so may read only public environment variables. */
92
+ const BROWSER_CODE = new Set<string>(["route-frontend", "frontend", "shared"])
93
+
94
+ const isUrl = (spec: string): boolean => /^(?:https?:|data:)/i.test(spec)
95
+ const isBuiltinSpec = (spec: string): boolean =>
96
+ spec.startsWith("node:") || spec.startsWith("bun:") || isBareNodeBuiltin(spec)
97
+
98
+ /** Check every module and edge of a client graph against the zone rules. */
99
+ export function verifyClientGraph(
100
+ graph: ClientModuleGraph,
101
+ options: VerifyClientGraphOptions,
102
+ ): ClientGraphVerdict {
103
+ const { classifier, sourceOf } = options
104
+ const display = (file: string): string => shownPath(classifier.appRoot, file)
105
+ const labelOf = (id: string): string => {
106
+ const source = sourceOf(id)
107
+ return source.kind !== "builtin" && source.file !== undefined ? display(source.file) : id
108
+ }
109
+
110
+ const idByFile = new Map<string, string>()
111
+ for (const id of Object.keys(graph.modules)) {
112
+ const source = sourceOf(id)
113
+ if (source.kind === "file") idByFile.set(source.file, id)
114
+ }
115
+ const refused = new Set([...(options.refused ?? [])].map(realOrSelf))
116
+ const dropped = new Set<string>()
117
+ const resolveEdge = (im: GraphImport): string | undefined => {
118
+ const path = im.path
119
+ if (path === undefined) return undefined
120
+ if (graph.modules[path] !== undefined) return path
121
+ const source = sourceOf(path)
122
+ if (source.kind !== "file") return undefined
123
+ const known = idByFile.get(source.file)
124
+ if (known !== undefined) return known
125
+ if (!refused.has(realOrSelf(source.file))) return undefined
126
+ dropped.add(path)
127
+ return path
128
+ }
129
+ // A lazy `import()` of a split module is an edge to the OUTPUT chunk; its entry module continues the walk.
130
+ const entryByChunk = new Map<string, string>()
131
+ for (const [path, chunk] of Object.entries(graph.chunks)) {
132
+ if (chunk.entryPoint !== undefined) entryByChunk.set(basename(path), chunk.entryPoint)
133
+ }
134
+ const chunkNames = new Set(Object.keys(graph.chunks).map((path) => basename(path)))
135
+ const outputKey = (path: string): string => posix.normalize(path.replaceAll("\\", "/"))
136
+ const outputKeys = new Set(Object.keys(graph.chunks).map(outputKey))
137
+ // When every output reports what its code imports, that is the evidence; a module-level external
138
+ // edge may be one the bundler dropped (an unused re-export of a `sideEffects: false` package).
139
+ const chunkList = Object.values(graph.chunks)
140
+ const outputImportsKnown =
141
+ chunkList.length > 0 && chunkList.every((chunk) => chunk.imports !== undefined)
142
+
143
+ const gaps: string[] = []
144
+ for (const [path, chunk] of Object.entries(graph.chunks)) {
145
+ for (const spec of chunk.imports ?? []) {
146
+ if (outputKeys.has(outputKey(spec)) || isBuiltinSpec(spec) || isUrl(spec)) continue
147
+ gaps.push(
148
+ `${basename(path)} imports "${spec}", which is not one of the build's own outputs: browser output may import only its own chunks`,
149
+ )
150
+ }
151
+ }
152
+ const edges = new Map<string, Array<{ readonly to: string; readonly label: string }>>()
153
+ const specifiers = new Map<string, Set<string>>()
154
+ for (const [id, module] of Object.entries(graph.modules)) {
155
+ const out: Array<{ readonly to: string; readonly label: string }> = []
156
+ for (const im of module.imports) {
157
+ const spec = im.original ?? im.path ?? ""
158
+ if (im.external === true) {
159
+ const chunk = im.path === undefined ? undefined : basename(im.path)
160
+ if (chunk !== undefined && chunkNames.has(chunk)) {
161
+ const entry = entryByChunk.get(chunk)
162
+ if (entry !== undefined) out.push({ to: entry, label: spec })
163
+ continue
164
+ }
165
+ if (outputImportsKnown || isBuiltinSpec(spec) || isUrl(spec)) continue
166
+ gaps.push(
167
+ `${labelOf(id)} imports "${spec}", which the bundle left external: browser output may import only its own chunks`,
168
+ )
169
+ continue
170
+ }
171
+ // An edge with no module behind it was never loaded (a `sideEffects: false` package's unused
172
+ // re-export), so it is not missing evidence - unless the zone guard refused the file it names.
173
+ const target = resolveEdge(im)
174
+ if (target === undefined) continue
175
+ out.push({ to: target, label: im.original ?? labelOf(target) })
176
+ if (im.original !== undefined) {
177
+ const set = specifiers.get(target) ?? new Set<string>()
178
+ set.add(im.original)
179
+ specifiers.set(target, set)
180
+ }
181
+ }
182
+ edges.set(id, out)
183
+ }
184
+ for (const [path, chunk] of Object.entries(graph.chunks)) {
185
+ for (const id of chunk.modules) {
186
+ if (graph.modules[id] === undefined && sourceOf(id).kind !== "builtin") {
187
+ gaps.push(
188
+ `${basename(path)} contains ${id}, which the build's module graph does not describe`,
189
+ )
190
+ }
191
+ }
192
+ }
193
+
194
+ const denied = new Map<string, string>()
195
+ for (const id of [...Object.keys(graph.modules), ...dropped]) {
196
+ const source = sourceOf(id)
197
+ const file = source.kind === "builtin" ? undefined : source.file
198
+ if (file === undefined) continue
199
+ const classification = classifier.classify(file)
200
+ let reason = browserDenial(classification)
201
+ for (const spec of specifiers.get(id) ?? []) reason ??= browserDenial(classification, spec)
202
+ if (reason === undefined && BROWSER_CODE.has(classification.zone))
203
+ reason = options.privateEnv?.(file)
204
+ if (reason !== undefined) denied.set(id, reason)
205
+ }
206
+
207
+ // Chains start at a route file where they can: a lazily split module is an entry of its own chunk,
208
+ // but the import that pulled it in sits in a route.
209
+ const entries = [
210
+ ...new Set(
211
+ Object.values(graph.chunks)
212
+ .map((chunk) => chunk.entryPoint)
213
+ .filter((entry): entry is string => entry !== undefined),
214
+ ),
215
+ ]
216
+ const zoneOfEntry = (id: string) => {
217
+ const source = sourceOf(id)
218
+ return source.kind === "file" ? classifier.classify(source.file).zone : undefined
219
+ }
220
+ const routeEntries = entries.filter((id) => zoneOfEntry(id) === "route-frontend")
221
+ const generatedEntries = entries.filter((id) => zoneOfEntry(id) === "generated")
222
+ const roots =
223
+ routeEntries.length + generatedEntries.length > 0
224
+ ? [...routeEntries, ...generatedEntries]
225
+ : entries
226
+ const chains = denied.size === 0 ? new Map<string, string[]>() : shortestChains(roots, edges)
227
+ const leaks = [...denied]
228
+ .map(([id, reason]) => ({
229
+ module: labelOf(id),
230
+ reason,
231
+ chain: (chains.get(id) ?? [id]).map((label, i) => (i === 0 ? labelOf(label) : label)),
232
+ }))
233
+ .sort((a, b) => codeUnitOrder(a.module, b.module))
234
+
235
+ const misplacedExports: string[] = []
236
+ for (const [path, chunk] of Object.entries(graph.chunks)) {
237
+ if (chunk.entryPoint === undefined || chunk.exports === undefined) continue
238
+ if (!/\.[cm]?js$/.test(path)) continue
239
+ const source = sourceOf(chunk.entryPoint)
240
+ if (source.kind !== "file" || classifier.classify(source.file).zone !== "route-frontend")
241
+ continue
242
+ const file = display(source.file)
243
+ for (const name of chunk.exports) {
244
+ if (BACKEND_ROUTE_EXPORTS.has(name)) {
245
+ misplacedExports.push(
246
+ `"${file}" exports "${name}", which runs on the server only. Move it to "${backendFileFor(file)}"`,
247
+ )
248
+ }
249
+ }
250
+ }
251
+
252
+ return { leaks, gaps: [...new Set(gaps)].sort(), misplacedExports: misplacedExports.sort() }
253
+ }
254
+
255
+ /** BFS from every entry at once: each reached module's shortest chain, entry first. */
256
+ function shortestChains(
257
+ entries: readonly string[],
258
+ edges: ReadonlyMap<string, ReadonlyArray<{ readonly to: string; readonly label: string }>>,
259
+ ): Map<string, string[]> {
260
+ const chains = new Map<string, string[]>()
261
+ let frontier: string[] = []
262
+ for (const entry of entries) {
263
+ if (chains.has(entry)) continue
264
+ chains.set(entry, [entry])
265
+ frontier.push(entry)
266
+ }
267
+ while (frontier.length > 0) {
268
+ const next: string[] = []
269
+ for (const node of frontier) {
270
+ const chain = chains.get(node) ?? [node]
271
+ for (const edge of edges.get(node) ?? []) {
272
+ if (chains.has(edge.to)) continue
273
+ chains.set(edge.to, [...chain, edge.label])
274
+ next.push(edge.to)
275
+ }
276
+ }
277
+ frontier = next
278
+ }
279
+ return chains
280
+ }
281
+
282
+ export interface ServerGraphVerdict {
283
+ /** Unzoned files and imports the zone rules refuse, each with the chain from an entry. */
284
+ readonly violations: readonly ZoneLeak[]
285
+ }
286
+
287
+ /**
288
+ * Check a server bundle's graph against the zone rules. Unlike the browser build, backend code is
289
+ * expected here; what is refused is a first-party file in no zone, an import the rules forbid
290
+ * (backend reaching frontend code, shared reaching anything but shared code), and shared code reaching
291
+ * a server built-in or the backend-only marker. The build's own entries and generated modules are
292
+ * `generated`, so they may import both halves.
293
+ */
294
+ export function verifyServerGraph(
295
+ graph: ClientModuleGraph,
296
+ options: VerifyClientGraphOptions,
297
+ ): ServerGraphVerdict {
298
+ const { classifier, sourceOf } = options
299
+ const display = (file: string): string => shownPath(classifier.appRoot, file)
300
+ const fileOf = (id: string): string | undefined => {
301
+ const source = sourceOf(id)
302
+ return source.kind === "builtin" ? undefined : source.file
303
+ }
304
+ const labelOf = (id: string): string => {
305
+ const file = fileOf(id)
306
+ return file === undefined ? id : display(file)
307
+ }
308
+ const classes = new Map<string, Classification | undefined>()
309
+ const classOf = (id: string): Classification | undefined => {
310
+ if (!classes.has(id)) {
311
+ const source = sourceOf(id)
312
+ classes.set(id, source.kind === "file" ? classifier.classify(source.file) : undefined)
313
+ }
314
+ return classes.get(id)
315
+ }
316
+ const idByFile = new Map<string, string>()
317
+ for (const id of Object.keys(graph.modules)) {
318
+ const source = sourceOf(id)
319
+ if (source.kind === "file") idByFile.set(source.file, id)
320
+ }
321
+ const resolveEdge = (im: GraphImport): string | undefined => {
322
+ if (im.path === undefined || im.external === true) return undefined
323
+ if (graph.modules[im.path] !== undefined) return im.path
324
+ const source = sourceOf(im.path)
325
+ if (source.kind !== "file") return undefined
326
+ // Bun keeps the edge to a module tree-shaking removed and drops the module; its zone still counts.
327
+ return idByFile.get(source.file) ?? (existsSync(source.file) ? im.path : undefined)
328
+ }
329
+
330
+ const edges = new Map<string, Array<{ readonly to: string; readonly label: string }>>()
331
+ const found: Array<{
332
+ readonly at: string
333
+ readonly module: string
334
+ readonly reason: string
335
+ readonly tail?: string
336
+ }> = []
337
+ for (const [id, module] of Object.entries(graph.modules)) {
338
+ const from = classOf(id)
339
+ if (from?.zone === "error") found.push({ at: id, module: labelOf(id), reason: from.reason })
340
+ const file = fileOf(id)
341
+ const env =
342
+ from !== undefined && file !== undefined && BROWSER_CODE.has(from.zone)
343
+ ? options.privateEnv?.(file)
344
+ : undefined
345
+ if (env !== undefined) found.push({ at: id, module: labelOf(id), reason: env })
346
+ const out: Array<{ readonly to: string; readonly label: string }> = []
347
+ for (const im of module.imports) {
348
+ const spec = im.original ?? im.path ?? ""
349
+ if (from?.zone === "shared") {
350
+ const builtin =
351
+ isBuiltinSpec(spec) || (im.path !== undefined && sourceOf(im.path).kind === "builtin")
352
+ if (builtin || spec === BACKEND_ONLY_MARKER) {
353
+ found.push({
354
+ at: id,
355
+ module: labelOf(id),
356
+ reason: `shared code runs on both sides, so it may not import "${spec}"; move the code that needs it under backend/`,
357
+ tail: spec,
358
+ })
359
+ }
360
+ }
361
+ const target = resolveEdge(im)
362
+ if (target === undefined) continue
363
+ out.push({ to: target, label: im.original ?? labelOf(target) })
364
+ const to = classOf(target)
365
+ if (from === undefined || to === undefined || from.zone === "error" || to.zone === "error")
366
+ continue
367
+ if (!importAllowed(from.zone, to.zone)) {
368
+ found.push({
369
+ at: id,
370
+ module: labelOf(target),
371
+ reason: importRuleMessage(labelOf(id), from.zone, labelOf(target), to.zone),
372
+ tail: im.original ?? labelOf(target),
373
+ })
374
+ }
375
+ }
376
+ edges.set(id, out)
377
+ }
378
+ if (found.length === 0) return { violations: [] }
379
+
380
+ const entries = [
381
+ ...new Set(
382
+ Object.values(graph.chunks)
383
+ .map((chunk) => chunk.entryPoint)
384
+ .filter((entry): entry is string => entry !== undefined),
385
+ ),
386
+ ]
387
+ const chains = shortestChains(entries, edges)
388
+ const violations = found
389
+ .map(({ at, module, reason, tail }) => {
390
+ const chain = (chains.get(at) ?? [at]).map((label, i) => (i === 0 ? labelOf(label) : label))
391
+ return { module, reason, chain: tail === undefined ? chain : [...chain, tail] }
392
+ })
393
+ .sort((a, b) => codeUnitOrder(a.module, b.module) || codeUnitOrder(a.reason, b.reason))
394
+ return { violations }
395
+ }
396
+
397
+ /** The build-failing message for a server graph that breaks the zone rules. */
398
+ export function formatServerGraphVerdict(verdict: ServerGraphVerdict): string | undefined {
399
+ if (verdict.violations.length === 0) return undefined
400
+ const lines = verdict.violations.map(
401
+ (v) =>
402
+ ` - ${v.module}: ${v.reason}${v.chain.length > 1 ? `\n via ${v.chain.join(" → ")}` : ""}`,
403
+ )
404
+ return `[nifra/web] the server build breaks the zone rules:\n${lines.join("\n")}`
405
+ }
406
+
407
+ /** One emitted file, as the accounting sees it. */
408
+ export interface EmittedFile {
409
+ /** Output path relative to the output directory. */
410
+ readonly name: string
411
+ readonly kind: "code" | "css" | "asset" | "map"
412
+ /** Source files the bundler says an asset came from, when it says. */
413
+ readonly sources?: readonly string[]
414
+ /** The file's text, for code and maps. */
415
+ readonly text?: string
416
+ }
417
+
418
+ /**
419
+ * Why a file may not ship as an asset - its bytes copied or inlined into the bundle - or `undefined`
420
+ * when it may. A server function reaches the browser only as the stub its import is replaced with, so
421
+ * as an asset it is the server source itself.
422
+ */
423
+ export function assetDenial(classification: Classification): string | undefined {
424
+ return (
425
+ browserDenial(classification) ??
426
+ (classification.zone === "fn"
427
+ ? "it is a server function, and this asset is its source rather than the calls a browser makes"
428
+ : undefined)
429
+ )
430
+ }
431
+
432
+ /**
433
+ * Trace every emitted file back to the graph. A code or CSS file must be a graph chunk; an asset must
434
+ * name the module it was copied from; a source map (external or inline) may name browser-allowed
435
+ * sources only.
436
+ */
437
+ export function accountEmittedFiles(
438
+ files: readonly EmittedFile[],
439
+ graph: ClientModuleGraph,
440
+ options: VerifyClientGraphOptions & {
441
+ /** Where the files will be written; source map paths resolve against it. */
442
+ readonly outDir: string
443
+ },
444
+ ): string[] {
445
+ const { classifier, sourceOf } = options
446
+ const problems: string[] = []
447
+ const chunkNames = new Set(Object.keys(graph.chunks).map((path) => basename(path)))
448
+ const moduleFiles = new Map<string, string[]>()
449
+ for (const id of Object.keys(graph.modules)) {
450
+ const source = sourceOf(id)
451
+ if (source.kind !== "file") continue
452
+ const name = basename(source.file)
453
+ moduleFiles.set(name, [...(moduleFiles.get(name) ?? []), source.file])
454
+ }
455
+ const checkSources = (where: string, sources: readonly string[], asset = false): void => {
456
+ for (const file of sources) {
457
+ const classification = classifier.classify(file)
458
+ const reason = asset ? assetDenial(classification) : browserDenial(classification)
459
+ if (reason !== undefined)
460
+ problems.push(`${where} names ${shownPath(classifier.appRoot, file)}: ${reason}`)
461
+ }
462
+ }
463
+ for (const file of files) {
464
+ const name = basename(file.name)
465
+ if (file.kind === "code" || file.kind === "css") {
466
+ if (!chunkNames.has(name))
467
+ problems.push(`${file.name} is in the output but not in the module graph`)
468
+ const inline = file.kind === "code" ? inlineSourceMap(file.text) : undefined
469
+ if (inline !== undefined) {
470
+ checkSources(`${file.name}'s inline source map`, mapSourceFiles(inline, options.outDir))
471
+ }
472
+ continue
473
+ }
474
+ if (file.kind === "map") {
475
+ const map = parseMap(file.text)
476
+ if (map === undefined) {
477
+ problems.push(`${file.name} is not a readable source map`)
478
+ continue
479
+ }
480
+ checkSources(file.name, mapSourceFiles(map, dirname(resolve(options.outDir, file.name))))
481
+ continue
482
+ }
483
+ const sources = file.sources ?? assetSourcesByName(name, moduleFiles)
484
+ if (sources.length === 0) {
485
+ problems.push(`${file.name} was emitted, but no module in the graph accounts for it`)
486
+ continue
487
+ }
488
+ checkSources(file.name, sources, true)
489
+ }
490
+ return problems
491
+ }
492
+
493
+ /** `logo-a1b2c3d4.png` came from a graph module named `logo.png`. */
494
+ function assetSourcesByName(name: string, moduleFiles: ReadonlyMap<string, string[]>): string[] {
495
+ const ext = extname(name)
496
+ const stem = name.slice(0, name.length - ext.length)
497
+ const dash = stem.lastIndexOf("-")
498
+ if (dash <= 0) return moduleFiles.get(name) ?? []
499
+ return moduleFiles.get(`${stem.slice(0, dash)}${ext}`) ?? []
500
+ }
501
+
502
+ interface SourceMapLike {
503
+ readonly sources?: readonly (string | null)[]
504
+ readonly sourceRoot?: string
505
+ readonly sections?: ReadonlyArray<{ readonly map?: SourceMapLike }>
506
+ }
507
+
508
+ function parseMap(text: string | undefined): SourceMapLike | undefined {
509
+ if (text === undefined) return undefined
510
+ try {
511
+ const parsed: unknown = JSON.parse(text)
512
+ return typeof parsed === "object" && parsed !== null ? (parsed as SourceMapLike) : undefined
513
+ } catch {
514
+ return undefined
515
+ }
516
+ }
517
+
518
+ function inlineSourceMap(text: string | undefined): SourceMapLike | undefined {
519
+ if (text === undefined) return undefined
520
+ const match = /[#@] sourceMappingURL=data:application\/json[^,]*?(;base64)?,([^\s'"]+)/.exec(text)
521
+ if (match === null) return undefined
522
+ const payload = match[2] ?? ""
523
+ try {
524
+ const json =
525
+ match[1] !== undefined
526
+ ? Buffer.from(payload, "base64").toString("utf8")
527
+ : decodeURIComponent(payload)
528
+ return parseMap(json) ?? { sources: ["<unreadable inline source map>"] }
529
+ } catch {
530
+ return { sources: ["<unreadable inline source map>"] }
531
+ }
532
+ }
533
+
534
+ /** Every source a map (or an indexed map's sections) names that exists on disk, as absolute paths. */
535
+ function mapSourceFiles(map: SourceMapLike, base: string): string[] {
536
+ const files: string[] = []
537
+ for (const section of map.sections ?? []) {
538
+ if (section.map !== undefined) files.push(...mapSourceFiles(section.map, base))
539
+ }
540
+ const root = map.sourceRoot ?? ""
541
+ for (const source of map.sources ?? []) {
542
+ if (source === null || source === "") continue
543
+ const path = source.replace(/^file:\/\//, "")
544
+ const absolute = isAbsolute(path) ? path : resolve(base, root, path)
545
+ // Virtual sources (`\0id`, bundler helpers) have no file to classify; their code came from a
546
+ // graph module that was classified already.
547
+ if (existsSync(absolute)) files.push(absolute)
548
+ }
549
+ return files
550
+ }
551
+
552
+ /** `file` as a message names it: relative to the app with `/` separators, or as given outside it. */
553
+ const shownPath = (appRoot: string, file: string): string => {
554
+ const rel = relative(appRoot, file).replaceAll("\\", "/")
555
+ return rel.startsWith("..") || isAbsolute(rel) ? file : rel
556
+ }
557
+
558
+ const indent = (lines: readonly string[]): string => lines.map((line) => ` - ${line}`).join("\n")
559
+
560
+ /** The build-failing message for a verdict, or `undefined` when the build is clean. */
561
+ export function formatClientGraphVerdict(verdict: ClientGraphVerdict): string | undefined {
562
+ const sections: string[] = []
563
+ if (verdict.leaks.length > 0) {
564
+ const lines = verdict.leaks.map(
565
+ (leak) =>
566
+ ` - ${leak.module}: ${leak.reason}${leak.chain.length > 1 ? `\n via ${leak.chain.join(" → ")}` : ""}`,
567
+ )
568
+ sections.push(
569
+ `[nifra/web] the browser build reached code that may not ship to a browser:\n${lines.join("\n")}\n` +
570
+ "Reach backend code through a route's backend half (x.backend.ts) or a *.fn.ts server function; " +
571
+ "code both sides need belongs in shared/.",
572
+ )
573
+ }
574
+ if (verdict.misplacedExports.length > 0) {
575
+ sections.push(
576
+ `[nifra/web] route frontend files export server-only names:\n${indent(verdict.misplacedExports)}\n` +
577
+ "`nifra migrate layout` splits every route of an app into its two halves.",
578
+ )
579
+ }
580
+ if (verdict.gaps.length > 0) {
581
+ sections.push(
582
+ "[nifra/web] the client build's module graph does not account for everything it reached, so " +
583
+ `nifra cannot prove the bundle is browser-only:\n${indent(verdict.gaps)}`,
584
+ )
585
+ }
586
+ return sections.length > 0 ? sections.join("\n\n") : undefined
587
+ }
588
+
589
+ /** The build-failing message for emitted files the graph does not account for. */
590
+ export function formatUnaccountedOutput(problems: readonly string[]): string | undefined {
591
+ if (problems.length === 0) return undefined
592
+ return `[nifra/web] the client build emitted files nifra cannot trace to browser code:\n${indent(problems)}`
593
+ }