@nifrajs/web 5.5.0 → 5.6.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 (457) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/dist/boundary.d.ts +67 -7
  3. package/dist/boundary.d.ts.map +1 -1
  4. package/dist/boundary.js +6 -4
  5. package/dist/boundary.js.map +1 -1
  6. package/dist/build-plan.d.ts +3 -0
  7. package/dist/build-plan.d.ts.map +1 -1
  8. package/dist/build-plan.js.map +1 -1
  9. package/dist/build-vite.d.ts.map +1 -1
  10. package/dist/build-vite.js +2 -0
  11. package/dist/build-vite.js.map +1 -1
  12. package/dist/build.d.ts +9 -2
  13. package/dist/build.d.ts.map +1 -1
  14. package/dist/build.js +9 -0
  15. package/dist/build.js.map +1 -1
  16. package/dist/client.d.ts +2 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +140 -12
  19. package/dist/client.js.map +1 -1
  20. package/dist/csp.d.ts +8 -0
  21. package/dist/csp.d.ts.map +1 -1
  22. package/dist/csp.js +11 -2
  23. package/dist/csp.js.map +1 -1
  24. package/dist/deferred.d.ts +6 -4
  25. package/dist/deferred.d.ts.map +1 -1
  26. package/dist/deferred.js +10 -6
  27. package/dist/deferred.js.map +1 -1
  28. package/dist/dev-feed.d.ts.map +1 -1
  29. package/dist/dev-feed.js +11 -2
  30. package/dist/dev-feed.js.map +1 -1
  31. package/dist/dev-session.d.ts.map +1 -1
  32. package/dist/dev-session.js +13 -6
  33. package/dist/dev-session.js.map +1 -1
  34. package/dist/dev.d.ts +4 -1
  35. package/dist/dev.d.ts.map +1 -1
  36. package/dist/dev.js +12 -5
  37. package/dist/dev.js.map +1 -1
  38. package/dist/diagnostic-catalog.d.ts +4 -0
  39. package/dist/diagnostic-catalog.d.ts.map +1 -1
  40. package/dist/diagnostic-catalog.js +48 -0
  41. package/dist/diagnostic-catalog.js.map +1 -1
  42. package/dist/diagnostic.d.ts +1 -1
  43. package/dist/diagnostic.d.ts.map +1 -1
  44. package/dist/diagnostic.js +1 -1
  45. package/dist/diagnostic.js.map +1 -1
  46. package/dist/fn.d.ts +5 -3
  47. package/dist/fn.d.ts.map +1 -1
  48. package/dist/fn.js +20 -5
  49. package/dist/fn.js.map +1 -1
  50. package/dist/fs.d.ts.map +1 -1
  51. package/dist/fs.js +13 -3
  52. package/dist/fs.js.map +1 -1
  53. package/dist/identity.d.ts +22 -0
  54. package/dist/identity.d.ts.map +1 -0
  55. package/dist/identity.js +60 -0
  56. package/dist/identity.js.map +1 -0
  57. package/dist/index.d.ts +7 -6
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js +4 -2
  60. package/dist/index.js.map +1 -1
  61. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  62. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  63. package/dist/internal/cdn/cloudflare.js +2 -1
  64. package/dist/internal/cdn/cloudflare.js.map +1 -1
  65. package/dist/internal/certified-components.d.ts +1 -1
  66. package/dist/internal/certified-components.js +1 -1
  67. package/dist/internal/codegen.d.ts +11 -10
  68. package/dist/internal/codegen.d.ts.map +1 -1
  69. package/dist/internal/codegen.js +65 -8
  70. package/dist/internal/codegen.js.map +1 -1
  71. package/dist/internal/dev-websocket.d.ts +73 -0
  72. package/dist/internal/dev-websocket.d.ts.map +1 -0
  73. package/dist/internal/dev-websocket.js +145 -0
  74. package/dist/internal/dev-websocket.js.map +1 -0
  75. package/dist/internal/hole-runtime.d.ts +66 -0
  76. package/dist/internal/hole-runtime.d.ts.map +1 -0
  77. package/dist/internal/hole-runtime.js +211 -0
  78. package/dist/internal/hole-runtime.js.map +1 -0
  79. package/dist/internal/identity-epoch.d.ts +50 -0
  80. package/dist/internal/identity-epoch.d.ts.map +1 -0
  81. package/dist/internal/identity-epoch.js +179 -0
  82. package/dist/internal/identity-epoch.js.map +1 -0
  83. package/dist/internal/invalidation-contract.d.ts +23 -0
  84. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  85. package/dist/internal/invalidation-contract.js +11 -0
  86. package/dist/internal/invalidation-contract.js.map +1 -0
  87. package/dist/internal/invalidation.d.ts +62 -0
  88. package/dist/internal/invalidation.d.ts.map +1 -0
  89. package/dist/internal/invalidation.js +149 -0
  90. package/dist/internal/invalidation.js.map +1 -0
  91. package/dist/internal/island-bindings.d.ts +51 -0
  92. package/dist/internal/island-bindings.d.ts.map +1 -0
  93. package/dist/internal/island-bindings.js +147 -0
  94. package/dist/internal/island-bindings.js.map +1 -0
  95. package/dist/internal/island-list.d.ts +26 -0
  96. package/dist/internal/island-list.d.ts.map +1 -0
  97. package/dist/internal/island-list.js +78 -0
  98. package/dist/internal/island-list.js.map +1 -0
  99. package/dist/internal/island-mount.d.ts +4 -0
  100. package/dist/internal/island-mount.d.ts.map +1 -0
  101. package/dist/internal/island-mount.js +66 -0
  102. package/dist/internal/island-mount.js.map +1 -0
  103. package/dist/internal/island-owner.d.ts +5 -0
  104. package/dist/internal/island-owner.d.ts.map +1 -0
  105. package/dist/internal/island-owner.js +20 -0
  106. package/dist/internal/island-owner.js.map +1 -0
  107. package/dist/internal/island-resource.d.ts +30 -0
  108. package/dist/internal/island-resource.d.ts.map +1 -0
  109. package/dist/internal/island-resource.js +57 -0
  110. package/dist/internal/island-resource.js.map +1 -0
  111. package/dist/internal/island-signals.d.ts +51 -0
  112. package/dist/internal/island-signals.d.ts.map +1 -0
  113. package/dist/internal/island-signals.js +220 -0
  114. package/dist/internal/island-signals.js.map +1 -0
  115. package/dist/internal/keep-alive-table.d.ts +23 -0
  116. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  117. package/dist/internal/keep-alive-table.js +53 -0
  118. package/dist/internal/keep-alive-table.js.map +1 -0
  119. package/dist/internal/live-contract.d.ts +96 -0
  120. package/dist/internal/live-contract.d.ts.map +1 -0
  121. package/dist/internal/live-contract.js +5 -0
  122. package/dist/internal/live-contract.js.map +1 -0
  123. package/dist/internal/live-queries.d.ts +22 -0
  124. package/dist/internal/live-queries.d.ts.map +1 -0
  125. package/dist/internal/live-queries.js +54 -0
  126. package/dist/internal/live-queries.js.map +1 -0
  127. package/dist/internal/page-events.d.ts +10 -0
  128. package/dist/internal/page-events.d.ts.map +1 -0
  129. package/dist/internal/page-events.js +26 -0
  130. package/dist/internal/page-events.js.map +1 -0
  131. package/dist/internal/page-execution.d.ts +14 -1
  132. package/dist/internal/page-execution.d.ts.map +1 -1
  133. package/dist/internal/page-execution.js +415 -31
  134. package/dist/internal/page-execution.js.map +1 -1
  135. package/dist/internal/ppr/admission.d.ts +83 -0
  136. package/dist/internal/ppr/admission.d.ts.map +1 -0
  137. package/dist/internal/ppr/admission.js +221 -0
  138. package/dist/internal/ppr/admission.js.map +1 -0
  139. package/dist/internal/ppr/canary.d.ts +24 -0
  140. package/dist/internal/ppr/canary.d.ts.map +1 -0
  141. package/dist/internal/ppr/canary.js +102 -0
  142. package/dist/internal/ppr/canary.js.map +1 -0
  143. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  144. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  145. package/dist/internal/ppr/compile-shell.js +298 -0
  146. package/dist/internal/ppr/compile-shell.js.map +1 -0
  147. package/dist/internal/ppr/contract.d.ts +177 -0
  148. package/dist/internal/ppr/contract.d.ts.map +1 -0
  149. package/dist/internal/ppr/contract.js +80 -0
  150. package/dist/internal/ppr/contract.js.map +1 -0
  151. package/dist/internal/ppr/frames.d.ts +71 -0
  152. package/dist/internal/ppr/frames.d.ts.map +1 -0
  153. package/dist/internal/ppr/frames.js +119 -0
  154. package/dist/internal/ppr/frames.js.map +1 -0
  155. package/dist/internal/ppr/integration.d.ts +19 -0
  156. package/dist/internal/ppr/integration.d.ts.map +1 -0
  157. package/dist/internal/ppr/integration.js +106 -0
  158. package/dist/internal/ppr/integration.js.map +1 -0
  159. package/dist/internal/ppr/options.d.ts +96 -0
  160. package/dist/internal/ppr/options.d.ts.map +1 -0
  161. package/dist/internal/ppr/options.js +125 -0
  162. package/dist/internal/ppr/options.js.map +1 -0
  163. package/dist/internal/ppr/route-rules.d.ts +12 -0
  164. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  165. package/dist/internal/ppr/route-rules.js +53 -0
  166. package/dist/internal/ppr/route-rules.js.map +1 -0
  167. package/dist/internal/ppr/runtime.d.ts +29 -0
  168. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  169. package/dist/internal/ppr/runtime.js +32 -0
  170. package/dist/internal/ppr/runtime.js.map +1 -0
  171. package/dist/internal/ppr/server.d.ts +29 -0
  172. package/dist/internal/ppr/server.d.ts.map +1 -0
  173. package/dist/internal/ppr/server.js +1407 -0
  174. package/dist/internal/ppr/server.js.map +1 -0
  175. package/dist/internal/ppr/shell-store.d.ts +38 -0
  176. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  177. package/dist/internal/ppr/shell-store.js +90 -0
  178. package/dist/internal/ppr/shell-store.js.map +1 -0
  179. package/dist/internal/prerendered-pages.d.ts +1 -1
  180. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  181. package/dist/internal/prerendered-pages.js +7 -4
  182. package/dist/internal/prerendered-pages.js.map +1 -1
  183. package/dist/internal/query-digest.d.ts +9 -0
  184. package/dist/internal/query-digest.d.ts.map +1 -0
  185. package/dist/internal/query-digest.js +23 -0
  186. package/dist/internal/query-digest.js.map +1 -0
  187. package/dist/internal/query-runtime.d.ts +6 -1
  188. package/dist/internal/query-runtime.d.ts.map +1 -1
  189. package/dist/internal/query-runtime.js +9 -0
  190. package/dist/internal/query-runtime.js.map +1 -1
  191. package/dist/internal/query-seam.d.ts +29 -0
  192. package/dist/internal/query-seam.d.ts.map +1 -0
  193. package/dist/internal/query-seam.js +23 -0
  194. package/dist/internal/query-seam.js.map +1 -0
  195. package/dist/internal/render-document.d.ts +28 -1
  196. package/dist/internal/render-document.d.ts.map +1 -1
  197. package/dist/internal/render-document.js +83 -18
  198. package/dist/internal/render-document.js.map +1 -1
  199. package/dist/internal/request-abort.d.ts +8 -0
  200. package/dist/internal/request-abort.d.ts.map +1 -0
  201. package/dist/internal/request-abort.js +22 -0
  202. package/dist/internal/request-abort.js.map +1 -0
  203. package/dist/internal/server-boundary.d.ts +2 -0
  204. package/dist/internal/server-boundary.d.ts.map +1 -1
  205. package/dist/internal/server-boundary.js +8 -1
  206. package/dist/internal/server-boundary.js.map +1 -1
  207. package/dist/internal/share-data.d.ts +10 -0
  208. package/dist/internal/share-data.d.ts.map +1 -0
  209. package/dist/internal/share-data.js +135 -0
  210. package/dist/internal/share-data.js.map +1 -0
  211. package/dist/internal/store-signature.d.ts +15 -0
  212. package/dist/internal/store-signature.d.ts.map +1 -0
  213. package/dist/internal/store-signature.js +114 -0
  214. package/dist/internal/store-signature.js.map +1 -0
  215. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  216. package/dist/internal/vite-plugin-compat.js +2 -2
  217. package/dist/internal/web-app.d.ts +25 -0
  218. package/dist/internal/web-app.d.ts.map +1 -1
  219. package/dist/internal/web-app.js +78 -3
  220. package/dist/internal/web-app.js.map +1 -1
  221. package/dist/islands-swap.d.ts +41 -0
  222. package/dist/islands-swap.d.ts.map +1 -0
  223. package/dist/islands-swap.js +270 -0
  224. package/dist/islands-swap.js.map +1 -0
  225. package/dist/islands.d.ts +86 -24
  226. package/dist/islands.d.ts.map +1 -1
  227. package/dist/islands.js +138 -78
  228. package/dist/islands.js.map +1 -1
  229. package/dist/isr.d.ts +67 -13
  230. package/dist/isr.d.ts.map +1 -1
  231. package/dist/isr.js +247 -99
  232. package/dist/isr.js.map +1 -1
  233. package/dist/keep-alive.d.ts +147 -0
  234. package/dist/keep-alive.d.ts.map +1 -0
  235. package/dist/keep-alive.js +1069 -0
  236. package/dist/keep-alive.js.map +1 -0
  237. package/dist/live/bus.d.ts +56 -0
  238. package/dist/live/bus.d.ts.map +1 -0
  239. package/dist/live/bus.js +32 -0
  240. package/dist/live/bus.js.map +1 -0
  241. package/dist/live/core.d.ts +25 -0
  242. package/dist/live/core.d.ts.map +1 -0
  243. package/dist/live/core.js +29 -0
  244. package/dist/live/core.js.map +1 -0
  245. package/dist/live/history.d.ts +38 -0
  246. package/dist/live/history.d.ts.map +1 -0
  247. package/dist/live/history.js +104 -0
  248. package/dist/live/history.js.map +1 -0
  249. package/dist/live/hub-protocol.d.ts +46 -0
  250. package/dist/live/hub-protocol.d.ts.map +1 -0
  251. package/dist/live/hub-protocol.js +108 -0
  252. package/dist/live/hub-protocol.js.map +1 -0
  253. package/dist/live/hub.d.ts +181 -0
  254. package/dist/live/hub.d.ts.map +1 -0
  255. package/dist/live/hub.js +539 -0
  256. package/dist/live/hub.js.map +1 -0
  257. package/dist/live/keyring.d.ts +87 -0
  258. package/dist/live/keyring.d.ts.map +1 -0
  259. package/dist/live/keyring.js +260 -0
  260. package/dist/live/keyring.js.map +1 -0
  261. package/dist/live/read-capped.d.ts +6 -0
  262. package/dist/live/read-capped.d.ts.map +1 -0
  263. package/dist/live/read-capped.js +42 -0
  264. package/dist/live/read-capped.js.map +1 -0
  265. package/dist/live/revocations.d.ts +52 -0
  266. package/dist/live/revocations.d.ts.map +1 -0
  267. package/dist/live/revocations.js +116 -0
  268. package/dist/live/revocations.js.map +1 -0
  269. package/dist/live/settings.d.ts +14 -0
  270. package/dist/live/settings.d.ts.map +1 -0
  271. package/dist/live/settings.js +38 -0
  272. package/dist/live/settings.js.map +1 -0
  273. package/dist/live-client.d.ts +34 -0
  274. package/dist/live-client.d.ts.map +1 -0
  275. package/dist/live-client.js +840 -0
  276. package/dist/live-client.js.map +1 -0
  277. package/dist/live-hub.d.ts +130 -0
  278. package/dist/live-hub.d.ts.map +1 -0
  279. package/dist/live-hub.js +507 -0
  280. package/dist/live-hub.js.map +1 -0
  281. package/dist/live-redis.d.ts +48 -0
  282. package/dist/live-redis.d.ts.map +1 -0
  283. package/dist/live-redis.js +201 -0
  284. package/dist/live-redis.js.map +1 -0
  285. package/dist/live.d.ts +94 -0
  286. package/dist/live.d.ts.map +1 -0
  287. package/dist/live.js +869 -0
  288. package/dist/live.js.map +1 -0
  289. package/dist/manifest.d.ts +113 -2
  290. package/dist/manifest.d.ts.map +1 -1
  291. package/dist/manifest.js +109 -5
  292. package/dist/manifest.js.map +1 -1
  293. package/dist/navigation.d.ts +9 -3
  294. package/dist/navigation.d.ts.map +1 -1
  295. package/dist/navigation.js +13 -3
  296. package/dist/navigation.js.map +1 -1
  297. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  298. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  299. package/dist/plugins/vite-zone-guard.js +8 -2
  300. package/dist/plugins/vite-zone-guard.js.map +1 -1
  301. package/dist/ppr.d.ts +73 -0
  302. package/dist/ppr.d.ts.map +1 -0
  303. package/dist/ppr.js +76 -0
  304. package/dist/ppr.js.map +1 -0
  305. package/dist/query.d.ts +8 -13
  306. package/dist/query.d.ts.map +1 -1
  307. package/dist/query.js +112 -18
  308. package/dist/query.js.map +1 -1
  309. package/dist/render-seam.d.ts +30 -0
  310. package/dist/render-seam.d.ts.map +1 -1
  311. package/dist/render-seam.js +5 -0
  312. package/dist/render-seam.js.map +1 -1
  313. package/dist/route-types.d.ts.map +1 -1
  314. package/dist/route-types.js +2 -2
  315. package/dist/route-types.js.map +1 -1
  316. package/dist/router.d.ts +72 -4
  317. package/dist/router.d.ts.map +1 -1
  318. package/dist/router.js +374 -99
  319. package/dist/router.js.map +1 -1
  320. package/dist/server-components/build.d.ts.map +1 -1
  321. package/dist/server-components/build.js +3 -3
  322. package/dist/server-components/build.js.map +1 -1
  323. package/dist/server-components/client.d.ts +23 -1
  324. package/dist/server-components/client.d.ts.map +1 -1
  325. package/dist/server-components/client.js +92 -5
  326. package/dist/server-components/client.js.map +1 -1
  327. package/dist/server-components/hole.d.ts +8 -0
  328. package/dist/server-components/hole.d.ts.map +1 -0
  329. package/dist/server-components/hole.js +20 -0
  330. package/dist/server-components/hole.js.map +1 -0
  331. package/dist/server-components/html-policy.d.ts.map +1 -1
  332. package/dist/server-components/html-policy.js +62 -4
  333. package/dist/server-components/html-policy.js.map +1 -1
  334. package/dist/server-components/html-scan.d.ts +17 -0
  335. package/dist/server-components/html-scan.d.ts.map +1 -0
  336. package/dist/server-components/html-scan.js +180 -0
  337. package/dist/server-components/html-scan.js.map +1 -0
  338. package/dist/server-components/html.d.ts.map +1 -1
  339. package/dist/server-components/html.js +19 -4
  340. package/dist/server-components/html.js.map +1 -1
  341. package/dist/server-components/router-integration.d.ts +4 -1
  342. package/dist/server-components/router-integration.d.ts.map +1 -1
  343. package/dist/server-components/router-integration.js +23 -2
  344. package/dist/server-components/router-integration.js.map +1 -1
  345. package/dist/server-components/runtime.d.ts +6 -1
  346. package/dist/server-components/runtime.d.ts.map +1 -1
  347. package/dist/server-components/runtime.js +86 -28
  348. package/dist/server-components/runtime.js.map +1 -1
  349. package/dist/server-components/state.d.ts +3 -1
  350. package/dist/server-components/state.d.ts.map +1 -1
  351. package/dist/server-components/state.js +50 -38
  352. package/dist/server-components/state.js.map +1 -1
  353. package/dist/server-components/types.d.ts +9 -0
  354. package/dist/server-components/types.d.ts.map +1 -1
  355. package/dist/vite.d.ts +5 -1
  356. package/dist/vite.d.ts.map +1 -1
  357. package/dist/vite.js +58 -4
  358. package/dist/vite.js.map +1 -1
  359. package/package.json +48 -8
  360. package/src/boundary.ts +77 -5
  361. package/src/build-plan.ts +3 -0
  362. package/src/build-vite.ts +2 -0
  363. package/src/build.ts +19 -1
  364. package/src/client.ts +147 -10
  365. package/src/csp.ts +16 -2
  366. package/src/deferred.ts +11 -5
  367. package/src/dev-feed.ts +11 -2
  368. package/src/dev-session.ts +22 -11
  369. package/src/dev.ts +29 -8
  370. package/src/diagnostic-catalog.ts +53 -0
  371. package/src/diagnostic.ts +1 -0
  372. package/src/fn.ts +24 -7
  373. package/src/fs.ts +22 -8
  374. package/src/identity.ts +61 -0
  375. package/src/index.ts +19 -0
  376. package/src/internal/cdn/cloudflare.ts +2 -1
  377. package/src/internal/certified-components.ts +1 -1
  378. package/src/internal/codegen.ts +87 -8
  379. package/src/internal/dev-websocket.ts +230 -0
  380. package/src/internal/hole-runtime.ts +286 -0
  381. package/src/internal/identity-epoch.ts +235 -0
  382. package/src/internal/invalidation-contract.ts +35 -0
  383. package/src/internal/invalidation.ts +213 -0
  384. package/src/internal/island-bindings.ts +191 -0
  385. package/src/internal/island-list.ts +92 -0
  386. package/src/internal/island-mount.ts +68 -0
  387. package/src/internal/island-owner.ts +20 -0
  388. package/src/internal/island-resource.ts +76 -0
  389. package/src/internal/island-signals.ts +249 -0
  390. package/src/internal/keep-alive-table.ts +72 -0
  391. package/src/internal/live-contract.ts +106 -0
  392. package/src/internal/live-queries.ts +60 -0
  393. package/src/internal/page-events.ts +27 -0
  394. package/src/internal/page-execution.ts +579 -40
  395. package/src/internal/ppr/admission.ts +300 -0
  396. package/src/internal/ppr/canary.ts +122 -0
  397. package/src/internal/ppr/compile-shell.ts +379 -0
  398. package/src/internal/ppr/contract.ts +265 -0
  399. package/src/internal/ppr/frames.ts +170 -0
  400. package/src/internal/ppr/integration.ts +137 -0
  401. package/src/internal/ppr/options.ts +221 -0
  402. package/src/internal/ppr/route-rules.ts +77 -0
  403. package/src/internal/ppr/runtime.ts +36 -0
  404. package/src/internal/ppr/server.ts +1764 -0
  405. package/src/internal/ppr/shell-store.ts +139 -0
  406. package/src/internal/prerendered-pages.ts +7 -4
  407. package/src/internal/query-digest.ts +34 -0
  408. package/src/internal/query-runtime.ts +14 -1
  409. package/src/internal/query-seam.ts +48 -0
  410. package/src/internal/render-document.ts +149 -22
  411. package/src/internal/request-abort.ts +26 -0
  412. package/src/internal/server-boundary.ts +10 -1
  413. package/src/internal/share-data.ts +136 -0
  414. package/src/internal/store-signature.ts +149 -0
  415. package/src/internal/vite-plugin-compat.ts +2 -2
  416. package/src/internal/web-app.ts +121 -3
  417. package/src/islands-swap.ts +283 -0
  418. package/src/islands.ts +169 -72
  419. package/src/isr.ts +348 -106
  420. package/src/keep-alive.ts +1315 -0
  421. package/src/live/bus.ts +77 -0
  422. package/src/live/core.ts +52 -0
  423. package/src/live/history.ts +109 -0
  424. package/src/live/hub-protocol.ts +133 -0
  425. package/src/live/hub.ts +615 -0
  426. package/src/live/keyring.ts +379 -0
  427. package/src/live/read-capped.ts +36 -0
  428. package/src/live/revocations.ts +155 -0
  429. package/src/live/settings.ts +46 -0
  430. package/src/live-client.ts +973 -0
  431. package/src/live-hub.ts +658 -0
  432. package/src/live-redis.ts +259 -0
  433. package/src/live.ts +1096 -0
  434. package/src/manifest.ts +274 -6
  435. package/src/navigation.ts +14 -2
  436. package/src/plugins/vite-zone-guard.ts +15 -2
  437. package/src/ppr.ts +153 -0
  438. package/src/query.ts +147 -20
  439. package/src/render-seam.ts +35 -0
  440. package/src/route-types.ts +3 -1
  441. package/src/router.ts +494 -101
  442. package/src/server-components/build.ts +9 -3
  443. package/src/server-components/client.ts +89 -5
  444. package/src/server-components/hole.ts +23 -0
  445. package/src/server-components/html-policy.ts +63 -4
  446. package/src/server-components/html-scan.ts +159 -0
  447. package/src/server-components/html.ts +18 -4
  448. package/src/server-components/router-integration.ts +21 -2
  449. package/src/server-components/runtime.ts +112 -30
  450. package/src/server-components/state.ts +66 -44
  451. package/src/server-components/types.ts +16 -0
  452. package/src/vite.ts +75 -6
  453. package/dist/nano.d.ts +0 -125
  454. package/dist/nano.d.ts.map +0 -1
  455. package/dist/nano.js +0 -190
  456. package/dist/nano.js.map +0 -1
  457. package/src/nano.ts +0 -246
