@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/src/build-plan.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  import { basename as pathBasename } from "node:path"
2
+ import type { CssLoadingMode } from "./css-contract.ts"
3
+ import { codeUnitOrder } from "./internal/code-unit-order.ts"
4
+ import { isBareNodeBuiltin } from "./internal/node-builtins.ts"
5
+ import type { SecretExemption } from "./internal/secret-scan.ts"
2
6
  import type { ClientModuleGraph } from "./module-graph.ts"
3
7
 
4
8
  export interface BuildManifest {
@@ -19,6 +23,10 @@ export interface BuildManifest {
19
23
  * imported the CSS; the always-safe fallback `createWebApp` links when a route has no per-route entry
20
24
  * in {@link routeStyles}. Omitted when the app imports no CSS. */
21
25
  readonly css?: readonly string[]
26
+ /** Whether the Vite client build kept CSS split by entry/chunk. Absent for the native Bun build. */
27
+ readonly cssCodeSplit?: boolean
28
+ /** How framework-owned SSR stylesheet links should become active. Defaults to `blocking`. */
29
+ readonly cssLoading?: CssLoadingMode
22
30
  /** `routeId → [chain CSS URLs]` - only the stylesheets the matched route's layout chain + own file
23
31
  * actually use (Bun emits a per-entrypoint CSS bundle per route/layout, with shared-component CSS
24
32
  * inlined into each consumer). `createWebApp` links these instead of the aggregate, so a page ships
@@ -46,7 +54,7 @@ export interface ServerBuild {
46
54
  // ===================================================================================================
47
55
 
48
56
  /** A deploy target `nifra build --target <t>` can emit. `static` is pure SSG (no server). */
49
- export const BUILD_TARGETS = ["bun", "node", "deno", "cf-pages", "vercel", "static"] as const
57
+ export const BUILD_TARGETS = ["bun", "node", "deno", "cloudflare", "vercel", "static"] as const
50
58
  export type BuildTarget = (typeof BUILD_TARGETS)[number]
51
59
 
52
60
  /** A type guard narrowing an arbitrary string to a {@link BuildTarget}. */
@@ -54,6 +62,16 @@ export function isBuildTarget(value: string): value is BuildTarget {
54
62
  return (BUILD_TARGETS as readonly string[]).includes(value)
55
63
  }
56
64
 
65
+ /** `value` as a {@link BuildTarget}, or throw naming the valid ones (and the renamed `cf-pages`). */
66
+ export function parseBuildTarget(value: string, label = "target"): BuildTarget {
67
+ if (isBuildTarget(value)) return value
68
+ throw new Error(
69
+ value === "cf-pages"
70
+ ? `[nifra] the ${label} "cf-pages" is now "cloudflare"`
71
+ : `[nifra] unknown ${label} "${value}". Valid: ${BUILD_TARGETS.join(", ")}`,
72
+ )
73
+ }
74
+
57
75
  export type ServerBuildTarget = "browser" | "node" | "bun"
58
76
 
59
77
  export interface StaticBuildTargetPlan {
@@ -61,6 +79,7 @@ export interface StaticBuildTargetPlan {
61
79
  readonly kind: "static"
62
80
  readonly serverTarget: undefined
63
81
  readonly outputFile: undefined
82
+ readonly staticDir: ""
64
83
  readonly run: string
65
84
  }
66
85
 
@@ -68,8 +87,10 @@ export interface ServerBuildTargetPlan {
68
87
  readonly target: Exclude<BuildTarget, "static">
69
88
  readonly kind: "server"
70
89
  readonly serverTarget: ServerBuildTarget
71
- /** The worker's final filename inside the assembled deploy directory. */
72
- readonly outputFile: "_worker.js" | "index.js" | "server.js"
90
+ /** The worker's final path inside the assembled deploy directory. */
91
+ readonly outputFile: "_worker.js" | "functions/index.func/index.js" | "server.js"
92
+ /** Where the host serves static files from, relative to the deploy directory (`""` = its root). */
93
+ readonly staticDir: "" | "static"
73
94
  readonly run: string
74
95
  }
75
96
 
@@ -101,15 +122,17 @@ export function planBuildTarget(target: BuildTarget, outDir: string): BuildTarge
101
122
  kind: "static",
102
123
  serverTarget: undefined,
103
124
  outputFile: undefined,
125
+ staticDir: "",
104
126
  run: `static site → ${outDir} (serve the directory with any static host)`,
105
127
  }
106
128
  }
107
- if (target === "cf-pages") {
129
+ if (target === "cloudflare") {
108
130
  return {
109
131
  target,
110
132
  kind: "server",
111
133
  serverTarget: "browser",
112
134
  outputFile: "_worker.js",
135
+ staticDir: "",
113
136
  run: `Cloudflare Pages → ${outDir} (deploy: wrangler pages deploy ${finalPathSegment(outDir)})`,
114
137
  }
115
138
  }
@@ -118,8 +141,10 @@ export function planBuildTarget(target: BuildTarget, outDir: string): BuildTarge
118
141
  target,
119
142
  kind: "server",
120
143
  serverTarget: "browser",
121
- outputFile: "index.js",
122
- run: `Vercel edge function → ${outDir}/index.js (wrap with your vercel.json or Build Output API)`,
144
+ // Vercel's Build Output API: `vercel deploy --prebuilt` uploads this tree as it is.
145
+ outputFile: "functions/index.func/index.js",
146
+ staticDir: "static",
147
+ run: `Vercel (Build Output API) → ${outDir} (deploy: vercel deploy --prebuilt)`,
123
148
  }
124
149
  }
