@nifrajs/web 5.5.1 → 5.7.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 (481) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +21 -11
  3. package/dist/boundary.d.ts +67 -7
  4. package/dist/boundary.d.ts.map +1 -1
  5. package/dist/boundary.js +6 -4
  6. package/dist/boundary.js.map +1 -1
  7. package/dist/build-plan.d.ts +3 -0
  8. package/dist/build-plan.d.ts.map +1 -1
  9. package/dist/build-plan.js.map +1 -1
  10. package/dist/build-vite.d.ts.map +1 -1
  11. package/dist/build-vite.js +2 -0
  12. package/dist/build-vite.js.map +1 -1
  13. package/dist/build.d.ts +9 -2
  14. package/dist/build.d.ts.map +1 -1
  15. package/dist/build.js +10 -0
  16. package/dist/build.js.map +1 -1
  17. package/dist/client.d.ts +3 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +174 -23
  20. package/dist/client.js.map +1 -1
  21. package/dist/csp.d.ts +8 -0
  22. package/dist/csp.d.ts.map +1 -1
  23. package/dist/csp.js +11 -2
  24. package/dist/csp.js.map +1 -1
  25. package/dist/deferred.d.ts +6 -4
  26. package/dist/deferred.d.ts.map +1 -1
  27. package/dist/deferred.js +10 -6
  28. package/dist/deferred.js.map +1 -1
  29. package/dist/dev-feed.d.ts.map +1 -1
  30. package/dist/dev-feed.js +11 -2
  31. package/dist/dev-feed.js.map +1 -1
  32. package/dist/dev-session.d.ts.map +1 -1
  33. package/dist/dev-session.js +13 -6
  34. package/dist/dev-session.js.map +1 -1
  35. package/dist/dev.d.ts +4 -1
  36. package/dist/dev.d.ts.map +1 -1
  37. package/dist/dev.js +12 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/diagnostic-catalog.d.ts +4 -0
  40. package/dist/diagnostic-catalog.d.ts.map +1 -1
  41. package/dist/diagnostic-catalog.js +48 -0
  42. package/dist/diagnostic-catalog.js.map +1 -1
  43. package/dist/diagnostic.d.ts +1 -1
  44. package/dist/diagnostic.d.ts.map +1 -1
  45. package/dist/diagnostic.js +1 -1
  46. package/dist/diagnostic.js.map +1 -1
  47. package/dist/fn.d.ts +5 -3
  48. package/dist/fn.d.ts.map +1 -1
  49. package/dist/fn.js +20 -5
  50. package/dist/fn.js.map +1 -1
  51. package/dist/fs.d.ts.map +1 -1
  52. package/dist/fs.js +13 -3
  53. package/dist/fs.js.map +1 -1
  54. package/dist/identity.d.ts +22 -0
  55. package/dist/identity.d.ts.map +1 -0
  56. package/dist/identity.js +60 -0
  57. package/dist/identity.js.map +1 -0
  58. package/dist/index.d.ts +8 -6
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +6 -3
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  63. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  64. package/dist/internal/cdn/cloudflare.js +2 -1
  65. package/dist/internal/cdn/cloudflare.js.map +1 -1
  66. package/dist/internal/certified-components.d.ts +1 -1
  67. package/dist/internal/certified-components.js +1 -1
  68. package/dist/internal/codegen.d.ts +11 -10
  69. package/dist/internal/codegen.d.ts.map +1 -1
  70. package/dist/internal/codegen.js +73 -10
  71. package/dist/internal/codegen.js.map +1 -1
  72. package/dist/internal/dev-websocket.d.ts +73 -0
  73. package/dist/internal/dev-websocket.d.ts.map +1 -0
  74. package/dist/internal/dev-websocket.js +145 -0
  75. package/dist/internal/dev-websocket.js.map +1 -0
  76. package/dist/internal/hole-runtime.d.ts +66 -0
  77. package/dist/internal/hole-runtime.d.ts.map +1 -0
  78. package/dist/internal/hole-runtime.js +211 -0
  79. package/dist/internal/hole-runtime.js.map +1 -0
  80. package/dist/internal/identity-epoch.d.ts +50 -0
  81. package/dist/internal/identity-epoch.d.ts.map +1 -0
  82. package/dist/internal/identity-epoch.js +180 -0
  83. package/dist/internal/identity-epoch.js.map +1 -0
  84. package/dist/internal/invalidation-contract.d.ts +23 -0
  85. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  86. package/dist/internal/invalidation-contract.js +11 -0
  87. package/dist/internal/invalidation-contract.js.map +1 -0
  88. package/dist/internal/invalidation.d.ts +62 -0
  89. package/dist/internal/invalidation.d.ts.map +1 -0
  90. package/dist/internal/invalidation.js +149 -0
  91. package/dist/internal/invalidation.js.map +1 -0
  92. package/dist/internal/island-bindings.d.ts +51 -0
  93. package/dist/internal/island-bindings.d.ts.map +1 -0
  94. package/dist/internal/island-bindings.js +147 -0
  95. package/dist/internal/island-bindings.js.map +1 -0
  96. package/dist/internal/island-list.d.ts +26 -0
  97. package/dist/internal/island-list.d.ts.map +1 -0
  98. package/dist/internal/island-list.js +78 -0
  99. package/dist/internal/island-list.js.map +1 -0
  100. package/dist/internal/island-mount.d.ts +4 -0
  101. package/dist/internal/island-mount.d.ts.map +1 -0
  102. package/dist/internal/island-mount.js +66 -0
  103. package/dist/internal/island-mount.js.map +1 -0
  104. package/dist/internal/island-owner.d.ts +5 -0
  105. package/dist/internal/island-owner.d.ts.map +1 -0
  106. package/dist/internal/island-owner.js +20 -0
  107. package/dist/internal/island-owner.js.map +1 -0
  108. package/dist/internal/island-resource.d.ts +30 -0
  109. package/dist/internal/island-resource.d.ts.map +1 -0
  110. package/dist/internal/island-resource.js +57 -0
  111. package/dist/internal/island-resource.js.map +1 -0
  112. package/dist/internal/island-signals.d.ts +51 -0
  113. package/dist/internal/island-signals.d.ts.map +1 -0
  114. package/dist/internal/island-signals.js +220 -0
  115. package/dist/internal/island-signals.js.map +1 -0
  116. package/dist/internal/keep-alive-table.d.ts +23 -0
  117. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  118. package/dist/internal/keep-alive-table.js +53 -0
  119. package/dist/internal/keep-alive-table.js.map +1 -0
  120. package/dist/internal/live-contract.d.ts +101 -0
  121. package/dist/internal/live-contract.d.ts.map +1 -0
  122. package/dist/internal/live-contract.js +5 -0
  123. package/dist/internal/live-contract.js.map +1 -0
  124. package/dist/internal/live-queries.d.ts +22 -0
  125. package/dist/internal/live-queries.d.ts.map +1 -0
  126. package/dist/internal/live-queries.js +54 -0
  127. package/dist/internal/live-queries.js.map +1 -0
  128. package/dist/internal/page-events.d.ts +10 -0
  129. package/dist/internal/page-events.d.ts.map +1 -0
  130. package/dist/internal/page-events.js +26 -0
  131. package/dist/internal/page-events.js.map +1 -0
  132. package/dist/internal/page-execution.d.ts +16 -1
  133. package/dist/internal/page-execution.d.ts.map +1 -1
  134. package/dist/internal/page-execution.js +432 -31
  135. package/dist/internal/page-execution.js.map +1 -1
  136. package/dist/internal/parity.d.ts +3 -0
  137. package/dist/internal/parity.d.ts.map +1 -1
  138. package/dist/internal/parity.js +28 -5
  139. package/dist/internal/parity.js.map +1 -1
  140. package/dist/internal/ppr/admission.d.ts +83 -0
  141. package/dist/internal/ppr/admission.d.ts.map +1 -0
  142. package/dist/internal/ppr/admission.js +221 -0
  143. package/dist/internal/ppr/admission.js.map +1 -0
  144. package/dist/internal/ppr/canary.d.ts +24 -0
  145. package/dist/internal/ppr/canary.d.ts.map +1 -0
  146. package/dist/internal/ppr/canary.js +102 -0
  147. package/dist/internal/ppr/canary.js.map +1 -0
  148. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  149. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  150. package/dist/internal/ppr/compile-shell.js +298 -0
  151. package/dist/internal/ppr/compile-shell.js.map +1 -0
  152. package/dist/internal/ppr/contract.d.ts +177 -0
  153. package/dist/internal/ppr/contract.d.ts.map +1 -0
  154. package/dist/internal/ppr/contract.js +80 -0
  155. package/dist/internal/ppr/contract.js.map +1 -0
  156. package/dist/internal/ppr/frames.d.ts +71 -0
  157. package/dist/internal/ppr/frames.d.ts.map +1 -0
  158. package/dist/internal/ppr/frames.js +119 -0
  159. package/dist/internal/ppr/frames.js.map +1 -0
  160. package/dist/internal/ppr/integration.d.ts +19 -0
  161. package/dist/internal/ppr/integration.d.ts.map +1 -0
  162. package/dist/internal/ppr/integration.js +106 -0
  163. package/dist/internal/ppr/integration.js.map +1 -0
  164. package/dist/internal/ppr/options.d.ts +96 -0
  165. package/dist/internal/ppr/options.d.ts.map +1 -0
  166. package/dist/internal/ppr/options.js +125 -0
  167. package/dist/internal/ppr/options.js.map +1 -0
  168. package/dist/internal/ppr/route-rules.d.ts +12 -0
  169. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  170. package/dist/internal/ppr/route-rules.js +53 -0
  171. package/dist/internal/ppr/route-rules.js.map +1 -0
  172. package/dist/internal/ppr/runtime.d.ts +29 -0
  173. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  174. package/dist/internal/ppr/runtime.js +35 -0
  175. package/dist/internal/ppr/runtime.js.map +1 -0
  176. package/dist/internal/ppr/server.d.ts +29 -0
  177. package/dist/internal/ppr/server.d.ts.map +1 -0
  178. package/dist/internal/ppr/server.js +1409 -0
  179. package/dist/internal/ppr/server.js.map +1 -0
  180. package/dist/internal/ppr/shell-store.d.ts +39 -0
  181. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  182. package/dist/internal/ppr/shell-store.js +106 -0
  183. package/dist/internal/ppr/shell-store.js.map +1 -0
  184. package/dist/internal/prerendered-pages.d.ts +1 -1
  185. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  186. package/dist/internal/prerendered-pages.js +7 -4
  187. package/dist/internal/prerendered-pages.js.map +1 -1
  188. package/dist/internal/present.d.ts +11 -0
  189. package/dist/internal/present.d.ts.map +1 -0
  190. package/dist/internal/present.js +13 -0
  191. package/dist/internal/present.js.map +1 -0
  192. package/dist/internal/query-digest.d.ts +9 -0
  193. package/dist/internal/query-digest.d.ts.map +1 -0
  194. package/dist/internal/query-digest.js +23 -0
  195. package/dist/internal/query-digest.js.map +1 -0
  196. package/dist/internal/query-runtime.d.ts +6 -1
  197. package/dist/internal/query-runtime.d.ts.map +1 -1
  198. package/dist/internal/query-runtime.js +9 -0
  199. package/dist/internal/query-runtime.js.map +1 -1
  200. package/dist/internal/query-seam.d.ts +29 -0
  201. package/dist/internal/query-seam.d.ts.map +1 -0
  202. package/dist/internal/query-seam.js +23 -0
  203. package/dist/internal/query-seam.js.map +1 -0
  204. package/dist/internal/render-document.d.ts +34 -64
  205. package/dist/internal/render-document.d.ts.map +1 -1
  206. package/dist/internal/render-document.js +82 -64
  207. package/dist/internal/render-document.js.map +1 -1
  208. package/dist/internal/request-abort.d.ts +8 -0
  209. package/dist/internal/request-abort.d.ts.map +1 -0
  210. package/dist/internal/request-abort.js +22 -0
  211. package/dist/internal/request-abort.js.map +1 -0
  212. package/dist/internal/server-boundary.d.ts +2 -0
  213. package/dist/internal/server-boundary.d.ts.map +1 -1
  214. package/dist/internal/server-boundary.js +8 -1
  215. package/dist/internal/server-boundary.js.map +1 -1
  216. package/dist/internal/share-data.d.ts +10 -0
  217. package/dist/internal/share-data.d.ts.map +1 -0
  218. package/dist/internal/share-data.js +135 -0
  219. package/dist/internal/share-data.js.map +1 -0
  220. package/dist/internal/status-page.d.ts +68 -0
  221. package/dist/internal/status-page.d.ts.map +1 -0
  222. package/dist/internal/status-page.js +66 -0
  223. package/dist/internal/status-page.js.map +1 -0
  224. package/dist/internal/store-signature.d.ts +15 -0
  225. package/dist/internal/store-signature.d.ts.map +1 -0
  226. package/dist/internal/store-signature.js +114 -0
  227. package/dist/internal/store-signature.js.map +1 -0
  228. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  229. package/dist/internal/vite-plugin-compat.js +2 -2
  230. package/dist/internal/web-app.d.ts +26 -0
  231. package/dist/internal/web-app.d.ts.map +1 -1
  232. package/dist/internal/web-app.js +79 -3
  233. package/dist/internal/web-app.js.map +1 -1
  234. package/dist/islands-swap.d.ts +41 -0
  235. package/dist/islands-swap.d.ts.map +1 -0
  236. package/dist/islands-swap.js +270 -0
  237. package/dist/islands-swap.js.map +1 -0
  238. package/dist/islands.d.ts +86 -24
  239. package/dist/islands.d.ts.map +1 -1
  240. package/dist/islands.js +138 -78
  241. package/dist/islands.js.map +1 -1
  242. package/dist/isr.d.ts +67 -13
  243. package/dist/isr.d.ts.map +1 -1
  244. package/dist/isr.js +247 -99
  245. package/dist/isr.js.map +1 -1
  246. package/dist/keep-alive.d.ts +147 -0
  247. package/dist/keep-alive.d.ts.map +1 -0
  248. package/dist/keep-alive.js +1102 -0
  249. package/dist/keep-alive.js.map +1 -0
  250. package/dist/live/bus.d.ts +70 -0
  251. package/dist/live/bus.d.ts.map +1 -0
  252. package/dist/live/bus.js +55 -0
  253. package/dist/live/bus.js.map +1 -0
  254. package/dist/live/core.d.ts +25 -0
  255. package/dist/live/core.d.ts.map +1 -0
  256. package/dist/live/core.js +29 -0
  257. package/dist/live/core.js.map +1 -0
  258. package/dist/live/history.d.ts +38 -0
  259. package/dist/live/history.d.ts.map +1 -0
  260. package/dist/live/history.js +104 -0
  261. package/dist/live/history.js.map +1 -0
  262. package/dist/live/hub-protocol.d.ts +46 -0
  263. package/dist/live/hub-protocol.d.ts.map +1 -0
  264. package/dist/live/hub-protocol.js +108 -0
  265. package/dist/live/hub-protocol.js.map +1 -0
  266. package/dist/live/hub.d.ts +185 -0
  267. package/dist/live/hub.d.ts.map +1 -0
  268. package/dist/live/hub.js +544 -0
  269. package/dist/live/hub.js.map +1 -0
  270. package/dist/live/keyring.d.ts +92 -0
  271. package/dist/live/keyring.d.ts.map +1 -0
  272. package/dist/live/keyring.js +265 -0
  273. package/dist/live/keyring.js.map +1 -0
  274. package/dist/live/read-capped.d.ts +6 -0
  275. package/dist/live/read-capped.d.ts.map +1 -0
  276. package/dist/live/read-capped.js +42 -0
  277. package/dist/live/read-capped.js.map +1 -0
  278. package/dist/live/revocations.d.ts +52 -0
  279. package/dist/live/revocations.d.ts.map +1 -0
  280. package/dist/live/revocations.js +116 -0
  281. package/dist/live/revocations.js.map +1 -0
  282. package/dist/live/settings.d.ts +14 -0
  283. package/dist/live/settings.d.ts.map +1 -0
  284. package/dist/live/settings.js +38 -0
  285. package/dist/live/settings.js.map +1 -0
  286. package/dist/live-client.d.ts +34 -0
  287. package/dist/live-client.d.ts.map +1 -0
  288. package/dist/live-client.js +878 -0
  289. package/dist/live-client.js.map +1 -0
  290. package/dist/live-hub.d.ts +130 -0
  291. package/dist/live-hub.d.ts.map +1 -0
  292. package/dist/live-hub.js +507 -0
  293. package/dist/live-hub.js.map +1 -0
  294. package/dist/live-redis.d.ts +60 -0
  295. package/dist/live-redis.d.ts.map +1 -0
  296. package/dist/live-redis.js +212 -0
  297. package/dist/live-redis.js.map +1 -0
  298. package/dist/live.d.ts +150 -0
  299. package/dist/live.d.ts.map +1 -0
  300. package/dist/live.js +972 -0
  301. package/dist/live.js.map +1 -0
  302. package/dist/manifest.d.ts +113 -2
  303. package/dist/manifest.d.ts.map +1 -1
  304. package/dist/manifest.js +109 -5
  305. package/dist/manifest.js.map +1 -1
  306. package/dist/navigation.d.ts +9 -3
  307. package/dist/navigation.d.ts.map +1 -1
  308. package/dist/navigation.js +13 -3
  309. package/dist/navigation.js.map +1 -1
  310. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  311. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  312. package/dist/plugins/vite-zone-guard.js +8 -2
  313. package/dist/plugins/vite-zone-guard.js.map +1 -1
  314. package/dist/ppr.d.ts +73 -0
  315. package/dist/ppr.d.ts.map +1 -0
  316. package/dist/ppr.js +76 -0
  317. package/dist/ppr.js.map +1 -0
  318. package/dist/query.d.ts +8 -13
  319. package/dist/query.d.ts.map +1 -1
  320. package/dist/query.js +112 -18
  321. package/dist/query.js.map +1 -1
  322. package/dist/render-seam.d.ts +32 -0
  323. package/dist/render-seam.d.ts.map +1 -1
  324. package/dist/render-seam.js +7 -0
  325. package/dist/render-seam.js.map +1 -1
  326. package/dist/route-types.d.ts.map +1 -1
  327. package/dist/route-types.js +2 -2
  328. package/dist/route-types.js.map +1 -1
  329. package/dist/router.d.ts +72 -4
  330. package/dist/router.d.ts.map +1 -1
  331. package/dist/router.js +416 -129
  332. package/dist/router.js.map +1 -1
  333. package/dist/server-components/build.d.ts.map +1 -1
  334. package/dist/server-components/build.js +3 -3
  335. package/dist/server-components/build.js.map +1 -1
  336. package/dist/server-components/client.d.ts +23 -1
  337. package/dist/server-components/client.d.ts.map +1 -1
  338. package/dist/server-components/client.js +92 -5
  339. package/dist/server-components/client.js.map +1 -1
  340. package/dist/server-components/definition.d.ts.map +1 -1
  341. package/dist/server-components/definition.js +2 -1
  342. package/dist/server-components/definition.js.map +1 -1
  343. package/dist/server-components/hole.d.ts +8 -0
  344. package/dist/server-components/hole.d.ts.map +1 -0
  345. package/dist/server-components/hole.js +20 -0
  346. package/dist/server-components/hole.js.map +1 -0
  347. package/dist/server-components/html-policy.d.ts.map +1 -1
  348. package/dist/server-components/html-policy.js +62 -4
  349. package/dist/server-components/html-policy.js.map +1 -1
  350. package/dist/server-components/html-scan.d.ts +17 -0
  351. package/dist/server-components/html-scan.d.ts.map +1 -0
  352. package/dist/server-components/html-scan.js +180 -0
  353. package/dist/server-components/html-scan.js.map +1 -0
  354. package/dist/server-components/html.d.ts.map +1 -1
  355. package/dist/server-components/html.js +19 -4
  356. package/dist/server-components/html.js.map +1 -1
  357. package/dist/server-components/router-integration.d.ts +4 -1
  358. package/dist/server-components/router-integration.d.ts.map +1 -1
  359. package/dist/server-components/router-integration.js +23 -2
  360. package/dist/server-components/router-integration.js.map +1 -1
  361. package/dist/server-components/runtime.d.ts +6 -1
  362. package/dist/server-components/runtime.d.ts.map +1 -1
  363. package/dist/server-components/runtime.js +88 -29
  364. package/dist/server-components/runtime.js.map +1 -1
  365. package/dist/server-components/state.d.ts +3 -1
  366. package/dist/server-components/state.d.ts.map +1 -1
  367. package/dist/server-components/state.js +50 -38
  368. package/dist/server-components/state.js.map +1 -1
  369. package/dist/server-components/types.d.ts +9 -0
  370. package/dist/server-components/types.d.ts.map +1 -1
  371. package/dist/server-components/vite.d.ts.map +1 -1
  372. package/dist/server-components/vite.js +26 -9
  373. package/dist/server-components/vite.js.map +1 -1
  374. package/dist/vite.d.ts +5 -1
  375. package/dist/vite.d.ts.map +1 -1
  376. package/dist/vite.js +58 -4
  377. package/dist/vite.js.map +1 -1
  378. package/package.json +48 -8
  379. package/src/boundary.ts +77 -5
  380. package/src/build-plan.ts +3 -0
  381. package/src/build-vite.ts +2 -0
  382. package/src/build.ts +20 -1
  383. package/src/client.ts +183 -23
  384. package/src/csp.ts +16 -2
  385. package/src/deferred.ts +11 -5
  386. package/src/dev-feed.ts +11 -2
  387. package/src/dev-session.ts +22 -11
  388. package/src/dev.ts +29 -8
  389. package/src/diagnostic-catalog.ts +53 -0
  390. package/src/diagnostic.ts +1 -0
  391. package/src/fn.ts +24 -7
  392. package/src/fs.ts +22 -8
  393. package/src/identity.ts +61 -0
  394. package/src/index.ts +20 -4
  395. package/src/internal/cdn/cloudflare.ts +2 -1
  396. package/src/internal/certified-components.ts +1 -1
  397. package/src/internal/codegen.ts +95 -9
  398. package/src/internal/dev-websocket.ts +230 -0
  399. package/src/internal/hole-runtime.ts +286 -0
  400. package/src/internal/identity-epoch.ts +239 -0
  401. package/src/internal/invalidation-contract.ts +35 -0
  402. package/src/internal/invalidation.ts +213 -0
  403. package/src/internal/island-bindings.ts +191 -0
  404. package/src/internal/island-list.ts +92 -0
  405. package/src/internal/island-mount.ts +68 -0
  406. package/src/internal/island-owner.ts +20 -0
  407. package/src/internal/island-resource.ts +76 -0
  408. package/src/internal/island-signals.ts +249 -0
  409. package/src/internal/keep-alive-table.ts +72 -0
  410. package/src/internal/live-contract.ts +111 -0
  411. package/src/internal/live-queries.ts +60 -0
  412. package/src/internal/page-events.ts +27 -0
  413. package/src/internal/page-execution.ts +605 -45
  414. package/src/internal/parity.ts +35 -6
  415. package/src/internal/ppr/admission.ts +300 -0
  416. package/src/internal/ppr/canary.ts +122 -0
  417. package/src/internal/ppr/compile-shell.ts +379 -0
  418. package/src/internal/ppr/contract.ts +265 -0
  419. package/src/internal/ppr/frames.ts +170 -0
  420. package/src/internal/ppr/integration.ts +137 -0
  421. package/src/internal/ppr/options.ts +221 -0
  422. package/src/internal/ppr/route-rules.ts +77 -0
  423. package/src/internal/ppr/runtime.ts +39 -0
  424. package/src/internal/ppr/server.ts +1766 -0
  425. package/src/internal/ppr/shell-store.ts +152 -0
  426. package/src/internal/prerendered-pages.ts +7 -4
  427. package/src/internal/present.ts +20 -0
  428. package/src/internal/query-digest.ts +34 -0
  429. package/src/internal/query-runtime.ts +14 -1
  430. package/src/internal/query-seam.ts +48 -0
  431. package/src/internal/render-document.ts +158 -113
  432. package/src/internal/request-abort.ts +26 -0
  433. package/src/internal/server-boundary.ts +10 -1
  434. package/src/internal/share-data.ts +136 -0
  435. package/src/internal/status-page.ts +95 -0
  436. package/src/internal/store-signature.ts +149 -0
  437. package/src/internal/vite-plugin-compat.ts +2 -2
  438. package/src/internal/web-app.ts +123 -3
  439. package/src/islands-swap.ts +283 -0
  440. package/src/islands.ts +169 -72
  441. package/src/isr.ts +348 -106
  442. package/src/keep-alive.ts +1354 -0
  443. package/src/live/bus.ts +104 -0
  444. package/src/live/core.ts +52 -0
  445. package/src/live/history.ts +109 -0
  446. package/src/live/hub-protocol.ts +133 -0
  447. package/src/live/hub.ts +620 -0
  448. package/src/live/keyring.ts +384 -0
  449. package/src/live/read-capped.ts +36 -0
  450. package/src/live/revocations.ts +155 -0
  451. package/src/live/settings.ts +46 -0
  452. package/src/live-client.ts +1011 -0
  453. package/src/live-hub.ts +658 -0
  454. package/src/live-redis.ts +284 -0
  455. package/src/live.ts +1257 -0
  456. package/src/manifest.ts +274 -6
  457. package/src/navigation.ts +14 -2
  458. package/src/plugins/vite-zone-guard.ts +15 -2
  459. package/src/ppr.ts +153 -0
  460. package/src/query.ts +147 -20
  461. package/src/render-seam.ts +37 -0
  462. package/src/route-types.ts +3 -1
  463. package/src/router.ts +534 -132
  464. package/src/server-components/build.ts +9 -3
  465. package/src/server-components/client.ts +89 -5
  466. package/src/server-components/definition.ts +2 -1
  467. package/src/server-components/hole.ts +23 -0
  468. package/src/server-components/html-policy.ts +63 -4
  469. package/src/server-components/html-scan.ts +159 -0
  470. package/src/server-components/html.ts +18 -4
  471. package/src/server-components/router-integration.ts +21 -2
  472. package/src/server-components/runtime.ts +114 -31
  473. package/src/server-components/state.ts +66 -44
  474. package/src/server-components/types.ts +16 -0
  475. package/src/server-components/vite.ts +33 -14
  476. package/src/vite.ts +75 -6
  477. package/dist/nano.d.ts +0 -125
  478. package/dist/nano.d.ts.map +0 -1
  479. package/dist/nano.js +0 -190
  480. package/dist/nano.js.map +0 -1
  481. package/src/nano.ts +0 -246
