@nifrajs/web 3.3.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (407) 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 +35 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +89 -35
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +18 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +97 -17
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +72 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +411 -98
  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 +21 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +226 -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/css-contract.d.ts +21 -0
  40. package/dist/css-contract.d.ts.map +1 -0
  41. package/dist/css-contract.js +28 -0
  42. package/dist/css-contract.js.map +1 -0
  43. package/dist/dev-client.d.ts +76 -0
  44. package/dist/dev-client.d.ts.map +1 -0
  45. package/dist/dev-client.js +280 -0
  46. package/dist/dev-client.js.map +1 -0
  47. package/dist/dev-diagnostics.d.ts +26 -2
  48. package/dist/dev-diagnostics.d.ts.map +1 -1
  49. package/dist/dev-diagnostics.js +38 -7
  50. package/dist/dev-diagnostics.js.map +1 -1
  51. package/dist/dev-error.d.ts +4 -13
  52. package/dist/dev-error.d.ts.map +1 -1
  53. package/dist/dev-error.js +46 -4
  54. package/dist/dev-error.js.map +1 -1
  55. package/dist/dev-feed.d.ts +298 -0
  56. package/dist/dev-feed.d.ts.map +1 -0
  57. package/dist/dev-feed.js +797 -0
  58. package/dist/dev-feed.js.map +1 -0
  59. package/dist/dev-indicator.d.ts +38 -0
  60. package/dist/dev-indicator.d.ts.map +1 -0
  61. package/dist/dev-indicator.js +93 -0
  62. package/dist/dev-indicator.js.map +1 -0
  63. package/dist/dev-session.d.ts +77 -0
  64. package/dist/dev-session.d.ts.map +1 -0
  65. package/dist/dev-session.js +580 -0
  66. package/dist/dev-session.js.map +1 -0
  67. package/dist/dev-sourcemap.d.ts +27 -0
  68. package/dist/dev-sourcemap.d.ts.map +1 -0
  69. package/dist/dev-sourcemap.js +235 -0
  70. package/dist/dev-sourcemap.js.map +1 -0
  71. package/dist/dev-ssr-graph.d.ts.map +1 -1
  72. package/dist/dev-ssr-graph.js +13 -5
  73. package/dist/dev-ssr-graph.js.map +1 -1
  74. package/dist/dev.d.ts +15 -2
  75. package/dist/dev.d.ts.map +1 -1
  76. package/dist/dev.js +69 -44
  77. package/dist/dev.js.map +1 -1
  78. package/dist/diagnostic-catalog.d.ts +35 -0
  79. package/dist/diagnostic-catalog.d.ts.map +1 -0
  80. package/dist/diagnostic-catalog.js +203 -0
  81. package/dist/diagnostic-catalog.js.map +1 -0
  82. package/dist/diagnostic-prompt.d.ts +49 -0
  83. package/dist/diagnostic-prompt.d.ts.map +1 -0
  84. package/dist/diagnostic-prompt.js +303 -0
  85. package/dist/diagnostic-prompt.js.map +1 -0
  86. package/dist/diagnostic.d.ts +17 -23
  87. package/dist/diagnostic.d.ts.map +1 -1
  88. package/dist/diagnostic.js +31 -39
  89. package/dist/diagnostic.js.map +1 -1
  90. package/dist/draft.d.ts.map +1 -1
  91. package/dist/draft.js +4 -1
  92. package/dist/draft.js.map +1 -1
  93. package/dist/fn.d.ts +5 -0
  94. package/dist/fn.d.ts.map +1 -1
  95. package/dist/fn.js +13 -3
  96. package/dist/fn.js.map +1 -1
  97. package/dist/fs.d.ts.map +1 -1
  98. package/dist/fs.js +4 -2
  99. package/dist/fs.js.map +1 -1
  100. package/dist/index.d.ts +15 -15
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +13 -12
  103. package/dist/index.js.map +1 -1
  104. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  105. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  106. package/dist/internal/cdn/cloudflare.js +94 -0
  107. package/dist/internal/cdn/cloudflare.js.map +1 -0
  108. package/dist/internal/cdn/core.d.ts +91 -0
  109. package/dist/internal/cdn/core.d.ts.map +1 -0
  110. package/dist/internal/cdn/core.js +220 -0
  111. package/dist/internal/cdn/core.js.map +1 -0
  112. package/dist/internal/cdn/fastly.d.ts +17 -0
  113. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  114. package/dist/internal/cdn/fastly.js +43 -0
  115. package/dist/internal/cdn/fastly.js.map +1 -0
  116. package/dist/internal/cdn/vercel.d.ts +31 -0
  117. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  118. package/dist/internal/cdn/vercel.js +56 -0
  119. package/dist/internal/cdn/vercel.js.map +1 -0
  120. package/dist/internal/code-unit-order.d.ts +4 -0
  121. package/dist/internal/code-unit-order.d.ts.map +1 -0
  122. package/dist/internal/code-unit-order.js +6 -0
  123. package/dist/internal/code-unit-order.js.map +1 -0
  124. package/dist/internal/codegen.d.ts +3 -0
  125. package/dist/internal/codegen.d.ts.map +1 -1
  126. package/dist/internal/codegen.js +151 -30
  127. package/dist/internal/codegen.js.map +1 -1
  128. package/dist/internal/dev-csp.d.ts +24 -0
  129. package/dist/internal/dev-csp.d.ts.map +1 -0
  130. package/dist/internal/dev-csp.js +160 -0
  131. package/dist/internal/dev-csp.js.map +1 -0
  132. package/dist/internal/dev-reserved.d.ts +25 -0
  133. package/dist/internal/dev-reserved.d.ts.map +1 -0
  134. package/dist/internal/dev-reserved.js +39 -0
  135. package/dist/internal/dev-reserved.js.map +1 -0
  136. package/dist/internal/html-spans.d.ts +10 -0
  137. package/dist/internal/html-spans.d.ts.map +1 -0
  138. package/dist/internal/html-spans.js +46 -0
  139. package/dist/internal/html-spans.js.map +1 -0
  140. package/dist/internal/loading-runtime.d.ts +44 -0
  141. package/dist/internal/loading-runtime.d.ts.map +1 -0
  142. package/dist/internal/loading-runtime.js +206 -0
  143. package/dist/internal/loading-runtime.js.map +1 -0
  144. package/dist/internal/matches-runtime.d.ts +8 -0
  145. package/dist/internal/matches-runtime.d.ts.map +1 -0
  146. package/dist/internal/matches-runtime.js +61 -0
  147. package/dist/internal/matches-runtime.js.map +1 -0
  148. package/dist/internal/mount-shadow.d.ts +36 -0
  149. package/dist/internal/mount-shadow.d.ts.map +1 -0
  150. package/dist/internal/mount-shadow.js +65 -0
  151. package/dist/internal/mount-shadow.js.map +1 -0
  152. package/dist/internal/node-builtins.d.ts +3 -0
  153. package/dist/internal/node-builtins.d.ts.map +1 -0
  154. package/dist/internal/node-builtins.js +79 -0
  155. package/dist/internal/node-builtins.js.map +1 -0
  156. package/dist/internal/output-guard.d.ts +65 -0
  157. package/dist/internal/output-guard.d.ts.map +1 -0
  158. package/dist/internal/output-guard.js +630 -0
  159. package/dist/internal/output-guard.js.map +1 -0
  160. package/dist/internal/page-execution.d.ts +69 -0
  161. package/dist/internal/page-execution.d.ts.map +1 -0
  162. package/dist/internal/page-execution.js +1021 -0
  163. package/dist/internal/page-execution.js.map +1 -0
  164. package/dist/internal/parity.d.ts +21 -0
  165. package/dist/internal/parity.d.ts.map +1 -1
  166. package/dist/internal/parity.js +104 -22
  167. package/dist/internal/parity.js.map +1 -1
  168. package/dist/internal/private-env.d.ts +12 -0
  169. package/dist/internal/private-env.d.ts.map +1 -0
  170. package/dist/internal/private-env.js +335 -0
  171. package/dist/internal/private-env.js.map +1 -0
  172. package/dist/internal/public-url.d.ts +3 -0
  173. package/dist/internal/public-url.d.ts.map +1 -0
  174. package/dist/internal/public-url.js +18 -0
  175. package/dist/internal/public-url.js.map +1 -0
  176. package/dist/internal/render-document.d.ts +37 -21
  177. package/dist/internal/render-document.d.ts.map +1 -1
  178. package/dist/internal/render-document.js +104 -65
  179. package/dist/internal/render-document.js.map +1 -1
  180. package/dist/internal/request-url.d.ts +3 -0
  181. package/dist/internal/request-url.d.ts.map +1 -0
  182. package/dist/internal/request-url.js +14 -0
  183. package/dist/internal/request-url.js.map +1 -0
  184. package/dist/internal/response-controls.d.ts +75 -0
  185. package/dist/internal/response-controls.d.ts.map +1 -0
  186. package/dist/internal/response-controls.js +256 -0
  187. package/dist/internal/response-controls.js.map +1 -0
  188. package/dist/internal/runtime-contract.d.ts +11 -16
  189. package/dist/internal/runtime-contract.d.ts.map +1 -1
  190. package/dist/internal/runtime-contract.js.map +1 -1
  191. package/dist/internal/secret-scan.d.ts +104 -0
  192. package/dist/internal/secret-scan.d.ts.map +1 -0
  193. package/dist/internal/secret-scan.js +485 -0
  194. package/dist/internal/secret-scan.js.map +1 -0
  195. package/dist/internal/server-boundary.d.ts +6 -29
  196. package/dist/internal/server-boundary.d.ts.map +1 -1
  197. package/dist/internal/server-boundary.js +6 -66
  198. package/dist/internal/server-boundary.js.map +1 -1
  199. package/dist/internal/source-map.d.ts +47 -0
  200. package/dist/internal/source-map.d.ts.map +1 -0
  201. package/dist/internal/source-map.js +225 -0
  202. package/dist/internal/source-map.js.map +1 -0
  203. package/dist/internal/target-compat.d.ts +27 -0
  204. package/dist/internal/target-compat.d.ts.map +1 -0
  205. package/dist/internal/target-compat.js +77 -0
  206. package/dist/internal/target-compat.js.map +1 -0
  207. package/dist/internal/timing-safe-equal.d.ts +6 -0
  208. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  209. package/dist/internal/timing-safe-equal.js +10 -0
  210. package/dist/internal/timing-safe-equal.js.map +1 -1
  211. package/dist/internal/vitals-runtime.d.ts +18 -0
  212. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  213. package/dist/internal/vitals-runtime.js +38 -0
  214. package/dist/internal/vitals-runtime.js.map +1 -0
  215. package/dist/internal/web-app.d.ts +56 -8
  216. package/dist/internal/web-app.d.ts.map +1 -1
  217. package/dist/internal/web-app.js +215 -772
  218. package/dist/internal/web-app.js.map +1 -1
  219. package/dist/internal/zone-graph.d.ts +89 -0
  220. package/dist/internal/zone-graph.d.ts.map +1 -0
  221. package/dist/internal/zone-graph.js +492 -0
  222. package/dist/internal/zone-graph.js.map +1 -0
  223. package/dist/isr.d.ts +83 -6
  224. package/dist/isr.d.ts.map +1 -1
  225. package/dist/isr.js +301 -39
  226. package/dist/isr.js.map +1 -1
  227. package/dist/llms-txt.d.ts +3 -0
  228. package/dist/llms-txt.d.ts.map +1 -1
  229. package/dist/llms-txt.js +37 -9
  230. package/dist/llms-txt.js.map +1 -1
  231. package/dist/manifest.d.ts +221 -14
  232. package/dist/manifest.d.ts.map +1 -1
  233. package/dist/manifest.js +386 -9
  234. package/dist/manifest.js.map +1 -1
  235. package/dist/module-graph.d.ts +26 -5
  236. package/dist/module-graph.d.ts.map +1 -1
  237. package/dist/module-graph.js +17 -7
  238. package/dist/module-graph.js.map +1 -1
  239. package/dist/navigation.d.ts +17 -3
  240. package/dist/navigation.d.ts.map +1 -1
  241. package/dist/navigation.js +11 -5
  242. package/dist/navigation.js.map +1 -1
  243. package/dist/nonce.d.ts +47 -0
  244. package/dist/nonce.d.ts.map +1 -0
  245. package/dist/nonce.js +63 -0
  246. package/dist/nonce.js.map +1 -0
  247. package/dist/plugins/css-modules.d.ts.map +1 -1
  248. package/dist/plugins/css-modules.js +4 -3
  249. package/dist/plugins/css-modules.js.map +1 -1
  250. package/dist/plugins/kit.d.ts +9 -0
  251. package/dist/plugins/kit.d.ts.map +1 -1
  252. package/dist/plugins/kit.js +18 -0
  253. package/dist/plugins/kit.js.map +1 -1
  254. package/dist/plugins/svg.d.ts +22 -3
  255. package/dist/plugins/svg.d.ts.map +1 -1
  256. package/dist/plugins/svg.js +112 -1
  257. package/dist/plugins/svg.js.map +1 -1
  258. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  259. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  260. package/dist/plugins/vite-leak-guard.js +378 -17
  261. package/dist/plugins/vite-leak-guard.js.map +1 -1
  262. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  263. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  264. package/dist/plugins/vite-zone-guard.js +208 -0
  265. package/dist/plugins/vite-zone-guard.js.map +1 -0
  266. package/dist/plugins/zone-guard.d.ts +27 -0
  267. package/dist/plugins/zone-guard.d.ts.map +1 -0
  268. package/dist/plugins/zone-guard.js +72 -0
  269. package/dist/plugins/zone-guard.js.map +1 -0
  270. package/dist/prerender.d.ts +10 -0
  271. package/dist/prerender.d.ts.map +1 -1
  272. package/dist/prerender.js +35 -3
  273. package/dist/prerender.js.map +1 -1
  274. package/dist/public-dir.d.ts +9 -0
  275. package/dist/public-dir.d.ts.map +1 -1
  276. package/dist/public-dir.js +131 -48
  277. package/dist/public-dir.js.map +1 -1
  278. package/dist/pwa-manifest.d.ts +68 -0
  279. package/dist/pwa-manifest.d.ts.map +1 -0
  280. package/dist/pwa-manifest.js +178 -0
  281. package/dist/pwa-manifest.js.map +1 -0
  282. package/dist/query.js +4 -4
  283. package/dist/query.js.map +1 -1
  284. package/dist/render-seam.d.ts +42 -2
  285. package/dist/render-seam.d.ts.map +1 -1
  286. package/dist/render-seam.js +6 -0
  287. package/dist/render-seam.js.map +1 -1
  288. package/dist/route-manifest.d.ts +3 -0
  289. package/dist/route-manifest.d.ts.map +1 -1
  290. package/dist/route-manifest.js +26 -3
  291. package/dist/route-manifest.js.map +1 -1
  292. package/dist/route-types.d.ts +24 -0
  293. package/dist/route-types.d.ts.map +1 -0
  294. package/dist/route-types.js +195 -0
  295. package/dist/route-types.js.map +1 -0
  296. package/dist/router.d.ts +28 -6
  297. package/dist/router.d.ts.map +1 -1
  298. package/dist/router.js +129 -40
  299. package/dist/router.js.map +1 -1
  300. package/dist/trusted-html.d.ts +34 -0
  301. package/dist/trusted-html.d.ts.map +1 -0
  302. package/dist/trusted-html.js +35 -0
  303. package/dist/trusted-html.js.map +1 -0
  304. package/dist/vitals.d.ts +33 -0
  305. package/dist/vitals.d.ts.map +1 -0
  306. package/dist/vitals.js +16 -0
  307. package/dist/vitals.js.map +1 -0
  308. package/dist/vite.d.ts +16 -1
  309. package/dist/vite.d.ts.map +1 -1
  310. package/dist/vite.js +187 -36
  311. package/dist/vite.js.map +1 -1
  312. package/dist/zones.d.ts +70 -0
  313. package/dist/zones.d.ts.map +1 -0
  314. package/dist/zones.js +365 -0
  315. package/dist/zones.js.map +1 -0
  316. package/package.json +67 -14
  317. package/src/backend-only.ts +16 -0
  318. package/src/boundary.ts +18 -5
  319. package/src/build-plan.ts +113 -36
  320. package/src/build-vite.ts +124 -15
  321. package/src/build.ts +535 -102
  322. package/src/bun-dev-entry.ts +1 -1
  323. package/src/cdn.ts +216 -0
  324. package/src/client.ts +251 -18
  325. package/src/conformance.ts +16 -3
  326. package/src/csp.ts +194 -0
  327. package/src/css-contract.ts +44 -0
  328. package/src/dev-client.ts +349 -0
  329. package/src/dev-diagnostics.ts +60 -7
  330. package/src/dev-error.ts +49 -5
  331. package/src/dev-feed.ts +1140 -0
  332. package/src/dev-indicator.ts +113 -0
  333. package/src/dev-session.ts +729 -0
  334. package/src/dev-sourcemap.ts +281 -0
  335. package/src/dev-ssr-graph.ts +16 -5
  336. package/src/dev.ts +86 -43
  337. package/src/diagnostic-catalog.ts +243 -0
  338. package/src/diagnostic-prompt.ts +355 -0
  339. package/src/diagnostic.ts +63 -66
  340. package/src/draft.ts +4 -1
  341. package/src/fn.ts +19 -3
  342. package/src/fs.ts +13 -2
  343. package/src/index.ts +57 -10
  344. package/src/internal/cdn/cloudflare.ts +146 -0
  345. package/src/internal/cdn/core.ts +316 -0
  346. package/src/internal/cdn/fastly.ts +62 -0
  347. package/src/internal/cdn/vercel.ts +95 -0
  348. package/src/internal/code-unit-order.ts +5 -0
  349. package/src/internal/codegen.ts +180 -29
  350. package/src/internal/dev-csp.ts +176 -0
  351. package/src/internal/dev-reserved.ts +52 -0
  352. package/src/internal/html-spans.ts +50 -0
  353. package/src/internal/loading-runtime.ts +244 -0
  354. package/src/internal/matches-runtime.ts +62 -0
  355. package/src/internal/mount-shadow.ts +88 -0
  356. package/src/internal/node-builtins.ts +79 -0
  357. package/src/internal/output-guard.ts +701 -0
  358. package/src/internal/page-execution.ts +1371 -0
  359. package/src/internal/parity.ts +150 -36
  360. package/src/internal/private-env.ts +334 -0
  361. package/src/internal/public-url.ts +22 -0
  362. package/src/internal/render-document.ts +153 -73
  363. package/src/internal/request-url.ts +14 -0
  364. package/src/internal/response-controls.ts +284 -0
  365. package/src/internal/runtime-contract.ts +10 -15
  366. package/src/internal/secret-scan.ts +623 -0
  367. package/src/internal/server-boundary.ts +6 -69
  368. package/src/internal/source-map.ts +256 -0
  369. package/src/internal/target-compat.ts +92 -0
  370. package/src/internal/timing-safe-equal.ts +13 -0
  371. package/src/internal/vitals-runtime.ts +53 -0
  372. package/src/internal/web-app.ts +290 -879
  373. package/src/internal/zone-graph.ts +593 -0
  374. package/src/isr.ts +366 -39
  375. package/src/llms-txt.ts +44 -8
  376. package/src/manifest.ts +671 -17
  377. package/src/module-graph.ts +53 -9
  378. package/src/navigation.ts +29 -6
  379. package/src/nonce.ts +104 -0
  380. package/src/plugins/css-modules.ts +5 -3
  381. package/src/plugins/kit.ts +24 -0
  382. package/src/plugins/svg.ts +135 -4
  383. package/src/plugins/vite-leak-guard.ts +529 -16
  384. package/src/plugins/vite-zone-guard.ts +263 -0
  385. package/src/plugins/zone-guard.ts +110 -0
  386. package/src/prerender.ts +49 -3
  387. package/src/public-dir.ts +135 -49
  388. package/src/pwa-manifest.ts +226 -0
  389. package/src/query.ts +4 -4
  390. package/src/render-seam.ts +46 -1
  391. package/src/route-manifest.ts +15 -3
  392. package/src/route-types.ts +264 -0
  393. package/src/router.ts +163 -63
  394. package/src/trusted-html.ts +44 -0
  395. package/src/vitals.ts +44 -0
  396. package/src/vite.ts +207 -36
  397. package/src/zones.ts +430 -0
  398. package/dist/plugins/vite-server-only.d.ts +0 -49
  399. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  400. package/dist/plugins/vite-server-only.js +0 -57
  401. package/dist/plugins/vite-server-only.js.map +0 -1
  402. package/dist/server-only.d.ts +0 -21
  403. package/dist/server-only.d.ts.map +0 -1
  404. package/dist/server-only.js +0 -25
  405. package/dist/server-only.js.map +0 -1
  406. package/src/plugins/vite-server-only.ts +0 -75
  407. package/src/server-only.ts +0 -25