125
150
  return {
@@ -127,6 +152,7 @@ export function planBuildTarget(target: BuildTarget, outDir: string): BuildTarge
127
152
  kind: "server",
128
153
  serverTarget: target === "node" ? "node" : target === "bun" ? "bun" : "browser",
129
154
  outputFile: "server.js",
155
+ staticDir: "",
130
156
  run: `${target} server → ${outDir} (run: ${target === "node" ? "node" : target} ${finalPathSegment(outDir)}/server.js)`,
131
157
  }
132
158
  }
@@ -150,8 +176,14 @@ export interface Bundler {
150
176
  readonly define?: Readonly<Record<string, string>>
151
177
  readonly publicDir?: string | false
152
178
  readonly publicEnvPrefix?: string
179
+ /** Vite CSS splitting policy; the Bun strategy rejects `false` and otherwise preserves its native behavior. */
180
+ readonly cssCodeSplit?: boolean
181
+ /** Framework-owned stylesheet activation policy; supported by both server strategies. */
182
+ readonly cssLoading?: CssLoadingMode
153
183
  /** Project root (Vite needs it; the Bun strategy ignores it). */
154
184
  readonly root?: string
185
+ /** Reviewed false positives of the secret scan. */
186
+ readonly secretExemptions?: readonly SecretExemption[]
155
187
  }): Promise<BuildManifest>
156
188
  /** Build the server worker → the shared {@link ServerBuild}. */
157
189
  buildServer(input: {
@@ -160,15 +192,26 @@ export interface Bundler {
160
192
  readonly outDir: string
161
193
  readonly clientEntry: string
162
194
  readonly target: "browser" | "node" | "bun"
195
+ /** The client build's aggregate stylesheet URLs to bake into the server manifest. */
196
+ readonly styles?: readonly string[]
197
+ /** The client build's per-route stylesheet map to bake into the server manifest. */
198
+ readonly routeStyles?: Readonly<Record<string, readonly string[]>>
163
199
  readonly plugins?: readonly unknown[]
164
200
  readonly define?: Readonly<Record<string, string>>
201
+ /** CSS activation policy to bake into the generated server manifest. */
202
+ readonly cssLoading?: CssLoadingMode
165
203
  readonly root?: string
204
+ /** Modules the zone rules treat as generated: the adapter module the generated entry imports. */
205
+ readonly generatedFiles?: readonly string[]
206
+ /** The public-env prefix browser code may read (default `"PUBLIC_"`). */
207
+ readonly publicEnvPrefix?: string
166
208
  }): Promise<ServerBuild>
167
209
  }
168
210
 
169
211
  interface GraphImport {
170
212
  readonly path?: string
171
213
  readonly original?: string
214
+ readonly external?: boolean
172
215
  }
173
216
 