package/src/build.ts CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  import { sanitizeOutputNames } from "./chunk-names.ts"
55
55
  import { type CssLoadingMode, normalizeCssCodeSplit, normalizeCssLoading } from "./css-contract.ts"
56
56
  import { discoverRoutes } from "./fs.ts"
57
- import { generateClientEntry, generateServerManifest } from "./index.ts"
57
+ import { generateClientEntry, generateServerManifest, type LiveClientEntry } from "./index.ts"
58
58
  import { missingClientEntry } from "./internal/codegen.ts"
59
59
  import { loadCopySplit } from "./internal/copy-split.ts"
60
60
  import { assertDisposableDir } from "./internal/disposable-dir.ts"
@@ -208,6 +208,11 @@ export interface BuildClientOptions {
208
208
  * every table whole; the server entry passes the manifest's `copyChunks` to `createWebApp`.
209
209
  */
210
210
  readonly copySplit?: boolean
211
+ /**
212
+ * The server runs `createWebApp({ live })`: the client entry sends route loads through its live load
213
+ * endpoint and keeps what is on screen fresh: the runtime's `clientEntry`.
214
+ */
215
+ readonly live?: LiveClientEntry | undefined
211
216
  }
212
217
 
213
218
  /**
@@ -479,6 +484,7 @@ export async function buildClient(options: BuildClientOptions): Promise<BuildMan
479
484
  clientModule,
480
485
  resolve,
481
486
  copyChunks: copySplit !== undefined,
487
+ live: options.live,
482
488
  })}`,
483
489
  )
484
490
 
@@ -1331,6 +1337,8 @@ export const SERVER_ENTRY_OPTIONS = [
1331
1337
  "nonce",
1332
1338
  "i18n",
1333
1339
  "hostPolicy",
1340
+ "live",
1341
+ "keepAlive",
1334
1342
  ] as const
1335
1343
  export type ServerEntryOption = (typeof SERVER_ENTRY_OPTIONS)[number]
1336
1344
  /** Each importable option mapped to the specifier of the module that exports it. */
