@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
package/dist/build.js CHANGED
@@ -5,22 +5,30 @@
5
5
  * are Bun-specific and never on the request path (own subpath, like `@nifrajs/web/fs`); the *output*
6
6
  * runs on any runtime.
7
7
  */
8
- import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
+ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { cp, lstat, mkdir, realpath } from "node:fs/promises";
10
- import { dirname, join, basename as pathBasename, relative, resolve as resolvePath, sep, } from "node:path";
10
+ import { dirname, join, basename as pathBasename, posix, relative, resolve as resolvePath, sep, } from "node:path";
11
11
  import { singleCopyPlugin as declaredSingleCopyPlugin, readSingleCopyDeclaration, } from "@nifrajs/core/single-copy";
12
12
  import { Glob } from "bun";
13
- import { aggregateSizeReport, detectNodeBuiltinsInClient, detectServerOnlyInClient, formatNodeBuiltinLeak, formatServerOnlyLeak, parseManifestClientEntry, parseManifestCssLoading, parseManifestRouteStyles, parseManifestStyles, planBuildTarget, } from "./build-plan.js";
13
+ import { aggregateSizeReport, detectNodeBuiltinsInClient, detectServerOnlyInClient, formatNodeBuiltinLeak, formatServerOnlyLeak, parseBuildTarget, parseManifestClientEntry, parseManifestCssLoading, parseManifestRouteStyles, parseManifestStyles, planBuildTarget, } from "./build-plan.js";
14
14
  import { sanitizeOutputNames } from "./chunk-names.js";
15
15
  import { normalizeCssCodeSplit, normalizeCssLoading } from "./css-contract.js";
16
16
  import { discoverRoutes } from "./fs.js";
17
17
  import { generateClientEntry, generateServerManifest } from "./index.js";
18
18
  import { dedupePolicyFor } from "./internal/identity-policy.js";
19
19
  import { assertDevelopmentProductionParity, assertIdentityParity, collectDevelopmentParityInput, } from "./internal/parity.js";
20
- import { generateServerFnStub, SERVER_FN_MODULE, SERVER_ONLY_MODULE, SERVER_ONLY_REPLACEMENT, serverFnNamespace, } from "./internal/server-boundary.js";
21
- // `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
20
+ import { privateEnvCheck } from "./internal/private-env.js";
21
+ import { publicUrlPath } from "./internal/public-url.js";
22
+ import { assertNoSecrets, buildEnvironment, emittedScanFiles, formatSecretFindings, graphScanInput, originName, publicScanFiles, scanForSecrets, textOf, } from "./internal/secret-scan.js";
23
+ import { generateServerFnStub, SERVER_FN_MODULE, serverFnNamespace, } from "./internal/server-boundary.js";
24
+ import { formatUnsupportedBuiltins, unsupportedBuiltins } from "./internal/target-compat.js";
25
+ import { accountEmittedFiles, bunModuleSource, formatClientGraphVerdict, formatServerGraphVerdict, formatUnaccountedOutput, verifyClientGraph, verifyServerGraph, } from "./internal/zone-graph.js";
22
26
  import { fromBunMetafile } from "./module-graph.js";
27
+ import { zoneGuardPlugin } from "./plugins/zone-guard.js";
28
+ // `buildTarget(static)` drives the SSG prerender engine directly (it's also re-exported below).
23
29
  import { prerenderRoutes } from "./prerender.js";
30
+ import { CONTENT_TYPES, IMMUTABLE, ONE_DAY } from "./public-dir.js";
31
+ import { createZoneClassifier } from "./zones.js";
24
32
  export * from "./build-plan.js";
25
33
  const basename = (path) => pathBasename(path);
26
34
  // The lazy shape declares `const loaders`, the eager shape `const modules`. Anchored at a line start,
@@ -55,18 +63,6 @@ export function publicEnvDefines(prefix, env) {
55
63
  }
56
64
  return defines;
57
65
  }
