@nifrajs/web 5.5.1 → 5.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/CHANGELOG.md +243 -0
  2. package/README.md +21 -11
  3. package/dist/boundary.d.ts +67 -7
  4. package/dist/boundary.d.ts.map +1 -1
  5. package/dist/boundary.js +6 -4
  6. package/dist/boundary.js.map +1 -1
  7. package/dist/build-plan.d.ts +3 -0
  8. package/dist/build-plan.d.ts.map +1 -1
  9. package/dist/build-plan.js.map +1 -1
  10. package/dist/build-vite.d.ts.map +1 -1
  11. package/dist/build-vite.js +2 -0
  12. package/dist/build-vite.js.map +1 -1
  13. package/dist/build.d.ts +9 -2
  14. package/dist/build.d.ts.map +1 -1
  15. package/dist/build.js +10 -0
  16. package/dist/build.js.map +1 -1
  17. package/dist/client.d.ts +3 -1
  18. package/dist/client.d.ts.map +1 -1
  19. package/dist/client.js +174 -23
  20. package/dist/client.js.map +1 -1
  21. package/dist/csp.d.ts +8 -0
  22. package/dist/csp.d.ts.map +1 -1
  23. package/dist/csp.js +11 -2
  24. package/dist/csp.js.map +1 -1
  25. package/dist/deferred.d.ts +6 -4
  26. package/dist/deferred.d.ts.map +1 -1
  27. package/dist/deferred.js +10 -6
  28. package/dist/deferred.js.map +1 -1
  29. package/dist/dev-feed.d.ts.map +1 -1
  30. package/dist/dev-feed.js +11 -2
  31. package/dist/dev-feed.js.map +1 -1
  32. package/dist/dev-session.d.ts.map +1 -1
  33. package/dist/dev-session.js +13 -6
  34. package/dist/dev-session.js.map +1 -1
  35. package/dist/dev.d.ts +4 -1
  36. package/dist/dev.d.ts.map +1 -1
  37. package/dist/dev.js +12 -5
  38. package/dist/dev.js.map +1 -1
  39. package/dist/diagnostic-catalog.d.ts +4 -0
  40. package/dist/diagnostic-catalog.d.ts.map +1 -1
  41. package/dist/diagnostic-catalog.js +48 -0
  42. package/dist/diagnostic-catalog.js.map +1 -1
  43. package/dist/diagnostic.d.ts +1 -1
  44. package/dist/diagnostic.d.ts.map +1 -1
  45. package/dist/diagnostic.js +1 -1
  46. package/dist/diagnostic.js.map +1 -1
  47. package/dist/fn.d.ts +5 -3
  48. package/dist/fn.d.ts.map +1 -1
  49. package/dist/fn.js +20 -5
  50. package/dist/fn.js.map +1 -1
  51. package/dist/fs.d.ts.map +1 -1
  52. package/dist/fs.js +13 -3
  53. package/dist/fs.js.map +1 -1
  54. package/dist/identity.d.ts +22 -0
  55. package/dist/identity.d.ts.map +1 -0
  56. package/dist/identity.js +60 -0
  57. package/dist/identity.js.map +1 -0
  58. package/dist/index.d.ts +8 -6
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +6 -3
  61. package/dist/index.js.map +1 -1
  62. package/dist/internal/cdn/cloudflare.d.ts +2 -1
  63. package/dist/internal/cdn/cloudflare.d.ts.map +1 -1
  64. package/dist/internal/cdn/cloudflare.js +2 -1
  65. package/dist/internal/cdn/cloudflare.js.map +1 -1
  66. package/dist/internal/certified-components.d.ts +1 -1
  67. package/dist/internal/certified-components.js +1 -1
  68. package/dist/internal/codegen.d.ts +11 -10
  69. package/dist/internal/codegen.d.ts.map +1 -1
  70. package/dist/internal/codegen.js +73 -10
  71. package/dist/internal/codegen.js.map +1 -1
  72. package/dist/internal/dev-websocket.d.ts +73 -0
  73. package/dist/internal/dev-websocket.d.ts.map +1 -0
  74. package/dist/internal/dev-websocket.js +145 -0
  75. package/dist/internal/dev-websocket.js.map +1 -0
  76. package/dist/internal/hole-runtime.d.ts +66 -0
  77. package/dist/internal/hole-runtime.d.ts.map +1 -0
  78. package/dist/internal/hole-runtime.js +211 -0
  79. package/dist/internal/hole-runtime.js.map +1 -0
  80. package/dist/internal/identity-epoch.d.ts +50 -0
  81. package/dist/internal/identity-epoch.d.ts.map +1 -0
  82. package/dist/internal/identity-epoch.js +180 -0
  83. package/dist/internal/identity-epoch.js.map +1 -0
  84. package/dist/internal/invalidation-contract.d.ts +23 -0
  85. package/dist/internal/invalidation-contract.d.ts.map +1 -0
  86. package/dist/internal/invalidation-contract.js +11 -0
  87. package/dist/internal/invalidation-contract.js.map +1 -0
  88. package/dist/internal/invalidation.d.ts +62 -0
  89. package/dist/internal/invalidation.d.ts.map +1 -0
  90. package/dist/internal/invalidation.js +149 -0
  91. package/dist/internal/invalidation.js.map +1 -0
  92. package/dist/internal/island-bindings.d.ts +51 -0
  93. package/dist/internal/island-bindings.d.ts.map +1 -0
  94. package/dist/internal/island-bindings.js +147 -0
  95. package/dist/internal/island-bindings.js.map +1 -0
  96. package/dist/internal/island-list.d.ts +26 -0
  97. package/dist/internal/island-list.d.ts.map +1 -0
  98. package/dist/internal/island-list.js +78 -0
  99. package/dist/internal/island-list.js.map +1 -0
  100. package/dist/internal/island-mount.d.ts +4 -0
  101. package/dist/internal/island-mount.d.ts.map +1 -0
  102. package/dist/internal/island-mount.js +66 -0
  103. package/dist/internal/island-mount.js.map +1 -0
  104. package/dist/internal/island-owner.d.ts +5 -0
  105. package/dist/internal/island-owner.d.ts.map +1 -0
  106. package/dist/internal/island-owner.js +20 -0
  107. package/dist/internal/island-owner.js.map +1 -0
  108. package/dist/internal/island-resource.d.ts +30 -0
  109. package/dist/internal/island-resource.d.ts.map +1 -0
  110. package/dist/internal/island-resource.js +57 -0
  111. package/dist/internal/island-resource.js.map +1 -0
  112. package/dist/internal/island-signals.d.ts +51 -0
  113. package/dist/internal/island-signals.d.ts.map +1 -0
  114. package/dist/internal/island-signals.js +220 -0
  115. package/dist/internal/island-signals.js.map +1 -0
  116. package/dist/internal/keep-alive-table.d.ts +23 -0
  117. package/dist/internal/keep-alive-table.d.ts.map +1 -0
  118. package/dist/internal/keep-alive-table.js +53 -0
  119. package/dist/internal/keep-alive-table.js.map +1 -0
  120. package/dist/internal/live-contract.d.ts +101 -0
  121. package/dist/internal/live-contract.d.ts.map +1 -0
  122. package/dist/internal/live-contract.js +5 -0
  123. package/dist/internal/live-contract.js.map +1 -0
  124. package/dist/internal/live-queries.d.ts +22 -0
  125. package/dist/internal/live-queries.d.ts.map +1 -0
  126. package/dist/internal/live-queries.js +54 -0
  127. package/dist/internal/live-queries.js.map +1 -0
  128. package/dist/internal/page-events.d.ts +10 -0
  129. package/dist/internal/page-events.d.ts.map +1 -0
  130. package/dist/internal/page-events.js +26 -0
  131. package/dist/internal/page-events.js.map +1 -0
  132. package/dist/internal/page-execution.d.ts +16 -1
  133. package/dist/internal/page-execution.d.ts.map +1 -1
  134. package/dist/internal/page-execution.js +432 -31
  135. package/dist/internal/page-execution.js.map +1 -1
  136. package/dist/internal/parity.d.ts +3 -0
  137. package/dist/internal/parity.d.ts.map +1 -1
  138. package/dist/internal/parity.js +28 -5
  139. package/dist/internal/parity.js.map +1 -1
  140. package/dist/internal/ppr/admission.d.ts +83 -0
  141. package/dist/internal/ppr/admission.d.ts.map +1 -0
  142. package/dist/internal/ppr/admission.js +221 -0
  143. package/dist/internal/ppr/admission.js.map +1 -0
  144. package/dist/internal/ppr/canary.d.ts +24 -0
  145. package/dist/internal/ppr/canary.d.ts.map +1 -0
  146. package/dist/internal/ppr/canary.js +102 -0
  147. package/dist/internal/ppr/canary.js.map +1 -0
  148. package/dist/internal/ppr/compile-shell.d.ts +39 -0
  149. package/dist/internal/ppr/compile-shell.d.ts.map +1 -0
  150. package/dist/internal/ppr/compile-shell.js +298 -0
  151. package/dist/internal/ppr/compile-shell.js.map +1 -0
  152. package/dist/internal/ppr/contract.d.ts +177 -0
  153. package/dist/internal/ppr/contract.d.ts.map +1 -0
  154. package/dist/internal/ppr/contract.js +80 -0
  155. package/dist/internal/ppr/contract.js.map +1 -0
  156. package/dist/internal/ppr/frames.d.ts +71 -0
  157. package/dist/internal/ppr/frames.d.ts.map +1 -0
  158. package/dist/internal/ppr/frames.js +119 -0
  159. package/dist/internal/ppr/frames.js.map +1 -0
  160. package/dist/internal/ppr/integration.d.ts +19 -0
  161. package/dist/internal/ppr/integration.d.ts.map +1 -0
  162. package/dist/internal/ppr/integration.js +106 -0
  163. package/dist/internal/ppr/integration.js.map +1 -0
  164. package/dist/internal/ppr/options.d.ts +96 -0
  165. package/dist/internal/ppr/options.d.ts.map +1 -0
  166. package/dist/internal/ppr/options.js +125 -0
  167. package/dist/internal/ppr/options.js.map +1 -0
  168. package/dist/internal/ppr/route-rules.d.ts +12 -0
  169. package/dist/internal/ppr/route-rules.d.ts.map +1 -0
  170. package/dist/internal/ppr/route-rules.js +53 -0
  171. package/dist/internal/ppr/route-rules.js.map +1 -0
  172. package/dist/internal/ppr/runtime.d.ts +29 -0
  173. package/dist/internal/ppr/runtime.d.ts.map +1 -0
  174. package/dist/internal/ppr/runtime.js +35 -0
  175. package/dist/internal/ppr/runtime.js.map +1 -0
  176. package/dist/internal/ppr/server.d.ts +29 -0
  177. package/dist/internal/ppr/server.d.ts.map +1 -0
  178. package/dist/internal/ppr/server.js +1409 -0
  179. package/dist/internal/ppr/server.js.map +1 -0
  180. package/dist/internal/ppr/shell-store.d.ts +39 -0
  181. package/dist/internal/ppr/shell-store.d.ts.map +1 -0
  182. package/dist/internal/ppr/shell-store.js +106 -0
  183. package/dist/internal/ppr/shell-store.js.map +1 -0
  184. package/dist/internal/prerendered-pages.d.ts +1 -1
  185. package/dist/internal/prerendered-pages.d.ts.map +1 -1
  186. package/dist/internal/prerendered-pages.js +7 -4
  187. package/dist/internal/prerendered-pages.js.map +1 -1
  188. package/dist/internal/present.d.ts +11 -0
  189. package/dist/internal/present.d.ts.map +1 -0
  190. package/dist/internal/present.js +13 -0
  191. package/dist/internal/present.js.map +1 -0
  192. package/dist/internal/query-digest.d.ts +9 -0
  193. package/dist/internal/query-digest.d.ts.map +1 -0
  194. package/dist/internal/query-digest.js +23 -0
  195. package/dist/internal/query-digest.js.map +1 -0
  196. package/dist/internal/query-runtime.d.ts +6 -1
  197. package/dist/internal/query-runtime.d.ts.map +1 -1
  198. package/dist/internal/query-runtime.js +9 -0
  199. package/dist/internal/query-runtime.js.map +1 -1
  200. package/dist/internal/query-seam.d.ts +29 -0
  201. package/dist/internal/query-seam.d.ts.map +1 -0
  202. package/dist/internal/query-seam.js +23 -0
  203. package/dist/internal/query-seam.js.map +1 -0
  204. package/dist/internal/render-document.d.ts +34 -64
  205. package/dist/internal/render-document.d.ts.map +1 -1
  206. package/dist/internal/render-document.js +82 -64
  207. package/dist/internal/render-document.js.map +1 -1
  208. package/dist/internal/request-abort.d.ts +8 -0
  209. package/dist/internal/request-abort.d.ts.map +1 -0
  210. package/dist/internal/request-abort.js +22 -0
  211. package/dist/internal/request-abort.js.map +1 -0
  212. package/dist/internal/server-boundary.d.ts +2 -0
  213. package/dist/internal/server-boundary.d.ts.map +1 -1
  214. package/dist/internal/server-boundary.js +8 -1
  215. package/dist/internal/server-boundary.js.map +1 -1
  216. package/dist/internal/share-data.d.ts +10 -0
  217. package/dist/internal/share-data.d.ts.map +1 -0
  218. package/dist/internal/share-data.js +135 -0
  219. package/dist/internal/share-data.js.map +1 -0
  220. package/dist/internal/status-page.d.ts +68 -0
  221. package/dist/internal/status-page.d.ts.map +1 -0
  222. package/dist/internal/status-page.js +66 -0
  223. package/dist/internal/status-page.js.map +1 -0
  224. package/dist/internal/store-signature.d.ts +15 -0
  225. package/dist/internal/store-signature.d.ts.map +1 -0
  226. package/dist/internal/store-signature.js +114 -0
  227. package/dist/internal/store-signature.js.map +1 -0
  228. package/dist/internal/vite-plugin-compat.d.ts +2 -2
  229. package/dist/internal/vite-plugin-compat.js +2 -2
  230. package/dist/internal/web-app.d.ts +26 -0
  231. package/dist/internal/web-app.d.ts.map +1 -1
  232. package/dist/internal/web-app.js +79 -3
  233. package/dist/internal/web-app.js.map +1 -1
  234. package/dist/islands-swap.d.ts +41 -0
  235. package/dist/islands-swap.d.ts.map +1 -0
  236. package/dist/islands-swap.js +270 -0
  237. package/dist/islands-swap.js.map +1 -0
  238. package/dist/islands.d.ts +86 -24
  239. package/dist/islands.d.ts.map +1 -1
  240. package/dist/islands.js +138 -78
  241. package/dist/islands.js.map +1 -1
  242. package/dist/isr.d.ts +67 -13
  243. package/dist/isr.d.ts.map +1 -1
  244. package/dist/isr.js +247 -99
  245. package/dist/isr.js.map +1 -1
  246. package/dist/keep-alive.d.ts +147 -0
  247. package/dist/keep-alive.d.ts.map +1 -0
  248. package/dist/keep-alive.js +1102 -0
  249. package/dist/keep-alive.js.map +1 -0
  250. package/dist/live/bus.d.ts +70 -0
  251. package/dist/live/bus.d.ts.map +1 -0
  252. package/dist/live/bus.js +55 -0
  253. package/dist/live/bus.js.map +1 -0
  254. package/dist/live/core.d.ts +25 -0
  255. package/dist/live/core.d.ts.map +1 -0
  256. package/dist/live/core.js +29 -0
  257. package/dist/live/core.js.map +1 -0
  258. package/dist/live/history.d.ts +38 -0
  259. package/dist/live/history.d.ts.map +1 -0
  260. package/dist/live/history.js +104 -0
  261. package/dist/live/history.js.map +1 -0
  262. package/dist/live/hub-protocol.d.ts +46 -0
  263. package/dist/live/hub-protocol.d.ts.map +1 -0
  264. package/dist/live/hub-protocol.js +108 -0
  265. package/dist/live/hub-protocol.js.map +1 -0
  266. package/dist/live/hub.d.ts +185 -0
  267. package/dist/live/hub.d.ts.map +1 -0
  268. package/dist/live/hub.js +544 -0
  269. package/dist/live/hub.js.map +1 -0
  270. package/dist/live/keyring.d.ts +92 -0
  271. package/dist/live/keyring.d.ts.map +1 -0
  272. package/dist/live/keyring.js +265 -0
  273. package/dist/live/keyring.js.map +1 -0
  274. package/dist/live/read-capped.d.ts +6 -0
  275. package/dist/live/read-capped.d.ts.map +1 -0
  276. package/dist/live/read-capped.js +42 -0
  277. package/dist/live/read-capped.js.map +1 -0
  278. package/dist/live/revocations.d.ts +52 -0
  279. package/dist/live/revocations.d.ts.map +1 -0
  280. package/dist/live/revocations.js +116 -0
  281. package/dist/live/revocations.js.map +1 -0
  282. package/dist/live/settings.d.ts +14 -0
  283. package/dist/live/settings.d.ts.map +1 -0
  284. package/dist/live/settings.js +38 -0
  285. package/dist/live/settings.js.map +1 -0
  286. package/dist/live-client.d.ts +34 -0
  287. package/dist/live-client.d.ts.map +1 -0
  288. package/dist/live-client.js +878 -0
  289. package/dist/live-client.js.map +1 -0
  290. package/dist/live-hub.d.ts +130 -0
  291. package/dist/live-hub.d.ts.map +1 -0
  292. package/dist/live-hub.js +507 -0
  293. package/dist/live-hub.js.map +1 -0
  294. package/dist/live-redis.d.ts +60 -0
  295. package/dist/live-redis.d.ts.map +1 -0
  296. package/dist/live-redis.js +212 -0
  297. package/dist/live-redis.js.map +1 -0
  298. package/dist/live.d.ts +150 -0
  299. package/dist/live.d.ts.map +1 -0
  300. package/dist/live.js +972 -0
  301. package/dist/live.js.map +1 -0
  302. package/dist/manifest.d.ts +113 -2
  303. package/dist/manifest.d.ts.map +1 -1
  304. package/dist/manifest.js +109 -5
  305. package/dist/manifest.js.map +1 -1
  306. package/dist/navigation.d.ts +9 -3
  307. package/dist/navigation.d.ts.map +1 -1
  308. package/dist/navigation.js +13 -3
  309. package/dist/navigation.js.map +1 -1
  310. package/dist/plugins/vite-zone-guard.d.ts +6 -0
  311. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  312. package/dist/plugins/vite-zone-guard.js +8 -2
  313. package/dist/plugins/vite-zone-guard.js.map +1 -1
  314. package/dist/ppr.d.ts +73 -0
  315. package/dist/ppr.d.ts.map +1 -0
  316. package/dist/ppr.js +76 -0
  317. package/dist/ppr.js.map +1 -0
  318. package/dist/query.d.ts +8 -13
  319. package/dist/query.d.ts.map +1 -1
  320. package/dist/query.js +112 -18
  321. package/dist/query.js.map +1 -1
  322. package/dist/render-seam.d.ts +32 -0
  323. package/dist/render-seam.d.ts.map +1 -1
  324. package/dist/render-seam.js +7 -0
  325. package/dist/render-seam.js.map +1 -1
  326. package/dist/route-types.d.ts.map +1 -1
  327. package/dist/route-types.js +2 -2
  328. package/dist/route-types.js.map +1 -1
  329. package/dist/router.d.ts +72 -4
  330. package/dist/router.d.ts.map +1 -1
  331. package/dist/router.js +416 -129
  332. package/dist/router.js.map +1 -1
  333. package/dist/server-components/build.d.ts.map +1 -1
  334. package/dist/server-components/build.js +3 -3
  335. package/dist/server-components/build.js.map +1 -1
  336. package/dist/server-components/client.d.ts +23 -1
  337. package/dist/server-components/client.d.ts.map +1 -1
  338. package/dist/server-components/client.js +92 -5
  339. package/dist/server-components/client.js.map +1 -1
  340. package/dist/server-components/definition.d.ts.map +1 -1
  341. package/dist/server-components/definition.js +2 -1
  342. package/dist/server-components/definition.js.map +1 -1
  343. package/dist/server-components/hole.d.ts +8 -0
  344. package/dist/server-components/hole.d.ts.map +1 -0
  345. package/dist/server-components/hole.js +20 -0
  346. package/dist/server-components/hole.js.map +1 -0
  347. package/dist/server-components/html-policy.d.ts.map +1 -1
  348. package/dist/server-components/html-policy.js +62 -4
  349. package/dist/server-components/html-policy.js.map +1 -1
  350. package/dist/server-components/html-scan.d.ts +17 -0
  351. package/dist/server-components/html-scan.d.ts.map +1 -0
  352. package/dist/server-components/html-scan.js +180 -0
  353. package/dist/server-components/html-scan.js.map +1 -0
  354. package/dist/server-components/html.d.ts.map +1 -1
  355. package/dist/server-components/html.js +19 -4
  356. package/dist/server-components/html.js.map +1 -1
  357. package/dist/server-components/router-integration.d.ts +4 -1
  358. package/dist/server-components/router-integration.d.ts.map +1 -1
  359. package/dist/server-components/router-integration.js +23 -2
  360. package/dist/server-components/router-integration.js.map +1 -1
  361. package/dist/server-components/runtime.d.ts +6 -1
  362. package/dist/server-components/runtime.d.ts.map +1 -1
  363. package/dist/server-components/runtime.js +88 -29
  364. package/dist/server-components/runtime.js.map +1 -1
  365. package/dist/server-components/state.d.ts +3 -1
  366. package/dist/server-components/state.d.ts.map +1 -1
  367. package/dist/server-components/state.js +50 -38
  368. package/dist/server-components/state.js.map +1 -1
  369. package/dist/server-components/types.d.ts +9 -0
  370. package/dist/server-components/types.d.ts.map +1 -1
  371. package/dist/server-components/vite.d.ts.map +1 -1
  372. package/dist/server-components/vite.js +26 -9
  373. package/dist/server-components/vite.js.map +1 -1
  374. package/dist/vite.d.ts +5 -1
  375. package/dist/vite.d.ts.map +1 -1
  376. package/dist/vite.js +58 -4
  377. package/dist/vite.js.map +1 -1
  378. package/package.json +48 -8
  379. package/src/boundary.ts +77 -5
  380. package/src/build-plan.ts +3 -0
  381. package/src/build-vite.ts +2 -0
  382. package/src/build.ts +20 -1
  383. package/src/client.ts +183 -23
  384. package/src/csp.ts +16 -2
  385. package/src/deferred.ts +11 -5
  386. package/src/dev-feed.ts +11 -2
  387. package/src/dev-session.ts +22 -11
  388. package/src/dev.ts +29 -8
  389. package/src/diagnostic-catalog.ts +53 -0
  390. package/src/diagnostic.ts +1 -0
  391. package/src/fn.ts +24 -7
  392. package/src/fs.ts +22 -8
  393. package/src/identity.ts +61 -0
  394. package/src/index.ts +20 -4
  395. package/src/internal/cdn/cloudflare.ts +2 -1
  396. package/src/internal/certified-components.ts +1 -1
  397. package/src/internal/codegen.ts +95 -9
  398. package/src/internal/dev-websocket.ts +230 -0
  399. package/src/internal/hole-runtime.ts +286 -0
  400. package/src/internal/identity-epoch.ts +239 -0
  401. package/src/internal/invalidation-contract.ts +35 -0
  402. package/src/internal/invalidation.ts +213 -0
  403. package/src/internal/island-bindings.ts +191 -0
  404. package/src/internal/island-list.ts +92 -0
  405. package/src/internal/island-mount.ts +68 -0
  406. package/src/internal/island-owner.ts +20 -0
  407. package/src/internal/island-resource.ts +76 -0
  408. package/src/internal/island-signals.ts +249 -0
  409. package/src/internal/keep-alive-table.ts +72 -0
  410. package/src/internal/live-contract.ts +111 -0
  411. package/src/internal/live-queries.ts +60 -0
  412. package/src/internal/page-events.ts +27 -0
  413. package/src/internal/page-execution.ts +605 -45
  414. package/src/internal/parity.ts +35 -6
  415. package/src/internal/ppr/admission.ts +300 -0
  416. package/src/internal/ppr/canary.ts +122 -0
  417. package/src/internal/ppr/compile-shell.ts +379 -0
  418. package/src/internal/ppr/contract.ts +265 -0
  419. package/src/internal/ppr/frames.ts +170 -0
  420. package/src/internal/ppr/integration.ts +137 -0
  421. package/src/internal/ppr/options.ts +221 -0
  422. package/src/internal/ppr/route-rules.ts +77 -0
  423. package/src/internal/ppr/runtime.ts +39 -0
  424. package/src/internal/ppr/server.ts +1766 -0
  425. package/src/internal/ppr/shell-store.ts +152 -0
  426. package/src/internal/prerendered-pages.ts +7 -4
  427. package/src/internal/present.ts +20 -0
  428. package/src/internal/query-digest.ts +34 -0
  429. package/src/internal/query-runtime.ts +14 -1
  430. package/src/internal/query-seam.ts +48 -0
  431. package/src/internal/render-document.ts +158 -113
  432. package/src/internal/request-abort.ts +26 -0
  433. package/src/internal/server-boundary.ts +10 -1
  434. package/src/internal/share-data.ts +136 -0
  435. package/src/internal/status-page.ts +95 -0
  436. package/src/internal/store-signature.ts +149 -0
  437. package/src/internal/vite-plugin-compat.ts +2 -2
  438. package/src/internal/web-app.ts +123 -3
  439. package/src/islands-swap.ts +283 -0
  440. package/src/islands.ts +169 -72
  441. package/src/isr.ts +348 -106
  442. package/src/keep-alive.ts +1354 -0
  443. package/src/live/bus.ts +104 -0
  444. package/src/live/core.ts +52 -0
  445. package/src/live/history.ts +109 -0
  446. package/src/live/hub-protocol.ts +133 -0
  447. package/src/live/hub.ts +620 -0
  448. package/src/live/keyring.ts +384 -0
  449. package/src/live/read-capped.ts +36 -0
  450. package/src/live/revocations.ts +155 -0
  451. package/src/live/settings.ts +46 -0
  452. package/src/live-client.ts +1011 -0
  453. package/src/live-hub.ts +658 -0
  454. package/src/live-redis.ts +284 -0
  455. package/src/live.ts +1257 -0
  456. package/src/manifest.ts +274 -6
  457. package/src/navigation.ts +14 -2
  458. package/src/plugins/vite-zone-guard.ts +15 -2
  459. package/src/ppr.ts +153 -0
  460. package/src/query.ts +147 -20
  461. package/src/render-seam.ts +37 -0
  462. package/src/route-types.ts +3 -1
  463. package/src/router.ts +534 -132
  464. package/src/server-components/build.ts +9 -3
  465. package/src/server-components/client.ts +89 -5
  466. package/src/server-components/definition.ts +2 -1
  467. package/src/server-components/hole.ts +23 -0
  468. package/src/server-components/html-policy.ts +63 -4
  469. package/src/server-components/html-scan.ts +159 -0
  470. package/src/server-components/html.ts +18 -4
  471. package/src/server-components/router-integration.ts +21 -2
  472. package/src/server-components/runtime.ts +114 -31
  473. package/src/server-components/state.ts +66 -44
  474. package/src/server-components/types.ts +16 -0
  475. package/src/server-components/vite.ts +33 -14
  476. package/src/vite.ts +75 -6
  477. package/dist/nano.d.ts +0 -125
  478. package/dist/nano.d.ts.map +0 -1
  479. package/dist/nano.js +0 -190
  480. package/dist/nano.js.map +0 -1
  481. package/src/nano.ts +0 -246