package/dist/build.js CHANGED
@@ -5,21 +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, 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
+ import { normalizeCssCodeSplit, normalizeCssLoading } from "./css-contract.js";
15
16
  import { discoverRoutes } from "./fs.js";
16
17
  import { generateClientEntry, generateServerManifest } from "./index.js";
17
18
  import { dedupePolicyFor } from "./internal/identity-policy.js";
18
19
  import { assertDevelopmentProductionParity, assertIdentityParity, collectDevelopmentParityInput, } from "./internal/parity.js";
19
- import { generateServerFnStub, SERVER_FN_MODULE, SERVER_ONLY_MODULE, SERVER_ONLY_REPLACEMENT, serverFnNamespace, } from "./internal/server-boundary.js";
20
- // `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";
21
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).
22
29
  import { prerenderRoutes } from "./prerender.js";
30
+ import { CONTENT_TYPES, IMMUTABLE, ONE_DAY } from "./public-dir.js";
31
+ import { createZoneClassifier } from "./zones.js";
23
32
  export * from "./build-plan.js";
24
33
  const basename = (path) => pathBasename(path);
25
34
  // The lazy shape declares `const loaders`, the eager shape `const modules`. Anchored at a line start,
@@ -54,18 +63,6 @@ export function publicEnvDefines(prefix, env) {
54
63
  }
