@nifrajs/web 5.5.1 → 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 +154 -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
@@ -0,0 +1,283 @@
1
+ /**
2
+ * @nifrajs/web/islands/swap - refresh one region of a server-rendered page from a plain link or form,
3
+ * without a full reload and without a client router.
4
+ *
5
+ * <form action="/hotels" data-swap="replace:results"><input name="q"></form>
6
+ * <ul data-swap-slot="results">...</ul>
7
+ * <a href="/hotels?page=2" data-swap="append:results">More</a>
8
+ *
9
+ * The request is the one the link or form makes anyway, plus an `x-nifra-swap: <slot>` header. The
10
+ * server runs the page as usual and answers with that slot's HTML alone (`slot()` in
11
+ * `@nifrajs/web-vanilla`), so the fragment is a slice of the real page. Any other answer - a redirect,
12
+ * another origin, a page without that slot - loads the URL normally. Only GET requests are enhanced:
13
+ * a POST form submits natively, so the browser applies its response's security policy and scripts,
14
+ * and the form is never sent twice.
15
+ */
16
+
17
+ import { mountIsland } from "./internal/island-mount.ts"
18
+ import type { IslandEnhancer } from "./islands.ts"
19
+ import { SWAP_HEADER } from "./render-seam.ts"
20
+
21
+ /** Where `enableSwap` looks for links, forms, slots and islands. */
22
+ export interface SwapOptions {
23
+ /** Default `document`. */
24
+ readonly root?: Document | Element
25
+ }
26
+
27
+ const MODE = /^(replace|append):([A-Za-z][\w-]{0,63})$/
28
+ const SLOTS = "[data-swap-slot]:not([data-island-ignore],[data-island-ignore] *)"
29
+ const HISTORY_KEY = "nifraSwap"
30
+
31
+ // A control named like a DOM method shadows it on its form (`<input name="closest">`).
32
+ const attr = (el: Element, name: string): string | null =>
33
+ Element.prototype.getAttribute.call(el, name)
34
+
35
+ const here = (): string => location.pathname + location.search
36
+
37
+ /** The slot names a history entry's swaps changed, from the first one in this tab up to it. */
38
+ const swappedIn = (state: unknown): string[] => {
39
+ const names: unknown =
40
+ typeof state === "object" && state !== null ? Reflect.get(state, HISTORY_KEY) : undefined
41
+ return Array.isArray(names) ? names.filter((name) => typeof name === "string") : []
42
+ }
43
+
44
+ /**
45
+ * Mount the page's islands, as `mountIslands` does, and make every `data-swap` link and GET form under
46
+ * `root` refresh its slot in place. Call it instead of `mountIslands`.
47
+ *
48
+ * - `data-swap="replace:<slot>"` replaces the content of the `data-swap-slot="<slot>"` element;
49
+ * `append:<slot>` adds to it, and a link or form inside that slot is spent: the slot's child holding
50
+ * it is removed when the next content arrives, so a "more" link at the end of a list gives way to
51
+ * the next page and its own "more" link.
52
+ * - A replacing GET pushes its URL, so a filtered list survives reload and can be shared; back and
53
+ * forward fetch the slots again for the URL they land on.
54
+ * - The islands inside a slot mount with it and unmount when it is replaced.
55
+ * - While a slot loads it carries `aria-busy="true"`; a newer swap of the same slot cancels the
56
+ * older one. When the request fails, the page stays as it is and the link or form gets
57
+ * `data-swap-error`.
58
+ *
59
+ * Nothing inside a `data-island-ignore` element swaps. Returns a disposer that removes the listeners,
60
+ * cancels pending swaps and unmounts every island.
61
+ */
62
+ export function enableSwap(
63
+ // `never` admits an enhancer of any props type, as in `mountIslands`.
64
+ enhancers: Readonly<Record<string, IslandEnhancer<never>>>,
65
+ options: SwapOptions = {},
66
+ ): () => void {
67
+ const root: ParentNode & Node = options.root ?? document
68
+ const stops = new Map<Node, () => void>()
69
+ const pending = new Map<Element, AbortController>()
70
+ let swapped = swappedIn(history.state)
71
+ let shown = here()
72
+
73
+ const islands = "[data-island]:not([data-island-ignore],[data-island-ignore] *)"
74
+ const attach = (scope: ParentNode & Node): void => {
75
+ const mount = (el: HTMLElement): void => {
76
+ const stop = mountIsland(el, enhancers)
77
+ if (stop !== undefined) stops.set(el, stop)
78
+ }
79
+ if (scope instanceof HTMLElement && scope.matches(islands)) mount(scope)
80
+ for (const el of scope.querySelectorAll<HTMLElement>(islands)) if (scope.contains(el)) mount(el)
81
+ }
82
+ const release = (scope: Node): void => {
83
+ const cleanups: Array<() => void> = []
84
+ for (const [node, stop] of stops)
85
+ if (scope.contains(node)) {
86
+ stops.delete(node)
87
+ cleanups.push(stop)
88
+ }
89
+ // Cleanup may itself detach descendants, so collect every owner before stopping any of them.
90
+ for (const stop of cleanups) stop()
91
+ }
92
+ // The server answers with the first slot of a name in the page, so the client takes the first too.
93
+ const slotNamed = (name: string): Element | undefined => {
94
+ for (const slot of root.querySelectorAll(SLOTS))
95
+ if (slot.getAttribute("data-swap-slot") === name) return slot
96
+ return undefined
97
+ }
98
+
99
+ const put = (slot: Element, html: string, append: boolean, source: Element | undefined): void => {
100
+ const template = document.createElement("template")
101
+ // Parsed inert: scripts in it never run, as for any HTML set this way.
102
+ template.innerHTML = html
103
+ const focused = slot.contains(document.activeElement)
104
+ if (append) {
105
+ for (const child of [...slot.children])
106
+ if (child === source || (source !== undefined && child.contains(source))) {
107
+ release(child)
108
+ slot.removeChild(child)
109
+ }
110
+ slot.append(template.content)
111
+ } else {
112
+ release(slot)
113
+ slot.replaceChildren(template.content)
114
+ }
115
+ attach(slot)
116
+ if (focused && !slot.contains(document.activeElement) && slot instanceof HTMLElement) {
117
+ if (!slot.hasAttribute("tabindex")) slot.tabIndex = -1
118
+ slot.focus({ preventScroll: true })
119
+ }
120
+ }
121
+
122
+ const swap = async (
123
+ slot: Element,
124
+ name: string,
125
+ append: boolean,
126
+ url: URL,
127
+ source?: Element,
128
+ ): Promise<void> => {
129
+ pending.get(slot)?.abort()
130
+ const abort = new AbortController()
131
+ pending.set(slot, abort)
132
+ slot.setAttribute("aria-busy", "true")
133
+ try {
134
+ const response = await fetch(url, {
135
+ cache: "no-store",
136
+ headers: { [SWAP_HEADER]: name },
137
+ signal: abort.signal,
138
+ redirect: "manual",
139
+ })
140
+ const load = (): void => {
141
+ if (source === undefined) location.reload()
142
+ else location.assign(response.redirected ? response.url : url)
143
+ }
144
+ if (
145
+ response.type !== "basic" ||
146
+ !response.headers.get("content-type")?.startsWith("text/html")
147
+ ) {
148
+ load()
149
+ return
150
+ }
151
+ // A slot answer joins this document, so this document's CSP governs it, whatever headers the
152
+ // fetch carried; anything that is not one loads as its own document under its own policy.
153
+ if (response.headers.get(SWAP_HEADER) !== name) {
154
+ load()
155
+ return
156
+ }
157
+ const html = await response.text()
158
+ if (!slot.isConnected) return
159
+ try {
160
+ put(slot, html, append, source)
161
+ } catch {
162
+ // Trusted Types without a default policy refuses the HTML before the slot changes.
163
+ load()
164
+ return
165
+ }
166
+ if (!append && source !== undefined) {
167
+ if (!swapped.includes(name)) swapped = [...swapped, name]
168
+ history.pushState({ [HISTORY_KEY]: swapped }, "", url)
169
+ shown = url.pathname + url.search
170
+ }
171
+ } catch {
172
+ if (abort.signal.aborted) return
173
+ // Back or forward left the URL ahead of the page: show what the URL shows.
174
+ if (source === undefined) location.reload()
175
+ else Element.prototype.setAttribute.call(source, "data-swap-error", "")
176
+ } finally {
177
+ if (pending.get(slot) === abort) {
178
+ pending.delete(slot)
179
+ slot.removeAttribute("aria-busy")
180
+ }
181
+ }
182
+ }
183
+
184
+ const start = (event: Event, source: Element, url: URL): void => {
185
+ const mode = MODE.exec(attr(source, "data-swap") ?? "")
186
+ const name = mode?.[2]
187
+ if (
188
+ name === undefined ||
189
+ url.origin !== location.origin ||
190
+ Element.prototype.closest.call(source, "[data-island-ignore]") !== null
191
+ )
192
+ return
193
+ const slot = slotNamed(name)
194
+ if (slot === undefined) return
195
+ event.preventDefault()
196
+ Element.prototype.removeAttribute.call(source, "data-swap-error")
197
+ void swap(slot, name, mode?.[1] === "append", url, source)
198
+ }
199
+
200
+ const onClick = (event: Event): void => {
201
+ if (!(event instanceof MouseEvent) || event.defaultPrevented || event.button !== 0) return
202
+ if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
203
+ const link = event.target instanceof Element ? event.target.closest("a[data-swap]") : null
204
+ if (!(link instanceof HTMLAnchorElement) || !link.hasAttribute("href")) return
205
+ const target = link.getAttribute("target")
206
+ if ((target !== null && target !== "" && target !== "_self") || link.hasAttribute("download"))
207
+ return
208
+ const url = new URL(link.href)
209
+ // An in-page anchor keeps its native jump.
210
+ if (url.hash !== "" && url.pathname + url.search === here()) return
211
+ start(event, link, url)
212
+ }
213
+
214
+ const onSubmit = (event: Event): void => {
215
+ const form = event.target
216
+ if (
217
+ !(event instanceof SubmitEvent) ||
218
+ event.defaultPrevented ||
219
+ !(form instanceof HTMLFormElement) ||
220
+ attr(form, "data-swap") === null
221
+ )
222
+ return
223
+ const submitter = event.submitter
224
+ // The button that submitted can override the form's method, action, target and encoding.
225
+ const pick = (name: string): string | null =>
226
+ submitter?.hasAttribute(`form${name}`)
227
+ ? submitter.getAttribute(`form${name}`)
228
+ : attr(form, name)
229
+ // An empty method is a GET, as for the browser.
230
+ const method = (pick("method") || "get").toUpperCase()
231
+ const target = pick("target")
232
+ const enctype = pick("enctype")?.toLowerCase()
233
+ if (
234
+ method !== "GET" ||
235
+ (target !== null && target !== "" && target !== "_self") ||
236
+ enctype === "text/plain"
237
+ )
238
+ return
239
+ const action = pick("action")
240
+ const url = new URL(action === null || action === "" ? location.href : action, document.baseURI)
241
+ const data = new FormData(form, submitter)
242
+ const fields = new URLSearchParams()
243
+ for (const [key, value] of data)
244
+ fields.append(key, typeof value === "string" ? value : value.name)
245
+ url.search = fields.toString()
246
+ start(event, form, url)
247
+ }
248
+
249
+ const onPopState = (event: PopStateEvent): void => {
250
+ if (here() === shown) return
251
+ shown = here()
252
+ const reached = swappedIn(event.state)
253
+ // Every slot a swap changed between the entry left and the one reached is in the later one's list.
254
+ const names = new Set([...swapped, ...reached])
255
+ swapped = reached
256
+ const targets: Array<[string, Element]> = []
257
+ for (const name of names) {
258
+ const slot = slotNamed(name)
259
+ if (slot === undefined) {
260
+ location.reload()
261
+ return
262
+ }
263
+ targets.push([name, slot])
264
+ }
265
+ for (const [name, slot] of targets) void swap(slot, name, false, new URL(location.href))
266
+ }
267
+
268
+ const listen = (method: "addEventListener" | "removeEventListener"): void => {
269
+ root[method]("click", onClick)
270
+ root[method]("submit", onSubmit)
271
+ }
272
+
273
+ attach(root)
274
+ listen("addEventListener")
275
+ window.addEventListener("popstate", onPopState)
276
+ return () => {
277
+ listen("removeEventListener")
278
+ window.removeEventListener("popstate", onPopState)
279
+ for (const abort of pending.values()) abort.abort()
280
+ for (const stop of stops.values()) stop()
281
+ stops.clear()
282
+ }
283
+ }
package/src/islands.ts CHANGED
@@ -1,27 +1,52 @@
1
1
  /**
2
- * @nifrajs/web/islands - the framework-agnostic islands client runtime.
2
+ * @nifrajs/web/islands - the islands client runtime: interactivity for server-rendered pages without a
3
+ * framework runtime.
3
4
  *
4
- * An "island" is a server-rendered `<nifra-island>` element whose interactivity is mounted by a small
5
- * **plain-DOM** enhancer instead of a framework runtime. On a route that opts out of full-document
6
- * hydration (`export const hydrate = false`), the page ships **zero** framework JS; only the island
7
- * bundle (this runtime + your enhancers) loads. This variant ships ~99% less client JS than full
8
- * React hydration on a mostly-static page - the framework runtime is the cost, and islands skip it
9
- * entirely here.
5
+ * An island is a server-rendered element marked `data-island="<id>"`. Its interactivity is mounted by
6
+ * an enhancer instead of hydration, so on a route with `export const hydrate = false` the page ships
7
+ * no framework JS; only the island bundle (this runtime + your enhancers) loads via `islandScripts`.
10
8
  *
11
- * Server side: render the marker with `@nifrajs/web-react`'s `<Island>` (or any framework's host element
12
- * `<nifra-island data-id data-strategy data-props>`). Props are JSON-encoded inline in `data-props`
13
- * (attribute-escaped by the framework) - no central registry, so concurrent SSR renders never share
14
- * state. Client side: bundle one entry that calls `mountIslands({ <id>: enhancer })` and load it via
15
- * the route's `islandScripts`.
9
+ * Two tiers, one mount:
10
+ * - `defineIsland((el, props) => cleanup)` - plain DOM code. The default.
11
+ * - `bindIsland(({ state, onDispose }) => handlers)` - signals the markup binds by name through a
12
+ * closed set of `data-bind-*` attributes. No expression language: names only.
13
+ * `signal`/`computed`/`effect`, a keyed `bindList` and an async `resource` work inside either tier;
14
+ * whatever an enhancer creates stops when its island unmounts.
15
+ *
16
+ * Server side: `<div data-island="counter" data-island-props="${islandProps({ start: 0 })}">`, or
17
+ * `@nifrajs/web-react`'s `<Island>`. Optional `data-island-strategy` (`load` | `idle` | `visible` |
18
+ * `media`, with `data-island-media`). Props travel as JSON in the attribute - no central registry, so
19
+ * concurrent SSR renders never share state. Nothing inside a `data-island-ignore` element mounts or
20
+ * binds: render user HTML there.
16
21
  */
