@nifrajs/web 4.0.2 → 5.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 (384) hide show
  1. package/README.md +11 -0
  2. package/dist/build-vite.d.ts +19 -0
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +31 -10
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +8 -10
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +90 -31
  9. package/dist/build.js.map +1 -1
  10. package/dist/cdn.d.ts +8 -0
  11. package/dist/cdn.d.ts.map +1 -1
  12. package/dist/cdn.js +14 -15
  13. package/dist/cdn.js.map +1 -1
  14. package/dist/client.d.ts +1 -1
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +25 -7
  17. package/dist/client.js.map +1 -1
  18. package/dist/deferred.d.ts +2 -2
  19. package/dist/deferred.d.ts.map +1 -1
  20. package/dist/deferred.js +28 -7
  21. package/dist/deferred.js.map +1 -1
  22. package/dist/dev-feed.d.ts.map +1 -1
  23. package/dist/dev-feed.js +24 -5
  24. package/dist/dev-feed.js.map +1 -1
  25. package/dist/dev-session.d.ts.map +1 -1
  26. package/dist/dev-session.js +4 -14
  27. package/dist/dev-session.js.map +1 -1
  28. package/dist/dev.d.ts +7 -17
  29. package/dist/dev.d.ts.map +1 -1
  30. package/dist/dev.js +13 -1
  31. package/dist/dev.js.map +1 -1
  32. package/dist/draft.d.ts +8 -5
  33. package/dist/draft.d.ts.map +1 -1
  34. package/dist/draft.js +30 -13
  35. package/dist/draft.js.map +1 -1
  36. package/dist/fonts-google.d.ts.map +1 -1
  37. package/dist/fonts-google.js +7 -0
  38. package/dist/fonts-google.js.map +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/internal/codegen.d.ts +1 -0
  44. package/dist/internal/codegen.d.ts.map +1 -1
  45. package/dist/internal/codegen.js +56 -29
  46. package/dist/internal/codegen.js.map +1 -1
  47. package/dist/internal/disposable-dir.d.ts +8 -0
  48. package/dist/internal/disposable-dir.d.ts.map +1 -0
  49. package/dist/internal/disposable-dir.js +25 -0
  50. package/dist/internal/disposable-dir.js.map +1 -0
  51. package/dist/internal/entry-chunks.d.ts +9 -0
  52. package/dist/internal/entry-chunks.d.ts.map +1 -0
  53. package/dist/internal/entry-chunks.js +15 -0
  54. package/dist/internal/entry-chunks.js.map +1 -0
  55. package/dist/internal/evidence-traversal.d.ts +12 -0
  56. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  57. package/dist/internal/evidence-traversal.js +48 -0
  58. package/dist/internal/evidence-traversal.js.map +1 -0
  59. package/dist/internal/head-merge.d.ts +6 -0
  60. package/dist/internal/head-merge.d.ts.map +1 -1
  61. package/dist/internal/head-merge.js +9 -0
  62. package/dist/internal/head-merge.js.map +1 -1
  63. package/dist/internal/js-string.d.ts +2 -0
  64. package/dist/internal/js-string.d.ts.map +1 -1
  65. package/dist/internal/js-string.js +5 -1
  66. package/dist/internal/js-string.js.map +1 -1
  67. package/dist/internal/mount-shadow.js +1 -1
  68. package/dist/internal/mount-shadow.js.map +1 -1
  69. package/dist/internal/output-guard.d.ts +7 -0
  70. package/dist/internal/output-guard.d.ts.map +1 -1
  71. package/dist/internal/output-guard.js +80 -8
  72. package/dist/internal/output-guard.js.map +1 -1
  73. package/dist/internal/page-execution.d.ts +6 -0
  74. package/dist/internal/page-execution.d.ts.map +1 -1
  75. package/dist/internal/page-execution.js +377 -108
  76. package/dist/internal/page-execution.js.map +1 -1
  77. package/dist/internal/progressive-cookies.d.ts +14 -0
  78. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  79. package/dist/internal/progressive-cookies.js +43 -0
  80. package/dist/internal/progressive-cookies.js.map +1 -0
  81. package/dist/internal/public-env.d.ts +2 -0
  82. package/dist/internal/public-env.d.ts.map +1 -0
  83. package/dist/internal/public-env.js +13 -0
  84. package/dist/internal/public-env.js.map +1 -0
  85. package/dist/internal/render-document.d.ts +1 -0
  86. package/dist/internal/render-document.d.ts.map +1 -1
  87. package/dist/internal/render-document.js +28 -5
  88. package/dist/internal/render-document.js.map +1 -1
  89. package/dist/internal/response-controls.d.ts +2 -0
  90. package/dist/internal/response-controls.d.ts.map +1 -1
  91. package/dist/internal/response-controls.js +26 -16
  92. package/dist/internal/response-controls.js.map +1 -1
  93. package/dist/internal/revive-dates.d.ts +16 -0
  94. package/dist/internal/revive-dates.d.ts.map +1 -0
  95. package/dist/internal/revive-dates.js +32 -0
  96. package/dist/internal/revive-dates.js.map +1 -0
  97. package/dist/internal/route-path.d.ts +8 -0
  98. package/dist/internal/route-path.d.ts.map +1 -0
  99. package/dist/internal/route-path.js +8 -0
  100. package/dist/internal/route-path.js.map +1 -0
  101. package/dist/internal/secret-scan.d.ts.map +1 -1
  102. package/dist/internal/secret-scan.js +88 -20
  103. package/dist/internal/secret-scan.js.map +1 -1
  104. package/dist/internal/server-boundary.d.ts.map +1 -1
  105. package/dist/internal/server-boundary.js +33 -5
  106. package/dist/internal/server-boundary.js.map +1 -1
  107. package/dist/internal/web-app.d.ts +21 -16
  108. package/dist/internal/web-app.d.ts.map +1 -1
  109. package/dist/internal/web-app.js +73 -21
  110. package/dist/internal/web-app.js.map +1 -1
  111. package/dist/internal/wire-date.d.ts +3 -0
  112. package/dist/internal/wire-date.d.ts.map +1 -0
  113. package/dist/internal/wire-date.js +14 -0
  114. package/dist/internal/wire-date.js.map +1 -0
  115. package/dist/internal/zone-graph.d.ts +4 -0
  116. package/dist/internal/zone-graph.d.ts.map +1 -1
  117. package/dist/internal/zone-graph.js +54 -14
  118. package/dist/internal/zone-graph.js.map +1 -1
  119. package/dist/isr.d.ts +58 -2
  120. package/dist/isr.d.ts.map +1 -1
  121. package/dist/isr.js +296 -39
  122. package/dist/isr.js.map +1 -1
  123. package/dist/llms-txt.d.ts +2 -0
  124. package/dist/llms-txt.d.ts.map +1 -1
  125. package/dist/llms-txt.js +29 -3
  126. package/dist/llms-txt.js.map +1 -1
  127. package/dist/manifest.d.ts +28 -4
  128. package/dist/manifest.d.ts.map +1 -1
  129. package/dist/manifest.js +3 -0
  130. package/dist/manifest.js.map +1 -1
  131. package/dist/open-resolved.d.ts +18 -0
  132. package/dist/open-resolved.d.ts.map +1 -0
  133. package/dist/open-resolved.js +62 -0
  134. package/dist/open-resolved.js.map +1 -0
  135. package/dist/plugins/kit.d.ts +1 -0
  136. package/dist/plugins/kit.d.ts.map +1 -1
  137. package/dist/plugins/kit.js +26 -11
  138. package/dist/plugins/kit.js.map +1 -1
  139. package/dist/plugins/vite-leak-guard.d.ts +3 -1
  140. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  141. package/dist/plugins/vite-leak-guard.js +4 -0
  142. package/dist/plugins/vite-leak-guard.js.map +1 -1
  143. package/dist/public-dir.d.ts +8 -1
  144. package/dist/public-dir.d.ts.map +1 -1
  145. package/dist/public-dir.js +17 -6
  146. package/dist/public-dir.js.map +1 -1
  147. package/dist/query.d.ts +13 -4
  148. package/dist/query.d.ts.map +1 -1
  149. package/dist/query.js +89 -26
  150. package/dist/query.js.map +1 -1
  151. package/dist/render-seam.d.ts +2 -0
  152. package/dist/render-seam.d.ts.map +1 -1
  153. package/dist/render-seam.js +1 -0
  154. package/dist/render-seam.js.map +1 -1
  155. package/dist/route-types.d.ts.map +1 -1
  156. package/dist/route-types.js +20 -9
  157. package/dist/route-types.js.map +1 -1
  158. package/dist/router.d.ts +9 -2
  159. package/dist/router.d.ts.map +1 -1
  160. package/dist/router.js +39 -46
  161. package/dist/router.js.map +1 -1
  162. package/dist/server-components/build-info.d.ts +3 -0
  163. package/dist/server-components/build-info.d.ts.map +1 -0
  164. package/dist/server-components/build-info.js +7 -0
  165. package/dist/server-components/build-info.js.map +1 -0
  166. package/dist/server-components/build-target.d.ts +12 -0
  167. package/dist/server-components/build-target.d.ts.map +1 -0
  168. package/dist/server-components/build-target.js +153 -0
  169. package/dist/server-components/build-target.js.map +1 -0
  170. package/dist/server-components/build.d.ts +24 -0
  171. package/dist/server-components/build.d.ts.map +1 -0
  172. package/dist/server-components/build.js +366 -0
  173. package/dist/server-components/build.js.map +1 -0
  174. package/dist/server-components/bundle-proof.d.ts +3 -0
  175. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  176. package/dist/server-components/bundle-proof.js +23 -0
  177. package/dist/server-components/bundle-proof.js.map +1 -0
  178. package/dist/server-components/client.d.ts +46 -0
  179. package/dist/server-components/client.d.ts.map +1 -0
  180. package/dist/server-components/client.js +594 -0
  181. package/dist/server-components/client.js.map +1 -0
  182. package/dist/server-components/definition.d.ts +11 -0
  183. package/dist/server-components/definition.d.ts.map +1 -0
  184. package/dist/server-components/definition.js +94 -0
  185. package/dist/server-components/definition.js.map +1 -0
  186. package/dist/server-components/dev-http.d.ts +5 -0
  187. package/dist/server-components/dev-http.d.ts.map +1 -0
  188. package/dist/server-components/dev-http.js +71 -0
  189. package/dist/server-components/dev-http.js.map +1 -0
  190. package/dist/server-components/error.d.ts +9 -0
  191. package/dist/server-components/error.d.ts.map +1 -0
  192. package/dist/server-components/error.js +24 -0
  193. package/dist/server-components/error.js.map +1 -0
  194. package/dist/server-components/frames.d.ts +7 -0
  195. package/dist/server-components/frames.d.ts.map +1 -0
  196. package/dist/server-components/frames.js +169 -0
  197. package/dist/server-components/frames.js.map +1 -0
  198. package/dist/server-components/html-policy.d.ts +6 -0
  199. package/dist/server-components/html-policy.d.ts.map +1 -0
  200. package/dist/server-components/html-policy.js +75 -0
  201. package/dist/server-components/html-policy.js.map +1 -0
  202. package/dist/server-components/html.d.ts +4 -0
  203. package/dist/server-components/html.d.ts.map +1 -0
  204. package/dist/server-components/html.js +67 -0
  205. package/dist/server-components/html.js.map +1 -0
  206. package/dist/server-components/islands.d.ts +44 -0
  207. package/dist/server-components/islands.d.ts.map +1 -0
  208. package/dist/server-components/islands.js +42 -0
  209. package/dist/server-components/islands.js.map +1 -0
  210. package/dist/server-components/native.d.ts +4 -0
  211. package/dist/server-components/native.d.ts.map +1 -0
  212. package/dist/server-components/native.js +81 -0
  213. package/dist/server-components/native.js.map +1 -0
  214. package/dist/server-components/portal.d.ts +6 -0
  215. package/dist/server-components/portal.d.ts.map +1 -0
  216. package/dist/server-components/portal.js +35 -0
  217. package/dist/server-components/portal.js.map +1 -0
  218. package/dist/server-components/props.d.ts +3 -0
  219. package/dist/server-components/props.d.ts.map +1 -0
  220. package/dist/server-components/props.js +14 -0
  221. package/dist/server-components/props.js.map +1 -0
  222. package/dist/server-components/protocol.d.ts +44 -0
  223. package/dist/server-components/protocol.d.ts.map +1 -0
  224. package/dist/server-components/protocol.js +139 -0
  225. package/dist/server-components/protocol.js.map +1 -0
  226. package/dist/server-components/range.d.ts +24 -0
  227. package/dist/server-components/range.d.ts.map +1 -0
  228. package/dist/server-components/range.js +263 -0
  229. package/dist/server-components/range.js.map +1 -0
  230. package/dist/server-components/registry.d.ts +6 -0
  231. package/dist/server-components/registry.d.ts.map +1 -0
  232. package/dist/server-components/registry.js +30 -0
  233. package/dist/server-components/registry.js.map +1 -0
  234. package/dist/server-components/response.d.ts +4 -0
  235. package/dist/server-components/response.d.ts.map +1 -0
  236. package/dist/server-components/response.js +32 -0
  237. package/dist/server-components/response.js.map +1 -0
  238. package/dist/server-components/router-integration.d.ts +9 -0
  239. package/dist/server-components/router-integration.d.ts.map +1 -0
  240. package/dist/server-components/router-integration.js +29 -0
  241. package/dist/server-components/router-integration.js.map +1 -0
  242. package/dist/server-components/runtime.d.ts +14 -0
  243. package/dist/server-components/runtime.d.ts.map +1 -0
  244. package/dist/server-components/runtime.js +276 -0
  245. package/dist/server-components/runtime.js.map +1 -0
  246. package/dist/server-components/scheduler.d.ts +14 -0
  247. package/dist/server-components/scheduler.d.ts.map +1 -0
  248. package/dist/server-components/scheduler.js +74 -0
  249. package/dist/server-components/scheduler.js.map +1 -0
  250. package/dist/server-components/schema.d.ts +4 -0
  251. package/dist/server-components/schema.d.ts.map +1 -0
  252. package/dist/server-components/schema.js +77 -0
  253. package/dist/server-components/schema.js.map +1 -0
  254. package/dist/server-components/server-entry.d.ts +12 -0
  255. package/dist/server-components/server-entry.d.ts.map +1 -0
  256. package/dist/server-components/server-entry.js +90 -0
  257. package/dist/server-components/server-entry.js.map +1 -0
  258. package/dist/server-components/source.d.ts +15 -0
  259. package/dist/server-components/source.d.ts.map +1 -0
  260. package/dist/server-components/source.js +269 -0
  261. package/dist/server-components/source.js.map +1 -0
  262. package/dist/server-components/state.d.ts +4 -0
  263. package/dist/server-components/state.d.ts.map +1 -0
  264. package/dist/server-components/state.js +123 -0
  265. package/dist/server-components/state.js.map +1 -0
  266. package/dist/server-components/stream.d.ts +6 -0
  267. package/dist/server-components/stream.d.ts.map +1 -0
  268. package/dist/server-components/stream.js +70 -0
  269. package/dist/server-components/stream.js.map +1 -0
  270. package/dist/server-components/trusted-html.d.ts +3 -0
  271. package/dist/server-components/trusted-html.d.ts.map +1 -0
  272. package/dist/server-components/trusted-html.js +14 -0
  273. package/dist/server-components/trusted-html.js.map +1 -0
  274. package/dist/server-components/types.d.ts +107 -0
  275. package/dist/server-components/types.d.ts.map +1 -0
  276. package/dist/server-components/types.js +2 -0
  277. package/dist/server-components/types.js.map +1 -0
  278. package/dist/server-components/typescript.d.ts +11 -0
  279. package/dist/server-components/typescript.d.ts.map +1 -0
  280. package/dist/server-components/typescript.js +28 -0
  281. package/dist/server-components/typescript.js.map +1 -0
  282. package/dist/server-components/value.d.ts +5 -0
  283. package/dist/server-components/value.d.ts.map +1 -0
  284. package/dist/server-components/value.js +77 -0
  285. package/dist/server-components/value.js.map +1 -0
  286. package/dist/server-components/vite.d.ts +12 -0
  287. package/dist/server-components/vite.d.ts.map +1 -0
  288. package/dist/server-components/vite.js +197 -0
  289. package/dist/server-components/vite.js.map +1 -0
  290. package/dist/server-components.d.ts +5 -0
  291. package/dist/server-components.d.ts.map +1 -0
  292. package/dist/server-components.js +4 -0
  293. package/dist/server-components.js.map +1 -0
  294. package/dist/service-worker.d.ts +8 -1
  295. package/dist/service-worker.d.ts.map +1 -1
  296. package/dist/service-worker.js +20 -6
  297. package/dist/service-worker.js.map +1 -1
  298. package/dist/vite.d.ts +12 -0
  299. package/dist/vite.d.ts.map +1 -1
  300. package/dist/vite.js +7 -1
  301. package/dist/vite.js.map +1 -1
  302. package/dist/zones.d.ts +40 -0
  303. package/dist/zones.d.ts.map +1 -1
  304. package/dist/zones.js +170 -6
  305. package/dist/zones.js.map +1 -1
  306. package/package.json +97 -4
  307. package/src/build-vite.ts +31 -9
  308. package/src/build.ts +109 -31
  309. package/src/cdn.ts +18 -12
  310. package/src/client.ts +25 -6
  311. package/src/deferred.ts +27 -9
  312. package/src/dev-feed.ts +24 -5
  313. package/src/dev-session.ts +4 -13
  314. package/src/dev.ts +29 -1
  315. package/src/draft.ts +35 -14
  316. package/src/fonts-google.ts +10 -0
  317. package/src/index.ts +3 -2
  318. package/src/internal/codegen.ts +60 -29
  319. package/src/internal/disposable-dir.ts +24 -0
  320. package/src/internal/entry-chunks.ts +18 -0
  321. package/src/internal/evidence-traversal.ts +57 -0
  322. package/src/internal/head-merge.ts +16 -0
  323. package/src/internal/js-string.ts +6 -1
  324. package/src/internal/mount-shadow.ts +1 -1
  325. package/src/internal/output-guard.ts +88 -7
  326. package/src/internal/page-execution.ts +506 -108
  327. package/src/internal/progressive-cookies.ts +60 -0
  328. package/src/internal/public-env.ts +14 -0
  329. package/src/internal/render-document.ts +26 -11
  330. package/src/internal/response-controls.ts +31 -14
  331. package/src/internal/revive-dates.ts +35 -0
  332. package/src/internal/route-path.ts +8 -0
  333. package/src/internal/secret-scan.ts +90 -18
  334. package/src/internal/server-boundary.ts +29 -5
  335. package/src/internal/web-app.ts +116 -37
  336. package/src/internal/wire-date.ts +16 -0
  337. package/src/internal/zone-graph.ts +59 -19
  338. package/src/isr.ts +351 -43
  339. package/src/llms-txt.ts +32 -3
  340. package/src/manifest.ts +28 -4
  341. package/src/open-resolved.ts +64 -0
  342. package/src/plugins/kit.ts +31 -11
  343. package/src/plugins/vite-leak-guard.ts +5 -1
  344. package/src/public-dir.ts +27 -8
  345. package/src/query.ts +107 -26
  346. package/src/render-seam.ts +2 -0
  347. package/src/route-types.ts +34 -7
  348. package/src/router.ts +58 -55
  349. package/src/server-components/build-info.ts +11 -0
  350. package/src/server-components/build-target.ts +194 -0
  351. package/src/server-components/build.ts +419 -0
  352. package/src/server-components/bundle-proof.ts +33 -0
  353. package/src/server-components/client.ts +663 -0
  354. package/src/server-components/definition.ts +125 -0
  355. package/src/server-components/dev-http.ts +75 -0
  356. package/src/server-components/error.ts +24 -0
  357. package/src/server-components/frames.ts +181 -0
  358. package/src/server-components/html-policy.ts +94 -0
  359. package/src/server-components/html.ts +81 -0
  360. package/src/server-components/islands.ts +111 -0
  361. package/src/server-components/native.ts +101 -0
  362. package/src/server-components/portal.ts +42 -0
  363. package/src/server-components/props.ts +19 -0
  364. package/src/server-components/protocol.ts +167 -0
  365. package/src/server-components/range.ts +313 -0
  366. package/src/server-components/registry.ts +34 -0
  367. package/src/server-components/response.ts +44 -0
  368. package/src/server-components/router-integration.ts +40 -0
  369. package/src/server-components/runtime.ts +376 -0
  370. package/src/server-components/scheduler.ts +84 -0
  371. package/src/server-components/schema.ts +69 -0
  372. package/src/server-components/server-entry.ts +98 -0
  373. package/src/server-components/source.ts +324 -0
  374. package/src/server-components/state.ts +143 -0
  375. package/src/server-components/stream.ts +71 -0
  376. package/src/server-components/trusted-html.ts +21 -0
  377. package/src/server-components/types.ts +138 -0
  378. package/src/server-components/typescript.ts +39 -0
  379. package/src/server-components/value.ts +82 -0
  380. package/src/server-components/vite.ts +242 -0
  381. package/src/server-components.ts +32 -0
  382. package/src/service-worker.ts +24 -7
  383. package/src/vite.ts +8 -1
  384. package/src/zones.ts +208 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/web",