package/src/manifest.ts CHANGED
@@ -12,7 +12,14 @@ import {
12
12
  paramConstraint,
13
13
  } from "@nifrajs/core/pattern"
14
14
  import type { CookieOptions, ResponseResult, StandardSchemaV1 } from "@nifrajs/core/server"
15
- import type { BoundaryDescriptor, BoundaryRegistration } from "./boundary.ts"
15
+ import type {
16
+ BoundaryDescriptor,
17
+ BoundaryRegistration,
18
+ BoundaryRequestCtx,
19
+ SharedHoleCtx,
20
+ ShellCtx,
21
+ StaticCtx,
22
+ } from "./boundary.ts"
16
23
  import { guardChannel, outputGuard } from "./internal/output-guard.ts"
17
24
  import type { RevalidateTags } from "./isr.ts"
18
25
 
@@ -60,6 +67,12 @@ export interface LoaderContext {
60
67
  readonly search: Record<string, unknown>
61
68
  /** Response headers and cookies for this page request. */
62
69
  readonly set: LoaderResponseControls
70
+ /**
71
+ * Declare what this loader's data depends on, as keys in the cache-tag charset. In a live load
72
+ * (`createWebApp({ live })`) the page reloads this data when one of them changes; elsewhere it does
73
+ * nothing. Keys count only when the loader resolves, and must be declared before it does.
74
+ */
75
+ readonly live: (...keys: string[]) => void
63
76
  }