174
217
  interface GraphInput {
@@ -180,7 +223,11 @@ const basename = (path: string): string => pathBasename(path)
180
223
  const nodeBuiltinOf = (im: GraphImport): string | undefined => {
181
224
  if (im.original?.startsWith("node:")) return im.original
182
225
  if (im.path?.startsWith("node:")) return im.path
183
- return undefined
226
+ // A bare built-in only counts when the bundler left it external: a bundled `fs` resolved to an
227
+ // installed package (or a polyfill keyed `node:fs`) is handled above or is not a built-in at all.
228
+ // Labeled with the prefix, so `fs/promises` reads the same from either bundler.
229
+ const spec = im.external === true ? (im.original ?? im.path) : undefined
230
+ return spec !== undefined && isBareNodeBuiltin(spec) ? `node:${spec}` : undefined
184
231
  }
185
232
 
186
233
  /** One `node:`-builtin-in-the-client finding: the offending builtin, the emitted chunk it landed in,
@@ -287,29 +334,43 @@ export function detectNodeBuiltinsInClient(
287
334
  chains.set(builtin, chain)
288
335
  return chain
289
336
  }
337
+ // (1b) Builtins the bundler left EXTERNAL: the import ships in the importer's chunk but the module
338
+ // is in no output, so (2) can't find it as a chunk member. Bun does this for a dynamic
339
+ // `import("node:fs")` in a browser build; the browser then fails to load it.
340
+ const externalsOf = new Map<string, string[]>()
341
+ for (const [inputKey, input] of Object.entries(inputs)) {
342
+ if (inputKey.startsWith("node:")) continue
343
+ for (const im of input.imports ?? []) {
344
+ if (im.external !== true) continue
345
+ const builtin = nodeBuiltinOf(im)
346
+ if (builtin === undefined) continue
347
+ const list = externalsOf.get(inputKey) ?? []
348
+ if (!list.includes(builtin)) list.push(builtin)
349
+ externalsOf.set(inputKey, list)
350
+ }
351
+ }
290
352
  // (2) Locate which emitted chunk each user-imported builtin reached, via the per-output `inputs`.
291
353
  const findings = new Map<string, NodeBuiltinFinding>()
354
+ const report = (builtin: string, outputPath: string): void => {
355
+ const chunk = basename(outputPath)
356
+ findings.set(`${builtin}\0${chunk}`, { builtin, chunk, chain: chainFor(builtin) })
357
+ }
292
358
  for (const [outputPath, output] of Object.entries(graph.chunks)) {
293
359
  for (const inputKey of output.modules) {
294
- if (!userImported.has(inputKey)) continue
295
- const chunk = basename(outputPath)
296
- findings.set(`${inputKey}\0${chunk}`, {
297
- builtin: inputKey,
298
- chunk,
299
- chain: chainFor(inputKey),
300
- })
360
+ if (userImported.has(inputKey)) report(inputKey, outputPath)
361
+ for (const builtin of externalsOf.get(inputKey) ?? []) report(builtin, outputPath)
301
362
  }
302
363
  }
303
364
  return [...findings.values()].sort((a, b) =>
304
- a.builtin === b.builtin ? a.chunk.localeCompare(b.chunk) : a.builtin.localeCompare(b.builtin),
365
+ a.builtin === b.builtin ? codeUnitOrder(a.chunk, b.chunk) : codeUnitOrder(a.builtin, b.builtin),
305
366
  )
306
367
  }
307
368
 
308
369
  // ---------------------------------------------------------------------------------------------------
309
- // `server-only` poison-import guard (§3.3/§5.1). The complement to the `.server` convention + the
370
+ // `backend-only` poison-import guard (§3.3/§5.1). The complement to the zones + the
310
371
  // node-builtin guard: a module of PURE server logic with NO `node:` import (a secret-bearing constant,
311
372
  // a server-only API call) that an author wants to FAIL LOUD if it reaches the client opts in with a
312
- // side-effect `import "@nifrajs/web/server-only"` (Next's `import "server-only"`). On the SERVER build
373
+ // side-effect `import "@nifrajs/web/backend-only"` (Next's `import "server-only"`). On the SERVER build
313
374
  // the marker is an empty no-op; the CLIENT build detects - via the SAME Bun metafile graph the
314
375
  // node-builtin guard walks - any module that imports the marker AND lands in a client chunk, and fails
315
376
  // the build with the import chain. Graph-based (never the emitted text), so it survives minification.
@@ -317,18 +378,18 @@ export function detectNodeBuiltinsInClient(
317
378
 
318
379
  /** The marker specifier an author imports to opt a module into the client-leak guard. Matched on the
319
380
  * import edge's *as-written* `original` first (the robust signal: it's exactly what the author typed,
320
- * before Bun resolves it to `src/server-only.ts` / `dist/server-only.js`). */
321
- export const SERVER_ONLY_MARKER = "@nifrajs/web/server-only"
381
+ * before Bun resolves it to `src/backend-only.ts` / `dist/backend-only.js`). */
382
+ export const SERVER_ONLY_MARKER = "@nifrajs/web/backend-only"
322
383
 
323
384
  const isServerOnlyMarkerImport = (im: GraphImport): boolean => {
324
385
  if (im.original === SERVER_ONLY_MARKER) return true
325
- return im.path !== undefined && /(^|\/)server-only\.[cm]?[jt]s$/.test(im.path)
386
+ return im.path !== undefined && /(^|\/)backend-only\.[cm]?[jt]s$/.test(im.path)
326
387
  }
327
388
 
328
- /** True when an INPUT graph key is the marker module file itself (`…/server-only.{ts,js}` under web).
389
+ /** True when an INPUT graph key is the marker module file itself (`…/backend-only.{ts,js}` under web).
329
390
  * Excluded from the "marked" set - the marker is the import target, not an opt-in module. */
330
391
  const isServerOnlyMarkerModule = (inputKey: string): boolean =>
331
- /(^|\/)server-only\.[cm]?[jt]s$/.test(inputKey)
392
+ /(^|\/)backend-only\.[cm]?[jt]s$/.test(inputKey)
332
393
 