@@ -1894,6 +1902,8 @@ export interface BuildTargetOptions {
1894
1902
  /** Trust the edge platform's client-address header for `c.clientIp` (`cloudflare`, `vercel`). Off by
1895
1903
  * default, leaving an edge app with no caller address. See {@link ServerEntryClientIp}. */
1896
1904
  readonly clientIp?: ServerEntryClientIp
1905
+ /** The client entry for a server built with `optionImports.live`: that runtime's `clientEntry`. */
1906
+ readonly live?: LiveClientEntry | undefined
1897
1907
  }
1898
1908
 
1899
1909
  /** Minimal app surface `buildTarget`'s static path needs - a fetch handler (a built `createWebApp`). */
@@ -1949,6 +1959,7 @@ export const bunBundler: Bundler = {
1949
1959
  ...(input.publicDir !== undefined ? { publicDir: input.publicDir } : {}),
1950
1960
  ...(input.publicEnvPrefix !== undefined ? { publicEnvPrefix: input.publicEnvPrefix } : {}),
1951
1961
  ...(input.secretExemptions !== undefined ? { secretExemptions: input.secretExemptions } : {}),
1962
+ live: input.live,
1952
1963
  })
1953
1964
  },
1954
1965
  buildServer: (input) =>
@@ -2053,6 +2064,13 @@ export async function buildTargetWith(
2053
2064
  bundler: Bundler,
2054
2065
  ): Promise<BuildTargetResult> {
2055
2066
  const { routesDir, outDir, workDir } = options
2067
+ // One without the other ships a client that posts to an endpoint the server lacks, or a live server
2068
+ // no client ever watches.
2069
+ if ((options.optionImports?.live === undefined) !== (options.live === undefined)) {
2070
+ throw new Error(
2071
+ "[nifra/web] buildTarget: `optionImports.live` and `live` go together - the server's runtime and the client entry that reaches it",
2072
+ )
2073
+ }
2056
2074
  const requestedCssCodeSplit =
2057
2075
  options.cssCodeSplit === undefined ? undefined : normalizeCssCodeSplit(options.cssCodeSplit)
2058
2076
  const requestedCssLoading =
@@ -2101,6 +2119,7 @@ export async function buildTargetWith(
2101
2119
  ...(requestedCssCodeSplit !== undefined ? { cssCodeSplit: requestedCssCodeSplit } : {}),
2102
2120
  ...(requestedCssLoading !== undefined ? { cssLoading: requestedCssLoading } : {}),
2103
2121
  root: resolvePath(dirname(routesDir)),
2122
+ live: options.live,
2104
2123
  })