64
77
 
65
78
  /** A route's optional data loader: params/request in, data out. */
@@ -157,10 +170,23 @@ export type ClientAction = (
157
170
  args: ClientActionArgs,
158
171
  ) => ClientActionResult | undefined | Promise<ClientActionResult | undefined>
159
172
 
173
+ /** A page's `keepAlive` export as an object. */
174
+ export interface KeepAliveOptions {
175
+ /** Reload the page's data when it is shown again (default `true`). A page marked stale while it was
176
+ * hidden reloads anyway. */
177
+ readonly revalidate?: boolean
178
+ /** Longest the page may stay hidden before it is dropped, in milliseconds (default 10 minutes). */
179
+ readonly maxAge?: number
180
+ }
181
+
160
182
  /** Client hooks populated by the generated route entry after a route chunk loads. */
161
183
  export interface ClientRouteHooks {
162
184
  readonly clientLoader?: ClientLoader
163
185
  readonly clientAction?: ClientAction
186
+ /** The page's `shareData` export: `false` gives every reload of it fresh data. */
187
+ readonly shareData?: boolean
188
+ /** The page's `keepAlive` export: the page stays mounted after a visitor leaves it. */
189
+ readonly keepAlive?: true | KeepAliveOptions
164
190
  /** Neutral boundary descriptors needed for soft-navigation interception. A boundary's loader lives in
165
191
  * the route's backend half, so the browser never learns whether one exists. */
166
192
  readonly boundaries?: ReadonlyArray<Omit<BoundaryDescriptor, "hasLoad">>
@@ -354,6 +380,19 @@ export interface RouteModule {
354
380
  readonly loaderOutput?: StandardSchemaV1
355
381
  /** Backend half only: the schema of what {@link action} sends back, held to the same rule. */
356
382
  readonly actionOutput?: StandardSchemaV1
383
+ /**
384
+ * Experimental, backend half only: the loader of a partially prerendered page's shared shell. It
385
+ * receives no request, so its output is the same for every visitor and the shell is cached.
386
+ */
387
+ readonly shellLoader?: (ctx: ShellCtx) => unknown
388
+ /** Experimental, backend half only: the schema of what {@link shellLoader} returns. */
389
+ readonly shellOutput?: StandardSchemaV1
390
+ /**
391
+ * Experimental, backend half only: query parameters that select a different shell, each with its
392
+ * closed set of values (`{ search: { color: ["red", "blue"] } }`). Any other parameter, or a value
393
+ * outside its set, builds the default shell; holes still read the whole query.
394
+ */
395
+ readonly shellVariations?: ShellVariations
357
396
  /** Optional client-only loader. It runs after hydration and on client navigations. */
358
397
  readonly clientLoader?: ClientLoader
359
398
  /** Optional client-only submit wrapper. The server action remains mandatory for mutations. */
@@ -374,6 +413,25 @@ export interface RouteModule {
374
413
  * Any other key change revalidates as usual - so omitting a data-affecting key here can never serve
375
414
  * stale data, it only forgoes the optimization. Client-only: the server always renders fresh. */
376
415
  readonly searchClientKeys?: readonly string[]
416
+ /**
417
+ * `false` gives every client reload of this page (an action's revalidation, `invalidate()`, a live
418
+ * refresh) fresh data. By default a reload keeps the identity of the parts of `data` and the layouts'
419
+ * data that it did not change, so an unchanged row keeps its DOM and component state, and a renderer
420
+ * that memoizes on identity skips it. Not every unchanged row, though: an array whose first 32
421
+ * elements all changed is taken whole, later unchanged elements included, so code that needs a row's
422
+ * identity must not count on it. Opt out for a page whose code compares `data` against its value
423
+ * from before the reload, or whose rows re-render whatever their identity, which then pay the
424
+ * comparison for nothing. Client-only.
425
+ */
426
+ readonly shareData?: boolean
427
+ /**
428
+ * Keep this page mounted after the visitor leaves it, so back and forward show it again as it was
429
+ * left: its component state, form fields, inner scroll offsets and focus. At most three pages are
430
+ * kept per tab, each for at most 10 minutes; a kept page is shown again only by a back or forward to
431
+ * its own history entry, and its data reloads in place. A sign-in or sign-out drops every kept page.
432
+ * Client-only.
433
+ */
434
+ readonly keepAlive?: true | KeepAliveOptions
377
435
  readonly meta?: MetaInput
378
436
  /**
379
437
  * Opt this route out of nifra's full-document client hydration. The server still renders the full
@@ -454,8 +512,58 @@ export interface BoundaryLoaderEntry {
454
512
  readonly load: NonNullable<BoundaryRegistration["load"]>
455
513
  /** The boundary data's output schema. Required whenever `load` returns data. */
456
514
  readonly output?: StandardSchemaV1
515
+ /**
516
+ * A `shared` boundary's freshness in seconds, required for one and refused for any other: an older
517
+ * entry is served while one rebuild runs. Write the entry inline, so the build can find it.
518
+ */
519
+ readonly revalidate?: number
520
+ /** A `shared` boundary's tags, as a route's `revalidateTags`: purging one drops its entries. */
521
+ readonly revalidateTags?: RevalidateTags
457
522
  }
458
523
 
524
+ type DeclaredBoundaryOf<Frontend> = Frontend extends {
525
+ readonly boundaries: readonly (infer Declared)[]
526
+ }
527
+ ? Declared
528
+ : never
529
+ type BoundaryNameOf<Declared> = Declared extends { readonly name: infer Name extends string }
530
+ ? Name
531
+ : never
532
+ // A hole needs an entry unless the page gives it a `load` of its own.
533
+ type HoleNameOf<Declared> = Declared extends { readonly load: (...args: never[]) => unknown }
534
+ ? never
535
+ : Declared extends { readonly mode: "dynamic" | "shared" }
536
+ ? BoundaryNameOf<Declared>
537
+ : never
538
+ type LoaderEntryOf<Declared> = Declared extends { readonly mode: "shared" }
539
+ ? Omit<BoundaryLoaderEntry, "load" | "revalidate"> & {
540
+ readonly load: (ctx: SharedHoleCtx) => unknown
541
+ readonly revalidate: number
542
+ }
543
+ : Omit<BoundaryLoaderEntry, "load" | "revalidate" | "revalidateTags"> & {
544
+ readonly load: (
545
+ ctx: Declared extends { readonly mode: "static" } ? StaticCtx : BoundaryRequestCtx,
546
+ ) => unknown
547
+ }
548
+
549
+ /**
550
+ * The `boundaryLoaders` a backend half may export, from the module types of both halves: an entry
551
+ * names a boundary the page declares, and a route with a `shellLoader` needs one for each hole.
552
+ * Unchecked when TypeScript cannot read the page (`.svelte`, `.vue`) or its boundary names are not
553
+ * literal types, which `as const` on each boundary gives.
554
+ */
555
+ export type BoundaryLoadersOf<Frontend, Backend> = unknown extends Frontend
556
+ ? Readonly<Record<string, BoundaryLoaderEntry>>
557
+ : string extends BoundaryNameOf<DeclaredBoundaryOf<Frontend>>
558
+ ? Readonly<Record<string, BoundaryLoaderEntry>>
559
+ : {
560
+ readonly [Declared in DeclaredBoundaryOf<Frontend> as BoundaryNameOf<Declared>]?: LoaderEntryOf<Declared>
561
+ } & (Backend extends { readonly shellLoader: unknown }
562
+ ? {
563
+ readonly [Declared in DeclaredBoundaryOf<Frontend> as HoleNameOf<Declared>]: LoaderEntryOf<Declared>
564
+ }
565
+ : unknown)
566
+
459
567
  /** A layout (or `_404`/`_error`) entry: its source file (for client codegen) + a lazy loader.
460
568
  *
461
569
  * A `_layout.tsx` may export `meta` (static {@link Meta} or a function of the loader data + params,
@@ -506,6 +614,9 @@ export interface RouteEntry {
506
614
  readonly loadingIds?: readonly string[]
507
615
  /** The frontend half: the file the browser bundle imports. */
508
616
  readonly file: string
617
+ /** Set when the frontend half exports `keepAlive` (found by a scan of its source): its page renders
618
+ * inside the slot wrappers a kept page needs, on the server and in the browser. */
619
+ readonly keepAlive?: true
509
620
  /** The backend half (`x.backend.ts`), when the route has one. Server-only. */
510
621
  readonly backend?: string
511
622
  /** Both halves, merged and checked. */
@@ -629,12 +740,101 @@ export const FRONTEND_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
629
740
  "HydrateFallback",
630
741
  "searchSchema",
631
742
  "searchClientKeys",
743
+ "shareData",
744
+ "keepAlive",
632
745
  "clientLoader",
633
746
  "clientAction",
634
747
  "boundaries",
635
748
  "ssr",
636
749
  ])