3
- "version": "4.0.2",
3
+ "version": "5.0.0",
4
4
  "description": "Framework-agnostic SSR core for nifra - the render seam + HTML document orchestration.",
5
5
  "keywords": [
6
6
  "bun",
@@ -227,6 +227,76 @@
227
227
  "bun": "./src/internal/matches-runtime.ts",
228
228
  "types": "./dist/internal/matches-runtime.d.ts",
229
229
  "default": "./dist/internal/matches-runtime.js"
230
+ },
231
+ "./server-components": {
232
+ "bun": "./src/server-components.ts",
233
+ "types": "./dist/server-components.d.ts",
234
+ "default": "./dist/server-components.js"
235
+ },
236
+ "./server-components/client": {
237
+ "bun": "./src/server-components/client.ts",
238
+ "types": "./dist/server-components/client.d.ts",
239
+ "default": "./dist/server-components/client.js"
240
+ },
241
+ "./server-components/vite": {
242
+ "bun": "./src/server-components/vite.ts",
243
+ "types": "./dist/server-components/vite.d.ts",
244
+ "default": "./dist/server-components/vite.js"
245
+ },
246
+ "./server-components/build": {
247
+ "bun": "./src/server-components/build.ts",
248
+ "types": "./dist/server-components/build.d.ts",
249
+ "default": "./dist/server-components/build.js"
250
+ },
251
+ "./server-components/source": {
252
+ "bun": "./src/server-components/source.ts",
253
+ "types": "./dist/server-components/source.d.ts",
254
+ "default": "./dist/server-components/source.js"
255
+ },
256
+ "./server-components/props": {
257
+ "bun": "./src/server-components/props.ts",
258
+ "types": "./dist/server-components/props.d.ts",
259
+ "default": "./dist/server-components/props.js"
260
+ },
261
+ "./server-components/stream": {
262
+ "bun": "./src/server-components/stream.ts",
263
+ "types": "./dist/server-components/stream.d.ts",
264
+ "default": "./dist/server-components/stream.js"
265
+ },
266
+ "./server-components/state": {
267
+ "bun": "./src/server-components/state.ts",
268
+ "types": "./dist/server-components/state.d.ts",
269
+ "default": "./dist/server-components/state.js"
270
+ },
271
+ "./server-components/native": {
272
+ "bun": "./src/server-components/native.ts",
273
+ "types": "./dist/server-components/native.d.ts",
274
+ "default": "./dist/server-components/native.js"
275
+ },
276
+ "./server-components/islands": {
277
+ "bun": "./src/server-components/islands.ts",
278
+ "types": "./dist/server-components/islands.d.ts",
279
+ "default": "./dist/server-components/islands.js"
280
+ },
281
+ "./server-components/range": {
282
+ "bun": "./src/server-components/range.ts",
283
+ "types": "./dist/server-components/range.d.ts",
284
+ "default": "./dist/server-components/range.js"
285
+ },
286
+ "./server-components/build-info": {
287
+ "bun": "./src/server-components/build-info.ts",
288
+ "types": "./dist/server-components/build-info.d.ts",
289
+ "default": "./dist/server-components/build-info.js"
290
+ },
291
+ "./server-components/portal": {
292
+ "bun": "./src/server-components/portal.ts",
293
+ "types": "./dist/server-components/portal.d.ts",
294
+ "default": "./dist/server-components/portal.js"
295
+ },
296
+ "./server-components/trusted-html": {
297
+ "bun": "./src/server-components/trusted-html.ts",
298
+ "types": "./dist/server-components/trusted-html.d.ts",
299
+ "default": "./dist/server-components/trusted-html.js"
230
300
  }