2105
2124
  // Bun's client builder has no CSS-loading field, but the server-side document policy is bundler
2106
2125
  // independent. Carry the requested policy into the result so static prerender callbacks and the
package/src/client.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  * a full page load). DOM-only - never imported on the server, so the store stays SSR-safe.
6
6
  */
7
7
  import { trustedHeadAttributes } from "./internal/head-attributes.ts"
8
+ import { setPresenter } from "./internal/present.ts"
8
9
  import { appUrl } from "./internal/route-path.ts"
9
10
  import { EXECUTABLE_SCRIPT_TYPES, INERT_SCRIPT_TYPES } from "./internal/script-types.ts"
10
11
  import type { Meta } from "./manifest.ts"
@@ -35,6 +36,7 @@ export function currentDocumentNonce(): string | undefined {
35
36
  return script.getAttribute("nonce") ?? undefined
36
37
  }
37
38
 
39
+ export { identityChanged } from "./identity.ts"
38
40
  /**
39
41
  * Everything the generated client entry needs, re-exported here so that entry imports ONE module and
40
42
  * that module is DOM-only. It used to take these from `@nifrajs/web`, whose graph includes `renderPage`
@@ -43,7 +45,11 @@ export function currentDocumentNonce(): string | undefined {
43
45
  * before hydrating. The fix is for the entry not to name the server module in the first place.
44
46
  */