333
394
  /** One `server-only`-module-in-the-client finding: the offending module (the as-written marker-import
334
395
  * chain's tail before the marker), the emitted chunk it landed in, and the shortest USER-module import
@@ -383,7 +444,7 @@ function shortestServerOnlyChain(
383
444
  ): string[] {
384
445
  // The label for the marked module's tail: its as-written specifier (filled when we cross the edge
385
446
  // that reaches it) suffixed with `(marked server-only)`; the entry case uses the entry key itself.
386
- const tail = (label: string): string => `${label} (marked server-only)`
447
+ const tail = (label: string): string => `${label} (marked backend-only)`
387
448
  // An entry that is ITSELF the marked module - the chain is just that one node.
388
449
  if (entryInputs.includes(markedModule)) return [tail(markedModule)]
389
450
  const seen = new Set(entryInputs)
@@ -412,7 +473,7 @@ function shortestServerOnlyChain(
412
473
 
413
474
  /**
414
475
  * Scan a build's metafile for any module that opts into the `server-only` marker (a side-effect
415
- * `import "@nifrajs/web/server-only"`) yet landed in a CLIENT output chunk, returning a sorted, deduped
476
+ * `import "@nifrajs/web/backend-only"`) yet landed in a CLIENT output chunk, returning a sorted, deduped
416
477
  * list of {@link ServerOnlyFinding}s. Mirrors {@link detectNodeBuiltinsInClient}: it reads the SAME
417
478
  * graph facts - which inputs import the marker (the "marked" modules), which chunk each landed in (the
418
479
  * per-output `inputs`), and the shortest import chain from a user entry to it. The marker module ITSELF
@@ -451,17 +512,28 @@ export function detectServerOnlyInClient(
451
512
  }
452
513
  // (2) Locate which emitted chunk each marked module reached, via the per-output `inputs`.
453
514
  const findings = new Map<string, ServerOnlyFinding>()
515
+ const placed = new Set<string>()
454
516
  for (const [outputPath, output] of Object.entries(graph.chunks)) {
455
517
  for (const inputKey of output.modules) {
456
518
  if (!marked.has(inputKey)) continue
457
519
  const chunk = basename(outputPath)
520
+ placed.add(inputKey)
458
521
  findings.set(`${inputKey}\0${chunk}`, { chunk, chain: chainFor(inputKey) })
459
522
  }
460
523
  }
524
+ // A marked module the build loaded but no chunk lists: the bundler inlined what it exports (a
525
+ // constant ships without its module) or dropped it. The import crossed the boundary either way.
526
+ for (const inputKey of marked) {
527
+ if (placed.has(inputKey)) continue
528
+ findings.set(`${inputKey}\0`, {
529
+ chunk: "none, inlined or shaken out",
530
+ chain: chainFor(inputKey),
531
+ })
532
+ }
461
533
  return [...findings.values()].sort((a, b) => {
462
534
  const am = a.chain[a.chain.length - 1] ?? ""
463
535
  const bm = b.chain[b.chain.length - 1] ?? ""
464
- return am === bm ? a.chunk.localeCompare(b.chunk) : am.localeCompare(bm)
536
+ return am === bm ? codeUnitOrder(a.chunk, b.chunk) : codeUnitOrder(am, bm)
465
537
  })
466
538
  }
467
539
 
@@ -483,9 +555,8 @@ export function formatNodeBuiltinLeak(
483
555
  : ` - ${finding.builtin} reached the client bundle via ${finding.chunk}`,
484
556
  )
485
557
  return (
486
- `[nifra/web] Node built-in(s) in the client bundle - move them behind a server-only path ` +
487
- `(a loader/action runs on the server; import the \`node:\` module there, not at a route's ` +
488
- `top level):\n${lines.join("\n")}`
558
+ `[nifra/web] Node built-in(s) in the client bundle - built-ins run on the server only: import ` +
559
+ `them in a route's backend half (x.backend.ts) or under backend/, never in browser code:\n${lines.join("\n")}`
489
560
  )
490
561
  }
491
562
 
@@ -496,14 +567,13 @@ export function formatServerOnlyLeak(
496
567
  if (findings.length === 0) return undefined
497
568
  const lines = findings.map((finding) =>
498
569
  finding.chain.length > 1
499
- ? ` - server-only module reached the client bundle via ${finding.chain.join(" → ")} (chunk: ${finding.chunk})`
500
- : ` - server-only module reached the client bundle via ${finding.chunk}`,
570
+ ? ` - backend-only module reached the client bundle via ${finding.chain.join(" → ")} (chunk: ${finding.chunk})`
571
+ : ` - backend-only module reached the client bundle via ${finding.chunk}`,
501
572
  )
502
573
  return (
503
- `[nifra/web] server-only module(s) in the client bundle - a module marked ` +
504
- `\`import "${SERVER_ONLY_MARKER}"\` reached the browser. Move it behind a server-only path ` +
505
- `(reach it via a loader/action, or rename it \`*.server.ts\`), so its server logic never ships ` +
506
- `to the client:\n${lines.join("\n")}`
574
+ `[nifra/web] backend-only module(s) in the client bundle - a module marked ` +
575
+ `\`import "${SERVER_ONLY_MARKER}"\` reached the browser. Move it under backend/ and reach it ` +
576
+ `from a route's backend half (x.backend.ts) or a *.fn.ts server function:\n${lines.join("\n")}`
507
577
  )
508
578
  }
509
579
 
@@ -540,7 +610,7 @@ export interface SizeReport {
540
610
  */