637
750
 
751
+ /** Query parameters that select a shell variant, each with its closed set of values. */
752
+ export interface ShellVariations {
753
+ readonly search?: Readonly<Record<string, readonly string[]>>
754
+ }
755
+
756
+ const VARIATION_NAME = /^[A-Za-z0-9_.-]{1,64}$/
757
+ const MAX_VARIATION_KEYS = 8
758
+ const MAX_VARIATION_VALUES = 32
759
+ const MAX_VARIATION_VALUE_LENGTH = 128
760
+
761
+ /** A closed, small set, so no client can mint unbounded shell keys. */
762
+ function assertShellVariations(backend: string, value: unknown): void {
763
+ const fail = (why: string): never => {
764
+ throw routeError(`"${backend}" exports shellVariations ${why}`)
765
+ }
766
+ if (typeof value !== "object" || value === null || Array.isArray(value))
767
+ fail("that is not an object")
768
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: checked just above to be a non-null object that is not an array.
769
+ const record = value as Readonly<Record<string, unknown>>
770
+ for (const key of Object.keys(record))
771
+ if (key !== "search") fail(`with "${key}"; only "search" is read`)
772
+ const search = record.search
773
+ if (search === undefined) return
774
+ if (typeof search !== "object" || search === null || Array.isArray(search))
775
+ fail("whose search is not an object of value lists")
776
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: checked just above to be a non-null object that is not an array.
777
+ const entries = Object.entries(search as Readonly<Record<string, unknown>>)
778
+ if (entries.length > MAX_VARIATION_KEYS) fail(`with more than ${MAX_VARIATION_KEYS} parameters`)
779
+ for (const [name, values] of entries) {
780
+ if (!VARIATION_NAME.test(name))
781
+ fail(`with a parameter name "${name}" outside ${VARIATION_NAME.source}`)
782
+ if (!Array.isArray(values) || values.length === 0 || values.length > MAX_VARIATION_VALUES)
783
+ fail(`whose "${name}" is not a list of 1 to ${MAX_VARIATION_VALUES} values`)
784
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: checked just above to be an array.
785
+ const list = values as readonly unknown[]
786
+ if (
787
+ new Set(list).size !== list.length ||
788
+ list.some(
789
+ (item) =>
790
+ typeof item !== "string" || item.length === 0 || item.length > MAX_VARIATION_VALUE_LENGTH,
791
+ )
792
+ )
793
+ fail(
794
+ `whose "${name}" values are not distinct strings of 1 to ${MAX_VARIATION_VALUE_LENGTH} characters`,
795
+ )
796
+ }
797
+ }
798
+
799
+ // A year: past it, an entry may as well never refresh.
800
+ const MAX_SHARED_REVALIDATE_SECONDS = 365 * 24 * 60 * 60
801
+
802
+ /** A `shared` boundary's entry carries its own freshness; any other boundary's carries none. */
803
+ function assertSharedEntry(
804
+ backend: string,
805
+ name: string,
806
+ shared: boolean,
807
+ entry: { readonly revalidate?: unknown; readonly revalidateTags?: unknown },
808
+ ): void {
809
+ const key = `boundaryLoaders["${name}"]`
810
+ if (!shared) {
811
+ if (entry.revalidate !== undefined || entry.revalidateTags !== undefined) {
812
+ throw routeError(
813
+ `${key} in "${backend}" has revalidate or revalidateTags, which only a shared boundary's entry takes`,
814
+ )
815
+ }
816
+ return
817
+ }
818
+ const { revalidate, revalidateTags } = entry
819
+ if (
820
+ typeof revalidate !== "number" ||
821
+ !Number.isInteger(revalidate) ||
822
+ revalidate < 1 ||
823
+ revalidate > MAX_SHARED_REVALIDATE_SECONDS
824
+ ) {
825
+ throw routeError(
826
+ `${key} in "${backend}" is for a shared boundary, so it needs revalidate: a whole number of seconds from 1 to ${MAX_SHARED_REVALIDATE_SECONDS}`,
827
+ )
828
+ }
829
+ if (
830
+ revalidateTags !== undefined &&
831
+ typeof revalidateTags !== "function" &&
832
+ !(Array.isArray(revalidateTags) && revalidateTags.every((tag) => typeof tag === "string"))
833
+ ) {
834
+ throw routeError(`${key}.revalidateTags in "${backend}" must be a list of tags or a function`)
835
+ }
836
+ }
837
+
638
838
  /** Exports only the server or the build reads. They live in the route's `x.backend.ts`. */