45
47
  export { mergeHeads, resolveChainMeta, resolveMeta } from "./internal/head-merge.ts"
48
+ // For route helpers shared by a loader and a component, which the browser loads too: imported from the
49
+ // root, they put its `node:crypto` import into Vite dev's browser deps on every page.
50
+ export { gone, notFound, statusPage } from "./internal/status-page.ts"
46
51
  export {
52
+ closeBoundary,
47
53
  getBrowserNavigate,
48
54
  IDLE_BLOCKER,
49
55
  registerBlocker,
@@ -209,6 +215,10 @@ export function waitForStyles(options: WaitForStylesOptions = {}): Promise<void>
209
215
  return Promise.all(promises).then(() => undefined)
210
216
  }
211
217
 
218
+ // How long a navigation's view transition waits for the frame that captures the page before its
219
+ // animation is skipped: several frames at any refresh rate, and short enough to go unnoticed.
220
+ const CAPTURE_DEADLINE_MS = 100
221
+
212
222
  /**
213
223
  * Resolve `href` against this page and keep it only when history may move there without a load: same
214
224
  * scheme and host as the document, or null. Compared by protocol + host rather than `URL.origin`,
@@ -255,6 +265,48 @@ export function installHistory(
255
265
  let reversing = false
256
266
  // The path the router was last seen loading, so a redirect it follows shows up as a change.
257
267
  let expected: string | undefined
268
+ // The entry holding the page last shown without an intercept boundary, so closing a boundary opened
269
+ // over it can go back there. Cleared once that entry is replaced or cut off by a push.
270
+ let pageEntry: number | undefined = index
271
+ // Before writing the entry at `index` (replace) or pushing past it, which drops every later entry.
272
+ const writingEntry = (mode: "push" | "replace"): void => {
273
+ if (mode === "replace" ? pageEntry === index : pageEntry !== undefined && pageEntry > index)
274
+ pageEntry = undefined
275
+ }
276
+ // Every entry this document owns carries its index, the first one included, so an entry without one
277
+ // is one the browser pushed itself: a same-page fragment link, which this router leaves native.
278
+ // Number it as the push it was, or index deltas (a guard's reversal, closeBoundary) count wrong.
279
+ const numbered = (): boolean => {
280
+ const current: unknown = history.state
281
+ return (
282
+ typeof current === "object" &&
283
+ current !== null &&
284
+ "nifraIndex" in current &&
285
+ typeof current.nifraIndex === "number"
286
+ )
287
+ }
288
+ const stampEntry = (): void => {
289
+ const current: unknown = history.state
290
+ history.replaceState(
291
+ Object.assign({}, typeof current === "object" ? current : null, { nifraIndex: index }),
292
+ "",
293
+ )
294
+ }
295
+ const adoptNativeEntry = (): boolean => {
296
+ if (numbered()) return false
297
+ writingEntry("push")
298
+ index += 1
299
+ stampEntry()
300
+ here = location.pathname + location.search + (location.hash ?? "")
301
+ return true
302
+ }
303
+ // An opaque or file: document routes nothing, so its history is left alone. A refused write (a
304
+ // browser's rate limit) only leaves fragment entries uncounted, as before.
305
+ if (location.origin !== "null" && location.protocol !== "file:" && !numbered()) {
306
+ try {
307
+ stampEntry()
308
+ } catch {}
309
+ }
258
310
  type Registration = {
259
311
  readonly shouldBlock: BlockerFunction
260
312
  readonly emit: (blocker: Blocker) => void
@@ -281,29 +333,60 @@ export function installHistory(
281
333
  return document.getElementById(id)
282
334
  }
283
335
 
284
- // Wrap a navigation's render in a View Transition when the browser supports it - a graceful
285
- // enhancement (no-op elsewhere). The captured "before" is held across the data fetch, so pair
286
- // with link prefetch (hover/focus warms the cache) to keep most transitions instant.
336
+ // A navigation loads first and hands only its page swap here, so where the browser has View
337
+ // Transitions the page keeps painting, pending UI included, while the data loads. The browser runs
338
+ // the swap once a frame has captured the page; one drawing no frames while it reports itself visible
339
+ // (an occluded window) would hold it forever, so the deadline skips the animation, running the swap.
287
340
  type ViewTransition = {
288
341
  readonly ready: Promise<unknown>
289
342
  readonly finished: Promise<unknown>
290
343
  readonly updateCallbackDone: Promise<unknown>
344
+ skipTransition(): void
291
345
  }
292
346
  type ViewTransitionDocument = Document & {
293
347
  startViewTransition?: (cb: () => unknown) => ViewTransition
294
348
  }
295
- const transition = (run: () => Promise<void>): void => {
349
+ setPresenter((swap) => {
296
350
  const doc = document as ViewTransitionDocument
297
- if (typeof doc.startViewTransition !== "function") {
298
- void run()
299
- return
351
+ const start = doc.startViewTransition
352
+ if (typeof start !== "function") {
353
+ swap()
354
+ return Promise.resolve()
300
355
  }
301
- const vt = doc.startViewTransition(run)
302
- // Rapid in-app clicks supersede an in-flight transition; the browser skips it and rejects its
303
- // promises. Reasons vary by engine - InvalidStateError "Transition was aborted because of
304
- // invalid state", or AbortError "Transition was skipped". The navigation still completes, so
305
- // we swallow these expected aborts rather than let them surface as unhandled rejections.
306
- for (const p of [vt.ready, vt.finished, vt.updateCallbackDone]) p.catch(() => {})
356
+ return new Promise((resolve, reject) => {
357
+ let swapped = false
358
+ const vt = start.call(doc, () => {
359
+ swapped = true
360
+ try {
361
+ swap()
362
+ resolve()
363
+ } catch (error) {
364
+ reject(error)
365
+ }
366
+ })
367
+ setTimeout(() => {
368
+ if (!swapped) vt.skipTransition()
369
+ }, CAPTURE_DEADLINE_MS)
370
+ // A newer navigation's transition, or the deadline, skips this one and rejects its promises -
371
+ // InvalidStateError or AbortError by engine. The swap still runs, so these are expected.
372
+ for (const p of [vt.ready, vt.finished, vt.updateCallbackDone]) p.catch(() => {})
373
+ })
374
+ })
375
+ // A navigation that loads takes the page from a partially prerendered first document, which then
376
+ // stops filling its holes and cancels the ones still loading on the server. `navigate` publishes
377
+ // its `pendingPath` before its first await; a client-only search change, or a boundary already
378
+ // showing its target, loads nothing, so the holes still pending keep filling.
379
+ const navigateFrom = (routePath: string, from: string): Promise<void> => {
380
+ const run = router.navigate(routePath).catch((error) => leave(error, from))
381
+ const ppr: unknown = Reflect.get(globalThis, "__nifraPpr")
382
+ if (
383
+ router.snapshot().pendingPath === routePath &&
384
+ typeof ppr === "function" &&
385
+ "leave" in ppr &&
386
+ typeof ppr.leave === "function"
387
+ )
388
+ ppr.leave()
389
+ return run
307
390
  }
308
391
 
309
392
  // Parse a path into the `{ pathname, search, hash }` a `BlockerFunction` decides on.
@@ -364,6 +447,7 @@ export function installHistory(
364
447
  // whole object on back/forward for free.
365
448
  const entryState =
366
449
  state !== undefined ? { nifraIndex: index, nifraState: state } : { nifraIndex: index }
450
+ writingEntry(mode)
367
451
  if (mode === "replace") {
368
452
  // Replace the current entry (same index) with the new URL. The entry we're leaving is discarded, so
369
453
  // its scroll isn't worth saving; the new route scrolls to its fragment target, else the top.
@@ -381,7 +465,7 @@ export function installHistory(
381
465
  here = url.pathname + url.search + url.hash
382
466
  expected = routePath
383
467
  // The data layer fetches by path+search; the #hash is client-only (never sent to the server).
384
- transition(() => router.navigate(routePath).catch((error) => leave(error, path)))
468
+ void navigateFrom(routePath, path)
385
469
  settle()
386
470
  }
387
471
 
@@ -417,7 +501,15 @@ export function installHistory(
417
501
  }
418
502
  go(to, navOptions?.replace === true ? "replace" : "push", navOptions?.state)
419
503
  }
420
- setBrowserNavigate(navigate, router)
504
+ // Back through the entries the boundary added to the page underneath; without that entry, replace
505
+ // the boundary's URL with the page's.
506
+ const closeBoundary = (name: string): void => {
507
+ const intercepted = router.snapshot().intercepted
508
+ if (intercepted === undefined || intercepted.boundary !== name) return
509
+ if (pageEntry !== undefined && pageEntry < index) history.go(pageEntry - index)
510
+ else go(intercepted.pagePath, "replace")
511
+ }
512
+ setBrowserNavigate(navigate, router, closeBoundary)
421
513
 
422
514
  // Publish the guard registry through the DOM-free bridge (`@nifrajs/web`'s `registerBlocker`, which an
423
515
  // adapter's `useBlocker` calls). One slot, latest registration wins; the unregister clears it only if
@@ -515,10 +607,12 @@ export function installHistory(
515
607
  reversing = false
516
608
  return
517
609
  }
610
+ adoptNativeEntry()
518
611
  const newIndex = (history.state as { nifraIndex?: number } | null)?.nifraIndex ?? 0
519
612
  const dest = location.pathname + location.search + (location.hash ?? "")
520
613
  const delta = newIndex - index
521
- if (guard(dest, () => history.go(delta))) {
614
+ // A fragment entry the browser just pushed is not a move between entries to reverse.
615
+ if (delta !== 0 && guard(dest, () => history.go(delta))) {
522
616
  reversing = true
523
617
  history.go(-delta)
524
618
  return
@@ -528,7 +622,7 @@ export function installHistory(
528
622
  pendingScroll = { pos: scrollOf(history.state) }
529
623
  const routePath = location.pathname + location.search
530
624
  expected = routePath
531
- transition(() => router.navigate(routePath).catch((error) => leave(error, dest)))
625
+ void navigateFrom(routePath, dest)
532
626
  settle()
533
627
  }
534
628
 
@@ -555,6 +649,7 @@ export function installHistory(
555
649
  // form post back to the page it was on adds nothing.
556
650
  const follow = (path: string): void => {
557
651
  if (path !== location.pathname + location.search) {
652
+ writingEntry(expected === undefined ? "push" : "replace")
558
653
  if (expected === undefined) {
559
654
  history.replaceState({ ...(history.state ?? {}), nifraScroll: [scrollX, scrollY] }, "")
560
655
  index += 1
@@ -573,13 +668,15 @@ export function installHistory(
573
668
  // A settle with no navigation (a submit's revalidation) scrolls nothing. Either way the rendered
574
669
  // page is then scanned for links to warm.
575
670
  const settled = (): void => {
576
- const { pending, pendingPath } = router.snapshot()
671
+ const { pending, pendingPath, intercepted } = router.snapshot()
577
672
  if (pending) {
578
673
  if (pendingPath !== undefined && pendingPath !== expected) follow(pendingPath)
579
674
  return
580
675
  }
581
676
  expected = undefined
582
- const target = pendingScroll
677
+ if (intercepted === undefined) pageEntry = index
678
+ // A boundary opened over the page leaves the page where it was scrolled.
679
+ const target = intercepted === undefined ? pendingScroll : null
583
680
  pendingScroll = null
584
681
  requestAnimationFrame(() => {
585
682
  if (target !== null && "hash" in target) {
@@ -599,16 +696,20 @@ export function installHistory(
599
696
  document.addEventListener("pointerover", onPrefetch)
600
697
  document.addEventListener("focusin", onPrefetch)
601
698
  window.addEventListener("popstate", onPopState)
699
+ // Where a fragment link fires no popstate, its entry is still numbered here.
700
+ window.addEventListener("hashchange", adoptNativeEntry)
602
701
  window.addEventListener("beforeunload", onBeforeUnload)
603
702
  return () => {
604
703
  unsubscribe()
605
704
  inView?.disconnect()
606
705
  setBrowserNavigate(undefined) // stop routing `useNavigate` to a torn-down router
607
706
  setBlockerController(undefined) // stop routing `useBlocker` to a torn-down registry
707
+ setPresenter(undefined)
608
708
  document.removeEventListener("click", onClick)
609
709
  document.removeEventListener("pointerover", onPrefetch)
610
710
  document.removeEventListener("focusin", onPrefetch)
611
711
  window.removeEventListener("popstate", onPopState)
712
+ window.removeEventListener("hashchange", adoptNativeEntry)
612
713
  window.removeEventListener("beforeunload", onBeforeUnload)
613
714
  }
614
715
  }
@@ -693,6 +794,47 @@ export function applyHead(head: Meta, path?: string): void {
693
794
  }
694
795
  }
695
796
 
797
+ /**
798
+ * The route path a form with no action posts to while an intercept boundary holds a target. The
799
+ * document URL is then the target's, but the form belongs to whichever route rendered it: the target
800
+ * when it sits inside the boundary's `data-nifra-boundary="<name>"` element, otherwise the page
801
+ * underneath. `undefined` when the form names its action, or no boundary holds a target.
802
+ */
803
+ function implicitActionOwner(
804
+ router: ClientRouter,
805
+ form: HTMLFormElement,
806
+ actionFrom: Element,
807
+ ): string | undefined {
808
+ const { intercepted, path } = router.snapshot()
809
+ if (intercepted === undefined) return undefined
810
+ const named = Element.prototype.getAttribute.call(actionFrom, actionAttribute(actionFrom, form))
811
+ if (named !== null && named.trim() !== "") return undefined
812
+ // Any ancestor marking the open boundary owns it, so a marker nested inside the modal (another
813
+ // boundary's) does not hand the form to the page. Boundary names are validated identifiers; the
814
+ // check keeps the selector literal regardless.
815
+ const inside =
816
+ BOUNDARY_NAME.test(intercepted.boundary) &&
817
+ Element.prototype.closest.call(form, `[data-nifra-boundary="${intercepted.boundary}"]`) !== null
818
+ return inside ? path : intercepted.pagePath
819
+ }
820
+
821
+ const BOUNDARY_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/
822
+
823
+ const actionAttribute = (actionFrom: Element, form: HTMLFormElement): string =>
824
+ actionFrom === form ? "action" : "formaction"
825
+
826
+ // Point the implicit action at `href` for one native submission; the returned function puts the
827
+ // attribute back, so a form that outlives the boundary (one in a layout) keeps posting to its page.
828
+ function pointAction(actionFrom: Element, form: HTMLFormElement, href: string): () => void {
829
+ const name = actionAttribute(actionFrom, form)
830
+ const previous = Element.prototype.getAttribute.call(actionFrom, name)
831
+ Element.prototype.setAttribute.call(actionFrom, name, href)
832
+ return () => {
833
+ if (previous === null) Element.prototype.removeAttribute.call(actionFrom, name)
834
+ else Element.prototype.setAttribute.call(actionFrom, name, previous)
835
+ }
836
+ }
837
+
696
838
  // A control named like a form property shadows it (`<input name="target">` makes `form.target` that