58
- /**
59
- * Percent-encode one path segment exactly the way a browser encodes it into a request URL.
60
- *
61
- * NOT `encodeURIComponent`. That escapes the sub-delimiters `, @ + = & ; $`, which a browser sends raw -
62
- * so a file named `report,2026.csv` would be recorded as `/report%2C2026.csv` while the request arrives
63
- * as `/report,2026.csv`, the allowlist lookup misses, and the file 404s in production only. `encodeURI`
64
- * agrees with `URL.pathname` on every character except `?` and `#`, which terminate a path and so must
65
- * be escaped explicitly here.
66
- */
67
- function encodePathSegment(segment) {
68
- return encodeURI(segment).replace(/[?#]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
69
- }
70
66
  /**
71
67
  * Cloudflare Pages rejects a `_routes.json` with more than 100 include+exclude rules combined, or any
72
68
  * single rule longer than 100 characters.
@@ -117,7 +113,7 @@ function collapsibleDirs(publicFiles, routePatterns) {
117
113
  return dirs;
118
114
  }
119
115
  /**
120
- * Build the cf-pages `_routes.json` rules for a set of copied public files, within Cloudflare's budget.
116
+ * Build the cloudflare `_routes.json` rules for a set of copied public files, within Cloudflare's budget.
121
117
  *
122
118
  * `exclude` is what Pages serves straight from the CDN instead of invoking the worker, so naming every
123
119
  * public file is ideal - and impossible past ~99 of them. A `public/` of icons, fonts and share images
@@ -201,7 +197,7 @@ export async function copyPublicDir(from, to) {
201
197
  const target = join(to, rel);
202
198
  await mkdir(join(target, ".."), { recursive: true });
203
199
  await cp(candidateReal, target);
204
- copied.push(`/${rel.split(sep).map(encodePathSegment).join("/")}`);
200
+ copied.push(publicUrlPath(rel));
205
201
  }
206
202
  return copied.sort();
207
203
  }
@@ -267,6 +263,7 @@ export async function buildClient(options) {
267
263
  // layered after these in the `define` object). `Bun` may be absent under non-Bun typecheck - guard it.
268
264
  const buildEnv = (typeof Bun !== "undefined" ? Bun.env : undefined) ?? process.env;
269
265
  const publicDefines = publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", buildEnv);
266
+ const privateEnv = privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_");
270
267
  // Keep the generated source beside the project, not inside `outDir`: module resolution starts at the
271
268
  // importing file, so an absolute `--out /tmp/deploy` must still resolve the app's dependencies.
272
269
  // A unique directory also avoids clobbering a user file or colliding with parallel builds.
@@ -284,6 +281,10 @@ export async function buildClient(options) {
284
281
  ...routeManifest.routes.map((r) => r.file),
285
282
  ...Object.values(routeManifest.layouts).map((l) => l.file),
286
283
  ...(routeManifest.notFound ? [routeManifest.notFound.file] : []),
284
+ // A nested `_404` is an entry only for its stylesheet: it renders on the server, unhydrated.
285
+ ...Object.values(routeManifest.notFounds ?? {}).map((page) => page.file),
286
+ // A `_loading` page is an entry for its stylesheet too: the bootstrap imports it lazily.
287
+ ...Object.values(routeManifest.loadings ?? {}).map((page) => page.file),
287
288
  ]),
288
289
  ].sort();
289
290
  // `metafile: true` asks Bun for the input/output graph - specifically `outputs[js].entryPoint`
@@ -291,12 +292,14 @@ export async function buildClient(options) {
291
292
  // entry→CSS link for per-route splitting: keyed by the unique source path, so it survives
292
293
  // same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. Not yet in
293
294
  // `@types/bun`'s `BuildConfig`, so spread it in (spread props skip the excess-property check).
294
- const buildExtras = { metafile: true };
295
+ const buildExtras = { metafile: true, throw: false };
296
+ const classifier = createZoneClassifier({ appRoot: root, routesDir, generatedFiles: [entryFile] });
297
+ const refused = new Map();
298
+ // No `outdir`: the bundle stays in memory until the graph proves it holds browser code only.
295
299
  const result = await (async () => {
296
300
  try {
297
301
  return await Bun.build({
298
302
  entrypoints: [entryFile, ...routeFiles.map(resolve)],
299
- outdir: outDir,
300
303
  target: "browser",
301
304
  naming: "[name]-[hash].[ext]",
302
305
  publicPath,
@@ -307,11 +310,16 @@ export async function buildClient(options) {
307
310
  ...buildExtras,
308
311
  minify: options.minify ?? true,
309
312
  plugins: [
313
+ // First, so it sees every file before another plugin's `onLoad` claims it.
314
+ zoneGuardPlugin({
315
+ appRoot: root,
316
+ classifier,
317
+ onDenied: (file, reason) => refused.set(file, reason),
318
+ }),
310
319
  ...declaredSingleCopyPlugins(root),
311
320
  reactDedupePlugin(routesDir),
312
321
  preactDedupePlugin(routesDir),
313
322
  svelteDedupePlugin(routesDir),
314
- serverOnlyEmptyPlugin(),
315
323
  serverFnStubPlugin(),
316
324
  ...(options.plugins ?? []),
317
325
  ],
@@ -319,10 +327,11 @@ export async function buildClient(options) {
319
327
  // Replace `process.env.*` at compile time so an app module reading config off `process.env` doesn't
320
328
  // hit a `process is not defined` crash in the browser. Bun does longest-match: NODE_ENV resolves to
321
329
  // the build mode (React's prod/dev branch); each PUBLIC_* var resolves to its baked VALUE; every
322
- // other `process.env.X` becomes undefined (the bare `process.env` → `({})` fallback - so secrets
323
- // never leak). Callers can override any of these via `options.define` (layered last).
330
+ // other `process.env.X` becomes undefined (the bare `process.env` → `{}` fallback - so secrets
331
+ // never leak). Callers can override any of these via `options.define` (layered last). The value
332
+ // must be JSON: Bun inlines anything else (`({})`) as a string literal.
324
333
  define: {
325
- "process.env": "({})",
334
+ "process.env": "{}",
326
335
  "process.env.NODE_ENV": JSON.stringify(mode),
327
336
  ...publicDefines,
328
337
  ...options.define,
@@ -333,36 +342,110 @@ export async function buildClient(options) {
333
342
  rmSync(entryDir, { recursive: true, force: true });
334
343
  }
335
344
  })();
345
+ const cwd = process.cwd();
346
+ const clientMeta = result.metafile;
336
347
  if (!result.success) {
348
+ // A refused file can be what broke the build (a backend module importing a server-only built-in),
349
+ // so the refusal is the error to show, not the bundler's symptom.
350
+ const verdict = refused.size > 0 && clientMeta !== undefined
351
+ ? formatClientGraphVerdict(verifyClientGraph(fromBunMetafile(clientMeta), {
352
+ classifier,
353
+ sourceOf: bunModuleSource(cwd),
354
+ privateEnv,
355
+ refused: new Set(refused.keys()),
356
+ }))
357
+ : undefined;
358
+ if (verdict !== undefined)
359
+ throw new Error(verdict);
360
+ if (refused.size > 0)
361
+ throw new Error(formatRefusedFiles(refused, root));
337
362
  throw new Error(`[nifra/web] client build failed:\n${result.logs.map((l) => String(l)).join("\n")}`);
338
363
  }
339
- // #4: a `node:` builtin (e.g. `node:crypto`) pulled into a CLIENT chunk builds fine (Bun substitutes
340
- // a browser polyfill) but breaks/leaks at runtime. Fail the build with a named, actionable error
341
- // instead - caught at build time, not by a confused user in the browser. Graph-based (the metafile's
342
- // per-output `inputs`), so it can't false-positive on a `"node:..."` string literal and survives
343
- // minification. Only the client build runs this; the server build's `node:` imports are legitimate.
344
- const clientMeta = result.metafile;
345
- const clientGraph = fromBunMetafile(clientMeta);
364
+ const emitted = await Promise.all(result.outputs.map(async (out) => {
365
+ if (out.kind === "sourcemap")
366
+ return { name: out.path, kind: "map", text: await out.text() };
367
+ if (out.kind === "asset") {
368
+ if (out.path.endsWith(".css"))
369
+ return { name: out.path, kind: "css", text: await out.text() };
370
+ const text = textOf(new Uint8Array(await out.arrayBuffer()));
371
+ return { name: out.path, kind: "asset", ...(text === undefined ? {} : { text }) };
372
+ }
373
+ return { name: out.path, kind: "code", text: await out.text() };
374
+ }));
375
+ const clientGraph = withEmittedImports(fromBunMetafile(clientMeta), emitted, publicPath);
376
+ const verdict = verifyClientGraph(clientGraph, {
377
+ classifier,
378
+ sourceOf: bunModuleSource(cwd),
379
+ privateEnv,
380
+ refused: new Set(refused.keys()),
381
+ });
382
+ // Two independent records of one fact: a file the plugin refused that the graph never shows means the
383
+ // graph is missing evidence, and the build cannot prove anything else about it either.
384
+ const reported = new Set(verdict.leaks.map((leak) => resolvePath(root, leak.module)));
385
+ const unseen = [...refused.keys()].filter((file) => !reported.has(file));
386
+ const graphMessage = formatClientGraphVerdict(unseen.length === 0
387
+ ? verdict
388
+ : {
389
+ ...verdict,
390
+ gaps: [
391
+ ...verdict.gaps,
392
+ ...unseen.map((file) => `${file} was refused while loading but is missing from the graph`),
393
+ ],
394
+ });
395
+ if (graphMessage !== undefined)
396
+ throw new Error(graphMessage);
346
397
  // #4: a `node:` builtin (e.g. `node:crypto`) pulled into a CLIENT chunk builds fine (Bun substitutes a
347
398
  // browser polyfill) but breaks/leaks at runtime. Fail with the chain (entry → … → builtin), through the
348
399
  // SHARED formatter so the Vite pipeline's identical guard reads byte-for-byte the same.
349
400
  const nodeBuiltinLeak = formatNodeBuiltinLeak(detectNodeBuiltinsInClient(clientGraph));
350
401
  if (nodeBuiltinLeak !== undefined)
351
402
  throw new Error(nodeBuiltinLeak);
352
- // §3.3/§5.1: a module that opted into the `server-only` marker yet reached a CLIENT chunk - catches
353
- // pure-server logic (a secret, a server-only API call) carrying no `node:` import and not named
354
- // `*.server`, so neither other guard fires. Same shared formatter as above.
403
+ // A module that imports the `backend-only` marker yet reached a client chunk: library code the zones
404
+ // cannot place on a side by path alone. Same shared formatter as the Vite pipeline.
355
405
  const serverOnlyLeak = formatServerOnlyLeak(detectServerOnlyInClient(clientGraph));
356
406
  if (serverOnlyLeak !== undefined)
357
407
  throw new Error(serverOnlyLeak);
408
+ const unaccounted = formatUnaccountedOutput(accountEmittedFiles(emitted, clientGraph, {
409
+ classifier,
410
+ sourceOf: bunModuleSource(cwd),
411
+ outDir: resolvePath(outDir),
412
+ }));
413
+ if (unaccounted !== undefined)
414
+ throw new Error(unaccounted);
415
+ const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public");
416
+ const { sources, originsOf } = graphScanInput(clientGraph, bunModuleSource(cwd), classifier);
417
+ const secrets = formatSecretFindings(scanForSecrets({
418
+ sources,
419
+ artifacts: [
420
+ ...emitted.flatMap((file) => {
421
+ if (file.text === undefined)
422
+ return [];
423
+ const name = file.name.replace(/^\.\//, "");
424
+ return emittedScanFiles(name, file.text, originsOf(name));
425
+ }),
426
+ ...(publicDir !== undefined && existsSync(publicDir)
427
+ ? publicScanFiles(publicDir, originName(root, resolvePath(publicDir)))
428
+ : []),
429
+ ],
430
+ env: buildEnv,
431
+ publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
432
+ ...(options.secretExemptions ? { exemptions: options.secretExemptions } : {}),
433
+ }));
434
+ if (secrets !== undefined)
435
+ throw new Error(secrets);
436
+ const outputs = await Promise.all(result.outputs.map(async (out) => {
437
+ const path = join(outDir, out.path);
438
+ await Bun.write(path, out);
439
+ return { kind: out.kind, path };
440
+ }));
358
441
  // Rename any chunk whose basename isn't URL-safe (dynamic-route files become `[slug]-hash.js`) and
359
442
  // rewrite the references - otherwise the lazy import 404s and the route silently never hydrates.
360
- const renamed = sanitizeOutputNames(result.outputs);
443
+ const renamed = sanitizeOutputNames(outputs);
361
444
  const toUrl = (path) => `${publicPath}${renamed.get(basename(path)) ?? basename(path)}`;
362
445
  // Entry-point outputs come back in entrypoint order: [bootstrap, ...routeFiles]. Map each route file
363
446
  // to its chunk URL by that order (guarded against drift), then a route's chunks = its layout chain +
364
447
  // own file.
365
- const entryPoints = result.outputs.filter((o) => o.kind === "entry-point");
448
+ const entryPoints = outputs.filter((o) => o.kind === "entry-point");
366
449
  const bootstrap = entryPoints[0];
367
450
  if (bootstrap === undefined)
368
451
  throw new Error("[nifra/web] build produced no entry-point output");
@@ -400,7 +483,7 @@ export async function buildClient(options) {
400
483
  // per-route `cssBundle` outputs follow. Dropping those per-route outputs (the old `aggregate`-only
401
484
  // shape) left them in `manifest.assets` but not `css`, so a route-scoped stylesheet normalized to
402
485
  // `asset:css` and tripped the module-graph parity contract - a bundler-dependent divergence.
403
- const cssAssets = result.outputs.filter((o) => o.kind === "asset" && o.path.endsWith(".css"));
486
+ const cssAssets = outputs.filter((o) => o.kind === "asset" && o.path.endsWith(".css"));
404
487
  const css = [
405
488
  ...cssAssets.filter((o) => cssNameOf(o.path) === bootstrapName),
406
489
  ...cssAssets.filter((o) => cssNameOf(o.path) !== bootstrapName),
@@ -411,7 +494,6 @@ export async function buildClient(options) {
411
494
  // same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. A page then
412
495
  // links only its layout chain + own CSS (deduped); an empty array means the page needs no CSS at all.
413
496
  // Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle - never silently incomplete.
414
- const cwd = process.cwd();
415
497
  const cssByEntry = new Map();
416
498
  for (const out of Object.values(clientMeta?.outputs ?? {})) {
417
499
  if (out.entryPoint !== undefined && out.cssBundle !== undefined) {
@@ -431,22 +513,32 @@ export async function buildClient(options) {
431
513
  // SSR pages unstyled. Keep routeStyles absent until at least one authored route entry owns CSS.
432
514
  const routeCssEntries = [...cssByEntry.keys()].filter((entry) => entry !== resolvePath(entryFile));
433
515
  if (css.length > 0 && routeCssEntries.length > 0) {
516
+ // A `_loading` page can show on a navigation from ANY page, and a document links only its own
517
+ // route's stylesheets - so every page that can start a navigation carries the loading pages' too.
518
+ const loadingFiles = Object.values(routeManifest.loadings ?? {}).map((page) => page.file);
434
519
  for (const route of routeManifest.routes) {
435
520
  routeStyles[route.id] = stylesFor([
436
521
  ...route.layoutIds.map((id) => routeManifest.layouts[id]?.file ?? ""),
437
522
  route.file,
523
+ ...loadingFiles,
524
+ ]);
525
+ }
526
+ if (routeManifest.notFound) {
527
+ routeStyles._404 = stylesFor([routeManifest.notFound.file, ...loadingFiles]);
528
+ }
529
+ for (const [id, page] of Object.entries(routeManifest.notFounds ?? {})) {
530
+ routeStyles[id] = stylesFor([
531
+ ...page.layoutIds.map((layoutId) => routeManifest.layouts[layoutId]?.file ?? ""),
532
+ page.file,
438
533
  ]);
439
534
  }
440
- if (routeManifest.notFound)
441
- routeStyles._404 = stylesFor([routeManifest.notFound.file]);
442
535
  }
443
536
  // Copy `public/` into the output next to the hashed assets. A missing directory is normal (most
444
537
  // apps have none) and must not fail the build.
445
- const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public");
446
538
  const publicFiles = publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : [];
447
539
  const manifest = {
448
540
  entry: toUrl(bootstrap.path),
449
- assets: result.outputs.map((o) => toUrl(o.path)),
541
+ assets: outputs.map((o) => toUrl(o.path)),
450
542
  routes,
451
543
  ...(publicFiles.length > 0 ? { publicFiles } : {}),
452
544
  ...(css.length > 0 ? { css } : {}),
@@ -543,12 +635,55 @@ const SVELTE_DEDUPE_PATTERN = dedupePolicyFor("svelte").bunPattern ?? /^svelte($
543
635
  * fixed subpath list), Svelte has many internal subpaths, so each matched import is resolved dynamically.
544
636
  * `svelte/compiler` (build-time only, not in the bundle) doesn't match the filter and is left alone. No-op
545
637
  * when Svelte isn't used / isn't resolvable from `from`.
638
+ *
639
+ * Svelte is NOT condition-agnostic, unlike React and Preact: its bare entry maps `browser` to the client
640
+ * runtime and `default` to the server one. `Bun.resolveSync` answers for the process running the build -
641
+ * a server runtime - so pinning its answer put the SERVER entry into a browser bundle, where `hydrate`
642
+ * throws and the page never becomes interactive. For a browser bundle the package is therefore pinned by
643
+ * DIRECTORY and its export map is read with the bundle's own conditions.
546
644
  */
547
645
  export const svelteDedupePlugin = (from) => ({
548
646
  name: "nifra-svelte-dedupe",
549
647
  setup(build) {
648
+ // Absent when the plugin runs as a runtime plugin rather than inside `Bun.build`; a build with no
649
+ // `target` is a browser build (Bun's default).
650
+ const config = build.config;
651
+ const browser = config !== undefined && (config.target ?? "browser") === "browser";
652
+ const conditions = new Set([
653
+ "browser",
654
+ "import",
655
+ ...(typeof config?.conditions === "string"
656
+ ? [config.conditions]
657
+ : (config?.conditions ?? [])),
658
+ ]);
659
+ // The pinned copy's export map, read once. `null` when Svelte is not resolvable from the app root.
660
+ let pinned;
661
+ const pin = () => {
662
+ if (pinned !== undefined)
663
+ return pinned;
664
+ try {
665
+ const manifest = Bun.resolveSync("svelte/package.json", from);
666
+ const map = JSON.parse(readFileSync(manifest, "utf8")).exports;
667
+ pinned =
668
+ typeof map === "object" && map !== null
669
+ ? { root: dirname(manifest), map: map }
670
+ : null;
671
+ }
672
+ catch {
673
+ pinned = null;
674
+ }
675
+ return pinned;
676
+ };
550
677
  build.onResolve({ filter: SVELTE_DEDUPE_PATTERN }, (args) => {
551
678
  try {
679
+ if (browser) {
680
+ const copy = pin();
681
+ const target = copy === null
682
+ ? undefined
683
+ : exportTarget(copy.map[`.${args.path.slice("svelte".length)}`], conditions);
684
+ if (copy !== null && target !== undefined)
685
+ return { path: join(copy.root, target) };
686
+ }
552
687
  return { path: Bun.resolveSync(args.path, from) };
553
688
  }
554
689
  catch {
@@ -557,6 +692,22 @@ export const svelteDedupePlugin = (from) => ({
557
692
  });
558
693
  },
559
694
  });
695
+ /** Walk one `exports` entry the way a resolver does: the first key, in the package's own order, that is
696
+ * an active condition (or `default`) wins. */
697
+ function exportTarget(entry, conditions) {
698
+ if (typeof entry === "string")
699
+ return entry;
700
+ if (typeof entry !== "object" || entry === null)
701
+ return undefined;
702
+ for (const [key, value] of Object.entries(entry)) {
703
+ if (key !== "default" && !conditions.has(key))
704
+ continue;
705
+ const target = exportTarget(value, conditions);
706
+ if (target !== undefined)
707
+ return target;
708
+ }
709
+ return undefined;
710
+ }
560
711
  /**
561
712
  * The app's declared single-copy rule, applied to the bundle.
562
713
  *
@@ -571,32 +722,50 @@ export const svelteDedupePlugin = (from) => ({
571
722
  */
572
723
  const declaredSingleCopyPlugins = (root) => readSingleCopyDeclaration(root) === undefined ? [] : [declaredSingleCopyPlugin({ cwd: root })];
573
724
  /**
574
- * Remix-style `.server` convention for the CLIENT build. A module named `*.server.ts(x)` (`db.server.ts`,
575
- * `auth.server.ts`, …) is server-only - empty it in the browser bundle so its (possibly `node:` / native /
576
- * Capacitor) import subtree never reaches the client. The body is CJS-with-a-Proxy so any named OR default
577
- * import resolves to `undefined` rather than a "missing export" bundle error (verified), and the real
578
- * import subtree is gone. The complement to the node-builtin guard: when a server-only import is co-located
579
- * in a route file (so it can't be tree-shaken out and the guard fails loud), moving it into a `*.server`
580
- * module is the fix. CLIENT-only - buildServer keeps the real module, which runs server-side.
725
+ * Give each output chunk the specifiers its emitted code imports. Bun's metafile does not record them
726
+ * (its output `imports` stay empty even for an external the code keeps), so they are read from the code.
727
+ * A specifier under `publicPath` or relative to the chunk is mapped back to the output it names.
581
728
  */
582
- export const serverOnlyEmptyPlugin = () => ({
583
- name: "nifra-server-only-empty",
584
- setup(build) {
585
- build.onLoad({ filter: SERVER_ONLY_MODULE }, () => ({
586
- contents: SERVER_ONLY_REPLACEMENT,
587
- loader: "js",
729
+ function withEmittedImports(graph, emitted, publicPath) {
730
+ const scanner = new Bun.Transpiler({ loader: "js" });
731
+ const imports = new Map();
732
+ for (const file of emitted) {
733
+ if (file.kind !== "code" || file.text === undefined) {
734
+ imports.set(posix.normalize(file.name), []);
735
+ continue;
736
+ }
737
+ imports.set(posix.normalize(file.name), scanner.scanImports(file.text).map(({ path }) => {
738
+ if (publicPath !== undefined && path.startsWith(publicPath))
739
+ return path.slice(publicPath.length);
740
+ if (path.startsWith("./") || path.startsWith("../"))
741
+ return posix.join(posix.dirname(posix.normalize(file.name)), path);
742
+ return path;
588
743
  }));
589
- },
590
- });
744
+ }
745
+ const chunks = {};
746
+ // A chunk with no emitted file keeps no `imports`, which the verifiers treat as missing evidence.
747
+ for (const [path, chunk] of Object.entries(graph.chunks)) {
748
+ const kept = imports.get(posix.normalize(path));
749
+ chunks[path] = kept === undefined ? chunk : { ...chunk, imports: kept };
750
+ }
751
+ return { ...graph, chunks };
752
+ }
753
+ /** Refusals the bundler stopped on before it produced a graph, so without import chains. */
754
+ function formatRefusedFiles(refused, root) {
755
+ const lines = [...refused].map(([file, reason]) => {
756
+ const rel = relative(root, file).replaceAll("\\", "/");
757
+ return ` - ${rel.startsWith("..") ? file : rel}: ${reason}`;
758
+ });
759
+ return `[nifra/web] the browser build reached code that may not ship to a browser:\n${lines.join("\n")}`;
760
+ }
761
+ export { zoneGuardPlugin } from "./plugins/zone-guard.js";
591
762
  /**
592
763
  * Server functions in the CLIENT build: replace each `*.fn.ts` module with stubs that call the routes
593
764
  * the server mounted, so the function bodies - and everything they import - never reach a browser.
594
765
  *
595
- * The sibling of {@link serverOnlyEmptyPlugin}, and a deliberate contrast: a `*.server` module is
596
- * EMPTIED because nothing may call it from the client, while a `*.fn` module is REPLACED because the
597
- * client is supposed to call it, just over HTTP. The generation itself is in
598
- * `internal/server-boundary.ts` so the Vite pipeline emits identical stubs from the same code; two
599
- * hand-written copies would be a client that works in dev and 404s in production.
766
+ * The generation itself is in `internal/server-boundary.ts` so the Vite pipeline emits identical stubs
767
+ * from the same code; two hand-written copies would be a client that works in dev and 404s in
768
+ * production.
600
769
  *
601
770
  * CLIENT-only. The server build keeps the real module, which is what `serverFunctions()` mounts.
602
771
  */
@@ -665,6 +834,7 @@ export async function buildServer(options) {
665
834
  entrypoints: [serverEntry],
666
835
  outdir: outDir,
667
836
  target,
837
+ metafile: true,
668
838
  conditions: [...conditions],
669
839
  define: {
670
840
  ...(options.define ?? { "process.env.NODE_ENV": '"production"' }),
@@ -692,6 +862,38 @@ export async function buildServer(options) {
692
862
  if (!result.success) {
693
863
  throw new Error(`[nifra/web] server build failed:\n${result.logs.map((l) => String(l)).join("\n")}`);
694
864
  }
865
+ // Named as the metafile names outputs: relative to `outdir`.
866
+ const nameOf = (path) => relative(outDir, path).replaceAll("\\", "/");
867
+ const emitted = await Promise.all(result.outputs.map(async (out) => out.kind === "sourcemap" || out.kind === "asset"
868
+ ? { name: nameOf(out.path), kind: out.kind === "sourcemap" ? "map" : "asset" }
869
+ : { name: nameOf(out.path), kind: "code", text: await out.text() }));
870
+ const serverGraph = withEmittedImports(fromBunMetafile(result.metafile), emitted);
871
+ const appRoot = resolvePath(options.root ?? dirname(routesDir));
872
+ const classifier = createZoneClassifier({
873
+ appRoot,
874
+ routesDir,
875
+ generatedFiles: [
876
+ serverEntry,
877
+ join(entryDir, manifestFile),
878
+ ...(options.generatedFiles ?? []),
879
+ ].map((file) => resolvePath(file)),
880
+ });
881
+ const sourceOf = bunModuleSource(process.cwd());
882
+ const labelOf = (id) => {
883
+ const source = sourceOf(id);
884
+ return source.kind === "file" ? relative(appRoot, source.file).replaceAll("\\", "/") : id;
885
+ };
886
+ const refusal = formatServerGraphVerdict(verifyServerGraph(serverGraph, {
887
+ classifier,
888
+ sourceOf,
889
+ privateEnv: privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_"),
890
+ })) ?? formatUnsupportedBuiltins(unsupportedBuiltins(serverGraph, target, labelOf), target);
891
+ if (refusal !== undefined) {
892
+ // Bun wrote the bundle already; a refused build leaves nothing a deploy step could pick up.
893
+ for (const output of result.outputs)
894
+ rmSync(output.path, { force: true });
895
+ throw new Error(refusal);
896
+ }
695
897
  const entryOutput = result.outputs.find((o) => o.kind === "entry-point");
696
898
  if (entryOutput === undefined) {
697
899
  throw new Error("[nifra/web] server build produced no entry-point output");
@@ -706,12 +908,24 @@ export async function buildServer(options) {
706
908
  // The ONLY thing apps used to hand-write per target was the server entry (`_worker.ts`, `server-bun.ts`,
707
909
  // …) - so we GENERATE it here (per target) instead of asking each app to ship five near-identical files.
708
910
  // ===================================================================================================
911
+ /** The `createWebApp` options a generated server entry can import from the app's framework module. */
912
+ export const SERVER_ENTRY_OPTIONS = ["apiPrefix", "apiStrip", "mounts", "csp", "nonce"];
913
+ /**
914
+ * The header each edge platform sets to the caller's address, replacing any value the client sent.
915
+ * That replacement is the whole trust: the same bundle served without the platform's edge in front
916
+ * (`wrangler pages dev`, a self-hosted workerd) hands the header straight from the client, which is
917
+ * why it is only believed on an explicit `clientIp: "platform"`.
918
+ */
919
+ const PLATFORM_CLIENT_IP_HEADERS = {
920
+ cloudflare: "cf-connecting-ip",
921
+ vercel: "x-real-ip",
922
+ };
709
923
  /**
710
924
  * Codegen the per-target **server entry** module (source text) for `buildServer` to bundle. It imports
711
925
  * the app's `adapter` (from `framework.ts`), the optional `backend` (from `backend.ts`), and the
712
926
  * generated `{ manifest, clientEntry }` (from `./server-manifest`), builds `createWebApp`, then wires
713
927
  * the right host:
714
- * - `cf-pages` / `vercel`: `export default` the fetch handler (the platform serves /assets/* itself).
928
+ * - `cloudflare` / `vercel`: `export default` the fetch handler (the platform serves /assets/* itself).
715
929
  * - `deno`: same fetch-handler default, plus `Deno.serve` self-host when run directly.
716
930
  * - `bun` / `node`: a self-hosting server that ALSO serves the client bundle from disk (those
717
931
  * runtimes have a filesystem; the static `/assets/*` sit next to the entry).
@@ -720,7 +934,23 @@ export async function buildServer(options) {
720
934
  * string out) so the generation is unit-testable without a real build.
721
935
  */
722
936
  export function generateServerEntry(options) {
723
- const { target, adapterImport, backendImport, useImport, title = "nifra", publicFiles = [], } = options;
937
+ const { target, adapterImport, backendImport, useImport, optionImports = {}, title = "nifra", publicFiles = [], clientIp, } = options;
938
+ if (clientIp !== undefined && clientIp !== "platform") {
939
+ throw new Error(`[nifra/web] generateServerEntry: clientIp must be "platform" or left out, got ${JSON.stringify(clientIp)}`);
940
+ }
941
+ const clientIpHeader = clientIp === "platform" ? PLATFORM_CLIENT_IP_HEADERS[target] : undefined;
942
+ // Identifiers come only from the fixed list, never from the caller's keys, so generated code names
943
+ // nothing the list does not.
944
+ const importedOptions = SERVER_ENTRY_OPTIONS.filter((name) => optionImports[name] !== undefined);
945
+ const optionModules = new Map();
946
+ for (const name of importedOptions) {
947
+ const from = optionImports[name];
948
+ const names = optionModules.get(from);
949
+ if (names === undefined)
950
+ optionModules.set(from, [name]);
951
+ else
952
+ names.push(name);
953
+ }
724
954
  if (target === "static") {
725
955
  throw new Error("[nifra/web] generateServerEntry: `static` has no server entry (SSG only)");
726
956
  }
@@ -730,18 +960,24 @@ export function generateServerEntry(options) {
730
960
  lines.push(`import { adapter } from ${JSON.stringify(adapterImport)}`);
731
961
  if (useImport !== undefined)
732
962
  lines.push(`import { use } from ${JSON.stringify(useImport)}`);
963
+ for (const [from, names] of optionModules) {
964
+ lines.push(`import { ${names.join(", ")} } from ${JSON.stringify(from)}`);
965
+ }
733
966
  if (backendImport !== undefined) {
734
967
  lines.push(`import { backend } from ${JSON.stringify(backendImport)}`);
735
968
  }
736
969
  lines.push('import { clientEntry, cssLoading, manifest, styles, routeStyles } from "./server-manifest"');
737
- // cf-pages/vercel/deno need the fetch-handler shape; bun/node call app.fetch directly.
738
- const usesToFetch = target === "cf-pages" || target === "vercel" || target === "deno";
739
- if (usesToFetch)
970
+ // Cloudflare needs the fetch-handler shape; the others call app.fetch directly.
971
+ if (target === "cloudflare")
740
972
  lines.push('import { toFetchHandler } from "@nifrajs/core/server"');
741
973
  if (target === "node")
742
974
  lines.push('import { serve } from "@nifrajs/node"');
743
- lines.push("", "const app = createWebApp({", " adapter,", ...(useImport !== undefined ? [" use,"] : []), " manifest,", " clientEntry,", " styles,", " routeStyles,", " cssLoading,", ...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []), ` title: ${JSON.stringify(title)},`, "})", "");
744
- if (target === "cf-pages") {
975
+ lines.push("", "const app = createWebApp({", " adapter,", ...(useImport !== undefined ? [" use,"] : []), ...importedOptions.map((name) => ` ${name},`), " manifest,", " clientEntry,", " styles,", " routeStyles,", " cssLoading,", ...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []), ` title: ${JSON.stringify(title)},`,
976
+ // Core's own `{ header }` trust, so the mounted backend and every loader see the derived caller.
977
+ ...(clientIpHeader !== undefined
978
+ ? [` server: { clientIp: { header: ${JSON.stringify(clientIpHeader)} } },`]
979
+ : []), "})", "");
980
+ if (target === "cloudflare") {
745
981
  // Cloudflare Pages advanced mode: `_routes.json` keeps static paths off the worker entirely, and
746
982
  // everything else falls through to this handler (SSR).
747
983
  //
@@ -757,20 +993,26 @@ export function generateServerEntry(options) {
757
993
  return `${lines.join("\n")}\n`;
758
994
  }
759
995
  // bun / node / deno self-host AND serve the client bundle from disk (it sits next to this entry).
760
- lines.push("// The client bundle and public files live next to this entry; serve only manifest-approved paths.", 'const STATIC_ROOT = new URL("./", import.meta.url)', `const PUBLIC_FILES = new Set(${JSON.stringify(publicFiles)})`, "const staticPath = (pathname: string): string | undefined => {", " if (PUBLIC_FILES.has(pathname)) return '.' + pathname", ' if (!pathname.startsWith("/assets/")) return undefined', ' const segments = pathname.slice(1).split("/")', " // `.` and `..` match the character class, and the path is joined onto a base URL that resolves", " // dot segments - so they are rejected explicitly rather than relying on the request URL having", " // been normalized upstream.", " const safe = (segment: string): boolean =>", ' segment !== "." && segment !== ".." && /^[A-Za-z0-9._-]+$/.test(segment)', " return segments.every(safe) ? '.' + pathname : undefined", "}", 'const TYPES = { js: "text/javascript", css: "text/css", map: "application/json" }');
996
+ lines.push("// The client bundle and public files live next to this entry; serve only manifest-approved paths.", 'const STATIC_ROOT = new URL("./", import.meta.url)', `const PUBLIC_FILES = new Set(${JSON.stringify(publicFiles)})`, "const staticPath = (pathname: string): string | undefined => {", " if (PUBLIC_FILES.has(pathname)) return '.' + pathname", ' if (!pathname.startsWith("/assets/")) return undefined', ' const segments = pathname.slice(1).split("/")', " // `.` and `..` match the character class, and the path is joined onto a base URL that resolves", " // dot segments - so they are rejected explicitly rather than relying on the request URL having", " // been normalized upstream.", " const safe = (segment: string): boolean =>", ' segment !== "." && segment !== ".." && /^[A-Za-z0-9._-]+$/.test(segment)', " return segments.every(safe) ? '.' + pathname : undefined", "}",
997
+ // The type table and cache policy `servePublicDir` uses. Without a known type, Bun answers a file
998
+ // as an `application/octet-stream` attachment, which a browser downloads instead of showing.
999
+ `const TYPES: Record<string, string> = ${JSON.stringify(CONTENT_TYPES)}`, "const staticHeaders = (pathname: string): Record<string, string> => ({", ' "content-type": TYPES[pathname.slice(pathname.lastIndexOf(".")).toLowerCase()] ?? "application/octet-stream",', ` "cache-control": pathname.startsWith("/assets/") ? ${JSON.stringify(IMMUTABLE)} : ${JSON.stringify(ONE_DAY)},`, ' "x-content-type-options": "nosniff",', "})");
1000
+ // Bun.serve and Deno.serve delimit the body themselves, so its declared length is the transport
1001
+ // frame - the guarantee core's own listen() and the Deno adapter mark (see markTrustedBodyFraming).
1002
+ const markFramed = '(req as unknown as Record<symbol, unknown>)[Symbol.for("nifra.body.trustedFraming")] = true';
761
1003
  if (target === "bun") {
762
- lines.push("const server = Bun.serve({", " port: Number(Bun.env.PORT ?? 3000),", " async fetch(req) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " const file = Bun.file(new URL(filePath, STATIC_ROOT))", ' if (!(await file.exists())) return new Response("not found", { status: 404 })', ' const ext = pathname.slice(pathname.lastIndexOf(".") + 1)', ' return new Response(file, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })', " }", " return app.fetch(req)", " },", "})",
1004
+ lines.push("const server = Bun.serve({", " port: Number(Bun.env.PORT ?? 3000),", " async fetch(req, server) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " const file = Bun.file(new URL(filePath, STATIC_ROOT))", ' if (!(await file.exists())) return new Response("not found", { status: 404 })', " return new Response(file, { headers: staticHeaders(pathname) })", " }", ` ${markFramed}`, " // The socket peer, looked up only when something reads it (as core's own listen() does).", " return app.fetch(req, { get clientIp() { return server.requestIP(req)?.address } })", " },", "})",
763
1005
  // The `${...}` here is literal OUTPUT (a template in the GENERATED file), not a template in this
764
1006
  // source - split so biome's noTemplateCurlyInString doesn't flag it; the emitted line is unchanged.
765
1007
  `console.log(\`nifra (Bun) → http://localhost:$${"{server.port}"}\`)`);
766
1008
  return `${lines.join("\n")}\n`;
767
1009
  }
768
1010
  if (target === "node") {
769
- lines.push('import { readFile } from "node:fs/promises"', "await serve(", " {", " async fetch(req) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " try {", " const body = await readFile(new URL(filePath, STATIC_ROOT))", ' const ext = pathname.slice(pathname.lastIndexOf(".") + 1)', ' return new Response(body, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })', " } catch {", ' return new Response("not found", { status: 404 })', " }", " }", " return app.fetch(req)", " },", " },", " { port: Number(process.env.PORT ?? 3000) },", ")");
1011
+ lines.push('import { readFile } from "node:fs/promises"', "await serve(", " {", " async fetch(req, platform) {", " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " try {", " const body = await readFile(new URL(filePath, STATIC_ROOT))", " return new Response(body, { headers: staticHeaders(pathname) })", " } catch {", ' return new Response("not found", { status: 404 })', " }", " }", " return app.fetch(req, platform)", " },", " },", " { port: Number(process.env.PORT ?? 3000) },", ")");
770
1012
  return `${lines.join("\n")}\n`;
771
1013
  }
772
1014
  // deno
773
- lines.push("const handler = toFetchHandler(app)", "// @ts-ignore - Deno global is present on the Deno runtime this output targets.", 'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req) => {', " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " try {", " // @ts-ignore - Deno.readFile is present on the Deno runtime.", " const body = await Deno.readFile(new URL(filePath, STATIC_ROOT))", ' const ext = pathname.slice(pathname.lastIndexOf(".") + 1)', ' return new Response(body, { headers: { "content-type": TYPES[ext] ?? "application/octet-stream" } })', " } catch {", ' return new Response("not found", { status: 404 })', " }", " }", " return handler.fetch(req)", "})");
1015
+ lines.push("// @ts-ignore - Deno global is present on the Deno runtime this output targets.", 'Deno.serve({ port: Number(Deno.env.get("PORT") ?? "3000") }, async (req, info) => {', " const { pathname } = new URL(req.url)", " const filePath = staticPath(pathname)", " if (filePath !== undefined) {", " try {", " // @ts-ignore - Deno.readFile is present on the Deno runtime.", " const body = await Deno.readFile(new URL(filePath, STATIC_ROOT))", " return new Response(body, { headers: staticHeaders(pathname) })", " } catch {", ' return new Response("not found", { status: 404 })', " }", " }", ` ${markFramed}`, " return app.fetch(req, { clientIp: info.remoteAddr.hostname })", "})");
774
1016
  return `${lines.join("\n")}\n`;
775
1017
  }
776
1018
  /** Measure each emitted output file's raw + gzip size. Reads the file off disk and gzips it with
@@ -793,15 +1035,13 @@ async function measureOutputs(paths) {
793
1035
  * `<outDir>/assets/*`, then per target:
794
1036
  * - `static`: prerenders opted-in routes (`prerenderRoutes`) to `<outDir>/<path>/index.html` (+
795
1037
  * `_data.json`); needs `prerenderApp`. No server.
796
- * - `cf-pages`: a `_worker.js` (edge bundle) + a `_routes.json` excluding /assets/* from the worker.
797
- * - `vercel`: a `.vercel/output`-shaped function isn't emitted here - `vercel` emits the bundled edge
798
- * entry as `<outDir>/index.js` (the CLI's docs point at `vercel`'s Build Output wrapper). [see note]
1038
+ * - `cloudflare`: a `_worker.js` (edge bundle) + a `_routes.json` excluding /assets/* from the worker.
1039
+ * - `vercel`: Vercel's Build Output API v3 - `config.json`, the client bundle and public files under
1040
+ * `static/`, and the edge function at `functions/index.func/index.js`; `vercel deploy --prebuilt`
1041
+ * uploads it as it is when `outDir` is `.vercel/output`.
799
1042
  * - `deno`/`node`/`bun`: the self-hosting server bundle (`server.js`) next to the assets.
800
1043
  * The server entry is GENERATED (`generateServerEntry`) and bundled (`buildServer`); the app supplies
801
1044
  * only adapter/backend/routes. Returns the manifest + a size report. Throws on any build failure.
802
- *
803
- * Note: the heavier platform wrappers (`.vercel/output` v3 layout, wrangler ISR `find_additional_modules`)
804
- * remain app-owned scripts; this command targets the common single-bundle deploys. See the CLI docs.
805
1045
  */
806
1046
  /** The default (Bun) strategy - `buildClient`/`buildServer` from this module. */
807
1047
  export const bunBundler = {
@@ -820,6 +1060,7 @@ export const bunBundler = {
820
1060
  ...(input.cssCodeSplit !== undefined ? { cssCodeSplit } : {}),
821
1061
  ...(input.publicDir !== undefined ? { publicDir: input.publicDir } : {}),
822
1062
  ...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
1063
+ ...(input.secretExemptions !== undefined ? { secretExemptions: input.secretExemptions } : {}),
823
1064
  });
824
1065
  },
825
1066
  buildServer: (input) => buildServer({
@@ -833,6 +1074,9 @@ export const bunBundler = {
833
1074
  ...(input.plugins ? { plugins: input.plugins } : {}),
834
1075
  ...(input.define ? { define: input.define } : {}),
835
1076
  ...(input.cssLoading !== undefined ? { cssLoading: input.cssLoading } : {}),
1077
+ ...(input.root !== undefined ? { root: input.root } : {}),
1078
+ ...(input.generatedFiles !== undefined ? { generatedFiles: input.generatedFiles } : {}),
1079
+ ...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
836
1080
  }),
837
1081
  };
838
1082
  /** Build a full deploy dir for `target` using the default Bun bundler. See {@link buildTargetWith}. */
@@ -849,14 +1093,15 @@ export async function buildTargetWith(target, options, bundler) {
849
1093
  const { routesDir, outDir, workDir } = options;
850
1094
  const requestedCssCodeSplit = options.cssCodeSplit === undefined ? undefined : normalizeCssCodeSplit(options.cssCodeSplit);
851
1095
  const requestedCssLoading = options.cssLoading === undefined ? undefined : normalizeCssLoading(options.cssLoading);
852
- const targetPlan = planBuildTarget(target, outDir);
1096
+ const targetPlan = planBuildTarget(parseBuildTarget(target), outDir);
853
1097
  const { rmSync } = await import("node:fs");
854
1098
  rmSync(outDir, { recursive: true, force: true });
855
1099
  rmSync(workDir, { recursive: true, force: true });
856
- const assetsDir = `${outDir}/assets`;
1100
+ const staticRoot = targetPlan.staticDir === "" ? outDir : `${outDir}/${targetPlan.staticDir}`;
1101
+ const assetsDir = `${staticRoot}/assets`;
857
1102
  mkdirSync(assetsDir, { recursive: true });
858
1103
  mkdirSync(workDir, { recursive: true });
859
- // (1) Client bundle → <outDir>/assets/* (every target ships the same hashed client bundle).
1104
+ // (1) Client bundle → <static root>/assets/* (every target ships the same hashed client bundle).
860
1105
  let client = await bundler.buildClient({
861
1106
  routesDir,
862
1107
  outDir: assetsDir,
@@ -866,6 +1111,9 @@ export async function buildTargetWith(target, options, bundler) {
866
1111
  define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
867
1112
  publicDir: false,
868
1113
  ...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
1114
+ ...(options.secretExemptions !== undefined
1115
+ ? { secretExemptions: options.secretExemptions }
1116
+ : {}),
869
1117
  ...(requestedCssCodeSplit !== undefined ? { cssCodeSplit: requestedCssCodeSplit } : {}),
870
1118
  ...(requestedCssLoading !== undefined ? { cssLoading: requestedCssLoading } : {}),
871
1119
  root: resolvePath(dirname(routesDir)),
@@ -879,7 +1127,20 @@ export async function buildTargetWith(target, options, bundler) {
879
1127
  const publicDir = options.publicDir === false
880
1128
  ? undefined
881
1129
  : resolvePath(options.publicDir ?? join(dirname(routesDir), "public"));
882
- const publicFiles = publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : [];
1130
+ const secretScan = {
1131
+ env: buildEnvironment(),
1132
+ publicEnvPrefix: options.publicEnvPrefix ?? "PUBLIC_",
1133
+ ...(options.secretExemptions !== undefined ? { exemptions: options.secretExemptions } : {}),
1134
+ };
1135
+ if (publicDir !== undefined && existsSync(publicDir)) {
1136
+ assertNoSecrets({
1137
+ ...secretScan,
1138
+ artifacts: publicScanFiles(publicDir, originName(dirname(routesDir), publicDir)),
1139
+ });
1140
+ }
1141
+ const publicFiles = publicDir !== undefined && existsSync(publicDir)
1142
+ ? await copyPublicDir(publicDir, staticRoot)
1143
+ : [];
883
1144
  if (publicFiles.length > 0) {
884
1145
  client = { ...client, publicFiles };
885
1146
  }
@@ -902,6 +1163,7 @@ export async function buildTargetWith(target, options, bundler) {
902
1163
  app,
903
1164
  routes: manifest.routes,
904
1165
  outDir,
1166
+ secrets: secretScan,
905
1167
  });
906
1168
  if (result.prerendered.length === 0) {
907
1169
  // A static build that renders nothing is almost always a misconfig (no `prerender = true` / no
@@ -927,9 +1189,11 @@ export async function buildTargetWith(target, options, bundler) {
927
1189
  target,
928
1190
  adapterImport: options.adapterImport,
929
1191
  ...(options.useImport !== undefined ? { useImport: options.useImport } : {}),
1192
+ ...(options.optionImports !== undefined ? { optionImports: options.optionImports } : {}),
930
1193
  ...(options.backendImport !== undefined ? { backendImport: options.backendImport } : {}),
931
1194
  ...(options.title !== undefined ? { title: options.title } : {}),
932
1195
  ...(publicFiles.length > 0 ? { publicFiles } : {}),
1196
+ ...(options.clientIp !== undefined ? { clientIp: options.clientIp } : {}),
933
1197
  }));
934
1198
  const { worker } = await bundler.buildServer({
935
1199
  routesDir,
@@ -947,10 +1211,12 @@ export async function buildTargetWith(target, options, bundler) {
947
1211
  ? { cssLoading: requestedCssLoading }
948
1212
  : {}),
949
1213
  root: resolvePath(dirname(routesDir)),
1214
+ generatedFiles: [resolvePath(workDir, options.adapterImport)],
1215
+ ...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
950
1216
  });
951
1217
  // (3) Assemble the deploy dir for the target.
952
1218
  const { cpSync } = await import("node:fs");
953
- if (targetPlan.target === "cf-pages") {
1219
+ if (targetPlan.target === "cloudflare") {
954
1220
  cpSync(worker, `${outDir}/${targetPlan.outputFile}`);
955
1221
  // The app's real patterns, so a directory is only collapsed into a glob once the route table
956
1222
  // proves nothing can be served beneath it.
@@ -965,7 +1231,12 @@ export async function buildTargetWith(target, options, bundler) {
965
1231
  }
966
1232
  }
967
1233
  else if (targetPlan.target === "vercel") {
1234
+ const fn = dirname(`${outDir}/${targetPlan.outputFile}`);
1235
+ mkdirSync(fn, { recursive: true });
968
1236
  cpSync(worker, `${outDir}/${targetPlan.outputFile}`);
1237
+ writeFileSync(`${fn}/.vc-config.json`, `${JSON.stringify({ runtime: "edge", entrypoint: "index.js" }, null, 2)}\n`);
1238
+ // Files the build wrote are served first; every other path is the SSR function's.
1239
+ writeFileSync(`${outDir}/config.json`, `${JSON.stringify({ version: 3, routes: [{ handle: "filesystem" }, { src: "/(.*)", dest: "/index" }] }, null, 2)}\n`);
969
1240
  }
970
1241
  else {
971
1242
  cpSync(worker, `${outDir}/${targetPlan.outputFile}`);