17
22
 
18
- import {
23
+ import { bindScope } from "./internal/island-bindings.ts"
24
+ import { mountIsland } from "./internal/island-mount.ts"
25
+ import { owned } from "./internal/island-owner.ts"
26
+ import { type Signal, signal } from "./internal/island-signals.ts"
27
+ import { PAGE_HIDE_EVENT, PAGE_SHOW_EVENT } from "./internal/page-events.ts"
28
+
29
+ export {
19
30
  type IslandStrategy,
20
31
  MAX_MEDIA_QUERY_LENGTH,
21
32
  scheduleTrigger,
22
33
  } from "@nifrajs/island-trigger"
23
-
24
- export { type IslandStrategy, MAX_MEDIA_QUERY_LENGTH, scheduleTrigger }
34
+ export {
35
+ type BindListOptions,
36
+ bindList,
37
+ type ListContainer,
38
+ type ListNode,
39
+ } from "./internal/island-list.ts"
40
+ export { type Resource, type ResourceState, resource } from "./internal/island-resource.ts"
41
+ export {
42
+ batch,
43
+ type Computed,
44
+ computed,
45
+ effect,
46
+ type Signal,
47
+ signal,
48
+ untrack,
49
+ } from "./internal/island-signals.ts"
25
50
 
26
51
  /** Optional teardown an enhancer returns (remove listeners/observers); run on `dispose()`. */
27
52
  export type IslandCleanup = () => void
@@ -36,7 +61,7 @@ export type IslandEnhancer<P = unknown> = (el: HTMLElement, props: P) => IslandC
36
61
 
37
62
  /**
38
63
  * Author a typed enhancer. Pure identity at runtime (zero cost, tree-shaken away) - its only job is
39
- * to pin the `data-props` shape so `el` and `props` are typed inside the body and the enhancer is
64
+ * to pin the props shape so `el` and `props` are typed inside the body and the enhancer is
40
65
  * assignable to `mountIslands`. Prefer this over an inline arrow whenever an island reads props:
41
66
  *
42
67
  * const counter = defineIsland<{ start: number }>((el, props) => {
@@ -48,78 +73,150 @@ export type IslandEnhancer<P = unknown> = (el: HTMLElement, props: P) => IslandC
48
73
  * })
49
74
  * mountIslands({ counter })
50
75
  *
51
- * The props type is a contract, not a validator - `data-props` is JSON authored by your own SSR, so
52
- * it is trusted framing, not user input. Keep the enhancer body imperative: read the DOM, wire
53
- * listeners, return cleanup. No hidden reactivity to get wrong.
76
+ * The props type is a contract, not a validator - `data-island-props` is JSON authored by your own
77
+ * SSR, so it is trusted framing, not user input. Keep the enhancer body imperative: read the DOM,
78
+ * wire listeners, return cleanup.
54
79
  */
55
80
  export function defineIsland<P = unknown>(enhancer: IslandEnhancer<P>): IslandEnhancer<P> {
56
81
  return enhancer
57
82
  }
58
83
 
59
- /** Parse an island's inline `data-props` JSON; malformed/absent → `undefined` (never throws). */
60
- function readProps(el: HTMLElement): unknown {
61
- const raw = el.dataset.props
62
- if (raw === undefined || raw === "") return undefined
63
- try {
64
- return JSON.parse(raw)
65
- } catch {
66
- return undefined
84
+ const onPage = (type: string, el: Element, callback: () => void): (() => void) => {
85
+ if (typeof document === "undefined") return () => {}
86
+ const listener = (event: Event): void => {
87
+ if (event.target instanceof Node && event.target.contains(el)) callback()
88
+ }
89
+ document.addEventListener(type, listener)
90
+ return () => document.removeEventListener(type, listener)
91
+ }
92
+
93
+ /**
94
+ * Run `callback` each time the `keepAlive` page `el` renders in is hidden behind another page, just
95
+ * before it leaves the document. An island in a kept page stays mounted, and its timers and window
96
+ * listeners keep running, while the page is hidden: pause them here. Returns its removal - hand it back
97
+ * as part of the enhancer's cleanup. Never runs outside a kept page.
98
+ *
99
+ * const clock = defineIsland((el) => {
100
+ * let timer = setInterval(tick, 1000)
101
+ * const hide = onPageHide(el, () => clearInterval(timer))
102
+ * const show = onPageShow(el, () => { timer = setInterval(tick, 1000) })
103
+ * return () => { hide(); show(); clearInterval(timer) }
104
+ * })
105
+ */
106
+ export function onPageHide(el: Element, callback: () => void): () => void {
107
+ return onPage(PAGE_HIDE_EVENT, el, callback)
108
+ }
109
+
110
+ /** Run `callback` each time the `keepAlive` page `el` renders in is shown again; returns its removal. */
111
+ export function onPageShow(el: Element, callback: () => void): () => void {
112
+ return onPage(PAGE_SHOW_EVENT, el, callback)
113
+ }
114
+
115
+ /** What a `bindIsland` setup receives. */
116
+ export interface BindIslandContext<P> {
117
+ /** The island's host element. */
118
+ readonly root: HTMLElement
119
+ /** The parsed `data-island-props`, or `undefined` when absent or malformed. */
120
+ readonly props: P | undefined
121
+ /**
122
+ * The signal the markup binds as `key`, seeded from `props[key]` when present, else `fallback`.
123
+ * Each key returns the same signal across calls.
124
+ */
125
+ state<T>(key: string, fallback: T): Signal<T>
126
+ /**
127
+ * Run `fn` when the island unmounts - for timers, requests and listeners the setup starts. Called
128
+ * after the island unmounted (say, once an awaited request settles), it runs `fn` at once.
129
+ */
130
+ onDispose(fn: () => void): void
131
+ }
132
+
133
+ /**
134
+ * The binding tier: `setup` creates the island's state and returns the event handlers its markup
135
+ * names; the island's `data-bind-*` attributes then bind them.
136
+ *
137
+ * data-bind-text="count" data-bind-show="open" data-bind-class="active:open"
138
+ * data-bind-attr="aria-expanded:open" data-bind-value="query" data-bind-on="click:add"
139
+ *
140
+ * Attribute values are signal and handler names, never code; an unknown name warns once and the
141
+ * server-rendered content stays as it is. A binding belongs to its nearest island.
142
+ *
143
+ * A setup or binding that throws stops everything the island started before the error reaches
144
+ * `mountIslands`. On unmount every cleanup runs; one that throws is logged and the rest still run.
145
+ */
146
+ export function bindIsland<P = Record<string, unknown>>(
147
+ setup: (ctx: BindIslandContext<P>) => Record<string, (event: Event) => void> | void,
148
+ ): IslandEnhancer<P> {
149
+ return (root, props) => {
150
+ const stops: Array<() => void> = []
151
+ let live = true
152
+ const seeded: Record<string, unknown> = typeof props === "object" && props !== null ? props : {}
153
+ // No prototype: a state key like `constructor` is an entry like any other.
154
+ const signals: Record<string, Signal<unknown>> = Object.create(null)
155
+ const ctx: BindIslandContext<P> = {
156
+ root,
157
+ props,
158
+ state<T>(key: string, fallback: T): Signal<T> {
159
+ signals[key] ??= signal(Object.hasOwn(seeded, key) ? seeded[key] : fallback)
160
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: each key's signal is created once, from this call's type
161
+ return signals[key] as Signal<T>
162
+ },
163
+ onDispose: (fn) => {
164
+ if (live) stops.push(fn)
165
+ else fn()
166
+ },
167
+ }
168
+ const dispose = (): void => {
169
+ live = false
170
+ for (let stop = stops.pop(); stop !== undefined; stop = stops.pop()) {
171
+ try {
172
+ stop()
173
+ } catch (err) {
174
+ console.error("[nifra/islands] cleanup failed:", err)
175
+ }
176
+ }
177
+ }
178
+ try {
179
+ // Owned, so a binding whose first run throws is registered before it throws.
180
+ owned(stops, () => bindScope(root, { signals, handlers: setup(ctx) ?? {} }, stops))
181
+ } catch (err) {
182
+ dispose()
183
+ throw err
184
+ }
185
+ return dispose
67
186
  }
68
187
  }
69
188
 
70
189
  /**
71
- * Find every `<nifra-island data-id>` under `root` (default `document`) and enhance each with the
72
- * matching enhancer, honoring its `data-strategy`. An island whose `id` has no enhancer is left as
73
- * inert SSR HTML (forward-compatible). An enhancer that throws is isolated - it never blocks the
74
- * others (each island is independent). Returns a disposer that cancels pending triggers and runs every
75
- * enhancer's cleanup (call it on soft-nav teardown; harmless if your app never navigates).
190
+ * The `data-island-props` value for `props`: JSON. Emit it through an escaping renderer into a quoted
191
+ * attribute (`data-island-props="${...}"`): unquoted, a space in the JSON ends the value and the rest
192
+ * parses as attributes.
193
+ */
194
+ export function islandProps(props: Record<string, unknown>): string {
195
+ return JSON.stringify(props)
196
+ }
197
+
198
+ /**
199
+ * Find every `[data-island]` element under `root` (default `document`) and enhance each with the
200
+ * matching enhancer, honoring its `data-island-strategy`. An island whose id has no enhancer, or that
201
+ * is already mounted, is left alone, so calling again after a soft navigation mounts only new hosts.
202
+ * An enhancer that throws is isolated - it never blocks the others. Every effect, list binding and
203
+ * resource an enhancer creates is owned by its island. Returns a disposer that cancels pending
204
+ * triggers and tears down every island it mounted (call it on soft-nav teardown).
76
205
  */
77
206
  export function mountIslands(
78
- enhancers: Readonly<Record<string, IslandEnhancer>>,
207
+ // `never` admits an enhancer of any props type: each one's props are its own island's JSON.
208
+ enhancers: Readonly<Record<string, IslandEnhancer<never>>>,
79
209
  options: { readonly root?: ParentNode } = {},
80
210
  ): () => void {
81
- const root = options.root ?? document
82
211
  const disposers: Array<() => void> = []
83
- for (const el of root.querySelectorAll<HTMLElement>("nifra-island[data-id]")) {
84
- const id = el.dataset.id
85
- if (id === undefined) continue
86
- const enhancer = enhancers[id]
87
- if (enhancer === undefined) continue // no enhancer for this id → leave it as inert SSR markup
88
- const props = readProps(el)
89
- const strategyAttr = el.dataset.strategy
90
- const strategy: IslandStrategy | undefined =
91
- strategyAttr === "idle" || strategyAttr === "visible" || strategyAttr === "load"
92
- ? strategyAttr
93
- : strategyAttr === "media" &&
94
- el.dataset.media !== undefined &&
95
- el.dataset.media.length > 0 &&
96
- el.dataset.media.length <= MAX_MEDIA_QUERY_LENGTH
97
- ? { media: el.dataset.media }
98
- : strategyAttr === undefined
99
- ? "load"
100
- : undefined
101
- // Invalid media markers remain inert rather than silently becoming eager. Their SSR content stays
102
- // usable, and untrusted HTML cannot turn an oversized query into an unexpected trigger.
103
- if (strategy === undefined) continue
104
- const run = (): void => {
105
- try {
106
- const cleanup = enhancer(el, props)
107
- if (typeof cleanup === "function") disposers.push(cleanup)
108
- } catch (err) {
109
- // One island's failure must not take down the others; surface it without throwing.
110
- console.error(`[nifra/islands] enhancer "${id}" failed:`, err)
111
- }
112
- }
113
- disposers.push(scheduleTrigger(strategy, run, { target: el }))
212
+ for (const el of (options.root ?? document).querySelectorAll<HTMLElement>(
213
+ "[data-island]:not([data-island-ignore],[data-island-ignore] *)",
214
+ )) {
215
+ const dispose = mountIsland(el, enhancers)
216
+ if (dispose !== undefined) disposers.push(dispose)
114
217
  }
115
218
  return () => {
116
- for (const dispose of disposers) {
117
- try {
118
- dispose()
119
- } catch {
120
- // best-effort teardown - a failing cleanup must not block the rest
121
- }
122
- }
219
+ for (const dispose of disposers.splice(0)) dispose()
123
220
  }
124
221
  }
125
222