639
839
  export const BACKEND_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
640
840
  "loader",
@@ -652,6 +852,9 @@ export const BACKEND_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
652
852
  "gate",
653
853
  "shouldRevalidate",
654
854
  "allowedOrigins",
855
+ "shellLoader",
856
+ "shellOutput",
857
+ "shellVariations",
655
858
  ])
656
859
 
657
860
  const routeError = (message: string): Error => new Error(`[nifra/web] ${message}`)
@@ -719,6 +922,12 @@ export function mergeRouteHalves(
719
922
  const cached = mergedHalves.get(front)
720
923
  if (cached !== undefined && cached.back === back) return cached.module
721
924
  assertFrontendHalf(file, front as unknown as Readonly<Record<string, unknown>>)
925
+ const shared = front.boundaries?.find((boundary) => boundary.mode === "shared")
926
+ if (shared !== undefined && back?.shellLoader === undefined) {
927
+ throw routeError(
928
+ `boundary "${shared.name}" in "${file}" is shared, which only a partially prerendered page has: its backend half must export shellLoader`,
929
+ )
930
+ }
722
931
  if (back === undefined || backend === undefined) {
723
932
  mergedHalves.set(front, { back, module: front })
724
933
  return front
@@ -731,12 +940,30 @@ export function mergeRouteHalves(
731
940
  action,
732
941
  loaderOutput,
733
942
  actionOutput,
943
+ shellLoader,
944
+ shellOutput,
734
945
  ...server
735
946
  } = back
736
- const guarded: { loader?: unknown; action?: unknown } = {}
947
+ if (shellLoader !== undefined && loader !== undefined) {
948
+ throw routeError(
949
+ `"${backend}" exports both shellLoader and loader. A partially prerendered page's per-visitor data comes from its dynamic boundaries' loaders`,
950
+ )
951
+ }
952
+ if (server.shellVariations !== undefined) {
953
+ if (shellLoader === undefined)
954
+ throw routeError(`"${backend}" exports shellVariations but no shellLoader for them to vary`)
955
+ assertShellVariations(backend, server.shellVariations)
956
+ }
957
+ if (shellLoader !== undefined && server.prerender === true) {
958
+ throw routeError(
959
+ `"${backend}" exports shellLoader with prerender = true. A page with no per-visitor part needs no shellLoader`,
960
+ )
961
+ }
962
+ const guarded: { loader?: unknown; action?: unknown; shellLoader?: unknown } = {}
737
963
  for (const [name, fn, schema, schemaName] of [
738
964
  ["loader", loader, loaderOutput, "loaderOutput"],
739
965
  ["action", action, actionOutput, "actionOutput"],
966
+ ["shellLoader", shellLoader, shellOutput, "shellOutput"],
740
967
  ] as const) {
741
968
  if (fn === undefined) {
742
969
  if (schema !== undefined) {
@@ -759,11 +986,20 @@ export function mergeRouteHalves(
759
986
  throw routeError(`"${backend}" exports boundaryLoaders that is not an object of loaders`)
760
987
  }
761
988
  const loaders = boundaryLoaders as Readonly<
762
- Record<string, { readonly load?: unknown; readonly output?: unknown }>
989
+ Record<
990
+ string,
991
+ {
992
+ readonly load?: unknown
993
+ readonly output?: unknown
994
+ readonly revalidate?: unknown
995
+ readonly revalidateTags?: unknown
996
+ }
997
+ >
763
998
  >
764
- const declared = new Set((front.boundaries ?? []).map((boundary) => boundary.name))
999
+ const declared = new Map((front.boundaries ?? []).map((boundary) => [boundary.name, boundary]))
765
1000
  for (const [name, entry] of Object.entries(loaders)) {
766
- if (!declared.has(name)) {
1001
+ const boundary = declared.get(name)
1002
+ if (boundary === undefined) {
767
1003
  throw routeError(
768
1004
  `"${backend}" has boundaryLoaders["${name}"], but "${file}" declares no boundary named "${name}"`,
769
1005
  )
@@ -771,6 +1007,7 @@ export function mergeRouteHalves(
771
1007
  if (typeof entry?.load !== "function") {
772
1008
  throw routeError(`boundaryLoaders["${name}"].load in "${backend}" must be a function`)
773
1009
  }
1010
+ assertSharedEntry(backend, name, boundary.mode === "shared", entry)
774
1011
  }
775
1012
  boundaries = front.boundaries?.map((boundary) => {
776
1013
  const entry = loaders[boundary.name]
@@ -784,7 +1021,16 @@ export function mergeRouteHalves(
784
1021
  : outputGuard(entry.output, `"${backend}"`, `${key}.output`),
785
1022
  missing: `${key}.output in "${backend}"`,
786
1023
  })
787
- return { ...boundary, load } as BoundaryRegistration
1024
+ const merged =
1025
+ boundary.mode === "shared"
1026
+ ? {
1027
+ ...boundary,
1028
+ load,
1029
+ revalidate: entry.revalidate,
1030
+ revalidateTags: entry.revalidateTags,
1031
+ }
1032
+ : { ...boundary, load }
1033
+ return merged as BoundaryRegistration
788
1034
  })
789
1035
  }
790
1036
  const module = {
@@ -1024,6 +1270,25 @@ const ancestorDirs = (file: string): string[] => {
1024
1270
  return dirs
1025
1271
  }
1026
1272
 
1273
+ /** Options for {@link buildManifest}. */
1274
+ export interface BuildManifestOptions {
1275
+ /** Route files (as in `files`) whose frontend half exports `keepAlive`: see {@link exportsKeepAlive}. */
1276
+ readonly keepAlive?: readonly string[]
1277
+ }
1278
+
1279
+ // A declaration or an export list naming `keepAlive`, at the start of a line: a comment or a string
1280
+ // that mentions one does not start a line with `export`.
1281
+ const KEEP_ALIVE_EXPORT =
1282
+ /^[ \t]*export[ \t]+(?:const|let|var)[ \t]+keepAlive\b|^[ \t]*export[ \t]*\{[^}]*\bkeepAlive\b[^}]*\}/m
1283
+
1284
+ /**
1285
+ * Whether a route file's source exports `keepAlive`, read without running it: the server and the
1286
+ * client build both need the answer for every route, before any route module has loaded. A `.svelte`
1287
+ * or `.vue` file exports it from its module script the same way. An export the scan cannot see (an
1288
+ * `export *`) keeps nothing.
1289
+ */
1290
+ export const exportsKeepAlive = (source: string): boolean => KEEP_ALIVE_EXPORT.test(source)
1291
+
1027
1292
  /**
1028
1293
  * Build a manifest from route file paths (relative to the routes dir) + an `importer` that
1029
1294
  * turns a path into a lazy module loader. Pure - no fs. Throws at boot (the loud-and-early
@@ -1036,7 +1301,9 @@ const ancestorDirs = (file: string): string[] => {
1036
1301
  export function buildManifest(
1037
1302
  files: readonly string[],
1038
1303
  importer: (file: string) => () => Promise<RouteModule>,
1304
+ options: BuildManifestOptions = {},
1039
1305
  ): Manifest {
1306
+ const keepAliveFiles = new Set(options.keepAlive)
1040
1307
  const layoutDirs = new Set<string>()
1041
1308
  const layouts: Record<string, LayoutEntry> = {}
1042
1309
  const errorDirs = new Set<string>()
@@ -1202,6 +1469,7 @@ export function buildManifest(
1202
1469
  ...(notFoundIds.length > 0 ? { notFoundIds } : {}),
1203
1470
  ...(loadingIds.length > 0 ? { loadingIds } : {}),
1204
1471
  file,
1472
+ ...(keepAliveFiles.has(file) ? { keepAlive: true } : {}),
1205
1473
  ...(backend === undefined ? {} : { backend }),
1206
1474
  load,
1207
1475
  })
package/src/navigation.ts CHANGED
@@ -126,15 +126,27 @@ export type BrowserNavigate = (to: string | number, options?: NavigateOptions) =
126
126
  // teardown). Module-scoped: one app per page; absent on the server and before hydration.
127
127
  let browserNavigate: BrowserNavigate | undefined
128
128
  let browserRouter: ClientRouter | undefined
129
+ let browserCloseBoundary: ((name: string) => void) | undefined
129
130
 
130
- /** Register (or clear, with `undefined`) the browser navigate and its router - called by
131
- * `installHistory`. Not for app use. */
131
+ /** Register (or clear, with `undefined`) the browser navigate, its router and its boundary close -
132
+ * called by `installHistory`. Not for app use. */
132
133
  export function setBrowserNavigate(
133
134
  navigate: BrowserNavigate | undefined,
134
135
  router?: ClientRouter,
136
+ close?: (name: string) => void,
135
137
  ): void {
136
138
  browserNavigate = navigate
137
139
  browserRouter = router
140
+ browserCloseBoundary = close
141
+ }
142
+
143
+ /**
144
+ * Close the intercept boundary `name` and return to the page underneath it: back through the history
145
+ * entries the boundary added when the page's entry is there, otherwise by replacing the boundary's URL
146
+ * with the page's. A no-op on the server, before hydration, and when `name` holds no target.
147
+ */
148
+ export function closeBoundary(name: string): void {
149
+ browserCloseBoundary?.(name)
138
150
  }
139
151
 
140
152
  /** The mounted client router, or `undefined` on the server / before `installHistory` has run. */
@@ -33,6 +33,12 @@ export interface ViteZoneGuardOptions {
33
33
  readonly generatedFiles?: readonly string[]
34
34
  /** The public-env prefix browser code may read (default `"PUBLIC_"`). */
35
35
  readonly publicEnvPrefix?: string
36
+ /**
37
+ * The coordinated compiler runs in this build and replaces each plain browser import of a
38
+ * `*.component.server.*` file with the component's reference, so such an import may resolve. Its
39
+ * file is still refused if it is ever loaded into the browser build.
40
+ */
41
+ readonly componentReferences?: boolean
36
42
  }
37
43
 
38
44
  /** Zones whose code runs in a browser, so may read only public environment variables. */
@@ -223,10 +229,17 @@ export function viteZoneGuard(options: ViteZoneGuardOptions): ViteZoneGuardPlugi
223
229
  if (!isClient(this, resolveOptions?.ssr)) return null
224
230
  const importerFile = withoutQuery(importer)
225
231
  if (!isAbsolute(importerFile)) return null
232
+ // The compiler replaces a plain import of a component with its reference; a query form such as
233
+ // `?raw` would ship the file itself.
234
+ const referenced = options.componentReferences === true && withoutQuery(source) === source
235
+ const denial = (classification: Classification, specifier?: string) =>
236
+ referenced && classification.zone === "component"
237
+ ? undefined
238
+ : browserDenial(classification, specifier)
226
239
  if (source.startsWith(".")) {
227
240
  const target = resolve(dirname(importerFile), withoutQuery(source))
228
241
  if (isFile(target)) {
229
- const reason = browserDenial(classifier.classify(target))
242
+ const reason = denial(classifier.classify(target))
230
243
  if (reason !== undefined) refuse(this, target, reason, importerFile)
231
244
  }
232
245
  return null
@@ -240,7 +253,7 @@ export function viteZoneGuard(options: ViteZoneGuardOptions): ViteZoneGuardPlugi
240
253
  const resolved = await this.resolve(source, importer, { ...resolveOptions, skipSelf: true })
241
254
  const target = resolved === null || resolved.external ? undefined : withoutQuery(resolved.id)
242
255
  if (target !== undefined && isAbsolute(target) && isFile(target)) {
243
- const reason = browserDenial(classifier.classify(target, source), source)
256
+ const reason = denial(classifier.classify(target, source), source)
244
257
  if (reason !== undefined) refuse(this, target, reason, importerFile)
245
258
  }
246
259
  return null
package/src/ppr.ts ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * `@nifrajs/web/ppr` - experimental and unstable: partial prerendering.
3
+ *
4
+ * A route whose backend half exports `shellLoader` streams a shell shared by every visitor, then each
5
+ * `dynamic` boundary as it settles. Turn it on with `createWebApp({ experimental: { ppr:
6
+ * partialPrerendering({ shellChecks }) } })`, where `shellChecks` is the record `compileShells` returns
7
+ * for this deployment; a route left out of it renders whole. `pprShells(app)` reaches the shell cache,
8
+ * for `createInvalidator`'s `dataCaches` and for metrics. The server lives in this module, so an app
9
+ * that never imports it ships none of it. Backend-only: a browser build that reaches this module fails.
10
+ */
11
+ import "@nifrajs/web/backend-only"
12
+ import type { SharedHoleCtx, ShellCtx } from "./boundary.ts"
13
+ import type { ShellCheckRecord } from "./internal/ppr/compile-shell.ts"
14
+ import {
15
+ PPR_SERVER,
16
+ PPR_SETUP,
17
+ type PprServer,
18
+ type PprServerStats,
19
+ type PprSetup,
20
+ } from "./internal/ppr/contract.ts"
21
+ import { createPprIntegration } from "./internal/ppr/integration.ts"
22
+ import { type PprOptions, resolvePprOptions } from "./internal/ppr/options.ts"
23
+ import { isStoreSecret, MIN_STORE_SECRET_LENGTH } from "./internal/store-signature.ts"
24
+ import type { CacheStore } from "./isr.ts"
25
+
26
+ export {
27
+ type CanaryCarrier,
28
+ checkShellCanaries,
29
+ type ShellCanaryFinding,
30
+ type ShellCanaryOptions,
31
+ } from "./internal/ppr/canary.ts"
32
+ export {
33
+ type CompileShellsOptions,
34
+ compileShells,
35
+ ShellCheckError,
36
+ type ShellCheckRecord,
37
+ } from "./internal/ppr/compile-shell.ts"
38
+ export { PPR_DEFAULTS, type PprOptions } from "./internal/ppr/options.ts"
39
+ export type { PprSetup, SharedHoleCtx, ShellCtx }
40
+
41
+ export interface PprConfig {
42
+ /** From `compileShells` for this deployment; `"unchecked-development"` is refused in production. */
43
+ readonly shellChecks: ShellCheckRecord | "unchecked-development"
44
+ /** Overrides for the defaults in `PPR_DEFAULTS`. */
45
+ readonly options?: PprOptions
46
+ /**
47
+ * The deployment's public origin, such as `https://shop.example`, given to `shellLoader` and to the
48
+ * shell's `meta` as `origin`. Without it both get `""`: a shell is shared, so no visitor's Host
49
+ * header may supply it.
50
+ */
51
+ readonly origin?: string
52
+ /**
53
+ * A store the app's processes share, such as `new KVCacheStore(env.SHELLS)`, behind each process's
54
+ * own shells and shared hole frames: a process that has not built one reads it from here. Needs
55
+ * `storeSecret` and checked shells. A route whose tags the store cannot drop keeps its entries
56
+ * in-process.
57
+ */
58
+ readonly store?: CacheStore
59
+ /**
60
+ * Signs every entry written to `store`, so a store other services can write cannot hand visitors
61
+ * a page this app did not build. At least 32 characters, the same in every process.
62
+ */
63
+ readonly storeSecret?: string
64
+ /**
65
+ * The platform's name for this deployment, such as a commit or a version id. Shells are already
66
+ * kept apart by the client build and by each route's checked shell code; this adds to that.
67
+ */
68
+ readonly deployment?: string
69
+ }
70
+
71
+ /** `value` is exactly an http(s) origin: a scheme, a host and an optional port, nothing after. */
72
+ function isOrigin(value: string): boolean {
73
+ let url: URL
74
+ try {
75
+ url = new URL(value)
76
+ } catch {
77
+ return false
78
+ }
79
+ return (url.protocol === "https:" || url.protocol === "http:") && url.origin === value
80
+ }
81
+
82
+ /**
83
+ * Experimental: partial prerendering for `createWebApp({ experimental: { ppr } })`. Options are
84
+ * checked here, at startup; each app the value is given to gets its own shell cache.
85
+ */
86
+ export function partialPrerendering(config: PprConfig): PprSetup {
87
+ const options = resolvePprOptions(config.options)
88
+ const { origin, shellChecks, store, storeSecret, deployment } = config
89
+ if (origin !== undefined && !isOrigin(origin)) {
90
+ throw new TypeError(
91
+ `[nifra/web] experimental.ppr.origin must be an http(s) origin such as "https://shop.example", with no path; got ${JSON.stringify(origin)}`,
92
+ )
93
+ }
94
+ if (deployment !== undefined && (typeof deployment !== "string" || deployment === "")) {
95
+ throw new TypeError("[nifra/web] experimental.ppr.deployment must be a non-empty string")
96
+ }
97
+ if (store !== undefined) {
98
+ if (shellChecks === "unchecked-development") {
99
+ throw new TypeError(
100
+ "[nifra/web] experimental.ppr.store needs the build's shell check records: an unchecked shell is never shared",
101
+ )
102
+ }
103
+ if (!isStoreSecret(storeSecret)) {
104
+ throw new TypeError(
105
+ `[nifra/web] experimental.ppr.store needs storeSecret, at least ${MIN_STORE_SECRET_LENGTH} characters, to sign the shells it shares`,
106
+ )
107
+ }
108
+ }
109
+ return {
110
+ [PPR_SETUP]: (environment) =>
111
+ createPprIntegration(options, shellChecks, origin, environment, {
112
+ store,
113
+ storeSecret,
114
+ deployment,
115
+ }),
116
+ }
117
+ }
118
+
119
+ /** Types a backend half's `shellLoader` from its context; returns it unchanged. */
120
+ export const defineShellLoader = <Data>(
121
+ loader: (ctx: ShellCtx) => Data | Promise<Data>,
122
+ ): ((ctx: ShellCtx) => Data | Promise<Data>) => loader
123
+
124
+ /** Admission, shedding and unsent-byte counters of one app's partially prerendered responses. */
125
+ export type PprStats = PprServerStats
126
+
127
+ /** One app's shell cache. */
128
+ export interface PprShells {
129
+ /**
130
+ * Drop every shell and shared hole frame whose `revalidateTags` listed `tag`, here and in the
131
+ * shared store; a build of either running now is not kept.
132
+ */
133
+ invalidateTag(tag: string): Promise<void>
134
+ stats(): PprStats
135
+ }
136
+
137
+ const isPprServer = (value: unknown): value is PprServer =>
138
+ typeof value === "object" && value !== null && "invalidateTag" in value && "stats" in value
139
+
140
+ /**
141
+ * The shell cache of an app made with `experimental.ppr`. Pass it in `createInvalidator`'s
142
+ * `dataCaches` so a purge by tag also drops the shells built from the purged data.
143
+ */
144
+ export function pprShells(app: object): PprShells {
145
+ const server: unknown = Reflect.get(app, PPR_SERVER)
146
+ if (!isPprServer(server)) {
147
+ throw new TypeError("[nifra/web/ppr] pprShells: this app was created without experimental.ppr")
148
+ }
149
+ return {
150
+ invalidateTag: (tag) => server.invalidateTag(tag),
151
+ stats: () => server.stats(),
152
+ }
153
+ }