55
64
  return defines;
56
65
  }
57
- /**
58
- * Percent-encode one path segment exactly the way a browser encodes it into a request URL.
59
- *
60
- * NOT `encodeURIComponent`. That escapes the sub-delimiters `, @ + = & ; $`, which a browser sends raw -
61
- * so a file named `report,2026.csv` would be recorded as `/report%2C2026.csv` while the request arrives
62
- * as `/report,2026.csv`, the allowlist lookup misses, and the file 404s in production only. `encodeURI`
63
- * agrees with `URL.pathname` on every character except `?` and `#`, which terminate a path and so must
64
- * be escaped explicitly here.
65
- */
66
- function encodePathSegment(segment) {
67
- return encodeURI(segment).replace(/[?#]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
68
- }
69
66
  /**
70
67
  * Cloudflare Pages rejects a `_routes.json` with more than 100 include+exclude rules combined, or any
71
68
  * single rule longer than 100 characters.
@@ -116,7 +113,7 @@ function collapsibleDirs(publicFiles, routePatterns) {
116
113
  return dirs;
117
114
  }
118
115
  /**
119
- * 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.
120
117
  *
121
118
  * `exclude` is what Pages serves straight from the CDN instead of invoking the worker, so naming every
122
119
  * public file is ideal - and impossible past ~99 of them. A `public/` of icons, fonts and share images
@@ -200,7 +197,7 @@ export async function copyPublicDir(from, to) {
200
197
  const target = join(to, rel);
201
198
  await mkdir(join(target, ".."), { recursive: true });
202
199
  await cp(candidateReal, target);
203
- copied.push(`/${rel.split(sep).map(encodePathSegment).join("/")}`);
200
+ copied.push(publicUrlPath(rel));
204
201
  }
205
202
  return copied.sort();
206
203
  }
@@ -213,11 +210,13 @@ export async function copyPublicDir(from, to) {
213
210
  * Pure: `source` + the discovered `manifest` in, new source out.
214
211
  */
215
212
  export function resyncServerManifestSource(source, manifest, routesPrefix) {
213
+ const cssLoading = parseManifestCssLoading(source);
216
214
  return generateServerManifest(manifest, {
217
215
  resolve: (file) => `${routesPrefix}${file}`,
218
216
  clientEntry: parseManifestClientEntry(source) ?? "",
219
217
  styles: parseManifestStyles(source),
220
218
  routeStyles: parseManifestRouteStyles(source),
219
+ ...(cssLoading !== undefined ? { cssLoading } : {}),
221
220
  lazy: MANIFEST_IS_LAZY.test(source),
222
221
  });
223
222
  }
@@ -264,6 +263,7 @@ export async function buildClient(options) {
264
263
  // layered after these in the `define` object). `Bun` may be absent under non-Bun typecheck - guard it.
265
264
  const buildEnv = (typeof Bun !== "undefined" ? Bun.env : undefined) ?? process.env;
266
265
  const publicDefines = publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", buildEnv);
266
+ const privateEnv = privateEnvCheck(options.publicEnvPrefix ?? "PUBLIC_");
267
267
  // Keep the generated source beside the project, not inside `outDir`: module resolution starts at the
268
268
  // importing file, so an absolute `--out /tmp/deploy` must still resolve the app's dependencies.
269
269
  // A unique directory also avoids clobbering a user file or colliding with parallel builds.
@@ -281,6 +281,10 @@ export async function buildClient(options) {
281
281
  ...routeManifest.routes.map((r) => r.file),
282
282
  ...Object.values(routeManifest.layouts).map((l) => l.file),
283
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),
284
288
  ]),
285
289
  ].sort();