697
839
  // input), so the form's own state is read through the prototype accessors.
698
840
  function formProperty(form: HTMLFormElement, name: "action" | "method" | "target"): string {
@@ -720,14 +862,26 @@ export function installForms(router: ClientRouter): () => void {
720
862
  const method = submitter?.hasAttribute("formmethod")
721
863
  ? submitter.formMethod
722
864
  : formProperty(form, "method")
723
- if (method.toLowerCase() !== "post") return // GET forms submit natively
865
+ const actionFrom = submitter?.hasAttribute("formaction") ? submitter : form
866
+ const owner = implicitActionOwner(router, form, actionFrom)
867
+ if (method.toLowerCase() !== "post") {
868
+ // GET forms submit natively. The browser reads the action after this event, so one the page
869
+ // rendered under a boundary is pointed at the page for this submission, then restored.
870
+ if (owner !== undefined)
871
+ setTimeout(pointAction(actionFrom, form, new URL(appUrl(owner), location.href).href), 0)
872
+ return
873
+ }
724
874
  const target = submitter?.hasAttribute("formtarget")
725
875
  ? submitter.formTarget
726
876
  : formProperty(form, "target")
727
877
  if (target !== "" && target !== "_self") return // it loads in another browsing context
728
878
  // Resolved (or the current document URL if unset).
729
879
  const url = sameDocumentUrl(
730
- submitter?.hasAttribute("formaction") ? submitter.formAction : formProperty(form, "action"),
880
+ owner !== undefined
881
+ ? appUrl(owner)
882
+ : actionFrom === submitter
883
+ ? submitter.formAction
884
+ : formProperty(form, "action"),
731
885
  )
732
886
  if (url === null) return
733
887
  if (router.match(url.pathname) === null) return // not an app route → native submit
@@ -742,9 +896,15 @@ export function installForms(router: ClientRouter): () => void {
742
896
  // The server answered: load the page that shows the result rather than run the action again.
743
897
  if (to !== undefined) location.assign(to)
744
898
  else {
745
- // Nothing reached the server - post natively, through the same button.
899
+ // Nothing reached the server - post natively, through the same button, to the same route.
746
900
  native.add(form)
747
- HTMLFormElement.prototype.requestSubmit.call(form, submitter)
901
+ // requestSubmit reads the action synchronously, so the attribute is restored right after.
902
+ const restore = owner === undefined ? undefined : pointAction(actionFrom, form, url.href)
903
+ try {
904
+ HTMLFormElement.prototype.requestSubmit.call(form, submitter)
905
+ } finally {
906
+ restore?.()
907
+ }
748
908
  }
749
909
  })
750
910
  }