231
301
  },
232
302
  "files": [
@@ -244,8 +314,9 @@
244
314
  "access": "public"
245
315
  },
246
316
  "dependencies": {
247
- "@nifrajs/core": "4.0.2",
248
- "@nifrajs/island-trigger": "4.0.2"
317
+ "@nifrajs/core": "5.0.0",
318
+ "@nifrajs/island-trigger": "5.0.0",
319
+ "parse5": "8.0.1"
249
320
  },
250
321
  "peerDependencies": {
251
322
  "@babel/core": "^7.26.0",
@@ -255,7 +326,9 @@
255
326
  "@stylexjs/babel-plugin": "^0.19.0",
256
327
  "sass": "^1.0.0",
257
328
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
258
- "web-vitals": "^6.0.0"
329
+ "web-vitals": "^6.0.0",
330
+ "typescript": "^5.9.0 || ^6.0.0",
331
+ "@vitejs/plugin-rsc": "0.5.35"
259
332
  },
260
333
  "peerDependenciesMeta": {
261
334
  "@babel/core": {
@@ -281,6 +354,12 @@
281
354
  },
282
355
  "web-vitals": {
283
356
  "optional": true
357
+ },
358
+ "typescript": {
359
+ "optional": true
360
+ },
361
+ "@vitejs/plugin-rsc": {
362
+ "optional": true
284
363
  }
285
364
  },