286
290
  // `metafile: true` asks Bun for the input/output graph - specifically `outputs[js].entryPoint`
@@ -288,12 +292,14 @@ export async function buildClient(options) {
288
292
  // entry→CSS link for per-route splitting: keyed by the unique source path, so it survives
289
293
  // same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. Not yet in
290
294
  // `@types/bun`'s `BuildConfig`, so spread it in (spread props skip the excess-property check).
291
- 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.
292
299
  const result = await (async () => {
293
300
  try {
294
301
  return await Bun.build({
295
302
  entrypoints: [entryFile, ...routeFiles.map(resolve)],
296
- outdir: outDir,
297
303
  target: "browser",
298
304
  naming: "[name]-[hash].[ext]",
299
305
  publicPath,
@@ -304,11 +310,16 @@ export async function buildClient(options) {
304
310
  ...buildExtras,
305
311
  minify: options.minify ?? true,
306
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
+ }),
307
319
  ...declaredSingleCopyPlugins(root),
308
320
  reactDedupePlugin(routesDir),
309
321
  preactDedupePlugin(routesDir),
310
322
  svelteDedupePlugin(routesDir),
311
- serverOnlyEmptyPlugin(),
312
323
  serverFnStubPlugin(),
313
324
  ...(options.plugins ?? []),
314
325
  ],
@@ -316,10 +327,11 @@ export async function buildClient(options) {
316
327
  // Replace `process.env.*` at compile time so an app module reading config off `process.env` doesn't
317
328
  // hit a `process is not defined` crash in the browser. Bun does longest-match: NODE_ENV resolves to
318
329
  // the build mode (React's prod/dev branch); each PUBLIC_* var resolves to its baked VALUE; every
319
- // other `process.env.X` becomes undefined (the bare `process.env` → `({})` fallback - so secrets
320
- // 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.
321
333
  define: {
322
- "process.env": "({})",
334
+ "process.env": "{}",
323
335
  "process.env.NODE_ENV": JSON.stringify(mode),
324
336
  ...publicDefines,
325
337
  ...options.define,
@@ -330,36 +342,110 @@ export async function buildClient(options) {
330
342
  rmSync(entryDir, { recursive: true, force: true });
331
343
  }
332
344
  })();
345
+ const cwd = process.cwd();
346
+ const clientMeta = result.metafile;
333
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));
334
362
  throw new Error(`[nifra/web] client build failed:\n${result.logs.map((l) => String(l)).join("\n")}`);
335
363
  }
336
- // #4: a `node:` builtin (e.g. `node:crypto`) pulled into a CLIENT chunk builds fine (Bun substitutes
337
- // a browser polyfill) but breaks/leaks at runtime. Fail the build with a named, actionable error
338
- // instead - caught at build time, not by a confused user in the browser. Graph-based (the metafile's
339
- // per-output `inputs`), so it can't false-positive on a `"node:..."` string literal and survives
340
- // minification. Only the client build runs this; the server build's `node:` imports are legitimate.
341
- const clientMeta = result.metafile;
342
- 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);
343
397
  // #4: a `node:` builtin (e.g. `node:crypto`) pulled into a CLIENT chunk builds fine (Bun substitutes a
