@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
@@ -139,6 +139,10 @@ export async function serverComponentsConfig(
139
139
  if (env.command === "serve") build = `d_${randomBytes(16).toString("hex")}`
140
140
  const config: UserConfig = {
141
141
  define: {
142
+ ...publicEnvDefines(
143
+ options.publicEnvPrefix ?? "PUBLIC_",
144
+ loadEnv(env.mode, root, options.publicEnvPrefix ?? "PUBLIC_"),
145
+ ),
142
146
  "process.env.NODE_ENV": JSON.stringify(
143
147
  env.command === "build" ? "production" : "development",
144
148
  ),
@@ -234,7 +238,7 @@ export async function serverComponentsConfig(
234
238
  // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The shared compiler guards implement Vite hooks; the shim differs only in optional hook metadata.
235
239
  viteAssetUrlGuard(guards) as Plugin,
236
240
  // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The shared compiler guards implement Vite hooks; the shim differs only in optional hook metadata.
237
- viteZoneGuard(guards) as Plugin,
241
+ viteZoneGuard({ ...guards, componentReferences: true }) as Plugin,
238
242
  ...(options.plugins ?? []),
239
243
  proof,
240
244
  ]
@@ -255,7 +259,6 @@ export async function serverComponentsConfig(
255
259
  // Without it Vite inlines every `VITE_*` value from the app's .env files into client code.
256
260
  envPrefix: vitePublicEnvPrefix(options.publicEnvPrefix),
257
261
  define: {
258
- ...publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", process.env),
259
262
  "process.env.NIFRA_SSR_BUNDLED": '"1"',
260
263
  "globalThis.__NIFRA_EDGE_RUNTIME__": edge ? "true" : "false",
261
264
  __NIFRA_COMPONENT_BUILD__: JSON.stringify(build),
@@ -355,7 +358,10 @@ export async function buildServerComponents(
355
358
  JSON.stringify({
356
359
  adapter: options.adapter,
357
360
  target: options.target ?? "bun",
358
- env: publicEnvDefines(options.publicEnvPrefix ?? "PUBLIC_", process.env),
361
+ env: publicEnvDefines(
362
+ options.publicEnvPrefix ?? "PUBLIC_",
363
+ loadEnv("production", root, options.publicEnvPrefix ?? "PUBLIC_"),
364
+ ),
359
365
  }),
360
366
  )
361
367
  for (const [file, hash] of [...modules].sort(([a], [b]) => codeUnitOrder(a, b)))
@@ -1,3 +1,5 @@
1
+ import { setHoleComponents } from "../internal/hole-runtime.ts"
2
+ import { onPageRelease } from "../internal/page-events.ts"
1
3
  import { ServerComponentError } from "./error.ts"
2
4
  import { readComponentFrames } from "./frames.ts"
3
5
  import {
@@ -9,7 +11,7 @@ import {
9
11
  import { registerComponentController, unregisterComponentController } from "./registry.ts"
10
12
  import { componentResponseFailure } from "./response.ts"
11
13
  import { BoundedComponentTasks } from "./scheduler.ts"
12
- import { parseComponentState } from "./state.ts"
14
+ import { parseComponentRecord, parseComponentState } from "./state.ts"
13
15
  import { readComponentBytes } from "./stream.ts"
14
16
  import type { ComponentKey, ComponentRecord, ComponentState, ServerComponent } from "./types.ts"
15
17
  import { componentInputKey, componentValue } from "./value.ts"
@@ -64,6 +66,8 @@ interface Mounted {
64
66
  abort: AbortController
65
67
  root?: ComponentClientRoot
66
68
  ready: Promise<void>
69
+ // Cancels the release a parked instance waits on from its page.
70
+ unwatch?: (() => void) | undefined
67
71
  }
68
72
  function sameRecord(a: ComponentRecord, b: ComponentRecord): boolean {
69
73
  return (
@@ -90,6 +94,8 @@ export class ComponentController {
90
94
  }
91
95
  readonly #options: ComponentControllerOptions
92
96
  readonly #mounted = new Map<string, Mounted>()
97
+ // Set aside with a `keepAlive` page while it is hidden; another page may mount the same instances.
98
+ readonly #parked = new Set<Mounted>()
93
99
  // Structural outlet anchors survive logout; their private records and native roots do not.
94
100
  readonly #restorable = new Map<string, Element>()
95
101
  readonly #teardown = new WeakMap<Element, Promise<void>>()
@@ -122,6 +128,8 @@ export class ComponentController {
122
128
  }
123
129
  async #mount(host: Element, instance: string, fresh: boolean): Promise<void> {
124
130
  await this.#teardown.get(host)
131
+ // The page that parked it takes it back when it is shown again.
132
+ if (this.isParked(instance, host)) return
125
133
  if (this.#disposed || this.#mounted.has(instance))
126
134
  throw new ServerComponentError("component_contract")
127
135
  const record = this.state.records.find((record) => record.instance === instance)
@@ -162,6 +170,8 @@ export class ComponentController {
162
170
  try {
163
171
  await mounted.ready
164
172
  } catch (error) {
173
+ // Parked while it hydrated: it mounts again when its page is shown.
174
+ if (this.#parked.has(mounted)) return
165
175
  if (this.#mounted.get(instance) === mounted) await this.unmount(instance)
166
176
  throw error
167
177
  }
@@ -439,6 +449,64 @@ export class ComponentController {
439
449
  mounted.generation++
440
450
  }
441
451
  }
452
+ /**
453
+ * Set aside the components inside `page`, a `keepAlive` page the router hides: their pending work is
454
+ * cancelled, and navigation neither updates nor unmounts them until `unpark` takes them back.
455
+ * `bindComponentRouter` calls it on the page events.
456
+ */
457
+ park(page: Node): void {
458
+ for (const mounted of this.#mounted.values())
459
+ if (page.contains(mounted.host)) {
460
+ this.#mounted.delete(mounted.record.instance)
461
+ this.#parked.add(mounted)
462
+ mounted.unwatch = onPageRelease(page, () => {
463
+ void this.#release(mounted).catch(() => this.reportError("component_failed"))
464
+ })
465
+ mounted.abort.abort()
466
+ mounted.abort = new AbortController()
467
+ mounted.generation++
468
+ }
469
+ }
470
+ /** Take back the components `park` set aside inside `page`, which is shown again. */
471
+ unpark(page: Node): void {
472
+ for (const mounted of this.#parked) {
473
+ if (!page.contains(mounted.host)) continue
474
+ const id = mounted.record.instance
475
+ if (this.#mounted.has(id) || this.#disposed) {
476
+ void this.#release(mounted)
477
+ continue
478
+ }
479
+ this.#parked.delete(mounted)
480
+ mounted.unwatch?.()
481
+ if (mounted.root !== undefined) this.#mounted.set(id, mounted)
482
+ // Hidden while it hydrated, which `park` cancelled: mount it again.
483
+ else
484
+ void this.#mount(mounted.host, id, mounted.host.childNodes.length === 0).catch(() =>
485
+ this.reportError("component_failed"),
486
+ )
487
+ }
488
+ }
489
+ /**
490
+ * Whether `instance` at `host` is parked with its hidden page. An outlet whose framework cleans up a
491
+ * hidden page's effects (React's `<Activity>`) leaves it parked: the page takes it back when shown
492
+ * again, and releases it when dropped.
493
+ */
494
+ isParked(instance: string, host: Element): boolean {
495
+ for (const mounted of this.#parked)
496
+ if (mounted.host === host && mounted.record.instance === instance) return true
497
+ return false
498
+ }
499
+ async #release(mounted: Mounted): Promise<void> {
500
+ if (!this.#parked.delete(mounted)) return
501
+ mounted.unwatch?.()
502
+ for (const child of this.#parked)
503
+ if (mounted.host.contains(child.host)) await this.#release(child)
504
+ mounted.abort.abort()
505
+ await mounted.ready.catch(() => {
506
+ /* A parked hydration was cancelled; nothing of it can commit. */
507
+ })
508
+ await mounted.root?.dispose()
509
+ }
442
510
  async adopt(state: ComponentState, ownerUrl?: string): Promise<void> {
443
511
  if (ownerUrl !== undefined) this.#ownerUrl = ownerUrl
444
512
  if (this.#disposed || state === this.#source) return
@@ -568,7 +636,11 @@ export class ComponentController {
568
636
  }
569
637
  }
570
638
  }
571
- async unmount(instance: string): Promise<void> {
639
+ /** Unmount `instance`; an outlet passes its `host`, which tells a parked page's instance apart. */
640
+ async unmount(instance: string, host?: Element): Promise<void> {
641
+ for (const mounted of this.#parked)
642
+ if (mounted.host === host && mounted.record.instance === instance)
643
+ return this.#release(mounted)
572
644
  this.#restorable.delete(instance)
573
645
  await this.#unmount(instance)
574
646
  }
@@ -594,7 +666,7 @@ export class ComponentController {
594
666
  if (this.#teardown.get(mounted.host) === teardown) this.#teardown.delete(mounted.host)
595
667
  }
596
668
  }
597
- async #clearScope(preserveOutlets = false): Promise<void> {
669
+ async #clearScope(preserveOutlets = false, keepParked = false): Promise<void> {
598
670
  this.#authEpoch++
599
671
  if (preserveOutlets) {
600
672
  for (const mounted of this.#mounted.values())
@@ -603,7 +675,10 @@ export class ComponentController {
603
675
  } else this.#restorable.clear()
604
676
  this.#state = { ...this.#state, records: [] }
605
677
  this.#source = undefined
606
- await Promise.all([...this.#mounted.keys()].map((instance) => this.#unmount(instance)))
678
+ await Promise.all([
679
+ ...[...this.#mounted.keys()].map((instance) => this.#unmount(instance)),
680
+ ...(keepParked ? [] : [...this.#parked].map((mounted) => this.#release(mounted))),
681
+ ])
607
682
  }
608
683
  async invalidateScope(): Promise<void> {
609
684
  this.#adoption++
@@ -611,7 +686,7 @@ export class ComponentController {
611
686
  }
612
687
  async navigate(): Promise<void> {
613
688
  this.cancelPending()
614
- await this.#clearScope()
689
+ await this.#clearScope(false, !this.#disposed)
615
690
  }
616
691
  async dispose(): Promise<void> {
617
692
  this.#disposed = true
@@ -651,6 +726,15 @@ export function resolveComponentRecord<Input>(
651
726
  return matches[0]!
652
727
  }
653
728
 
729
+ /**
730
+ * Experimental: how the client islands inside partially prerendered holes that show server
731
+ * components mount, as the adapter's component client, such as `vueComponentClient({ islands })`;
732
+ * `undefined` forgets it. Call it in the client entry before the router mounts. Without one such a
733
+ * hole shows its error slot.
734
+ */
735
+ export function holeComponents(client: ComponentClientAdapter | undefined): void {
736
+ setHoleComponents(client === undefined ? undefined : { client, record: parseComponentRecord })
737
+ }
654
738
  export { invalidateComponentScope } from "./registry.ts"
655
739
  export { bindComponentRouter, refreshAfterComponentMutation } from "./router-integration.ts"
656
740
  export { parseComponentState } from "./state.ts"
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Experimental: a partially prerendered hole whose content is a server component. The hole's loader
3
+ * returns the component's input; the server runs the component for each visitor and sends its HTML,
4
+ * and the browser mounts only the client islands inside it. Under the coordinated compiler a browser
5
+ * import of a `*.component.server` module is an opaque handle, so the component never ships.
6
+ */
7
+ import { SERVER_HOLE } from "../internal/ppr/contract.ts"
8
+ import type { ServerComponent } from "./types.ts"
9
+
10
+ /**
11
+ * A `dynamic` hole's `render`: the hole shows `component`, given the hole loader's output as its
12
+ * input. The page's app lists the component under `createServerComponents({ holes })` for the route.
13
+ * The hole cannot be `interactive`; its client islands are.
14
+ */
15
+ export function serverHole<Input, Props, View>(
16
+ component: ServerComponent<Input, Props, View>,
17
+ ): (data: Input) => never {
18
+ const render = (): never => {
19
+ throw new Error("[nifra/web] a server component hole renders on the server only")
20
+ }
21
+ Object.defineProperty(render, SERVER_HOLE, { value: component })
22
+ return Object.freeze(render)
23
+ }
@@ -35,10 +35,59 @@ const urls = new Set([
35
35
  "data",
36
36
  "codebase",
37
37
  ])
38
+ const IMAGE_HINT = new Set([
39
+ "rel",
40
+ "as",
41
+ "href",
42
+ "imagesrcset",
43
+ "imagesizes",
44
+ "crossorigin",
45
+ "fetchpriority",
46
+ "referrerpolicy",
47
+ ])
38
48
  const fail = (): never => {
39
49
  throw new ServerComponentError("component_protocol", 400)
40
50
  }
41
51
 
52
+ const unsafeUrl = (value: string): boolean =>
53
+ /^(?:javascript|vbscript|data):/.test(
54
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: Browsers discard these controls when interpreting URL schemes.
55
+ value.replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase(),
56
+ )
57
+
58
+ const isSpace = (char: string): boolean =>
59
+ char === " " || char === "\t" || char === "\n" || char === "\f" || char === "\r"
60
+
61
+ /**
62
+ * The image candidate URLs of a `srcset`, split as the HTML standard's "parse a srcset attribute"
63
+ * splits them, so each is the URL a browser would fetch: a URL is a run without whitespace, and its
64
+ * descriptors end at the first comma outside parentheses.
65
+ */
66
+ function srcsetUrls(value: string): string[] {
67
+ const found: string[] = []
68
+ let at = 0
69
+ while (at < value.length) {
70
+ while (at < value.length && (isSpace(value[at] ?? "") || value[at] === ",")) at++
71
+ const start = at
72
+ while (at < value.length && !isSpace(value[at] ?? "")) at++
73
+ const url = value.slice(start, at)
74
+ if (url === "") break
75
+ if (url.endsWith(",")) {
76
+ found.push(url.replace(/,+$/, ""))
77
+ continue
78
+ }
79
+ found.push(url)
80
+ let inParens = false
81
+ for (; at < value.length; at++) {
82
+ const char = value[at]
83
+ if (char === "(") inParens = true
84
+ else if (char === ")") inParens = false
85
+ else if (char === "," && !inParens) break
86
+ }
87
+ }
88
+ return found
89
+ }
90
+
42
91
  function css(value: string): string {
43
92
  return (
44
93
  value
@@ -66,6 +115,17 @@ export function validateComponentTag(
66
115
  const attrs = new Map(
67
116
  attributes.map((attribute) => [attribute.name.toLowerCase(), attribute.value]),
68
117
  )
118
+ // React writes an image's preload hint ahead of each `<img>` it renders without a document. Like
119
+ // every hint here it is cut from the range, so it only has to be a well-formed image hint.
120
+ if (attrs.get("rel") === "preload" && attrs.get("as") === "image") {
121
+ if (
122
+ [...attrs.keys()].some((name) => !IMAGE_HINT.has(name)) ||
123
+ unsafeUrl(attrs.get("href") ?? "") ||
124
+ srcsetUrls(attrs.get("imagesrcset") ?? "").some(unsafeUrl)
125
+ )
126
+ fail()
127
+ return
128
+ }
69
129
  if (
70
130
  !["preload", "modulepreload"].includes(attrs.get("rel") ?? "") ||
71
131
  !assets.has(attrs.get("href") ?? "") ||
@@ -79,10 +139,9 @@ export function validateComponentTag(
79
139
  if (forbidden.has(tag.toLowerCase())) fail()
80
140
  for (const attribute of attributes) {
81
141
  const name = attribute.name.toLowerCase()
82
- // biome-ignore lint/suspicious/noControlCharactersInRegex: Browsers discard these controls when interpreting URL schemes.
83
- const value = attribute.value.replace(/[\u0000-\u0020\u007f]/g, "").toLowerCase()
84
- if (name.startsWith("on") || ["srcdoc", "is", "srcset", "ping"].includes(name)) fail()
85
- if (urls.has(name) && /^(?:javascript|vbscript|data):/.test(value)) fail()
142
+ if (name.startsWith("on") || ["srcdoc", "is", "ping"].includes(name)) fail()
143
+ if (urls.has(name) && unsafeUrl(attribute.value)) fail()
144
+ if (name === "srcset" && srcsetUrls(attribute.value).some(unsafeUrl)) fail()
86
145
  if (
87
146
  name === "style" &&
88
147
  /expression\(|@import|(?:javascript|vbscript|data):|-moz-binding|behavior:/.test(
@@ -0,0 +1,159 @@
1
+ /**
2
+ * A linear scan of the HTML a component renderer writes, for the validator's common case. It reads
3
+ * only a subset that parse5's tokenizer reads without a parse error and with the same start tags:
4
+ * ASCII tag and attribute names, double-quoted values, a few character references and plain
5
+ * comments. Anything else returns `undefined`, and the tokenizer decides. A differential test holds
6
+ * the two to the same tags (server-components-html-scan.test.ts).
7
+ */
8
+
9
+ export interface ScannedTag {
10
+ readonly tagName: string
11
+ readonly attrs: { readonly name: string; readonly value: string }[]
12
+ }
13
+
14
+ const REFERENCES: ReadonlyArray<readonly [string, string]> = [
15
+ ["&amp;", "&"],
16
+ ["&lt;", "<"],
17
+ ["&gt;", ">"],
18
+ ["&quot;", '"'],
19
+ ["&#39;", "'"],
20
+ ["&#x27;", "'"],
21
+ ["&nbsp;", "\u00a0"],
22
+ ]
23
+
24
+ const referenceAt = (html: string, at: number): readonly [string, string] | undefined => {
25
+ for (const reference of REFERENCES) if (html.startsWith(reference[0], at)) return reference
26
+ return undefined
27
+ }
28
+
29
+ const isSpace = (code: number): boolean =>
30
+ code === 0x20 || code === 0x0a || code === 0x09 || code === 0x0c || code === 0x0d
31
+ const isLetter = (code: number): boolean =>
32
+ (code >= 0x61 && code <= 0x7a) || (code >= 0x41 && code <= 0x5a)
33
+ const isTagCode = (code: number): boolean =>
34
+ isLetter(code) || (code >= 0x30 && code <= 0x39) || code === 0x2d
35
+ const isAttributeStart = (code: number): boolean => isLetter(code) || code === 0x5f || code === 0x3a
36
+ const isAttributeCode = (code: number): boolean =>
37
+ isAttributeStart(code) || (code >= 0x30 && code <= 0x39) || code === 0x2d || code === 0x2e
38
+
39
+ /**
40
+ * The code units of the character at `at`, or 0 when parse5 reports it (NULL, a control, a
41
+ * noncharacter, a lone surrogate) or past the end.
42
+ */
43
+ function unitsAt(html: string, at: number): number {
44
+ const code = html.charCodeAt(at)
45
+ if (code >= 0x20 && code < 0x7f) return 1
46
+ if (code >= 0xd800 && code <= 0xdbff) {
47
+ const low = html.charCodeAt(at + 1)
48
+ if (!(low >= 0xdc00 && low <= 0xdfff)) return 0
49
+ const point = ((code - 0xd800) << 10) + (low - 0xdc00) + 0x10000
50
+ return (point & 0xfffe) === 0xfffe ? 0 : 2
51
+ }
52
+ if (Number.isNaN(code) || (code >= 0xdc00 && code <= 0xdfff)) return 0
53
+ if (code < 0x20) return isSpace(code) ? 1 : 0
54
+ if (code <= 0x9f) return 0
55
+ return (code >= 0xfdd0 && code <= 0xfdef) || code === 0xfffe || code === 0xffff ? 0 : 1
56
+ }
57
+
58
+ /** Where a start tag whose name begins at `at` ends, with `tags` given its tag, or -1. */
59
+ function scanStartTag(html: string, at: number, tags: ScannedTag[]): number {
60
+ let end = at
61
+ while (isTagCode(html.charCodeAt(end))) end++
62
+ const tagName = html.slice(at, end).toLowerCase()
63
+ // The validator reads these two as a browser does, which needs the text after them.
64
+ if (tagName === "title" || tagName === "textarea") return -1
65
+ let code = html.charCodeAt(end)
66
+ if (!(isSpace(code) || code === 0x2f || code === 0x3e)) return -1
67
+ const attrs: { name: string; value: string }[] = []
68
+ for (;;) {
69
+ while (isSpace(html.charCodeAt(end))) end++
70
+ code = html.charCodeAt(end)
71
+ if (code === 0x3e || (code === 0x2f && html.charCodeAt(end + 1) === 0x3e)) {
72
+ tags.push({ tagName, attrs })
73
+ return end + (code === 0x3e ? 1 : 2)
74
+ }
75
+ if (!isAttributeStart(code)) return -1
76
+ const nameStart = end
77
+ while (isAttributeCode(html.charCodeAt(end))) end++
78
+ const name = html.slice(nameStart, end).toLowerCase()
79
+ if (attrs.some((attr) => attr.name === name)) return -1
80
+ code = html.charCodeAt(end)
81
+ let value = ""
82
+ if (code === 0x3d) {
83
+ if (html.charCodeAt(end + 1) !== 0x22) return -1
84
+ let from = end + 2
85
+ end = from
86
+ for (;;) {
87
+ code = html.charCodeAt(end)
88
+ if (code === 0x22) break
89
+ if (code === 0x26) {
90
+ const reference = referenceAt(html, end)
91
+ if (reference === undefined) return -1
92
+ value += html.slice(from, end) + reference[1]
93
+ end += reference[0].length
94
+ from = end
95
+ continue
96
+ }
97
+ // A CR would reach the tokenizer's value as LF.
98
+ const units = code === 0x0d ? 0 : unitsAt(html, end)
99
+ if (units === 0) return -1
100
+ end += units
101
+ }
102
+ value += html.slice(from, end)
103
+ end++
104
+ code = html.charCodeAt(end)
105
+ if (!(isSpace(code) || code === 0x2f || code === 0x3e)) return -1
106
+ } else if (!(isSpace(code) || code === 0x2f || code === 0x3e)) return -1
107
+ attrs.push({ name, value })
108
+ }
109
+ }
110
+
111
+ /** The start tags of `html`, or `undefined` when it leaves the subset this scan reads. */
112
+ export function scanComponentHTML(html: string): ScannedTag[] | undefined {
113
+ const tags: ScannedTag[] = []
114
+ let at = 0
115
+ while (at < html.length) {
116
+ const code = html.charCodeAt(at)
117
+ if (code === 0x3c) {
118
+ const next = html.charCodeAt(at + 1)
119
+ if (isLetter(next)) {
120
+ at = scanStartTag(html, at + 1, tags)
121
+ if (at === -1) return undefined
122
+ } else if (next === 0x2f) {
123
+ let end = at + 2
124
+ if (!isLetter(html.charCodeAt(end))) return undefined
125
+ while (isTagCode(html.charCodeAt(end))) end++
126
+ while (isSpace(html.charCodeAt(end))) end++
127
+ if (html.charCodeAt(end) !== 0x3e) return undefined
128
+ at = end + 1
129
+ } else if (html.startsWith("!--", at + 1)) {
130
+ const from = at + 4
131
+ if (html.charCodeAt(from) === 0x3e || html.startsWith("->", from)) return undefined
132
+ const close = html.indexOf("-->", from)
133
+ if (close === -1) return undefined
134
+ for (let inner = from; inner < close; ) {
135
+ const units = unitsAt(html, inner)
136
+ if (units === 0) return undefined
137
+ // `<!` can open a nested comment and `--!` a `--!>` ending; both are parse errors.
138
+ if (
139
+ html.charCodeAt(inner) === 0x21 &&
140
+ ((inner > from && html.charCodeAt(inner - 1) === 0x3c) ||
141
+ (inner - 2 >= from && html.startsWith("--", inner - 2)))
142
+ )
143
+ return undefined
144
+ inner += units
145
+ }
146
+ at = close + 3
147
+ } else return undefined
148
+ } else if (code === 0x26) {
149
+ const reference = referenceAt(html, at)
150
+ if (reference === undefined) return undefined
151
+ at += reference[0].length
152
+ } else {
153
+ const units = unitsAt(html, at)
154
+ if (units === 0) return undefined
155
+ at += units
156
+ }
157
+ }
158
+ return tags
159
+ }
@@ -1,6 +1,7 @@
1
1
  import { Tokenizer } from "parse5"
2
2
  import { ServerComponentError } from "./error.ts"
3
3
  import { validateComponentTag } from "./html-policy.ts"
4
+ import { scanComponentHTML } from "./html-scan.ts"
4
5
  import { COMPONENT_LIMITS } from "./protocol.ts"
5
6
 
6
7
  /**
@@ -19,16 +20,26 @@ function closesAsText(html: string, tag: string, from: number): boolean {
19
20
  )
20
21
  }
21
22
 
23
+ function assertOutputSize(html: string): void {
24
+ // UTF-8 takes at most three bytes per UTF-16 unit, so only a long string is encoded to measure it.
25
+ if (
26
+ html.length * 3 > COMPONENT_LIMITS.outputBytes &&
27
+ new TextEncoder().encode(html).length > COMPONENT_LIMITS.outputBytes
28
+ )
29
+ throw new ServerComponentError("component_limit", 413)
30
+ }
31
+
22
32
  /** Tokenization validates attributes even in SVG/table contexts a document parser may discard. */
23
33
  function tokenize(
24
34
  html: string,
25
35
  assets?: ReadonlySet<string>,
26
36
  hints?: Array<{ start: number; end: number }>,
27
37
  ): void {
28
- if (new TextEncoder().encode(html).length > COMPONENT_LIMITS.outputBytes)
29
- throw new ServerComponentError("component_limit", 413)
38
+ assertOutputSize(html)
30
39
  const tokenizer = new Tokenizer(
31
- { sourceCodeLocationInfo: true },
40
+ // Offsets are read only for hints and for `title`/`textarea`, whose names a tokenizer lowercases
41
+ // from ASCII alone; tracking them costs every other string about a tenth of its check.
42
+ { sourceCodeLocationInfo: hints !== undefined || /<(?:title|textarea)/i.test(html) },
32
43
  {
33
44
  onStartTag(token) {
34
45
  validateComponentTag(token.tagName, token.attrs, assets)
@@ -63,7 +74,10 @@ function tokenize(
63
74
  }
64
75
 
65
76
  export function validateComponentHTML(html: string, assets?: ReadonlySet<string>): void {
66
- tokenize(html, assets)
77
+ assertOutputSize(html)
78
+ const tags = scanComponentHTML(html)
79
+ if (tags === undefined) tokenize(html, assets)
80
+ else for (const tag of tags) validateComponentTag(tag.tagName, tag.attrs, assets)
67
81
  }
68
82
 
69
83
  /** React may produce resource hints while decoding Flight. They are not owned range nodes. */
@@ -1,13 +1,27 @@
1
+ import { PAGE_HIDE_EVENT, PAGE_SHOW_EVENT } from "../internal/page-events.ts"
1
2
  import type { ClientRouter } from "../router.ts"
2
3
  import type { ComponentController } from "./client.ts"
3
4
  import { componentControllers } from "./registry.ts"
4
5
 
5
- /** Optional router integration; imported only by opted-in application entries. */
6
+ /**
7
+ * Optional router integration; imported only by opted-in application entries. The components of a
8
+ * `keepAlive` page are parked while it is hidden, so navigation leaves them as they are.
9
+ */
6
10
  export function bindComponentRouter(
7
11
  router: Pick<ClientRouter, "snapshot" | "subscribe">,
8
12
  ): () => void {
9
13
  let previous = router.snapshot()
10
- return router.subscribe(() => {
14
+ const onPage = (event: Event): void => {
15
+ const page = event.target
16
+ if (page instanceof Node)
17
+ for (const controller of componentControllers)
18
+ if (event.type === PAGE_HIDE_EVENT) controller.park(page)
19
+ else controller.unpark(page)
20
+ }
21
+ const target = typeof document === "undefined" ? undefined : document
22
+ target?.addEventListener(PAGE_HIDE_EVENT, onPage)
23
+ target?.addEventListener(PAGE_SHOW_EVENT, onPage)
24
+ const stop = router.subscribe(() => {
11
25
  const next = router.snapshot()
12
26
  if (next.pending && next.pendingPath !== previous.pendingPath)
13
27
  for (const controller of componentControllers) controller.cancelPending()
@@ -25,6 +39,11 @@ export function bindComponentRouter(
25
39
  }
26
40
  previous = next
27
41
  })
42
+ return () => {
43
+ stop()
44
+ target?.removeEventListener(PAGE_HIDE_EVENT, onPage)
45
+ target?.removeEventListener(PAGE_SHOW_EVENT, onPage)
46
+ }
28
47
  }
29
48
 
30
49
  /** Refresh only after the existing mutation transport reports success. */