package/src/csp.ts CHANGED
@@ -109,8 +109,9 @@ const inlineScriptBodies = (html: string): string[] => {
109
109
 
110
110
  const ENCODER = new TextEncoder()
111
111
 
112
- /** A CSP hash source for one inline script body: `'sha256-<base64 of the UTF-8 bytes>'`. */
113
- async function hashSource(body: string): Promise<string> {
112
+ /** A CSP hash source for one inline script body: `'sha256-<base64 of the UTF-8 bytes>'`. Shared with
113
+ * the PPR runtime; `index.ts` does not re-export it. */
114
+ export async function hashSource(body: string): Promise<string> {
114
115
  const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", ENCODER.encode(body)))
115
116
  let binary = ""
116
117
  for (const byte of digest) binary += String.fromCharCode(byte)
@@ -158,6 +159,19 @@ const preparedSources = (policy: CspPolicy, adapter: RenderAdapter): AdapterSour
158
159
  return known
159
160
  }
160
161
 
162
+ /**
163
+ * The policy value for a partially prerendered document: the adapter's hash sources plus `extra`
164
+ * (the PPR runtime and invoker hashes), with no nonce. Prepare the policy for `adapter` first.
165
+ */
166
+ export function cspHeaderWithSources(
167
+ policy: CspPolicy,
168
+ adapter: RenderAdapter,
169
+ extra: string,
170
+ ): string | undefined {
171
+ const { sources } = preparedSources(policy, adapter)
172
+ return policy[CSP_POLICY].header({ sources: sources === "" ? extra : `${sources} ${extra}` })
173
+ }
174
+
161
175
  /**
162
176
  * The nonce one document carries under `policy`: `undefined` unless the document needs one -
163
177
  * `needsNonce` (a deferred value or a head tag naming the nonce), or an adapter whose hydration head
package/src/deferred.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { plainJsonCodec, type TransportCodec } from "@nifrajs/core/transport-codec"
9
9
  import { isDeferredFree } from "./internal/deferred-free.ts"
10
+ import { reportDeferredRejection } from "./internal/request-abort.ts"
10
11
  import { parsePageData } from "./internal/revive-dates.ts"
11
12
 
12
13
  /**
@@ -171,15 +172,19 @@ const NDJSON_ENCODER = new TextEncoder()
171
172
  * `{__nifra_deferred: id}` placeholders (`forClient`), then one line per deferred as its promise
172
173
  * settles - `{"i": id, "v": value}` on resolve, `{"i": id, "e": "deferred_error"}` on reject (a
173
174
  * rejection is data, not a stream error; the opaque code never leaks the raw reason). Closes when all
174
- * settle. The client (`defaultFetchData`) returns the
175
+ * settle. The client (`fetchRouteData`) returns the
175
176
  * data after line 1 and settles `<Await>`'s markers as the resolution lines arrive. `JSON.stringify`
176
177
  * escapes newlines in values, so each line is NDJSON-safe; no HTML escaping (this is a fetch body,
177
- * parsed with `JSON.parse`, never injected into markup).
178
+ * parsed with `JSON.parse`, never injected into markup). A rejection is logged to `logError`, the
179
+ * app's error log (the console without one), unless the request `signal` aborting caused it: that is
180
+ * the client leaving, not a failure.
178
181
  */
179
182
  export function ndjsonStream(
180
183
  forClient: unknown,
181
184
  deferred: ReadonlyArray<{ readonly id: number; readonly promise: Promise<unknown> }>,
182
185
  codec: TransportCodec = plainJsonCodec,
186
+ signal?: AbortSignal,
187
+ logError?: (error: unknown) => void,
183
188
  ): ReadableStream<Uint8Array> {
184
189
  // A client that goes away cancels the body; a value settling after that has nowhere to go.
185
190
  let cancelled = false
@@ -194,7 +199,7 @@ export function ndjsonStream(
194
199
  } catch (err) {
195
200
  // Redact: stream a stable opaque code, never the raw error text; log the real reason
196
201
  // server-side. The client's <Await> errorFallback receives this code.
197
- console.error("[nifra/web] deferred value rejected:", err)
202
+ reportDeferredRejection(signal, err, logError)
198
203
  line = codec.encode({ i: d.id, e: DEFERRED_ERROR_CODE })
199
204
  }
200
205
  if (!cancelled) controller.enqueue(NDJSON_ENCODER.encode(`${line}\n`))
@@ -319,7 +324,7 @@ function markersFromPlaceholders(data: unknown, pending: Pending): unknown {
319
324
  }
320
325
 
321
326
  /**
322
- * Client side of {@link ndjsonStream} (used by the router's `defaultFetchData` on a soft nav): read
327
+ * Client side of {@link ndjsonStream} (used by the router's `fetchRouteData` on a soft nav): read
323
328
  * the NDJSON body into a data object whose deferred markers settle as resolution lines arrive.
324
329
  * Returns after **line 1** so the router can apply the critical data + render immediately; the
325
330
  * markers' promises settle/reject in the background. If the stream ends (or `signal` aborts) with
@@ -335,7 +340,8 @@ export async function parseNdjsonData(
335
340
  let buffer = ""
336
341
  // `{ once: true }` + explicit removal in the drain's `finally` so a reused long-lived signal doesn't
337
342
  // accumulate listeners across navigations (a per-nav signal would be GC'd, but don't rely on it).
338
- const onAbort = (): void => void reader.cancel()
343
+ // A body the abort has already errored (WebKit errors it first) rejects the cancel.
344
+ const onAbort = (): void => void reader.cancel().catch(() => {})
339
345
  if (signal) signal.addEventListener("abort", onAbort, { once: true })
340
346
  let ended = false
341
347
  // Every way out, a malformed line included, detaches from the signal and the body: a source left
package/src/dev-feed.ts CHANGED
@@ -36,6 +36,7 @@ import {
36
36
  buildDiagnostic,
37
37
  type Diagnostic,
38
38
  type DiagnosticFrame,
39
+ isConsoleDiagnostic,
39
40
  } from "./diagnostic.ts"
40
41
  import { createRedactor } from "./internal/secret-scan.ts"
41
42
  import { originalPosition, ssrSourceMaps } from "./internal/source-map.ts"
@@ -1140,8 +1141,16 @@ const ssrRemap = (
1140
1141
  export function captureInto(feed: DevFeed): () => void {
1141
1142
  return installCapture({
1142
1143
  feedId: feed.id,
1143
- line: (level, message, request) =>
1144
- feed.recordLog(level, message, { requestId: request?.requestId }),
1144
+ line: (level, message, request) => {
1145
+ feed.recordLog(level, message, { requestId: request?.requestId })
1146
+ // A diagnostic the server only logs (no throw reaches the feed) still becomes an entry.
1147
+ if ((level === "warn" || level === "error") && isConsoleDiagnostic(message)) {
1148
+ const warning = new Error(message)
1149
+ warning.name = "Warning"
1150
+ warning.stack = `Warning: ${message}`
1151
+ feed.recordError(warning, { category: "ssr", requestId: request?.requestId })
1152
+ }
1153
+ },
1145
1154
  structured: (entry, request) => {
1146
1155
  const level = isDevLogLevel(entry.level) ? entry.level : "log"
1147
1156
  feed.recordLog(level, structuredText(entry), { requestId: request?.requestId })
@@ -44,7 +44,13 @@ import {
44
44
  } from "./dev-feed.ts"
45
45
  import { DEV_INDICATOR_MAX_ISSUES, DEV_INDICATOR_SOURCE, type DevIssue } from "./dev-indicator.ts"
46
46
  import { createSourceMapper } from "./dev-sourcemap.ts"
47
- import { fixPrompts, isHydrationMismatch, LAST_ERROR_PATH, promptPath } from "./diagnostic.ts"
47
+ import {
48
+ fixPrompts,
49
+ isConsoleDiagnostic,
50
+ isHydrationMismatch,
51
+ LAST_ERROR_PATH,
52
+ promptPath,
53
+ } from "./diagnostic.ts"
48
54
  import { diagnosticHeadline } from "./diagnostic-prompt.ts"
49
55
  import { timingSafeEqual } from "./internal/timing-safe-equal.ts"
50
56
  import { ISR_STATUS_HEADER } from "./isr.ts"
@@ -439,16 +445,21 @@ export function createDevSession(options: DevSessionOptions): DevSession {
439
445
  if (event.kind === "console") {
440
446
  feed.recordLog(event.level, event.message, meta)
441
447
  if (event.error !== undefined) return (await recordBrowserError(event.error, meta)).entry
442
- // React 18, Vue and Svelte report a mismatch through the console rather than by throwing.
443
- if (
444
- (event.level === "error" || event.level === "warn") &&
445
- isHydrationMismatch(event.message)
446
- ) {
447
- const recorded = await recordBrowserError(
448
- { name: "HydrationMismatch", message: event.message, stack: "" },
449
- meta,
450
- )
451
- return recorded.entry
448
+ // React 18, Vue and Svelte report a mismatch through the console rather than by throwing, as
449
+ // nifra's own browser diagnostics do.
450
+ if (event.level === "error" || event.level === "warn") {
451
+ const name = isHydrationMismatch(event.message)
452
+ ? "HydrationMismatch"
453
+ : isConsoleDiagnostic(event.message)
454
+ ? "Warning"
455
+ : undefined
456
+ if (name !== undefined) {
457
+ const recorded = await recordBrowserError(
458
+ { name, message: event.message, stack: "" },
459
+ meta,
460
+ )
461
+ return recorded.entry
462
+ }
452
463
  }
453
464
  return undefined
454
465
  }