344
398
  // browser polyfill) but breaks/leaks at runtime. Fail with the chain (entry → … → builtin), through the
345
399
  // SHARED formatter so the Vite pipeline's identical guard reads byte-for-byte the same.
346
400
  const nodeBuiltinLeak = formatNodeBuiltinLeak(detectNodeBuiltinsInClient(clientGraph));
347
401
  if (nodeBuiltinLeak !== undefined)
348
402
  throw new Error(nodeBuiltinLeak);
349
- // §3.3/§5.1: a module that opted into the `server-only` marker yet reached a CLIENT chunk - catches
350
- // pure-server logic (a secret, a server-only API call) carrying no `node:` import and not named
351
- // `*.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.
352
405
  const serverOnlyLeak = formatServerOnlyLeak(detectServerOnlyInClient(clientGraph));
353
406
  if (serverOnlyLeak !== undefined)
354
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
+ }));
355
441
  // Rename any chunk whose basename isn't URL-safe (dynamic-route files become `[slug]-hash.js`) and
356
442
  // rewrite the references - otherwise the lazy import 404s and the route silently never hydrates.
357
- const renamed = sanitizeOutputNames(result.outputs);
443
+ const renamed = sanitizeOutputNames(outputs);
358
444
  const toUrl = (path) => `${publicPath}${renamed.get(basename(path)) ?? basename(path)}`;
359
445
  // Entry-point outputs come back in entrypoint order: [bootstrap, ...routeFiles]. Map each route file
360
446
  // to its chunk URL by that order (guarded against drift), then a route's chunks = its layout chain +
361
447
  // own file.
362
- const entryPoints = result.outputs.filter((o) => o.kind === "entry-point");
448
+ const entryPoints = outputs.filter((o) => o.kind === "entry-point");
363
449
  const bootstrap = entryPoints[0];
364
450
  if (bootstrap === undefined)
365
451
  throw new Error("[nifra/web] build produced no entry-point output");