541
611
  export function aggregateSizeReport(chunks: readonly ChunkSize[]): SizeReport {
542
612
  const sorted = [...chunks].sort(
543
- (a, b) => b.gzip - a.gzip || b.bytes - a.bytes || a.name.localeCompare(b.name),
613
+ (a, b) => b.gzip - a.gzip || b.bytes - a.bytes || codeUnitOrder(a.name, b.name),
544
614
  )
545
615
  let totalBytes = 0
546
616
  let totalGzip = 0
@@ -618,6 +688,8 @@ export interface ManifestDrift {
618
688
  const MANIFEST_ROUTE_ENTRY = /^[ \t]+(["'])([^"'\n]+)\1\s*:\s*(?:m\d+\b|\(\s*\)\s*=>\s*import\b)/gm
619
689
  // The baked client-entry line: `export const clientEntry = "…"`.
620
690
  const MANIFEST_CLIENT_ENTRY = /export\s+const\s+clientEntry\s*=\s*["']([^"']+)["']/
691
+ // The baked stylesheet activation line: `generateServerManifest` emits this after the style maps.
692
+ const MANIFEST_CSS_LOADING = /export\s+const\s+cssLoading\s*=\s*["'](blocking|deferred)["']/
621
693
 
622
694
  /**
623
695
  * Extract the route-relative file list a committed server-manifest declares, as the same
@@ -642,6 +714,11 @@ export function parseManifestClientEntry(source: string): string | undefined {
642
714
  return MANIFEST_CLIENT_ENTRY.exec(source)?.[1]
643
715
  }
644
716
 
717
+ /** The baked framework stylesheet activation mode, or `undefined` for an older manifest. Pure. */
718
+ export function parseManifestCssLoading(source: string): CssLoadingMode | undefined {
719
+ return MANIFEST_CSS_LOADING.exec(source)?.[1] as CssLoadingMode | undefined
720
+ }
721
+
645
722
  // The baked asset lines `generateServerManifest` emits: `export const styles = […]` then
646
723
  // `export const routeStyles = {…}`, each a JSON literal running to the next `export const`. Extracted
647
724
  // with `indexOf` slicing rather than a lazy `[\s\S]*?` regex: the slice is multi-line by nature (a
@@ -653,7 +730,7 @@ function bakedManifestLiteral(source: string, name: string): string | undefined
653
730
  if (start === -1) return undefined
654
731
  const from = start + opener.length
655
732
  const end = source.indexOf("\nexport const ", from)
656
- return end === -1 ? undefined : source.slice(from, end)
733
+ return source.slice(from, end === -1 ? source.length : end).trim()
657
734
  }
658
735
 
659
736
  /** Parse a baked JSON literal captured from a committed manifest, tolerating a formatter's TRAILING COMMAS
package/src/build-vite.ts CHANGED
@@ -28,6 +28,13 @@ import {
28
28
  publicEnvDefines,
29
29
  } from "./build.ts"
30
30
  import type { BuildManifest, BuildTarget, Bundler, ServerBuild } from "./build-plan.ts"
31
+ import {
32
+ assertCssLoadingCompatible,
33
+ type CssLoadingMode,
34
+ DEFAULT_CSS_LOADING,
35
+ normalizeCssCodeSplit,
36
+ normalizeCssLoading,
37
+ } from "./css-contract.ts"
31
38
  import { discoverRoutes } from "./fs.ts"
32
39
  import { generateClientEntry, generateServerManifest } from "./index.ts"
33
40
  import { viteDedupePackages } from "./internal/identity-policy.ts"
@@ -37,12 +44,17 @@ import {
37
44
  collectDevelopmentParityInput,
38
45
  } from "./internal/parity.ts"
39
46
  import { vitePublicEnvPrefix } from "./internal/server-boundary.ts"
47
+ import { unsupportedBuiltin } from "./internal/target-compat.ts"
40
48
  import { importVite, isViteUnresolved } from "./internal/vite-import.ts"
41
49
  import { scopedName } from "./plugins/css-modules.ts"
42
50
  import { reproduciblePath } from "./plugins/kit.ts"
43
- import { viteLeakGuard } from "./plugins/vite-leak-guard.ts"
51
+ import {
52
+ viteAssetUrlGuard,
53
+ viteBareBuiltinExternal,
54
+ viteLeakGuard,
55
+ viteServerZoneGuard,
56
+ } from "./plugins/vite-leak-guard.ts"
44
57
  import { viteServerFnStub } from "./plugins/vite-server-fn.ts"
45
- import { viteServerOnlyEmpty } from "./plugins/vite-server-only.ts"
46
58
 
47
59
  // ---------------------------------------------------------------------------------------------------
48
60
  // Structural Vite typings - no hard `vite` dependency (mirrors vite.ts). Only the build API is used.
@@ -117,6 +129,17 @@ export interface BuildClientViteOptions extends Omit<BuildClientOptions, "plugin
117
129
  readonly vitePlugins?: readonly unknown[]
118
130
  /** Vite project root (default: the parent of `routesDir`). Manifest keys are relative to it. */
119
131
  readonly root?: string
132
+ /**
133
+ * Vite's CSS output strategy (default `true`). Set to `false` to emit one aggregate stylesheet for
134
+ * the whole client, preventing lazy-route prefetch from attaching additional stylesheets.
135
+ */
136
+ readonly cssCodeSplit?: boolean
137
+ /**
138
+ * Activation policy for framework-owned SSR stylesheet links (default `"blocking"`). `"deferred"`
139
+ * fetches with `media="print"` and the generated client promotes the links before mounting. It must
140
+ * be paired with `cssCodeSplit: false`; split lazy chunks can otherwise attach CSS during prefetch.
141
+ */
142
+ readonly cssLoading?: CssLoadingMode
120
143
  }
121
144
 
122
145
  /**
@@ -130,6 +153,9 @@ export interface BuildClientViteOptions extends Omit<BuildClientOptions, "plugin
130
153
  */
131
154
  export async function buildClientVite(options: BuildClientViteOptions): Promise<BuildManifest> {
132
155
  const { routesDir, outDir, clientModule } = options
156
+ const cssCodeSplit = normalizeCssCodeSplit(options.cssCodeSplit)
157
+ const cssLoading = normalizeCssLoading(options.cssLoading ?? DEFAULT_CSS_LOADING)
158
+ assertCssLoadingCompatible(cssCodeSplit, cssLoading)
133
159
  const root = resolvePath(options.root ?? dirname(routesDir))
134
160
  await assertIdentityParity(root)
135
161
  const resolve = options.resolve ?? ((file: string) => `${routesDir}/${file}`)
@@ -153,6 +179,8 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
153
179
  ...routeManifest.routes.map((r) => r.file),
154
180
  ...Object.values(routeManifest.layouts).map((l) => l.file),
155
181
  ...(routeManifest.notFound ? [routeManifest.notFound.file] : []),
182
+ // A nested `_404` is an entry only for its stylesheet: it renders on the server, unhydrated.
183
+ ...Object.values(routeManifest.notFounds ?? {}).map((page) => page.file),
156
184
  ]),
157
185
  ].sort()
158
186
 
@@ -180,7 +208,28 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
180
208
  const buildEnv = (typeof Bun !== "undefined" ? Bun.env : undefined) ?? process.env
181
209
  const publicDefines = publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", buildEnv)
182
210
  const vite = await loadVite()
183
- const leakGuard = viteLeakGuard()
211
+ // A worker is bundled by its own sub-build the main graph never sees, so it gets its own guard; the
212
+ // main guard then accepts the worker chunks that guard verified.
213
+ const verified = new Set<string>()
214
+ const exemptions = options.secretExemptions
215
+ const guardOptions = {
216
+ appRoot: dirname(routesDir),
217
+ routesDir,
218
+ root,
219
+ outDir: resolvePath(outDir),
220
+ generatedFiles: [entryFile],
221
+ verified,
222
+ ...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
223
+ secrets: { env: buildEnv, ...(exemptions ? { exemptions } : {}) },
224
+ }
225
+ // nifra copies `public/` after the build; the main guard scans it with the bundle.
226
+ const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public")
227
+ const leakGuard = viteLeakGuard({
228
+ ...guardOptions,
229
+ secrets: { ...guardOptions.secrets, ...(publicDir !== undefined ? { publicDir } : {}) },
230
+ })
231
+ const assetUrlGuard = viteAssetUrlGuard(guardOptions)
232
+ const workerGuards: { readonly leak?: string | undefined }[] = []
184
233
  try {
185
234
  await withSerializedNodeEnv(mode, () =>
186
235
  vite.build({
@@ -214,11 +263,22 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
214
263
  scopedName(reproduciblePath(filename), name),
215
264
  },
216
265
  },
217
- plugins: [...(options.vitePlugins ?? [])],
266
+ // First, so a bare built-in is named before Vite turns it into an anonymous stub.
267
+ // The asset URL guard last, so it reads each module after any framework plugin made it JavaScript.
268
+ plugins: [viteBareBuiltinExternal(), ...(options.vitePlugins ?? []), assetUrlGuard],
269
+ worker: {
270
+ plugins: () => {
271
+ const guard = viteLeakGuard(guardOptions)
272
+ const urlGuard = viteAssetUrlGuard(guardOptions)
273
+ workerGuards.push(guard, urlGuard)
274
+ return [viteBareBuiltinExternal(), viteServerFnStub(), guard, urlGuard]
275
+ },
276
+ },
218
277
  build: {
219
278
  outDir,
220
279
  emptyOutDir: false, // buildTargetWith owns outDir lifecycle; never let Vite wipe sibling files
221
280
  manifest: true,
281
+ cssCodeSplit,
222
282
  minify: options.minify !== false,
223
283
  rollupOptions: {
224
284
  // Keep `node:` builtins as external specifiers so the leak guard sees `node:crypto` and can
@@ -229,7 +289,7 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
229
289
  external: [/^node:/],
230
290
  input,
231
291
  // The leak guard is a Rollup plugin - last, so it sees the final graph.
232
- plugins: [viteServerFnStub(), viteServerOnlyEmpty(), leakGuard],
292
+ plugins: [viteServerFnStub(), leakGuard],
233
293
  output: {
234
294
  entryFileNames: "[name]-[hash].js",
235
295
  chunkFileNames: "[name]-[hash].js",
@@ -245,7 +305,11 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
245
305
  // that construction fails, replacing a precise "node:crypto reached the client bundle" with an
246
306
  // internal complaint naming nothing. Raising it here puts the message beyond anything that can
247
307
  // rewrite it; the bundler's error is kept as `cause` for the rest of the context.
248
- if (leakGuard.leak !== undefined) throw new Error(leakGuard.leak, { cause: error })
308
+ const leak =
309
+ assetUrlGuard.leak ??
310
+ leakGuard.leak ??
311
+ workerGuards.find((guard) => guard.leak !== undefined)?.leak
312
+ if (leak !== undefined) throw new Error(leak, { cause: error })
249
313
  throw error
250
314
  } finally {
251
315
  rmSync(entryDir, { recursive: true, force: true })
@@ -298,16 +362,28 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
298
362
  }
299
363
  return [...urls]
300
364
  }
301
- // CSS aggregate - every stylesheet the app emits, the always-safe fallback.
365
+ // CSS aggregate - every stylesheet the app emits, the always-safe fallback. With native Vite
366
+ // `cssCodeSplit: false`, the aggregate is a standalone manifest asset and is not listed in any
367
+ // JavaScript entry's `css` array. Collect both shapes so the framework manifest never loses the only
368
+ // stylesheet in a single-file build.
302
369
  const allCss = new Set<string>()
303
370
  for (const entry of Object.values(viteManifest)) {
371
+ if (entry.file.endsWith(".css")) allCss.add(url(entry.file))
304
372
  for (const css of entry.css ?? []) allCss.add(url(css))
305
373
  }
306
374
  const css = [...allCss]
307
375
  const routeStyles: Record<string, readonly string[]> = {}
308
- if (css.length > 0) {
376
+ // A single aggregate stylesheet is deliberately the route fallback. A per-route `[]` map would
377
+ // suppress it in `createWebApp` and produce unstyled SSR on every route.
378
+ if (css.length > 0 && cssCodeSplit) {
309
379
  for (const route of routeManifest.routes) routeStyles[route.id] = stylesFor(chainFiles(route))
310
380
  if (routeManifest.notFound) routeStyles._404 = stylesFor([routeManifest.notFound.file])
381
+ for (const [id, page] of Object.entries(routeManifest.notFounds ?? {})) {
382
+ routeStyles[id] = stylesFor([
383
+ ...page.layoutIds.map((layoutId) => routeManifest.layouts[layoutId]?.file ?? ""),
384
+ page.file,
385
+ ])
386
+ }
311
387
  }
312
388
 
313
389
  // assets - every emitted file + stylesheet across the manifest (chunks, entries, css).
@@ -318,7 +394,6 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
318
394
  for (const asset of entry.assets ?? []) assets.add(url(asset))
319
395
  }
320
396
 
321
- const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public")
322
397
  const publicFiles =
323
398
  publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : []
324
399
 
@@ -326,6 +401,8 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
326
401
  entry: url(bootstrap.file),
327
402
  assets: [...assets],
328
403
  routes,
404
+ cssCodeSplit,
405
+ cssLoading,
329
406
  ...(publicFiles.length > 0 ? { publicFiles } : {}),
330
407
  ...(css.length > 0 ? { css } : {}),
331
408
  ...(Object.keys(routeStyles).length > 0 ? { routeStyles } : {}),
@@ -343,6 +420,8 @@ export interface BuildServerViteOptions {
343
420
  readonly clientEntry: string
344
421
  readonly styles?: readonly string[] | undefined
345
422
  readonly routeStyles?: Readonly<Record<string, readonly string[]>> | undefined
423
+ /** How framework-owned stylesheet links should activate before hydration (default `"blocking"`). */
424
+ readonly cssLoading?: CssLoadingMode
346
425
  readonly resolve?: (file: string) => string
347
426
  readonly manifestFile?: string
348
427
  readonly vitePlugins?: readonly unknown[]
@@ -353,6 +432,10 @@ export interface BuildServerViteOptions {
353
432
  readonly target?: "browser" | "node" | "bun"
354
433
  /** Vite project root (default: the parent of `routesDir`). */
355
434
  readonly root?: string
435
+ /** Modules the build generated besides the entry and manifest (a target's adapter import). */
436
+ readonly generatedFiles?: readonly string[]
437
+ /** The public-env prefix browser code may read (default `"PUBLIC_"`). */
438
+ readonly publicEnvPrefix?: string
356
439
  }
357
440
 
358
441
  interface EdgeBundleChunk {
@@ -399,12 +482,13 @@ function edgeBuiltinGuard(): EdgeGuardPlugin {
399
482
  for (const output of Object.values(bundle)) {
400
483
  if (output.type !== "chunk") continue
401
484
  for (const specifier of [...(output.imports ?? []), ...(output.dynamicImports ?? [])]) {
402
- if (specifier.startsWith("node:")) builtins.add(specifier)
485
+ if (unsupportedBuiltin(specifier, "browser")) builtins.add(specifier)
403
486
  }
404
487
  for (const id of output.moduleIds ?? []) {
405
488
  const info = this.getModuleInfo(id)
406
489
  const imports = [...(info?.importedIds ?? []), ...(info?.dynamicallyImportedIds ?? [])]
407
- if (imports.some((specifier) => specifier.startsWith("node:"))) importers.add(id)
490
+ if (imports.some((specifier) => unsupportedBuiltin(specifier, "browser")))
491
+ importers.add(id)
408
492
  }
409
493
  }
410
494
  if (builtins.size === 0) return
@@ -434,7 +518,7 @@ function edgeBuiltinGuard(): EdgeGuardPlugin {
434
518
  * The client-leak guards are deliberately NOT run here: a server bundle's `node:` imports are legitimate.
435
519
  */
436
520
  export async function buildServerVite(options: BuildServerViteOptions): Promise<ServerBuild> {
437
- const { routesDir, serverEntry, outDir, clientEntry, styles, routeStyles } = options
521
+ const { routesDir, serverEntry, outDir, clientEntry, styles, routeStyles, cssLoading } = options
438
522
  const root = resolvePath(options.root ?? dirname(routesDir))
439
523
  const entryDir = dirname(serverEntry)
440
524
  const manifestFile = options.manifestFile ?? "server-manifest.ts"
@@ -451,7 +535,13 @@ export async function buildServerVite(options: BuildServerViteOptions): Promise<
451
535
  const routeManifest = discoverRoutes(routesDir)
452
536
  writeFileSync(
453
537
  `${entryDir}/${manifestFile}`,
454
- generateServerManifest(routeManifest, { resolve, clientEntry, styles, routeStyles }),
538
+ generateServerManifest(routeManifest, {
539
+ resolve,
540
+ clientEntry,
541
+ styles,
542
+ routeStyles,
543
+ ...(cssLoading !== undefined ? { cssLoading } : {}),
544
+ }),
455
545
  )
456
546
 
457
547
  // Pin the mode to match NODE_ENV - the react plugin's JSX runtime (jsx vs jsxDEV) follows it, and a
@@ -459,6 +549,16 @@ export async function buildServerVite(options: BuildServerViteOptions): Promise<
459
549
  const mode = options.minify === false ? "development" : "production"
460
550
  const vite = await loadVite()
461
551
  const edgeGuard = edgeBuiltinGuard()
552
+ const zoneGuard = viteServerZoneGuard({
553
+ appRoot: root,
554
+ routesDir: resolvePath(routesDir),
555
+ generatedFiles: [
556
+ serverEntry,
557
+ join(entryDir, manifestFile),
558
+ ...(options.generatedFiles ?? []),
559
+ ].map((file) => resolvePath(file)),
560
+ ...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
561
+ })
462
562
  try {
463
563
  await withSerializedNodeEnv(mode, () =>
464
564
  vite.build({
@@ -487,7 +587,7 @@ export async function buildServerVite(options: BuildServerViteOptions): Promise<
487
587
  // Keep builtins external so Node/Bun use their native implementations. Edge builds add a
488
588
  // generateBundle guard below, so an external specifier can never silently ship to workerd.
489
589
  external: [/^node:/],
490
- ...(edge ? { plugins: [edgeGuard] } : {}),
590
+ plugins: [...(edge ? [edgeGuard] : []), zoneGuard],
491
591
  // ONE self-contained `server.js`. `inlineDynamicImports` forces the ENTRY chunk to absorb
492
592
  // every module - the app, the adapter, react/react-dom, @nifrajs/* - so no second chunk is
493
593
  // emitted. It is NOT redundant with `ssr.noExternal`: `noExternal` decides what gets bundled,
@@ -512,7 +612,8 @@ export async function buildServerVite(options: BuildServerViteOptions): Promise<
512
612
  } catch (error) {
513
613
  // Re-raise the guard's own message from here, where no bundler error-reporting path can rewrite it
514
614
  // (see the same handling in buildClientVite).
515
- if (edgeGuard.leak !== undefined) throw new Error(edgeGuard.leak, { cause: error })
615
+ const leak = zoneGuard.leak ?? edgeGuard.leak
616
+ if (leak !== undefined) throw new Error(leak, { cause: error })
516
617
  throw error
517
618
  }
518
619
 
@@ -536,8 +637,11 @@ export const viteBundler: Bundler = {
536
637
  ...(input.plugins ? { vitePlugins: input.plugins } : {}),
537
638
  ...(input.conditions ? { conditions: input.conditions } : {}),
538
639
  ...(input.define ? { define: input.define } : {}),
640
+ ...(input.cssCodeSplit !== undefined ? { cssCodeSplit: input.cssCodeSplit } : {}),
641
+ ...(input.cssLoading !== undefined ? { cssLoading: input.cssLoading } : {}),
539
642
  ...(input.publicDir !== undefined ? { publicDir: input.publicDir } : {}),
540
643
  ...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
644
+ ...(input.secretExemptions !== undefined ? { secretExemptions: input.secretExemptions } : {}),
541
645
  ...(input.root ? { root: input.root } : {}),
542
646
  }),
543
647
  buildServer: (input) =>
@@ -547,9 +651,14 @@ export const viteBundler: Bundler = {
547
651
  outDir: input.outDir,
548
652
  clientEntry: input.clientEntry,
549
653
  target: input.target,
654
+ ...(input.styles !== undefined ? { styles: input.styles } : {}),
655
+ ...(input.routeStyles !== undefined ? { routeStyles: input.routeStyles } : {}),
550
656
  ...(input.plugins ? { vitePlugins: input.plugins } : {}),
551
657
  ...(input.define ? { define: input.define } : {}),
658
+ ...(input.cssLoading !== undefined ? { cssLoading: input.cssLoading } : {}),
552
659
  ...(input.root ? { root: input.root } : {}),
660
+ ...(input.generatedFiles !== undefined ? { generatedFiles: input.generatedFiles } : {}),
661
+ ...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
553
662
  }),
554
663
  }
555
664