package/src/client.ts CHANGED
@@ -35,6 +35,7 @@ export function currentDocumentNonce(): string | undefined {
35
35
  return script.getAttribute("nonce") ?? undefined
36
36
  }
37
37
 
38
+ export { identityChanged } from "./identity.ts"
38
39
  /**
39
40
  * Everything the generated client entry needs, re-exported here so that entry imports ONE module and
40
41
  * that module is DOM-only. It used to take these from `@nifrajs/web`, whose graph includes `renderPage`
@@ -44,6 +45,7 @@ export function currentDocumentNonce(): string | undefined {
44
45
  */
45
46
  export { mergeHeads, resolveChainMeta, resolveMeta } from "./internal/head-merge.ts"
46
47
  export {
48
+ closeBoundary,
47
49
  getBrowserNavigate,
48
50
  IDLE_BLOCKER,
49
51
  registerBlocker,
@@ -255,6 +257,48 @@ export function installHistory(
255
257
  let reversing = false
256
258
  // The path the router was last seen loading, so a redirect it follows shows up as a change.
257
259
  let expected: string | undefined
260
+ // The entry holding the page last shown without an intercept boundary, so closing a boundary opened
261
+ // over it can go back there. Cleared once that entry is replaced or cut off by a push.
262
+ let pageEntry: number | undefined = index
263
+ // Before writing the entry at `index` (replace) or pushing past it, which drops every later entry.
264
+ const writingEntry = (mode: "push" | "replace"): void => {
265
+ if (mode === "replace" ? pageEntry === index : pageEntry !== undefined && pageEntry > index)
266
+ pageEntry = undefined
267
+ }
268
+ // Every entry this document owns carries its index, the first one included, so an entry without one
269
+ // is one the browser pushed itself: a same-page fragment link, which this router leaves native.
270
+ // Number it as the push it was, or index deltas (a guard's reversal, closeBoundary) count wrong.
271
+ const numbered = (): boolean => {
272
+ const current: unknown = history.state
273
+ return (
274
+ typeof current === "object" &&
275
+ current !== null &&
276
+ "nifraIndex" in current &&
277
+ typeof current.nifraIndex === "number"
278
+ )
279
+ }
280
+ const stampEntry = (): void => {
281
+ const current: unknown = history.state
282
+ history.replaceState(
283
+ Object.assign({}, typeof current === "object" ? current : null, { nifraIndex: index }),
284
+ "",
285
+ )
286
+ }
287
+ const adoptNativeEntry = (): boolean => {
288
+ if (numbered()) return false
289
+ writingEntry("push")
290
+ index += 1
291
+ stampEntry()
292
+ here = location.pathname + location.search + (location.hash ?? "")
293
+ return true
294
+ }
295
+ // An opaque or file: document routes nothing, so its history is left alone. A refused write (a
296
+ // browser's rate limit) only leaves fragment entries uncounted, as before.
297
+ if (location.origin !== "null" && location.protocol !== "file:" && !numbered()) {
298
+ try {
299
+ stampEntry()
300
+ } catch {}
301
+ }
258
302
  type Registration = {
259
303
  readonly shouldBlock: BlockerFunction
260
304
  readonly emit: (blocker: Blocker) => void
@@ -292,6 +336,23 @@ export function installHistory(
292
336
  type ViewTransitionDocument = Document & {
293
337
  startViewTransition?: (cb: () => unknown) => ViewTransition
294
338
  }
339
+ // A navigation that loads takes the page from a partially prerendered first document, which then
340
+ // stops filling its holes and cancels the ones still loading on the server. `navigate` publishes
341
+ // its `pendingPath` before its first await; a client-only search change, or a boundary already
342
+ // showing its target, loads nothing, so the holes still pending keep filling.
343
+ const navigateFrom = (routePath: string, from: string): Promise<void> => {
344
+ const run = router.navigate(routePath).catch((error) => leave(error, from))
345
+ const ppr: unknown = Reflect.get(globalThis, "__nifraPpr")
346
+ if (
347
+ router.snapshot().pendingPath === routePath &&
348
+ typeof ppr === "function" &&
349
+ "leave" in ppr &&
350
+ typeof ppr.leave === "function"
351
+ )
352
+ ppr.leave()
353
+ return run
354
+ }
355
+
295
356
  const transition = (run: () => Promise<void>): void => {
296
357
  const doc = document as ViewTransitionDocument
297
358
  if (typeof doc.startViewTransition !== "function") {
@@ -364,6 +425,7 @@ export function installHistory(
364
425
  // whole object on back/forward for free.
365
426
  const entryState =
366
427
  state !== undefined ? { nifraIndex: index, nifraState: state } : { nifraIndex: index }
428
+ writingEntry(mode)
367
429
  if (mode === "replace") {
368
430
  // Replace the current entry (same index) with the new URL. The entry we're leaving is discarded, so
369
431
  // its scroll isn't worth saving; the new route scrolls to its fragment target, else the top.
@@ -381,7 +443,7 @@ export function installHistory(
381
443
  here = url.pathname + url.search + url.hash
382
444
  expected = routePath
383
445
  // 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)))
446
+ transition(() => navigateFrom(routePath, path))
385
447
  settle()
386
448
  }
387
449
 
@@ -417,7 +479,15 @@ export function installHistory(
417
479
  }
418
480
  go(to, navOptions?.replace === true ? "replace" : "push", navOptions?.state)
419
481
  }
420
- setBrowserNavigate(navigate, router)
482
+ // Back through the entries the boundary added to the page underneath; without that entry, replace
483
+ // the boundary's URL with the page's.
484
+ const closeBoundary = (name: string): void => {
485
+ const intercepted = router.snapshot().intercepted
486
+ if (intercepted === undefined || intercepted.boundary !== name) return
487
+ if (pageEntry !== undefined && pageEntry < index) history.go(pageEntry - index)
488
+ else go(intercepted.pagePath, "replace")
489
+ }
490
+ setBrowserNavigate(navigate, router, closeBoundary)
421
491
 
422
492
  // Publish the guard registry through the DOM-free bridge (`@nifrajs/web`'s `registerBlocker`, which an
423
493
  // adapter's `useBlocker` calls). One slot, latest registration wins; the unregister clears it only if
@@ -515,10 +585,12 @@ export function installHistory(
515
585
  reversing = false
516
586
  return
517
587
  }
588
+ adoptNativeEntry()
518
589
  const newIndex = (history.state as { nifraIndex?: number } | null)?.nifraIndex ?? 0
519
590
  const dest = location.pathname + location.search + (location.hash ?? "")
520
591
  const delta = newIndex - index
521
- if (guard(dest, () => history.go(delta))) {
592
+ // A fragment entry the browser just pushed is not a move between entries to reverse.
593
+ if (delta !== 0 && guard(dest, () => history.go(delta))) {
522
594
  reversing = true
523
595
  history.go(-delta)
524
596
  return
@@ -528,7 +600,7 @@ export function installHistory(
528
600
  pendingScroll = { pos: scrollOf(history.state) }
529
601
  const routePath = location.pathname + location.search
530
602
  expected = routePath
531
- transition(() => router.navigate(routePath).catch((error) => leave(error, dest)))
603
+ transition(() => navigateFrom(routePath, dest))
532
604
  settle()
533
605
  }
534
606
 
@@ -555,6 +627,7 @@ export function installHistory(
555
627
  // form post back to the page it was on adds nothing.
556
628
  const follow = (path: string): void => {
557
629
  if (path !== location.pathname + location.search) {
630
+ writingEntry(expected === undefined ? "push" : "replace")
558
631
  if (expected === undefined) {
559
632
  history.replaceState({ ...(history.state ?? {}), nifraScroll: [scrollX, scrollY] }, "")
560
633
  index += 1
@@ -573,13 +646,15 @@ export function installHistory(
573
646
  // A settle with no navigation (a submit's revalidation) scrolls nothing. Either way the rendered
574
647
  // page is then scanned for links to warm.
575
648
  const settled = (): void => {
576
- const { pending, pendingPath } = router.snapshot()
649
+ const { pending, pendingPath, intercepted } = router.snapshot()
577
650
  if (pending) {
578
651
  if (pendingPath !== undefined && pendingPath !== expected) follow(pendingPath)
579
652
  return
580
653
  }
581
654
  expected = undefined
582
- const target = pendingScroll
655
+ if (intercepted === undefined) pageEntry = index
656
+ // A boundary opened over the page leaves the page where it was scrolled.
657
+ const target = intercepted === undefined ? pendingScroll : null
583
658
  pendingScroll = null
584
659
  requestAnimationFrame(() => {
585
660
  if (target !== null && "hash" in target) {
@@ -599,6 +674,8 @@ export function installHistory(
599
674
  document.addEventListener("pointerover", onPrefetch)
600
675
  document.addEventListener("focusin", onPrefetch)
601
676
  window.addEventListener("popstate", onPopState)
677
+ // Where a fragment link fires no popstate, its entry is still numbered here.
678
+ window.addEventListener("hashchange", adoptNativeEntry)
602
679
  window.addEventListener("beforeunload", onBeforeUnload)
603
680
  return () => {
604
681
  unsubscribe()
@@ -609,6 +686,7 @@ export function installHistory(
609
686
  document.removeEventListener("pointerover", onPrefetch)
610
687
  document.removeEventListener("focusin", onPrefetch)
611
688
  window.removeEventListener("popstate", onPopState)
689
+ window.removeEventListener("hashchange", adoptNativeEntry)
612
690
  window.removeEventListener("beforeunload", onBeforeUnload)
613
691
  }
614
692
  }
@@ -693,6 +771,47 @@ export function applyHead(head: Meta, path?: string): void {
693
771
  }
694
772
  }
695
773
 
774
+ /**
775
+ * The route path a form with no action posts to while an intercept boundary holds a target. The
776
+ * document URL is then the target's, but the form belongs to whichever route rendered it: the target
777
+ * when it sits inside the boundary's `data-nifra-boundary="<name>"` element, otherwise the page
778
+ * underneath. `undefined` when the form names its action, or no boundary holds a target.
779
+ */
780
+ function implicitActionOwner(
781
+ router: ClientRouter,
782
+ form: HTMLFormElement,
783
+ actionFrom: Element,
784
+ ): string | undefined {
785
+ const { intercepted, path } = router.snapshot()
786
+ if (intercepted === undefined) return undefined
787
+ const named = Element.prototype.getAttribute.call(actionFrom, actionAttribute(actionFrom, form))
788
+ if (named !== null && named.trim() !== "") return undefined
789
+ // Any ancestor marking the open boundary owns it, so a marker nested inside the modal (another
790
+ // boundary's) does not hand the form to the page. Boundary names are validated identifiers; the
791
+ // check keeps the selector literal regardless.
792
+ const inside =
793
+ BOUNDARY_NAME.test(intercepted.boundary) &&
794
+ Element.prototype.closest.call(form, `[data-nifra-boundary="${intercepted.boundary}"]`) !== null
795
+ return inside ? path : intercepted.pagePath
796
+ }
797
+
798
+ const BOUNDARY_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/
799
+
800
+ const actionAttribute = (actionFrom: Element, form: HTMLFormElement): string =>
801
+ actionFrom === form ? "action" : "formaction"
802
+
803
+ // Point the implicit action at `href` for one native submission; the returned function puts the
804
+ // attribute back, so a form that outlives the boundary (one in a layout) keeps posting to its page.
805
+ function pointAction(actionFrom: Element, form: HTMLFormElement, href: string): () => void {
806
+ const name = actionAttribute(actionFrom, form)
807
+ const previous = Element.prototype.getAttribute.call(actionFrom, name)
808
+ Element.prototype.setAttribute.call(actionFrom, name, href)
809
+ return () => {
810
+ if (previous === null) Element.prototype.removeAttribute.call(actionFrom, name)
811
+ else Element.prototype.setAttribute.call(actionFrom, name, previous)
812
+ }
813
+ }
814
+
696
815
  // A control named like a form property shadows it (`<input name="target">` makes `form.target` that
697
816
  // input), so the form's own state is read through the prototype accessors.
698
817
  function formProperty(form: HTMLFormElement, name: "action" | "method" | "target"): string {
@@ -720,14 +839,26 @@ export function installForms(router: ClientRouter): () => void {
720
839
  const method = submitter?.hasAttribute("formmethod")
721
840
  ? submitter.formMethod
722
841
  : formProperty(form, "method")
723
- if (method.toLowerCase() !== "post") return // GET forms submit natively
842
+ const actionFrom = submitter?.hasAttribute("formaction") ? submitter : form
843
+ const owner = implicitActionOwner(router, form, actionFrom)
844
+ if (method.toLowerCase() !== "post") {
845
+ // GET forms submit natively. The browser reads the action after this event, so one the page
846
+ // rendered under a boundary is pointed at the page for this submission, then restored.
847
+ if (owner !== undefined)
848
+ setTimeout(pointAction(actionFrom, form, new URL(appUrl(owner), location.href).href), 0)
849
+ return
850
+ }
724
851
  const target = submitter?.hasAttribute("formtarget")
725
852
  ? submitter.formTarget
726
853
  : formProperty(form, "target")
727
854
  if (target !== "" && target !== "_self") return // it loads in another browsing context
728
855
  // Resolved (or the current document URL if unset).
729
856
  const url = sameDocumentUrl(
730
- submitter?.hasAttribute("formaction") ? submitter.formAction : formProperty(form, "action"),
857
+ owner !== undefined
858
+ ? appUrl(owner)
859
+ : actionFrom === submitter
860
+ ? submitter.formAction
861
+ : formProperty(form, "action"),
731
862
  )
732
863
  if (url === null) return
733
864
  if (router.match(url.pathname) === null) return // not an app route → native submit
@@ -742,9 +873,15 @@ export function installForms(router: ClientRouter): () => void {
742
873
  // The server answered: load the page that shows the result rather than run the action again.
743
874
  if (to !== undefined) location.assign(to)
744
875
  else {
745
- // Nothing reached the server - post natively, through the same button.
876
+ // Nothing reached the server - post natively, through the same button, to the same route.
746
877
  native.add(form)
747
- HTMLFormElement.prototype.requestSubmit.call(form, submitter)
878
+ // requestSubmit reads the action synchronously, so the attribute is restored right after.
879
+ const restore = owner === undefined ? undefined : pointAction(actionFrom, form, url.href)
880
+ try {
881
+ HTMLFormElement.prototype.requestSubmit.call(form, submitter)
882
+ } finally {
883
+ restore?.()
884
+ }
748
885
  }
749
886
  })
750
887
  }
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
  }
package/src/dev.ts CHANGED
@@ -67,8 +67,15 @@ import {
67
67
  import { createSsrGraph, type SsrGraph } from "./dev-ssr-graph.ts"
68
68
  import { formatBuildLog } from "./diagnostic.ts"
69
69
  import { discoverRoutes } from "./fs.ts"
70
- import { DEFAULT_DEV_PORT, generateClientEntry } from "./index.ts"
70
+ import { DEFAULT_DEV_PORT, generateClientEntry, type LiveClientEntry } from "./index.ts"
71
71
  import { DEV_ENTRY_FILE } from "./internal/dev-reserved.ts"
72
+ import {
73
+ type BunUpgradeServer,
74
+ bunDevWebSocket,
75
+ isWebSocketUpgrade,
76
+ type UpgradableApp,
77
+ upgradeOnBun,
78
+ } from "./internal/dev-websocket.ts"
72
79
  import { DEV_HMR_ENV, DEV_ROOT_ENV, DEV_ROUTES_ENV } from "./plugins/kit.ts"
73
80
  import { servePublicDir } from "./public-dir.ts"
74
81
 
@@ -76,7 +83,7 @@ export type { DevAppHooks } from "./dev-session.ts"
76
83
  export { LAST_ERROR_PATH } from "./diagnostic.ts"
77
84
 
78
85
  /** Minimal app surface the dev server needs - `createWebApp(...)` satisfies it. */
79
- interface FetchApp {
86
+ interface FetchApp extends UpgradableApp {
80
87
  fetch(request: Request): Response | Promise<Response>
81
88
  }
82
89
 
@@ -169,7 +176,8 @@ interface BunServeOptions {
169
176
  readonly hostname: "127.0.0.1"
170
177
  readonly development: { readonly hmr: boolean }
171
178
  readonly routes: Record<string, HtmlBundle>
172
- fetch(request: Request): Promise<Response>
179
+ fetch(request: Request, server: BunUpgradeServer): Promise<Response | undefined>
180
+ readonly websocket: typeof bunDevWebSocket
173
181
  }
174
182
  interface BunServerHandle {
175
183
  readonly port: number
@@ -214,6 +222,7 @@ export function devHtml(entryHref: string): string {
214
222
  interface WriteDevFilesOptions {
215
223
  readonly routesDir: string
216
224
  readonly clientModule: string
225
+ readonly live?: LiveClientEntry | undefined
217
226
  readonly entryPath: string
218
227
  readonly htmlPath: string
219
228
  }
@@ -226,7 +235,7 @@ interface WriteDevFilesOptions {
226
235
  * the way the runtime does, so a leading slash there would mean the filesystem root.
227
236
  */
228
237
  export function writeDevFiles(options: WriteDevFilesOptions): void {
229
- const { routesDir, clientModule, entryPath, htmlPath } = options
238
+ const { routesDir, clientModule, entryPath, htmlPath, live } = options
230
239
  mkdirSync(dirname(entryPath), { recursive: true })
231
240
  const manifest = discoverRoutes(routesDir)
232
241
  const entryDir = dirname(entryPath)
@@ -244,7 +253,10 @@ export function writeDevFiles(options: WriteDevFilesOptions): void {
244
253
  // already been passed to createRoot()") and only then does the callback get to reload - trading an
245
254
  // accurate warning for a real error. Editing a component-only module never reaches this path: Bun
246
255
  // applies React Fast Refresh there and state is preserved.
247
- writeIfChanged(entryPath, generateClientEntry(manifest, { clientModule, resolve: toSpecifier }))
256
+ writeIfChanged(
257
+ entryPath,
258
+ generateClientEntry(manifest, { clientModule, resolve: toSpecifier, live }),
259
+ )
248
260
  const entryHref = `./${relative(dirname(htmlPath), entryPath).replaceAll("\\", "/")}`
249
261
  writeIfChanged(htmlPath, devHtml(entryHref))
250
262
  }
@@ -355,7 +367,7 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
355
367
  ? async (): Promise<undefined> => undefined
356
368
  : servePublicDir({ dir: resolve(publicDir) })
357
369
 
358
- writeDevFiles({ routesDir, clientModule, entryPath, htmlPath })
370
+ writeDevFiles({ routesDir, clientModule, entryPath, htmlPath, live: options.live })
359
371
  // Importing the generated HTML is what hands it to Bun's bundler. A runtime-computed path is fine:
360
372
  // Bun resolves and bundles the HTML, and everything it references, at import time.
361
373
  const htmlModule = (await import(htmlPath)) as { default: HtmlBundle }
@@ -486,7 +498,8 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
486
498
  development: { hmr: true },
487
499
  // The probe route is the ONLY path Bun owns; everything else falls through to nifra's SSR.
488
500
  routes: { [PROBE_PATH]: htmlModule.default },
489
- fetch: async (req: Request): Promise<Response> => {
501
+ websocket: bunDevWebSocket,
502
+ fetch: async (req: Request, bun: BunUpgradeServer): Promise<Response | undefined> => {
490
503
  const url = new URL(req.url)
491
504
  const host = req.headers.get("host") ?? url.host
492
505
  if (!isLoopbackHost(host) && !allowedHosts.has(hostName(host)))
@@ -494,6 +507,14 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
494
507
  `[nifra/web/dev] ${hostName(host)} is not a loopback host; add it to allowedHosts if this server should answer it.\n`,
495
508
  { status: 403, headers: { "content-type": "text/plain; charset=utf-8" } },
496
509
  )
510
+ if (isWebSocketUpgrade(req)) {
511
+ const answer = await upgradeOnBun(
512
+ await appForRequest((await currentEntry()).src),
513
+ req,
514
+ bun,
515
+ )
516
+ if (answer !== "pass") return answer
517
+ }
497
518
  // The stable client-entry URL. Pages reference THIS; it redirects to whichever hashed chunk Bun
498
519
  // is serving at the moment the browser asks. A redirect rather than a proxy so the module's final
499
520
  // URL is Bun's own - its HMR client derives its socket and module identity from `import.meta.url`,
@@ -554,7 +575,7 @@ export async function createDevServer(options: DevServerOptions): Promise<DevSer
554
575
  syncWatchedFiles(false)
555
576
  session.markChange()
556
577
  try {
557
- writeDevFiles({ routesDir, clientModule, entryPath, htmlPath })
578
+ writeDevFiles({ routesDir, clientModule, entryPath, htmlPath, live: options.live })
558
579
  } catch (err) {
559
580
  // A half-saved file can fail the scan mid-edit; the next change re-runs this.
560
581
  console.error("[nifra/web/dev] client entry regeneration failed:", err)
@@ -17,6 +17,8 @@ interface CatalogEntry {
17
17
  readonly fix: string
18
18
  readonly docsAnchor: string
19
19
  readonly options?: readonly FixOption[]
20
+ /** Reported by a console warning, not thrown: the dev feed records the warning as an entry. */
21
+ readonly console?: true
20
22
  }
21
23
 
22
24
  /**
@@ -191,6 +193,52 @@ export const DIAGNOSTIC_CATALOG: readonly CatalogEntry[] = [
191
193
  fix: "Read the reason in the line: a 401 or 403 means the token lacks purge permission for this zone, project or service; a 5xx or network_error is retried on its own.",
192
194
  docsAnchor: "errors#cdn-purge-failed",
193
195
  },
196
+ {
197
+ code: "NIFRA_KEEPALIVE_IDENTITY_COOKIE",
198
+ match: (_n, m) => m.includes("NIFRA_KEEPALIVE_IDENTITY_COOKIE"),
199
+ console: true,
200
+ cause:
201
+ "The app keeps pages alive, and a response changed an HttpOnly cookie nifra does not know as a sign-in cookie. If that cookie says who is signed in, a sign-in or sign-out through it leaves the previous visitor's kept pages in every tab, and back shows them.",
202
+ fix: "Name the cookie in createWebApp({ keepAlive: { identityCookies } }) if it carries the session, or in keepAlive.ignoreCookies if it does not.",
203
+ docsAnchor: "errors#keepalive-identity-cookie",
204
+ options: [
205
+ {
206
+ label: "It is the session cookie",
207
+ fix: 'Add it to createWebApp({ keepAlive: { identityCookies: ["<name>"] } }): a response that changes or deletes it rotates the identity epoch, and every tab drops its kept pages.',
208
+ },
209
+ {
210
+ label: "It says nothing about the visitor",
211
+ fix: 'Add it to createWebApp({ keepAlive: { ignoreCookies: ["<name>"] } }) so development stops reporting it.',
212
+ },
213
+ {
214
+ label: "Identity lives in a token",
215
+ fix: "When the session is a bearer token in client storage, call identityChanged() from @nifrajs/web/client at sign-in and sign-out; it drops kept pages in every tab.",
216
+ },
217
+ ],
218
+ },
219
+ {
220
+ code: "NIFRA_KEEPALIVE_DIRECT_CHILD_CSS",
221
+ match: (_n, m) => m.includes("NIFRA_KEEPALIVE_DIRECT_CHILD_CSS"),
222
+ console: true,
223
+ cause:
224
+ "A keepAlive page renders inside two display:contents wrappers, so a selector that reached its top-level element as a direct child (or by its position among siblings) no longer matches it.",
225
+ fix: "Rewrite the selector the message names: a descendant combinator, the wrapper as the parent, or a class on the page's root element.",
226
+ docsAnchor: "errors#keepalive-direct-child-css",
227
+ options: [
228
+ {
229
+ label: "Use a descendant combinator",
230
+ fix: "Write `main section` instead of `main > section`: the wrappers sit between the two.",
231
+ },
232
+ {
233
+ label: "Target the wrapper",
234
+ fix: "Write `[data-nifra-page] > section`: the inner wrapper is the page's parent while it is shown.",
235
+ },
236
+ {
237
+ label: "Put a class on the page root",
238
+ fix: "Give the page's root element a class and select it directly, instead of by its parent or its position.",
239
+ },
240
+ ],
241
+ },
194
242
  {
195
243
  code: "NIFRA_SCHEMA_PARSE",
196
244
  match: (n, m) =>
@@ -215,6 +263,11 @@ const HYDRATION_MESSAGES: readonly RegExp[] = [
215
263
  /Minified React error #(?:418|423|425)\b/,
216
264
  ]
217
265
 
266
+ /** True when a console `message` reports a catalog failure that is only ever logged, never thrown. */
267
+ export function isConsoleDiagnostic(message: string): boolean {
268
+ return DIAGNOSTIC_CATALOG.some((entry) => entry.console === true && entry.match("", message))
269
+ }
270
+
218
271
  /** True when `message` is a framework's report of a server/browser render mismatch. */
219
272
  export function isHydrationMismatch(message: string): boolean {
220
273
  return HYDRATION_MESSAGES.some((pattern) => pattern.test(message))
package/src/diagnostic.ts CHANGED
@@ -20,6 +20,7 @@ export {
20
20
  classify,
21
21
  DIAGNOSTIC_CATALOG,
22
22
  type FixOption,
23
+ isConsoleDiagnostic,
23
24
  isHydrationMismatch,
24
25
  } from "./diagnostic-catalog.ts"
25
26
  export {