@@ -397,7 +483,7 @@ export async function buildClient(options) {
397
483
  // per-route `cssBundle` outputs follow. Dropping those per-route outputs (the old `aggregate`-only
398
484
  // shape) left them in `manifest.assets` but not `css`, so a route-scoped stylesheet normalized to
399
485
  // `asset:css` and tripped the module-graph parity contract - a bundler-dependent divergence.
400
- 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"));
401
487
  const css = [
402
488
  ...cssAssets.filter((o) => cssNameOf(o.path) === bootstrapName),
403
489
  ...cssAssets.filter((o) => cssNameOf(o.path) !== bootstrapName),
@@ -408,7 +494,6 @@ export async function buildClient(options) {
408
494
  // same-basename collisions (`index.tsx` + `blog/index.tsx`) that a filename match can't. A page then
409
495
  // links only its layout chain + own CSS (deduped); an empty array means the page needs no CSS at all.
410
496
  // Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle - never silently incomplete.
411
- const cwd = process.cwd();
412
497
  const cssByEntry = new Map();
413
498
  for (const out of Object.values(clientMeta?.outputs ?? {})) {
414
499
  if (out.entryPoint !== undefined && out.cssBundle !== undefined) {
@@ -428,22 +513,32 @@ export async function buildClient(options) {
428
513
  // SSR pages unstyled. Keep routeStyles absent until at least one authored route entry owns CSS.
429
514
  const routeCssEntries = [...cssByEntry.keys()].filter((entry) => entry !== resolvePath(entryFile));
430
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);
431
519
  for (const route of routeManifest.routes) {
432
520
  routeStyles[route.id] = stylesFor([
433
521
  ...route.layoutIds.map((id) => routeManifest.layouts[id]?.file ?? ""),
434
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,
435
533
  ]);
436
534
  }
437
- if (routeManifest.notFound)
438
- routeStyles._404 = stylesFor([routeManifest.notFound.file]);
439
535
  }
440
536
  // Copy `public/` into the output next to the hashed assets. A missing directory is normal (most
441
537
  // apps have none) and must not fail the build.
442
- const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public");
443
538
  const publicFiles = publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : [];
444
539
  const manifest = {
445
540
  entry: toUrl(bootstrap.path),
446
- assets: result.outputs.map((o) => toUrl(o.path)),
541
+ assets: outputs.map((o) => toUrl(o.path)),
447
542
  routes,
448
543
  ...(publicFiles.length > 0 ? { publicFiles } : {}),
449
544
  ...(css.length > 0 ? { css } : {}),
@@ -540,12 +635,55 @@ const SVELTE_DEDUPE_PATTERN = dedupePolicyFor("svelte").bunPattern ?? /^svelte($
540
635
  * fixed subpath list), Svelte has many internal subpaths, so each matched import is resolved dynamically.
541
636
  * `svelte/compiler` (build-time only, not in the bundle) doesn't match the filter and is left alone. No-op
542
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.
543
644
  */
544
645
  export const svelteDedupePlugin = (from) => ({
545
646
  name: "nifra-svelte-dedupe",
546
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
+ };
547
677
  build.onResolve({ filter: SVELTE_DEDUPE_PATTERN }, (args) => {
548
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
+ }
549
687
  return { path: Bun.resolveSync(args.path, from) };
550
688
  }
551
689
  catch {
@@ -554,6 +692,22 @@ export const svelteDedupePlugin = (from) => ({
554
692
  });
555
693
  },
556
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
+ }
557
711
  /**
558
712
  * The app's declared single-copy rule, applied to the bundle.
559
713
  *
@@ -568,32 +722,50 @@ export const svelteDedupePlugin = (from) => ({
568
722
  */
569
723
  const declaredSingleCopyPlugins = (root) => readSingleCopyDeclaration(root) === undefined ? [] : [declaredSingleCopyPlugin({ cwd: root })];
570
724
  /**
571
- * Remix-style `.server` convention for the CLIENT build. A module named `*.server.ts(x)` (`db.server.ts`,
572
- * `auth.server.ts`, …) is server-only - empty it in the browser bundle so its (possibly `node:` / native /
573
- * Capacitor) import subtree never reaches the client. The body is CJS-with-a-Proxy so any named OR default
574
- * import resolves to `undefined` rather than a "missing export" bundle error (verified), and the real
575
- * import subtree is gone. The complement to the node-builtin guard: when a server-only import is co-located
576
- * in a route file (so it can't be tree-shaken out and the guard fails loud), moving it into a `*.server`
577
- * 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.
578
728
  */
579
- export const serverOnlyEmptyPlugin = () => ({
580
- name: "nifra-server-only-empty",
581
- setup(build) {
582
- build.onLoad({ filter: SERVER_ONLY_MODULE }, () => ({
583
- contents: SERVER_ONLY_REPLACEMENT,
584
- 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;
585
743
  }));
586
- },
587
- });
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";
588
762
  /**
589
763
  * Server functions in the CLIENT build: replace each `*.fn.ts` module with stubs that call the routes
590
764
  * the server mounted, so the function bodies - and everything they import - never reach a browser.
591
765
  *
592
- * The sibling of {@link serverOnlyEmptyPlugin}, and a deliberate contrast: a `*.server` module is
593
- * EMPTIED because nothing may call it from the client, while a `*.fn` module is REPLACED because the
594
- * client is supposed to call it, just over HTTP. The generation itself is in
595
- * `internal/server-boundary.ts` so the Vite pipeline emits identical stubs from the same code; two
596
- * 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.
597
769
  *
598
770
  * CLIENT-only. The server build keeps the real module, which is what `serverFunctions()` mounts.
599
771
  */
@@ -634,7 +806,7 @@ const solidWebServerPlugin = (from) => ({
634
806
  * silently ships a broken worker.
635
807
  */
636
808
  export async function buildServer(options) {
637
- const { routesDir, serverEntry, outDir, clientEntry, styles, routeStyles } = options;
809
+ const { routesDir, serverEntry, outDir, clientEntry, styles, routeStyles, cssLoading } = options;
638
810
  const entryDir = dirname(serverEntry);
639
811
  const manifestFile = options.manifestFile ?? "server-manifest.ts";
640
812
  // Default: import routes relative from the generated manifest (next to serverEntry) to routesDir.
@@ -650,11 +822,19 @@ export async function buildServer(options) {
650
822
  const edge = target === "browser";
651
823
  const conditions = options.conditions ?? (edge ? ["workerd", "edge-light", "browser"] : [target]);
652
824
  const manifest = discoverRoutes(routesDir);
653
- writeFileSync(`${entryDir}/${manifestFile}`, generateServerManifest(manifest, { resolve, clientEntry, styles, routeStyles, lazy }));
825
+ writeFileSync(`${entryDir}/${manifestFile}`, generateServerManifest(manifest, {
826
+ resolve,
827
+ clientEntry,
828
+ styles,
829
+ routeStyles,
830
+ ...(cssLoading !== undefined ? { cssLoading } : {}),
831
+ lazy,
832
+ }));
654
833
  const result = await Bun.build({
655
834
  entrypoints: [serverEntry],
656
835
  outdir: outDir,
657
836
  target,
837
+ metafile: true,
658
838
  conditions: [...conditions],
659
839
  define: {
660
840
  ...(options.define ?? { "process.env.NODE_ENV": '"production"' }),
@@ -682,6 +862,38 @@ export async function buildServer(options) {
682
862
  if (!result.success) {
683
863
  throw new Error(`[nifra/web] server build failed:\n${result.logs.map((l) => String(l)).join("\n")}`);
684
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
+ }
685
897
  const entryOutput = result.outputs.find((o) => o.kind === "entry-point");
686
898
  if (entryOutput === undefined) {
687
899
  throw new Error("[nifra/web] server build produced no entry-point output");
@@ -696,12 +908,24 @@ export async function buildServer(options) {
696
908
  // The ONLY thing apps used to hand-write per target was the server entry (`_worker.ts`, `server-bun.ts`,
697
909
  // …) - so we GENERATE it here (per target) instead of asking each app to ship five near-identical files.
698
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
+ };
699
923
  /**
700
924
  * Codegen the per-target **server entry** module (source text) for `buildServer` to bundle. It imports
701
925
  * the app's `adapter` (from `framework.ts`), the optional `backend` (from `backend.ts`), and the
702
926
  * generated `{ manifest, clientEntry }` (from `./server-manifest`), builds `createWebApp`, then wires
703
927
  * the right host:
704
- * - `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).
705
929
  * - `deno`: same fetch-handler default, plus `Deno.serve` self-host when run directly.
706
930
  * - `bun` / `node`: a self-hosting server that ALSO serves the client bundle from disk (those
707
931
  * runtimes have a filesystem; the static `/assets/*` sit next to the entry).
@@ -710,7 +934,23 @@ export async function buildServer(options) {
710
934
  * string out) so the generation is unit-testable without a real build.
711
935
  */
712
936
  export function generateServerEntry(options) {
713
- 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
+ }
714
954
  if (target === "static") {
715
955
  throw new Error("[nifra/web] generateServerEntry: `static` has no server entry (SSG only)");
716
956
  }
@@ -720,18 +960,24 @@ export function generateServerEntry(options) {
720
960
  lines.push(`import { adapter } from ${JSON.stringify(adapterImport)}`);
721
961
  if (useImport !== undefined)
722
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
+ }
723
966
  if (backendImport !== undefined) {
724
967
  lines.push(`import { backend } from ${JSON.stringify(backendImport)}`);
725
968
  }
726
- lines.push('import { clientEntry, manifest, styles, routeStyles } from "./server-manifest"');
727
- // cf-pages/vercel/deno need the fetch-handler shape; bun/node call app.fetch directly.
728
- const usesToFetch = target === "cf-pages" || target === "vercel" || target === "deno";
729
- if (usesToFetch)
969
+ lines.push('import { clientEntry, cssLoading, manifest, styles, routeStyles } from "./server-manifest"');
970
+ // Cloudflare needs the fetch-handler shape; the others call app.fetch directly.
971
+ if (target === "cloudflare")
730
972
  lines.push('import { toFetchHandler } from "@nifrajs/core/server"');
731
973
  if (target === "node")
732
974
  lines.push('import { serve } from "@nifrajs/node"');
733
- lines.push("", "const app = createWebApp({", " adapter,", ...(useImport !== undefined ? [" use,"] : []), " manifest,", " clientEntry,", " styles,", " routeStyles,", ...(backendImport !== undefined ? [" api: inProcessClient(backend),"] : []), ` title: ${JSON.stringify(title)},`, "})", "");
734
- 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") {
735
981
  // Cloudflare Pages advanced mode: `_routes.json` keeps static paths off the worker entirely, and
736
982
  // everything else falls through to this handler (SSR).
737
983
  //
@@ -747,20 +993,26 @@ export function generateServerEntry(options) {
747
993
  return `${lines.join("\n")}\n`;
748
994
  }
749
995
  // bun / node / deno self-host AND serve the client bundle from disk (it sits next to this entry).
750
- 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';
751
1003
  if (target === "bun") {
752
- 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 } })", " },", "})",
753
1005
  // The `${...}` here is literal OUTPUT (a template in the GENERATED file), not a template in this
754
1006
  // source - split so biome's noTemplateCurlyInString doesn't flag it; the emitted line is unchanged.
755
1007
  `console.log(\`nifra (Bun) → http://localhost:$${"{server.port}"}\`)`);
756
1008
  return `${lines.join("\n")}\n`;
757
1009
  }
758
1010
  if (target === "node") {
759
- 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) },", ")");
760
1012
  return `${lines.join("\n")}\n`;
761
1013
  }
762
1014
  // deno
763
- 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 })", "})");
764
1016
  return `${lines.join("\n")}\n`;