286
365
  "devDependencies": {
@@ -292,10 +371,24 @@
292
371
  "@nifrajs/schema": "workspace:*",
293
372
  "@nifrajs/test-utils": "workspace:*",
294
373
  "@nifrajs/web-react": "workspace:*",
374
+ "@preact/preset-vite": "2.10.5",
295
375
  "@stylexjs/babel-plugin": "^0.19.0",
296
376
  "@stylexjs/stylex": "^0.19.0",
377
+ "@sveltejs/vite-plugin-svelte": "7.3.1",
378
+ "@types/react": "19.2.17",
379
+ "@vitejs/plugin-rsc": "0.5.35",
380
+ "@vitejs/plugin-vue": "6.0.9",
381
+ "playwright": "1.63.0",
382
+ "preact": "10.29.8",
383
+ "preact-render-to-string": "6.7.0",
297
384
  "sass": "^1.77.0",
385
+ "solid-js": "1.9.14",
386
+ "svelte": "5.56.8",
387
+ "svelte-check": "4.7.6",
298
388
  "vite": "^8.2.1",
389
+ "vite-plugin-solid": "2.11.14",
390
+ "vue": "3.5.40",
391
+ "vue-tsc": "3.3.12",
299
392
  "web-vitals": "^6.2.2"
300
393
  },
301
394
  "repository": {
package/src/build-vite.ts CHANGED
@@ -17,8 +17,10 @@
17
17
  * error the Bun build raises. A second production pipeline without those guards is exactly the hole the
18
18
  * bundler-neutral module graph was built to close.
19
19
  */
20
+
20
21
  import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
21
22
  import { dirname, join, relative, resolve as resolvePath } from "node:path"
23
+ import { singleCopyPackageNames } from "@nifrajs/core/single-copy"
22
24
  import {
23
25
  type BuildClientOptions,
24
26
  type BuildTargetOptions,
@@ -72,6 +74,7 @@ type ViteBuildManifest = Readonly<Record<string, ViteManifestEntry>>
72
74
 
73
75
  interface ViteModule {
74
76
  build(config: Record<string, unknown>): Promise<unknown>
77
+ loadEnv(mode: string, envDir: string, prefixes: string): Record<string, string>
75
78
  }
76
79
 
77
80
  /** Load the app's Vite. Resolved from `root` so it's the project's copy, not this package's. */
@@ -181,6 +184,8 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
181
184
  ...(routeManifest.notFound ? [routeManifest.notFound.file] : []),
182
185
  // A nested `_404` is an entry only for its stylesheet: it renders on the server, unhydrated.
183
186
  ...Object.values(routeManifest.notFounds ?? {}).map((page) => page.file),
187
+ // A `_loading` page is an entry for its stylesheet too: the bootstrap imports it lazily.
188
+ ...Object.values(routeManifest.loadings ?? {}).map((page) => page.file),
184
189
  ]),
185
190
  ].sort()
186
191
 
@@ -220,7 +225,11 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
220
225
  generatedFiles: [entryFile],