765
1017
  }
766
1018
  /** Measure each emitted output file's raw + gzip size. Reads the file off disk and gzips it with
@@ -783,36 +1035,48 @@ async function measureOutputs(paths) {
783
1035
  * `<outDir>/assets/*`, then per target:
784
1036
  * - `static`: prerenders opted-in routes (`prerenderRoutes`) to `<outDir>/<path>/index.html` (+
785
1037
  * `_data.json`); needs `prerenderApp`. No server.
786
- * - `cf-pages`: a `_worker.js` (edge bundle) + a `_routes.json` excluding /assets/* from the worker.
787
- * - `vercel`: a `.vercel/output`-shaped function isn't emitted here - `vercel` emits the bundled edge
788
- * 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`.
789
1042
  * - `deno`/`node`/`bun`: the self-hosting server bundle (`server.js`) next to the assets.
790
1043
  * The server entry is GENERATED (`generateServerEntry`) and bundled (`buildServer`); the app supplies
791
1044
  * only adapter/backend/routes. Returns the manifest + a size report. Throws on any build failure.
792
- *
793
- * Note: the heavier platform wrappers (`.vercel/output` v3 layout, wrangler ISR `find_additional_modules`)
794
- * remain app-owned scripts; this command targets the common single-bundle deploys. See the CLI docs.
795
1045
  */
796
1046
  /** The default (Bun) strategy - `buildClient`/`buildServer` from this module. */
797
1047
  export const bunBundler = {
798
- buildClient: (input) => buildClient({
799
- routesDir: input.routesDir,
800
- outDir: input.outDir,
801
- clientModule: input.clientModule,
802
- ...(input.plugins ? { plugins: input.plugins } : {}),
803
- ...(input.conditions ? { conditions: input.conditions } : {}),
804
- ...(input.define ? { define: input.define } : {}),
805
- ...(input.publicDir !== undefined ? { publicDir: input.publicDir } : {}),
806
- ...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
807
- }),
1048
+ buildClient: (input) => {
1049
+ const cssCodeSplit = normalizeCssCodeSplit(input.cssCodeSplit);
1050
+ if (cssCodeSplit === false) {
1051
+ throw new Error("[nifra/web] cssCodeSplit: false is a Vite-only production option. Use buildTargetVite or remove it for the native Bun build.");
1052
+ }
1053
+ return buildClient({
1054
+ routesDir: input.routesDir,
1055
+ outDir: input.outDir,
1056
+ clientModule: input.clientModule,
1057
+ ...(input.plugins ? { plugins: input.plugins } : {}),
1058
+ ...(input.conditions ? { conditions: input.conditions } : {}),
1059
+ ...(input.define ? { define: input.define } : {}),
1060
+ ...(input.cssCodeSplit !== undefined ? { cssCodeSplit } : {}),
1061
+ ...(input.publicDir !== undefined ? { publicDir: input.publicDir } : {}),
1062
+ ...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
1063
+ ...(input.secretExemptions !== undefined ? { secretExemptions: input.secretExemptions } : {}),
1064
+ });
1065
+ },
808
1066
  buildServer: (input) => buildServer({
809
1067
  routesDir: input.routesDir,
810
1068
  serverEntry: input.serverEntry,
811
1069
  outDir: input.outDir,
812
1070
  clientEntry: input.clientEntry,
813
1071
  target: input.target,
1072
+ ...(input.styles !== undefined ? { styles: input.styles } : {}),
1073
+ ...(input.routeStyles !== undefined ? { routeStyles: input.routeStyles } : {}),
814
1074
  ...(input.plugins ? { plugins: input.plugins } : {}),
815
1075
  ...(input.define ? { define: input.define } : {}),
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 } : {}),
816
1080
  }),
817
1081
  };
818
1082
  /** Build a full deploy dir for `target` using the default Bun bundler. See {@link buildTargetWith}. */
@@ -827,14 +1091,17 @@ export async function buildTarget(target, options) {
827
1091
  */
828
1092
  export async function buildTargetWith(target, options, bundler) {
829
1093
  const { routesDir, outDir, workDir } = options;
830
- const targetPlan = planBuildTarget(target, outDir);
1094
+ const requestedCssCodeSplit = options.cssCodeSplit === undefined ? undefined : normalizeCssCodeSplit(options.cssCodeSplit);
1095
+ const requestedCssLoading = options.cssLoading === undefined ? undefined : normalizeCssLoading(options.cssLoading);
1096
+ const targetPlan = planBuildTarget(parseBuildTarget(target), outDir);
831
1097
  const { rmSync } = await import("node:fs");
832
1098
  rmSync(outDir, { recursive: true, force: true });
833
1099
  rmSync(workDir, { recursive: true, force: true });
834
- const assetsDir = `${outDir}/assets`;
1100
+ const staticRoot = targetPlan.staticDir === "" ? outDir : `${outDir}/${targetPlan.staticDir}`;
1101
+ const assetsDir = `${staticRoot}/assets`;
835
1102
  mkdirSync(assetsDir, { recursive: true });
836
1103
  mkdirSync(workDir, { recursive: true });
837
- // (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).
838
1105
  let client = await bundler.buildClient({
839
1106
  routesDir,
840
1107
  outDir: assetsDir,
@@ -844,14 +1111,43 @@ export async function buildTargetWith(target, options, bundler) {
844
1111
  define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
845
1112
  publicDir: false,
846
1113
  ...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
1114
+ ...(options.secretExemptions !== undefined
1115
+ ? { secretExemptions: options.secretExemptions }
1116
+ : {}),
1117
+ ...(requestedCssCodeSplit !== undefined ? { cssCodeSplit: requestedCssCodeSplit } : {}),
1118
+ ...(requestedCssLoading !== undefined ? { cssLoading: requestedCssLoading } : {}),
847
1119
  root: resolvePath(dirname(routesDir)),
848
1120
  });
1121
+ // Bun's client builder has no CSS-loading field, but the server-side document policy is bundler
1122
+ // independent. Carry the requested policy into the result so static prerender callbacks and the
1123
+ // generated server entry see the same choice on both build strategies.
1124
+ if (requestedCssLoading !== undefined && client.cssLoading !== requestedCssLoading) {
1125
+ client = { ...client, cssLoading: requestedCssLoading };
1126
+ }
849
1127
  const publicDir = options.publicDir === false
850
1128
  ? undefined
851
1129
  : resolvePath(options.publicDir ?? join(dirname(routesDir), "public"));
852
- 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
+ : [];
853
1144
  if (publicFiles.length > 0) {
854
1145
  client = { ...client, publicFiles };
1146
+ }
1147
+ // `buildClient` writes its first manifest before the target orchestration knows the activation policy
1148
+ // (the native Bun client builder has no CSS-loading option). Persist the target-level additions even
1149
+ // when the app has no public/ files; otherwise the returned manifest and the deploy artifact disagree.
1150
+ if (requestedCssLoading !== undefined || publicFiles.length > 0) {
855
1151
  writeFileSync(`${assetsDir}/manifest.json`, JSON.stringify(client, null, 2));
856
1152
  }
857
1153
  if (targetPlan.kind === "static") {
@@ -867,6 +1163,7 @@ export async function buildTargetWith(target, options, bundler) {
867
1163
  app,
868
1164
  routes: manifest.routes,
869
1165
  outDir,
1166
+ secrets: secretScan,
870
1167
  });
871
1168
  if (result.prerendered.length === 0) {
872
1169
  // A static build that renders nothing is almost always a misconfig (no `prerender = true` / no
@@ -892,9 +1189,11 @@ export async function buildTargetWith(target, options, bundler) {
892
1189
  target,
893
1190
  adapterImport: options.adapterImport,
894
1191
  ...(options.useImport !== undefined ? { useImport: options.useImport } : {}),
1192
+ ...(options.optionImports !== undefined ? { optionImports: options.optionImports } : {}),
895
1193
  ...(options.backendImport !== undefined ? { backendImport: options.backendImport } : {}),
896
1194
  ...(options.title !== undefined ? { title: options.title } : {}),
897
1195
  ...(publicFiles.length > 0 ? { publicFiles } : {}),
1196
+ ...(options.clientIp !== undefined ? { clientIp: options.clientIp } : {}),
898
1197
  }));
899
1198
  const { worker } = await bundler.buildServer({
900
1199
  routesDir,
@@ -902,13 +1201,22 @@ export async function buildTargetWith(target, options, bundler) {
902
1201
  outDir: `${workDir}/server`,
903
1202
  clientEntry: client.entry,
904
1203
  target: targetPlan.serverTarget,
1204
+ ...(client.css !== undefined ? { styles: client.css } : {}),
1205
+ ...(client.routeStyles !== undefined ? { routeStyles: client.routeStyles } : {}),
905
1206
  ...(options.serverPlugins ? { plugins: options.serverPlugins } : {}),
906
1207
  define: { "process.env.NODE_ENV": '"production"', ...(options.define ?? {}) },
1208
+ ...(client.cssLoading !== undefined
1209
+ ? { cssLoading: client.cssLoading }
1210
+ : requestedCssLoading !== undefined
1211
+ ? { cssLoading: requestedCssLoading }
1212
+ : {}),
907
1213
  root: resolvePath(dirname(routesDir)),
1214
+ generatedFiles: [resolvePath(workDir, options.adapterImport)],
1215
+ ...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
908
1216
  });
909
1217
  // (3) Assemble the deploy dir for the target.
910
1218
  const { cpSync } = await import("node:fs");
911
- if (targetPlan.target === "cf-pages") {
1219
+ if (targetPlan.target === "cloudflare") {
912
1220
  cpSync(worker, `${outDir}/${targetPlan.outputFile}`);
913
1221
  // The app's real patterns, so a directory is only collapsed into a glob once the route table
914
1222
  // proves nothing can be served beneath it.
@@ -923,7 +1231,12 @@ export async function buildTargetWith(target, options, bundler) {
923
1231
  }
924
1232
  }
925
1233
  else if (targetPlan.target === "vercel") {
1234
+ const fn = dirname(`${outDir}/${targetPlan.outputFile}`);
1235
+ mkdirSync(fn, { recursive: true });
926
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`);
927
1240
  }
928
1241
  else {
929
1242
  cpSync(worker, `${outDir}/${targetPlan.outputFile}`);