221
226
  verified,
222
227
  ...(options.publicEnvPrefix !== undefined ? { publicEnvPrefix: options.publicEnvPrefix } : {}),
223
- secrets: { env: buildEnv, ...(exemptions ? { exemptions } : {}) },
228
+ // Vite reads `.env.<mode>` files too; a value only there is still the app's secret to keep.
229
+ secrets: {
230
+ env: { ...vite.loadEnv(mode, root, ""), ...buildEnv },
231
+ ...(exemptions ? { exemptions } : {}),
232
+ },
224
233
  }
225
234
  // nifra copies `public/` after the build; the main guard scans it with the bundle.
226
235
  const publicDir = options.publicDir === false ? undefined : (options.publicDir ?? "public")
@@ -251,7 +260,7 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
251
260
  // Same framework coverage as the Bun build's dedupe plugins, from the one policy table: one
252
261
  // physical copy of react/react-dom, preact, and svelte, or a hook-using route gets a second
253
262
  // dispatcher/options global. No-op per framework the app does not use.
254
- dedupe: [...viteDedupePackages()],
263
+ dedupe: [...viteDedupePackages(), ...singleCopyPackageNames(root)],
255
264
  ...(options.conditions ? { conditions: [...options.conditions] } : {}),
256
265
  },
257
266
  css: {
@@ -352,14 +361,22 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
352
361
  routes._404 = c !== undefined ? [c] : []
353
362
  }
354
363
 
355
- // CSS per route - Vite's manifest `css` is the transitive stylesheet set for that entry.
364
+ // CSS per route. A manifest entry's `css` lists only that chunk's own stylesheets, so a page also
365
+ // needs those of every chunk it imports statically (a component two routes share gets a chunk of
366
+ // its own) and the bootstrap's, which every page loads (a stylesheet the client module imports).
367
+ // The bootstrap's come first: the whole-app stylesheet before any route-scoped one.
356
368
  const stylesFor = (files: readonly string[]): readonly string[] => {
357
369
  const urls = new Set<string>()
358
- for (const file of files) {
359
- const key = fileKey.get(file)
360
- const entry = key !== undefined ? viteManifest[key] : undefined
370
+ const seen = new Set<string>()
371
+ const visit = (key: string | undefined): void => {
372
+ if (key === undefined || seen.has(key)) return
373
+ seen.add(key)
374
+ const entry = viteManifest[key]
361
375
  for (const css of entry?.css ?? []) urls.add(url(css))
376
+ for (const imported of entry?.imports ?? []) visit(imported)
362
377
  }
378
+ visit(entryKey)
379
+ for (const file of files) visit(fileKey.get(file))
363
380
  return [...urls]
364
381
  }
365
382
  // CSS aggregate - every stylesheet the app emits, the always-safe fallback. With native Vite
@@ -376,8 +393,13 @@ export async function buildClientVite(options: BuildClientViteOptions): Promise<
376
393
  // A single aggregate stylesheet is deliberately the route fallback. A per-route `[]` map would
377
394
  // suppress it in `createWebApp` and produce unstyled SSR on every route.
378
395
  if (css.length > 0 && cssCodeSplit) {
379
- for (const route of routeManifest.routes) routeStyles[route.id] = stylesFor(chainFiles(route))
380
- if (routeManifest.notFound) routeStyles._404 = stylesFor([routeManifest.notFound.file])
396
+ // A `_loading` page can show on a navigation from ANY page, and a document links only its own
397
+ // route's stylesheets - so every page that can start a navigation carries the loading pages' too.
398
+ const loadingFiles = Object.values(routeManifest.loadings ?? {}).map((page) => page.file)
399
+ for (const route of routeManifest.routes)
400
+ routeStyles[route.id] = stylesFor([...chainFiles(route), ...loadingFiles])
401
+ if (routeManifest.notFound)
402
+ routeStyles._404 = stylesFor([routeManifest.notFound.file, ...loadingFiles])
381
403
  for (const [id, page] of Object.entries(routeManifest.notFounds ?? {})) {
382
404
  routeStyles[id] = stylesFor([
383
405
  ...page.layoutIds.map((layoutId) => routeManifest.layouts[layoutId]?.file ?? ""),
@@ -574,7 +596,7 @@ export async function buildServerVite(options: BuildServerViteOptions): Promise<
574
596
  "globalThis.__NIFRA_EDGE_RUNTIME__": edge ? "true" : "false",
575
597
  },
576
598
  resolve: {
577
- dedupe: ["react", "react-dom"],
599
+ dedupe: ["react", "react-dom", ...singleCopyPackageNames(root)],
578
600
  conditions: [...conditions],
579
601
  },
580
602
  plugins: [...(options.vitePlugins ?? [])],
package/src/build.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { publicEnvDefines } from "./internal/public-env.ts"
2
+
3
+ export { publicEnvDefines } from "./internal/public-env.ts"
4
+
1
5
  /**
2
6
  * `@nifrajs/web/build` - the production build (Bun-only, build-time). `buildClient` codegens + bundles
3
7
  * the client entry (content-hashed, code-split); `buildServer` codegens the static-import server
@@ -9,6 +13,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync
9
13
  import { cp, lstat, mkdir, realpath } from "node:fs/promises"
10
14
  import {
11
15
  dirname,
16
+ isAbsolute,
12
17
  join,
13
18
  basename as pathBasename,
14
19
  posix,
@@ -25,6 +30,7 @@ import {
25
30
  aggregateSizeReport,
26
31
  type BuildManifest,
27
32
  type BuildTarget,
33
+ type BuildTargetPlan,
28
34
  type Bundler,
29
35
  type ChunkSize,
30
36
  detectNodeBuiltinsInClient,
@@ -44,6 +50,8 @@ import { sanitizeOutputNames } from "./chunk-names.ts"
44
50
  import { type CssLoadingMode, normalizeCssCodeSplit, normalizeCssLoading } from "./css-contract.ts"
45
51
  import { discoverRoutes } from "./fs.ts"
46
52
  import { generateClientEntry, generateServerManifest } from "./index.ts"
53
+ import { assertDisposableDir } from "./internal/disposable-dir.ts"
54
+ import { entryChunks } from "./internal/entry-chunks.ts"
47
55
  import { dedupePolicyFor } from "./internal/identity-policy.ts"
48
56
  import {
49
57
  assertDevelopmentProductionParity,
@@ -106,7 +114,14 @@ const entryName = (file: string): string => {
106
114
  * `fromBunMetafile`; this smaller view is only for the per-route stylesheet map. */
107
115
  interface BunMetafile {
108
116
  readonly outputs?: Readonly<
109
- Record<string, { readonly entryPoint?: string; readonly cssBundle?: string }>
117
+ Record<
118
+ string,
119
+ {
120
+ readonly entryPoint?: string
121
+ readonly cssBundle?: string
122
+ readonly inputs?: Readonly<Record<string, unknown>>
123
+ }
124
+ >
110
125
  >
111
126
  }
112
127
 
@@ -180,20 +195,6 @@ export interface BuildClientOptions {
180
195
  * define resolves it to `undefined` in the client bundle. An empty `prefix` exposes nothing (the
181
196
  * opt-out). Pure + exported so the prefix/redaction contract is unit-testable without a real build.
182
197
  */
183
- export function publicEnvDefines(
184
- prefix: string,
185
- env: Readonly<Record<string, string | undefined>>,
186
- ): Record<string, string> {
187
- const defines: Record<string, string> = {}
188
- if (prefix === "") return defines // opt-out: bake in nothing
189
- for (const [name, value] of Object.entries(env)) {
190
- // Skip `undefined` (a deleted/unset key can still enumerate) so we never bake in `"undefined"`.
191
- if (name.startsWith(prefix) && value !== undefined) {
192
- defines[`process.env.${name}`] = JSON.stringify(value)
193
- }
194
- }
195
- return defines
196
- }
197
198
 
198
199
  /**
199
200
  * Cloudflare Pages rejects a `_routes.json` with more than 100 include+exclude rules combined, or any
@@ -629,21 +630,27 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
629
630
  const renamed = sanitizeOutputNames(outputs)
630
631
  const toUrl = (path: string): string =>
631
632
  `${publicPath}${renamed.get(basename(path)) ?? basename(path)}`
632
- // Entry-point outputs come back in entrypoint order: [bootstrap, ...routeFiles]. Map each route file
633
- // to its chunk URL by that order (guarded against drift), then a route's chunks = its layout chain +
634
- // own file.
633
+ // Each entry's chunk is the output the metafile names that entry's source for. The position among the
634
+ // entry-point outputs ([bootstrap, ...routeFiles]) is only the fallback, for an entry the metafile
635
+ // does not name by path (a `resolve` that returns a bare specifier). A route's chunks = its layout
636
+ // chain + own file.
635
637
  const entryPoints = outputs.filter((o) => o.kind === "entry-point")
636
- const bootstrap = entryPoints[0]
637
- if (bootstrap === undefined) throw new Error("[nifra/web] build produced no entry-point output")
638
638
  if (entryPoints.length !== routeFiles.length + 1) {
639
639
  throw new Error(
640
640
  `[nifra/web] expected ${routeFiles.length + 1} entry-point outputs (bootstrap + ${routeFiles.length} routes), got ${entryPoints.length}`,
641
641
  )
642
642
  }
643
+ const bySource = entryChunks(clientMeta?.outputs ?? {}, cwd)
644
+ const chunkOf = (source: string, position: number): string | undefined => {
645
+ const path = bySource.get(resolvePath(source)) ?? entryPoints[position]?.path
646
+ return path === undefined ? undefined : toUrl(path)
647
+ }
648
+ const entryUrl = chunkOf(entryFile, 0)
649
+ if (entryUrl === undefined) throw new Error("[nifra/web] build produced no entry-point output")
643
650
  const fileToChunk = new Map<string, string>()
644
651
  routeFiles.forEach((file, i) => {
645
- const out = entryPoints[i + 1] // in range - length checked above
646
- if (out !== undefined) fileToChunk.set(file, toUrl(out.path))
652
+ const url = chunkOf(resolve(file), i + 1)
653
+ if (url !== undefined) fileToChunk.set(file, url)
647
654
  })
648
655
  const chunksFor = (chainFiles: readonly string[]): string[] =>
649
656
  chainFiles.map((f) => fileToChunk.get(f)).filter((u): u is string => u !== undefined)
@@ -684,15 +691,30 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
684
691
  // links only its layout chain + own CSS (deduped); an empty array means the page needs no CSS at all.
685
692
  // Absent (→ aggregate fallback) only if Bun emits no metafile/cssBundle - never silently incomplete.
686
693
  const cssByEntry = new Map<string, string>()
694
+ const bootstrapEntry = resolvePath(entryFile)
695
+ let bootstrapSources: readonly string[] = []
696
+ const routeSources = new Set<string>()
687
697
  for (const out of Object.values(clientMeta?.outputs ?? {})) {
688
698
  if (out.entryPoint !== undefined && out.cssBundle !== undefined) {
689
- cssByEntry.set(resolvePath(cwd, out.entryPoint), toUrl(out.cssBundle))
699
+ const entry = resolvePath(cwd, out.entryPoint)
700
+ cssByEntry.set(entry, toUrl(out.cssBundle))
701
+ const sources = Object.keys(clientMeta?.outputs?.[out.cssBundle]?.inputs ?? {})
702
+ if (entry === bootstrapEntry) bootstrapSources = sources
703
+ else for (const source of sources) routeSources.add(source)
690
704
  }
691
705
  }
706
+ // A stylesheet the client module imports is bundled only into the bootstrap's CSS, which also holds
707
+ // every route's (the bootstrap imports each route lazily). When it carries CSS no route entry owns,
708
+ // every page links that bundle first, the whole-app stylesheet before any route-scoped one, as a
709
+ // Vite page links the bootstrap's; otherwise the page renders without it.
710
+ const leadingCss = bootstrapSources.some((source) => !routeSources.has(source))
711
+ ? cssByEntry.get(bootstrapEntry)
712
+ : undefined
692
713
  const stylesFor = (chainFiles: readonly string[]): readonly string[] => {
693
714
  const urls = chainFiles
694
715
  .map((f) => (f ? cssByEntry.get(resolvePath(resolve(f))) : undefined))
695
716
  .filter((u): u is string => u !== undefined)
717
+ if (leadingCss !== undefined) urls.unshift(leadingCss)
696
718
  return [...new Set(urls)]
697
719
  }
698
720
  const routeStyles: Record<string, readonly string[]> = {}
@@ -700,7 +722,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
700
722
  // route entry (StyleX's atomic CSS is one example). In that shape there is no useful per-route map:
701
723
  // emitting `routeId: []` would suppress the aggregate `styles` fallback in `createWebApp` and render
702
724
  // SSR pages unstyled. Keep routeStyles absent until at least one authored route entry owns CSS.
703
- const routeCssEntries = [...cssByEntry.keys()].filter((entry) => entry !== resolvePath(entryFile))
725
+ const routeCssEntries = [...cssByEntry.keys()].filter((entry) => entry !== bootstrapEntry)
704
726
  if (css.length > 0 && routeCssEntries.length > 0) {
705
727
  // A `_loading` page can show on a navigation from ANY page, and a document links only its own
706
728
  // route's stylesheets - so every page that can start a navigation carries the loading pages' too.
@@ -729,7 +751,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
729
751
  publicDir !== undefined && existsSync(publicDir) ? await copyPublicDir(publicDir, outDir) : []
730
752
 
731
753
  const manifest: BuildManifest = {
732
- entry: toUrl(bootstrap.path),
754
+ entry: entryUrl,
733
755
  assets: outputs.map((o) => toUrl(o.path)),
734
756
  routes,
735
757
  ...(publicFiles.length > 0 ? { publicFiles } : {}),
@@ -1009,11 +1031,22 @@ function withEmittedImports(
1009
1031
  return { ...graph, chunks }
1010
1032
  }
1011
1033
 
1012
- /** Refusals the bundler stopped on before it produced a graph, so without import chains. */
1034
+ /**
1035
+ * Refusals the bundler stopped on before it produced a graph, so without import chains. Files sharing
1036
+ * one reason share one line: with no chain to tell them apart, 50 lines would say one thing 50 times.
1037
+ */
1013
1038
  function formatRefusedFiles(refused: ReadonlyMap<string, string>, root: string): string {
1014
- const lines = [...refused].map(([file, reason]) => {
1039
+ const byReason = new Map<string, string[]>()
1040
+ for (const [file, reason] of refused) {
1015
1041
  const rel = relative(root, file).replaceAll("\\", "/")
1016
- return ` - ${rel.startsWith("..") ? file : rel}: ${reason}`
1042
+ const shown = rel.startsWith("..") ? file : rel
1043
+ const files = byReason.get(reason)
1044
+ if (files === undefined) byReason.set(reason, [shown])
1045
+ else files.push(shown)
1046
+ }
1047
+ const lines = [...byReason].map(([reason, files]) => {
1048
+ const more = files.length > 5 ? ` and ${files.length - 5} more` : ""
1049
+ return ` - ${files.slice(0, 5).join(", ")}${more}: ${reason}`
1017
1050
  })
1018
1051
  return `[nifra/web] the browser build reached code that may not ship to a browser:\n${lines.join("\n")}`
1019
1052
  }
@@ -1247,6 +1280,9 @@ export function generateServerEntry(options: {
1247
1280
  readonly title?: string
1248
1281
  /** Encoded root-relative public file paths copied into the deploy directory. */
1249
1282
  readonly publicFiles?: readonly string[]
1283
+ /** URL paths of the content-hashed client outputs (the build manifest's `assets`). When given, a
1284
+ * self-hosted entry caches only these immutably; otherwise every file under `/assets/` is. */
1285
+ readonly hashedFiles?: readonly string[]
1250
1286
  /** Trust the edge platform's client-address header for `c.clientIp`. See {@link ServerEntryClientIp}. */
1251
1287
  readonly clientIp?: ServerEntryClientIp
1252
1288
  }): string {
@@ -1258,6 +1294,7 @@ export function generateServerEntry(options: {
1258
1294
  optionImports = {},
1259
1295
  title = "nifra",
1260
1296
  publicFiles = [],
1297
+ hashedFiles,
1261
1298
  clientIp,
1262
1299
  } = options
1263
1300
  if (clientIp !== undefined && clientIp !== "platform") {
@@ -1355,6 +1392,9 @@ export function generateServerEntry(options: {
1355
1392
  "// The client bundle and public files live next to this entry; serve only manifest-approved paths.",
1356
1393
  'const STATIC_ROOT = new URL("./", import.meta.url)',
1357
1394
  `const PUBLIC_FILES = new Set(${JSON.stringify(publicFiles)})`,
1395
+ ...(hashedFiles !== undefined
1396
+ ? [`const HASHED_FILES = new Set(${JSON.stringify(hashedFiles)})`]
1397
+ : []),
1358
1398
  "const staticPath = (pathname: string): string | undefined => {",
1359
1399
  " if (PUBLIC_FILES.has(pathname)) return '.' + pathname",
1360
1400
  ' if (!pathname.startsWith("/assets/")) return undefined',
@@ -1371,7 +1411,7 @@ export function generateServerEntry(options: {
1371
1411
  `const TYPES: Record<string, string> = ${JSON.stringify(CONTENT_TYPES)}`,
1372
1412
  "const staticHeaders = (pathname: string): Record<string, string> => ({",
1373
1413
  ' "content-type": TYPES[pathname.slice(pathname.lastIndexOf(".")).toLowerCase()] ?? "application/octet-stream",',
1374
- ` "cache-control": pathname.startsWith("/assets/") ? ${JSON.stringify(IMMUTABLE)} : ${JSON.stringify(ONE_DAY)},`,
1414
+ ` "cache-control": ${hashedFiles !== undefined ? "HASHED_FILES.has(pathname)" : 'pathname.startsWith("/assets/")'} ? ${JSON.stringify(IMMUTABLE)} : ${JSON.stringify(ONE_DAY)},`,
1375
1415
  ' "x-content-type-options": "nosniff",',
1376
1416
  "})",
1377
1417
  )
@@ -1467,10 +1507,12 @@ async function measureOutputs(paths: readonly string[]): Promise<ChunkSize[]> {
1467
1507
  export interface BuildTargetOptions {
1468
1508
  /** The `routes/` directory to discover (absolute path). */
1469
1509
  readonly routesDir: string
1470
- /** Output directory for the assembled deploy dir (absolute path). Cleared and recreated. */
1510
+ /** Output directory for the assembled deploy dir (absolute path). Cleared and recreated, so the
1511
+ * build refuses one that is a filesystem root or is or contains the routes directory, the public
1512
+ * directory, an absolute entry module, or the working directory. */
1471
1513
  readonly outDir: string
1472
1514
  /** A scratch directory for intermediate codegen (the generated server entry + manifest) and the
1473
- * server bundle, cleaned up after. Absolute path. */
1515
+ * server bundle, cleaned up after. Absolute path. Checked the same way as `outDir`. */
1474
1516
  readonly workDir: string
1475
1517
  /** The adapter's client runtime module (exports `mountRouter`), e.g. `"@nifrajs/web-react/client"`. */
1476
1518
  readonly clientModule: string
@@ -1597,6 +1639,26 @@ export const bunBundler: Bundler = {
1597
1639
  }),
1598
1640
  }
1599
1641
 
1642
+ /** Refuse a public file that shares a path with a file the target writes into the same directory: the
1643
+ * build overwrites it, and the server would then answer that public path with its own bundle. */
1644
+ function assertNoPublicCollision(
1645
+ plan: BuildTargetPlan,
1646
+ publicFiles: readonly string[],
1647
+ outDir: string,
1648
+ ): void {
1649
+ if (plan.kind !== "server" || plan.staticDir !== "") return
1650
+ const owned = [`/${plan.outputFile}`]
1651
+ if (plan.target === "cloudflare") owned.push("/_routes.json")
1652
+ // Compared case-insensitively: on a case-insensitive filesystem `Server.js` is the same file.
1653
+ const clashes = publicFiles.filter((file) => owned.includes(file.toLowerCase()))
1654
+ if (clashes.length > 0) {
1655
+ throw new Error(
1656
+ `[nifra/web] public file(s) ${clashes.join(", ")} would be overwritten by the ${plan.target} ` +
1657
+ `build's own output in ${outDir}. Rename or move them out of the public directory.`,
1658
+ )
1659
+ }
1660
+ }
1661
+
1600
1662
  /** Build a full deploy dir for `target` using the default Bun bundler. See {@link buildTargetWith}. */
1601
1663
  export async function buildTarget(
1602
1664
  target: BuildTarget,
@@ -1622,6 +1684,20 @@ export async function buildTargetWith(
1622
1684
  const requestedCssLoading =
1623
1685
  options.cssLoading === undefined ? undefined : normalizeCssLoading(options.cssLoading)
1624
1686
  const targetPlan = planBuildTarget(parseBuildTarget(target), outDir)
1687
+ const sources = [
1688
+ routesDir,
1689
+ process.cwd(),
1690
+ resolvePath(
1691
+ typeof options.publicDir === "string"
1692
+ ? options.publicDir
1693
+ : join(dirname(routesDir), "public"),
1694
+ ),
1695
+ ...[options.adapterImport, options.backendImport, options.useImport].filter(
1696
+ (file): file is string => file !== undefined && isAbsolute(file),
1697
+ ),
1698
+ ]
1699
+ assertDisposableDir("outDir", outDir, sources)
1700
+ assertDisposableDir("workDir", workDir, sources)
1625
1701
  const { rmSync } = await import("node:fs")
1626
1702
  rmSync(outDir, { recursive: true, force: true })
1627
1703
  rmSync(workDir, { recursive: true, force: true })
@@ -1672,6 +1748,7 @@ export async function buildTargetWith(
1672
1748
  publicDir !== undefined && existsSync(publicDir)
1673
1749
  ? await copyPublicDir(publicDir, staticRoot)
1674
1750
  : []
1751
+ assertNoPublicCollision(targetPlan, publicFiles, outDir)
1675
1752
  if (publicFiles.length > 0) {
1676
1753
  client = { ...client, publicFiles }
1677
1754
  }
@@ -1734,6 +1811,7 @@ export async function buildTargetWith(
1734
1811
  ...(options.backendImport !== undefined ? { backendImport: options.backendImport } : {}),
1735
1812
  ...(options.title !== undefined ? { title: options.title } : {}),
1736
1813
  ...(publicFiles.length > 0 ? { publicFiles } : {}),
1814
+ hashedFiles: client.assets,
1737
1815
  ...(options.clientIp !== undefined ? { clientIp: options.clientIp } : {}),
1738
1816
  }),
1739
1817
  )
package/src/cdn.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  * for any other. Backend-only: a browser build that reaches this module fails.
12
12
  */
13
13
  import "@nifrajs/web/backend-only"
14
+ import { writableResponse } from "@nifrajs/core/server"
14
15
  import { isDraftEnabled } from "./draft.ts"
15
16
  import type { CdnProvider } from "./internal/cdn/core.ts"
16
17
  import { pathTag } from "./internal/cdn/core.ts"
@@ -84,15 +85,9 @@ const isHtml = (res: Response): boolean =>
84
85
  (res.headers.get("content-type") ?? "").toLowerCase().includes("text/html")
85
86
 
86
87
  function withHeaders(res: Response, set: Readonly<Record<string, string>>): Response {
87
- try {
88
- for (const [name, value] of Object.entries(set)) res.headers.set(name, value)
89
- return res
90
- } catch {
91
- // A `fetch()` response has immutable headers: rebuild it around the same body instead.
92
- const headers = new Headers(res.headers)
93
- for (const [name, value] of Object.entries(set)) headers.set(name, value)
94
- return new Response(res.body, { status: res.status, statusText: res.statusText, headers })
95
- }
88
+ const target = writableResponse(res)
89
+ for (const [name, value] of Object.entries(set)) target.headers.set(name, value)
90
+ return target
96
91
  }
97
92
 
98
93
  /**
@@ -150,6 +145,12 @@ export interface InvalidatorOptions {
150
145
  readonly key?: (path: string) => string | null
151
146
  /** The `query` policy given to `withISR`. Default `"bypass"`. Ignored with `key`. */
152
147
  readonly query?: ISRQuery
148
+ /**
149
+ * Caches below the pages whose entries carry the same tags, such as a `@nifrajs/cache` holding
150
+ * loader data. Tags reach each of them before the store, so a page regenerated mid-purge cannot be
151
+ * rebuilt from stale data; a failure rejects before the store is touched. Paths do not reach them.
152
+ */
153
+ readonly dataCaches?: readonly { invalidateTag(tag: string): unknown }[]
153
154
  }
154
155
 
155
156
  export interface InvalidateResult {
@@ -196,11 +197,16 @@ export function createInvalidator(options: InvalidatorOptions): {
196
197
  throw new TypeError("[nifra/web/cdn] invalidate paths must start with /")
197
198
  }
198
199
  }
200
+ if (store !== undefined && tags.length > 0 && store.invalidateTag === undefined) {
201
+ throw new TypeError("[nifra/web/cdn] this store cannot invalidate by tag")
202
+ }
203
+ if (tags.length > 0) {
204
+ for (const cache of options.dataCaches ?? []) {
205
+ for (const tag of tags) await cache.invalidateTag(tag)
206
+ }
207
+ }
199
208
  let origin: InvalidateResult["origin"] = "skipped"
200
209
  if (store !== undefined) {
201
- if (tags.length > 0 && store.invalidateTag === undefined) {
202
- throw new TypeError("[nifra/web/cdn] this store cannot invalidate by tag")
203
- }
204
210
  for (const path of paths) {
205
211
  const key = keyOf?.(path)
206
212
  if (key !== null && key !== undefined) await store.delete(key)
package/src/client.ts CHANGED
@@ -40,7 +40,7 @@ export function currentDocumentNonce(): string | undefined {
40
40
  * serves modules as written, so the browser evaluated `public-dir.ts` and died on `node:fs/promises`
41
41
  * before hydrating. The fix is for the entry not to name the server module in the first place.
42
42
  */
43
- export { mergeHeads, resolveMeta } from "./internal/head-merge.ts"
43
+ export { mergeHeads, resolveChainMeta, resolveMeta } from "./internal/head-merge.ts"
44
44
  export {
45
45
  getBrowserNavigate,
46
46
  IDLE_BLOCKER,
@@ -690,23 +690,42 @@ export function applyHead(head: Meta): void {
690
690
  }
691
691
 
692
692
  export function installForms(router: ClientRouter): () => void {
693
+ // Forms whose data submit failed before the server answered: their next submit event is the native
694
+ // post that replaces it.
695
+ const native = new WeakSet<HTMLFormElement>()
693
696
  const onSubmit = (event: SubmitEvent): void => {
694
697
  if (event.defaultPrevented) return
695
698
  const form = event.target
696
699
  if (!(form instanceof HTMLFormElement)) return
697
- if (form.method.toLowerCase() !== "post") return // GET forms submit natively
698
- const url = sameDocumentUrl(form.action) // resolved (or the current document URL if unset)
700
+ if (native.delete(form)) return
701
+ // The button that submitted can override the form's method, action and target, and its
702
+ // name=value is part of the submission (`<button name="intent" value="delete">`).
703
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: a submit event's submitter is the submit button or input that fired it, or null
704
+ const submitter = event.submitter as HTMLButtonElement | null
705
+ const method = submitter?.hasAttribute("formmethod") ? submitter.formMethod : form.method
706
+ if (method.toLowerCase() !== "post") return // GET forms submit natively
707
+ const target = submitter?.hasAttribute("formtarget") ? submitter.formTarget : form.target
708
+ if (target !== "" && target !== "_self") return // it loads in another browsing context
709
+ // Resolved (or the current document URL if unset).
710
+ const url = sameDocumentUrl(
711
+ submitter?.hasAttribute("formaction") ? submitter.formAction : form.action,
712
+ )
699
713
  if (url === null) return
700
714
  if (router.match(url.pathname) === null) return // not an app route → native submit
701
715
  event.preventDefault()
702
716
  // `data-nifra-revalidate="false"` opts out of the post-action loader revalidation (the action's
703
717
  // actionData drives the update); absent or any other value keeps the default revalidation.
704
718
  const revalidate = form.dataset.nifraRevalidate !== "false"
705
- router.submit(url.pathname + url.search, new FormData(form), { revalidate }).catch((error) => {
719
+ const body = new FormData(form, submitter)
720
+ router.submit(url.pathname + url.search, body, { revalidate }).catch((error) => {
706
721
  const to = redirectOf(error)
707
- // The action ran: load the page that shows its result rather than run it again.
722
+ // The server answered: load the page that shows the result rather than run the action again.
708
723
  if (to !== undefined) location.assign(to)
709
- else form.submit() // data submit failed - fall back to a full-page POST
724
+ else {
725
+ // Nothing reached the server - post natively, through the same button.
726
+ native.add(form)
727
+ form.requestSubmit(submitter)
728
+ }
710
729
  })
711
730
  }
712
731
  document.addEventListener("submit", onSubmit)