@nifrajs/web 3.5.0 → 4.0.2

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 (390) hide show
  1. package/dist/backend-only.d.ts +15 -0
  2. package/dist/backend-only.d.ts.map +1 -0
  3. package/dist/backend-only.js +16 -0
  4. package/dist/backend-only.js.map +1 -0
  5. package/dist/boundary.d.ts +5 -0
  6. package/dist/boundary.d.ts.map +1 -1
  7. package/dist/boundary.js +11 -2
  8. package/dist/boundary.js.map +1 -1
  9. package/dist/build-plan.d.ts +18 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +82 -34
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +4 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +68 -13
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +57 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +356 -85
  20. package/dist/build.js.map +1 -1
  21. package/dist/bun-dev-entry.d.ts +1 -1
  22. package/dist/bun-dev-entry.js +1 -1
  23. package/dist/cdn.d.ts +74 -0
  24. package/dist/cdn.d.ts.map +1 -0
  25. package/dist/cdn.js +132 -0
  26. package/dist/cdn.js.map +1 -0
  27. package/dist/client.d.ts +3 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +136 -35
  30. package/dist/client.js.map +1 -1
  31. package/dist/conformance.d.ts +4 -3
  32. package/dist/conformance.d.ts.map +1 -1
  33. package/dist/conformance.js +12 -3
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/csp.d.ts +93 -0
  36. package/dist/csp.d.ts.map +1 -0
  37. package/dist/csp.js +122 -0
  38. package/dist/csp.js.map +1 -0
  39. package/dist/dev-client.d.ts +76 -0
  40. package/dist/dev-client.d.ts.map +1 -0
  41. package/dist/dev-client.js +280 -0
  42. package/dist/dev-client.js.map +1 -0
  43. package/dist/dev-diagnostics.d.ts +26 -2
  44. package/dist/dev-diagnostics.d.ts.map +1 -1
  45. package/dist/dev-diagnostics.js +38 -7
  46. package/dist/dev-diagnostics.js.map +1 -1
  47. package/dist/dev-error.d.ts +4 -13
  48. package/dist/dev-error.d.ts.map +1 -1
  49. package/dist/dev-error.js +46 -4
  50. package/dist/dev-error.js.map +1 -1
  51. package/dist/dev-feed.d.ts +298 -0
  52. package/dist/dev-feed.d.ts.map +1 -0
  53. package/dist/dev-feed.js +797 -0
  54. package/dist/dev-feed.js.map +1 -0
  55. package/dist/dev-indicator.d.ts +38 -0
  56. package/dist/dev-indicator.d.ts.map +1 -0
  57. package/dist/dev-indicator.js +93 -0
  58. package/dist/dev-indicator.js.map +1 -0
  59. package/dist/dev-session.d.ts +77 -0
  60. package/dist/dev-session.d.ts.map +1 -0
  61. package/dist/dev-session.js +580 -0
  62. package/dist/dev-session.js.map +1 -0
  63. package/dist/dev-sourcemap.d.ts +27 -0
  64. package/dist/dev-sourcemap.d.ts.map +1 -0
  65. package/dist/dev-sourcemap.js +235 -0
  66. package/dist/dev-sourcemap.js.map +1 -0
  67. package/dist/dev-ssr-graph.d.ts.map +1 -1
  68. package/dist/dev-ssr-graph.js +13 -5
  69. package/dist/dev-ssr-graph.js.map +1 -1
  70. package/dist/dev.d.ts +15 -2
  71. package/dist/dev.d.ts.map +1 -1
  72. package/dist/dev.js +69 -44
  73. package/dist/dev.js.map +1 -1
  74. package/dist/diagnostic-catalog.d.ts +35 -0
  75. package/dist/diagnostic-catalog.d.ts.map +1 -0
  76. package/dist/diagnostic-catalog.js +203 -0
  77. package/dist/diagnostic-catalog.js.map +1 -0
  78. package/dist/diagnostic-prompt.d.ts +49 -0
  79. package/dist/diagnostic-prompt.d.ts.map +1 -0
  80. package/dist/diagnostic-prompt.js +303 -0
  81. package/dist/diagnostic-prompt.js.map +1 -0
  82. package/dist/diagnostic.d.ts +17 -23
  83. package/dist/diagnostic.d.ts.map +1 -1
  84. package/dist/diagnostic.js +31 -39
  85. package/dist/diagnostic.js.map +1 -1
  86. package/dist/draft.d.ts.map +1 -1
  87. package/dist/draft.js +4 -1
  88. package/dist/draft.js.map +1 -1
  89. package/dist/fn.d.ts +5 -0
  90. package/dist/fn.d.ts.map +1 -1
  91. package/dist/fn.js +13 -3
  92. package/dist/fn.js.map +1 -1
  93. package/dist/fs.d.ts.map +1 -1
  94. package/dist/fs.js +4 -2
  95. package/dist/fs.js.map +1 -1
  96. package/dist/index.d.ts +13 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +11 -12
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  101. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  102. package/dist/internal/cdn/cloudflare.js +94 -0
  103. package/dist/internal/cdn/cloudflare.js.map +1 -0
  104. package/dist/internal/cdn/core.d.ts +91 -0
  105. package/dist/internal/cdn/core.d.ts.map +1 -0
  106. package/dist/internal/cdn/core.js +220 -0
  107. package/dist/internal/cdn/core.js.map +1 -0
  108. package/dist/internal/cdn/fastly.d.ts +17 -0
  109. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  110. package/dist/internal/cdn/fastly.js +43 -0
  111. package/dist/internal/cdn/fastly.js.map +1 -0
  112. package/dist/internal/cdn/vercel.d.ts +31 -0
  113. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  114. package/dist/internal/cdn/vercel.js +56 -0
  115. package/dist/internal/cdn/vercel.js.map +1 -0
  116. package/dist/internal/code-unit-order.d.ts +4 -0
  117. package/dist/internal/code-unit-order.d.ts.map +1 -0
  118. package/dist/internal/code-unit-order.js +6 -0
  119. package/dist/internal/code-unit-order.js.map +1 -0
  120. package/dist/internal/codegen.d.ts.map +1 -1
  121. package/dist/internal/codegen.js +141 -25
  122. package/dist/internal/codegen.js.map +1 -1
  123. package/dist/internal/dev-csp.d.ts +24 -0
  124. package/dist/internal/dev-csp.d.ts.map +1 -0
  125. package/dist/internal/dev-csp.js +160 -0
  126. package/dist/internal/dev-csp.js.map +1 -0
  127. package/dist/internal/dev-reserved.d.ts +25 -0
  128. package/dist/internal/dev-reserved.d.ts.map +1 -0
  129. package/dist/internal/dev-reserved.js +39 -0
  130. package/dist/internal/dev-reserved.js.map +1 -0
  131. package/dist/internal/html-spans.d.ts +10 -0
  132. package/dist/internal/html-spans.d.ts.map +1 -0
  133. package/dist/internal/html-spans.js +46 -0
  134. package/dist/internal/html-spans.js.map +1 -0
  135. package/dist/internal/loading-runtime.d.ts +44 -0
  136. package/dist/internal/loading-runtime.d.ts.map +1 -0
  137. package/dist/internal/loading-runtime.js +206 -0
  138. package/dist/internal/loading-runtime.js.map +1 -0
  139. package/dist/internal/matches-runtime.d.ts +8 -0
  140. package/dist/internal/matches-runtime.d.ts.map +1 -0
  141. package/dist/internal/matches-runtime.js +61 -0
  142. package/dist/internal/matches-runtime.js.map +1 -0
  143. package/dist/internal/mount-shadow.d.ts +36 -0
  144. package/dist/internal/mount-shadow.d.ts.map +1 -0
  145. package/dist/internal/mount-shadow.js +65 -0
  146. package/dist/internal/mount-shadow.js.map +1 -0
  147. package/dist/internal/node-builtins.d.ts +3 -0
  148. package/dist/internal/node-builtins.d.ts.map +1 -0
  149. package/dist/internal/node-builtins.js +79 -0
  150. package/dist/internal/node-builtins.js.map +1 -0
  151. package/dist/internal/output-guard.d.ts +65 -0
  152. package/dist/internal/output-guard.d.ts.map +1 -0
  153. package/dist/internal/output-guard.js +630 -0
  154. package/dist/internal/output-guard.js.map +1 -0
  155. package/dist/internal/page-execution.d.ts +20 -8
  156. package/dist/internal/page-execution.d.ts.map +1 -1
  157. package/dist/internal/page-execution.js +571 -105
  158. package/dist/internal/page-execution.js.map +1 -1
  159. package/dist/internal/parity.d.ts +21 -0
  160. package/dist/internal/parity.d.ts.map +1 -1
  161. package/dist/internal/parity.js +104 -22
  162. package/dist/internal/parity.js.map +1 -1
  163. package/dist/internal/private-env.d.ts +12 -0
  164. package/dist/internal/private-env.d.ts.map +1 -0
  165. package/dist/internal/private-env.js +335 -0
  166. package/dist/internal/private-env.js.map +1 -0
  167. package/dist/internal/public-url.d.ts +3 -0
  168. package/dist/internal/public-url.d.ts.map +1 -0
  169. package/dist/internal/public-url.js +18 -0
  170. package/dist/internal/public-url.js.map +1 -0
  171. package/dist/internal/render-document.d.ts +25 -19
  172. package/dist/internal/render-document.d.ts.map +1 -1
  173. package/dist/internal/render-document.js +77 -61
  174. package/dist/internal/render-document.js.map +1 -1
  175. package/dist/internal/response-controls.d.ts +75 -0
  176. package/dist/internal/response-controls.d.ts.map +1 -0
  177. package/dist/internal/response-controls.js +256 -0
  178. package/dist/internal/response-controls.js.map +1 -0
  179. package/dist/internal/runtime-contract.d.ts +11 -16
  180. package/dist/internal/runtime-contract.d.ts.map +1 -1
  181. package/dist/internal/runtime-contract.js.map +1 -1
  182. package/dist/internal/secret-scan.d.ts +104 -0
  183. package/dist/internal/secret-scan.d.ts.map +1 -0
  184. package/dist/internal/secret-scan.js +503 -0
  185. package/dist/internal/secret-scan.js.map +1 -0
  186. package/dist/internal/server-boundary.d.ts +6 -29
  187. package/dist/internal/server-boundary.d.ts.map +1 -1
  188. package/dist/internal/server-boundary.js +6 -66
  189. package/dist/internal/server-boundary.js.map +1 -1
  190. package/dist/internal/source-map.d.ts +47 -0
  191. package/dist/internal/source-map.d.ts.map +1 -0
  192. package/dist/internal/source-map.js +225 -0
  193. package/dist/internal/source-map.js.map +1 -0
  194. package/dist/internal/target-compat.d.ts +27 -0
  195. package/dist/internal/target-compat.d.ts.map +1 -0
  196. package/dist/internal/target-compat.js +77 -0
  197. package/dist/internal/target-compat.js.map +1 -0
  198. package/dist/internal/timing-safe-equal.d.ts +6 -0
  199. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  200. package/dist/internal/timing-safe-equal.js +10 -0
  201. package/dist/internal/timing-safe-equal.js.map +1 -1
  202. package/dist/internal/vitals-runtime.d.ts +18 -0
  203. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  204. package/dist/internal/vitals-runtime.js +38 -0
  205. package/dist/internal/vitals-runtime.js.map +1 -0
  206. package/dist/internal/web-app.d.ts +38 -9
  207. package/dist/internal/web-app.d.ts.map +1 -1
  208. package/dist/internal/web-app.js +199 -59
  209. package/dist/internal/web-app.js.map +1 -1
  210. package/dist/internal/zone-graph.d.ts +89 -0
  211. package/dist/internal/zone-graph.d.ts.map +1 -0
  212. package/dist/internal/zone-graph.js +492 -0
  213. package/dist/internal/zone-graph.js.map +1 -0
  214. package/dist/isr.d.ts +83 -6
  215. package/dist/isr.d.ts.map +1 -1
  216. package/dist/isr.js +263 -36
  217. package/dist/isr.js.map +1 -1
  218. package/dist/llms-txt.d.ts.map +1 -1
  219. package/dist/llms-txt.js +31 -6
  220. package/dist/llms-txt.js.map +1 -1
  221. package/dist/manifest.d.ts +220 -13
  222. package/dist/manifest.d.ts.map +1 -1
  223. package/dist/manifest.js +394 -9
  224. package/dist/manifest.js.map +1 -1
  225. package/dist/module-graph.d.ts +26 -5
  226. package/dist/module-graph.d.ts.map +1 -1
  227. package/dist/module-graph.js +17 -7
  228. package/dist/module-graph.js.map +1 -1
  229. package/dist/navigation.d.ts +17 -3
  230. package/dist/navigation.d.ts.map +1 -1
  231. package/dist/navigation.js +11 -5
  232. package/dist/navigation.js.map +1 -1
  233. package/dist/nonce.d.ts +47 -0
  234. package/dist/nonce.d.ts.map +1 -0
  235. package/dist/nonce.js +63 -0
  236. package/dist/nonce.js.map +1 -0
  237. package/dist/plugins/css-modules.d.ts.map +1 -1
  238. package/dist/plugins/css-modules.js +4 -3
  239. package/dist/plugins/css-modules.js.map +1 -1
  240. package/dist/plugins/kit.d.ts +9 -0
  241. package/dist/plugins/kit.d.ts.map +1 -1
  242. package/dist/plugins/kit.js +18 -0
  243. package/dist/plugins/kit.js.map +1 -1
  244. package/dist/plugins/svg.d.ts +22 -3
  245. package/dist/plugins/svg.d.ts.map +1 -1
  246. package/dist/plugins/svg.js +112 -1
  247. package/dist/plugins/svg.js.map +1 -1
  248. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  249. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  250. package/dist/plugins/vite-leak-guard.js +378 -17
  251. package/dist/plugins/vite-leak-guard.js.map +1 -1
  252. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  253. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  254. package/dist/plugins/vite-zone-guard.js +208 -0
  255. package/dist/plugins/vite-zone-guard.js.map +1 -0
  256. package/dist/plugins/zone-guard.d.ts +27 -0
  257. package/dist/plugins/zone-guard.d.ts.map +1 -0
  258. package/dist/plugins/zone-guard.js +80 -0
  259. package/dist/plugins/zone-guard.js.map +1 -0
  260. package/dist/prerender.d.ts +10 -0
  261. package/dist/prerender.d.ts.map +1 -1
  262. package/dist/prerender.js +35 -3
  263. package/dist/prerender.js.map +1 -1
  264. package/dist/public-dir.d.ts +9 -0
  265. package/dist/public-dir.d.ts.map +1 -1
  266. package/dist/public-dir.js +67 -9
  267. package/dist/public-dir.js.map +1 -1
  268. package/dist/pwa-manifest.d.ts +68 -0
  269. package/dist/pwa-manifest.d.ts.map +1 -0
  270. package/dist/pwa-manifest.js +178 -0
  271. package/dist/pwa-manifest.js.map +1 -0
  272. package/dist/query.js +4 -4
  273. package/dist/query.js.map +1 -1
  274. package/dist/render-seam.d.ts +42 -2
  275. package/dist/render-seam.d.ts.map +1 -1
  276. package/dist/render-seam.js +6 -0
  277. package/dist/render-seam.js.map +1 -1
  278. package/dist/route-manifest.d.ts +3 -0
  279. package/dist/route-manifest.d.ts.map +1 -1
  280. package/dist/route-manifest.js +26 -3
  281. package/dist/route-manifest.js.map +1 -1
  282. package/dist/route-types.d.ts +24 -0
  283. package/dist/route-types.d.ts.map +1 -0
  284. package/dist/route-types.js +195 -0
  285. package/dist/route-types.js.map +1 -0
  286. package/dist/router.d.ts +28 -6
  287. package/dist/router.d.ts.map +1 -1
  288. package/dist/router.js +129 -40
  289. package/dist/router.js.map +1 -1
  290. package/dist/vitals.d.ts +33 -0
  291. package/dist/vitals.d.ts.map +1 -0
  292. package/dist/vitals.js +16 -0
  293. package/dist/vitals.js.map +1 -0
  294. package/dist/vite.d.ts +16 -1
  295. package/dist/vite.d.ts.map +1 -1
  296. package/dist/vite.js +248 -43
  297. package/dist/vite.js.map +1 -1
  298. package/dist/zones.d.ts +70 -0
  299. package/dist/zones.d.ts.map +1 -0
  300. package/dist/zones.js +365 -0
  301. package/dist/zones.js.map +1 -0
  302. package/package.json +67 -14
  303. package/src/backend-only.ts +16 -0
  304. package/src/boundary.ts +18 -5
  305. package/src/build-plan.ts +90 -35
  306. package/src/build-vite.ts +77 -11
  307. package/src/build.ts +466 -96
  308. package/src/bun-dev-entry.ts +1 -1
  309. package/src/cdn.ts +216 -0
  310. package/src/client.ts +135 -36
  311. package/src/conformance.ts +16 -3
  312. package/src/csp.ts +194 -0
  313. package/src/dev-client.ts +349 -0
  314. package/src/dev-diagnostics.ts +60 -7
  315. package/src/dev-error.ts +49 -5
  316. package/src/dev-feed.ts +1140 -0
  317. package/src/dev-indicator.ts +113 -0
  318. package/src/dev-session.ts +729 -0
  319. package/src/dev-sourcemap.ts +281 -0
  320. package/src/dev-ssr-graph.ts +16 -5
  321. package/src/dev.ts +86 -43
  322. package/src/diagnostic-catalog.ts +243 -0
  323. package/src/diagnostic-prompt.ts +355 -0
  324. package/src/diagnostic.ts +63 -66
  325. package/src/draft.ts +4 -1
  326. package/src/fn.ts +19 -3
  327. package/src/fs.ts +13 -2
  328. package/src/index.ts +42 -10
  329. package/src/internal/cdn/cloudflare.ts +146 -0
  330. package/src/internal/cdn/core.ts +316 -0
  331. package/src/internal/cdn/fastly.ts +62 -0
  332. package/src/internal/cdn/vercel.ts +95 -0
  333. package/src/internal/code-unit-order.ts +5 -0
  334. package/src/internal/codegen.ts +161 -24
  335. package/src/internal/dev-csp.ts +176 -0
  336. package/src/internal/dev-reserved.ts +52 -0
  337. package/src/internal/html-spans.ts +50 -0
  338. package/src/internal/loading-runtime.ts +244 -0
  339. package/src/internal/matches-runtime.ts +62 -0
  340. package/src/internal/mount-shadow.ts +88 -0
  341. package/src/internal/node-builtins.ts +79 -0
  342. package/src/internal/output-guard.ts +701 -0
  343. package/src/internal/page-execution.ts +795 -185
  344. package/src/internal/parity.ts +150 -36
  345. package/src/internal/private-env.ts +334 -0
  346. package/src/internal/public-url.ts +22 -0
  347. package/src/internal/render-document.ts +114 -67
  348. package/src/internal/response-controls.ts +284 -0
  349. package/src/internal/runtime-contract.ts +10 -15
  350. package/src/internal/secret-scan.ts +638 -0
  351. package/src/internal/server-boundary.ts +6 -69
  352. package/src/internal/source-map.ts +256 -0
  353. package/src/internal/target-compat.ts +92 -0
  354. package/src/internal/timing-safe-equal.ts +13 -0
  355. package/src/internal/vitals-runtime.ts +53 -0
  356. package/src/internal/web-app.ts +262 -84
  357. package/src/internal/zone-graph.ts +593 -0
  358. package/src/isr.ts +325 -37
  359. package/src/llms-txt.ts +28 -4
  360. package/src/manifest.ts +682 -16
  361. package/src/module-graph.ts +53 -9
  362. package/src/navigation.ts +29 -6
  363. package/src/nonce.ts +104 -0
  364. package/src/plugins/css-modules.ts +5 -3
  365. package/src/plugins/kit.ts +24 -0
  366. package/src/plugins/svg.ts +135 -4
  367. package/src/plugins/vite-leak-guard.ts +529 -16
  368. package/src/plugins/vite-zone-guard.ts +263 -0
  369. package/src/plugins/zone-guard.ts +119 -0
  370. package/src/prerender.ts +49 -3
  371. package/src/public-dir.ts +69 -8
  372. package/src/pwa-manifest.ts +226 -0
  373. package/src/query.ts +4 -4
  374. package/src/render-seam.ts +46 -1
  375. package/src/route-manifest.ts +15 -3
  376. package/src/route-types.ts +264 -0
  377. package/src/router.ts +163 -63
  378. package/src/vitals.ts +44 -0
  379. package/src/vite.ts +257 -42
  380. package/src/zones.ts +430 -0
  381. package/dist/plugins/vite-server-only.d.ts +0 -49
  382. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  383. package/dist/plugins/vite-server-only.js +0 -57
  384. package/dist/plugins/vite-server-only.js.map +0 -1
  385. package/dist/server-only.d.ts +0 -21
  386. package/dist/server-only.d.ts.map +0 -1
  387. package/dist/server-only.js +0 -25
  388. package/dist/server-only.js.map +0 -1
  389. package/src/plugins/vite-server-only.ts +0 -75
  390. package/src/server-only.ts +0 -25
@@ -1,16 +1,24 @@
1
1
  import {
2
+ type BackendEvidenceProvider,
2
3
  type BackendMount,
3
- type BackendMountHandler,
4
+ type BackendWebSocketMountHandler,
5
+ NIFRA_BACKEND_EVIDENCE,
4
6
  NIFRA_BACKEND_MOUNT,
7
+ NIFRA_BACKEND_WS_MOUNT,
8
+ NIFRA_BACKEND_WS_RUNTIME,
9
+ preRouteMountPaths,
5
10
  } from "@nifrajs/core/mount"
6
- import type { Platform } from "@nifrajs/core/server"
11
+ import type { MountableApp, MountOptions } from "@nifrajs/core/server"
7
12
  import { type ServerOptions, server } from "@nifrajs/core/server"
13
+ import { type CspPolicy, cspNonceResolver, DOCUMENT_POLICY, isCspPolicy } from "../csp.ts"
8
14
  import type { CssLoadingMode } from "../css-contract.ts"
15
+ import { CACHE_CHANNEL } from "../isr.ts"
9
16
  import { generateLlmsTxt } from "../llms-txt.ts"
10
17
  import type { Manifest } from "../manifest.ts"
11
18
  import type { RenderAdapter } from "../render-seam.ts"
19
+ import { PRERENDERED_LIST_PATH } from "../router.ts"
20
+ import { formatShadowedPages, shadowedPages } from "./mount-shadow.ts"
12
21
  import { createPageRequestExecutor, type NonceResolver } from "./page-execution.ts"
13
- import { urlPartsFor } from "./request-url.ts"
14
22
 
15
23
  export type { NonceResolver } from "./page-execution.ts"
16
24
  export interface CreateWebAppOptions<Env = unknown> {
@@ -28,6 +36,13 @@ export interface CreateWebAppOptions<Env = unknown> {
28
36
  * policy's allowed sources are app-specific.
29
37
  */
30
38
  readonly nonce?: NonceResolver<Env>
39
+ /**
40
+ * A hash-based Content-Security-Policy from `createCspPolicy`, instead of `nonce`. A document then
41
+ * carries a nonce only when it needs one (it `defer()`s a value, or `meta` names the nonce in
42
+ * `unsafeInlineScript`); every other page is nonce-free, gets the same CSP header on every request,
43
+ * and stays cacheable by `withISR` or a CDN. `meta` still receives a nonce to name.
44
+ */
45
+ readonly csp?: CspPolicy
31
46
  /**
32
47
  * Options for the underlying `server()` - `requestTimeoutMs`, `admission`, `gracefulSignals`, and
33
48
  * the rest of {@link ServerOptions}.
@@ -89,14 +104,26 @@ export interface CreateWebAppOptions<Env = unknown> {
89
104
  * mount interface from `@nifrajs/core/mount`; `createWebApp` also serves that backend over HTTP at
90
105
  * {@link apiPrefix} (default `/api`): a request whose pathname starts with the prefix is dispatched
91
106
  * before page routing with the same `env`/`waitUntil` platform context, and the backend's `Response`
92
- * is returned untouched. The mount runs in `nifra dev` too. Pass `apiPrefix: ""` to disable it. */
107
+ * is returned untouched. The mount runs in `nifra dev` too. Pass `apiPrefix: ""` to disable it.
108
+ *
109
+ * **Request-scoped loader calls.** While a page renders, its loaders, actions and boundaries get a
110
+ * `ctx.api` bound to that request's platform identity: a backend handler reached through it sees the
111
+ * visitor's `c.clientIp` (derived under this app's `server.clientIp` trust declaration), `c.env` and
112
+ * `c.waitUntil`, so a per-caller rate limit or audit field keys on the real visitor. `c.clientIp` is
113
+ * the supported way to read the caller - identity travels in the platform, so rebuilding a `Request`
114
+ * does not lose it. Headers do NOT travel: the page request's `cookie` and `authorization` are never
115
+ * copied, so a loader call is anonymous unless the loader passes headers on the call itself. */
93
116
  readonly api?: unknown
94
117
  /** HTTP path prefix the {@link api} backend is auto-mounted at (default `"/api"`). A request whose
95
118
  * pathname is exactly the prefix or starts with `prefix + "/"` is dispatched to the backend before
96
119
  * page routing; the backend therefore defines its routes at the **full** path (`server().post("/api/
97
120
  * sync", …)`), matching the in-process `inProcessClient` call sites. Set to `""` to disable the
98
121
  * auto-mount entirely (the app serves pages only and `api` stays a loader-only `ctx.api`). Mounting
99
- * is also a no-op when `api` does not expose the symbol mount. */
122
+ * is also a no-op when `api` does not expose the symbol mount.
123
+ *
124
+ * A page file under the prefix (`routes/api/report.tsx`) could never render - the backend answers
125
+ * first and its 404 is final - so `createWebApp` throws, naming the file. The same holds for every
126
+ * other pre-route mount. */
100
127
  readonly apiPrefix?: string
101
128
  /**
102
129
  * Strip {@link apiPrefix} from the pathname before dispatching to `api` (default `false`).
@@ -118,14 +145,16 @@ export interface CreateWebAppOptions<Env = unknown> {
118
145
  *
119
146
  * Tried longest-path-first and BEFORE the `api` mount, so a mount at `/api/auth` wins over a backend
120
147
  * at `/api` no matter which was declared first. `stripPrefix` is the per-mount form of {@link apiStrip}:
121
- * leave it off to pass the full path through.
148
+ * leave it off to pass the full path through. `opaque` states why a mount whose routes nifra cannot
149
+ * read (better-auth's handler) is outside assurance; it is then listed as a known gap.
122
150
  */
123
151
  readonly mounts?: ReadonlyArray<{
124
152
  readonly path: string
125
- readonly app: {
126
- fetch(request: Request, platform?: Platform<Env>): Response | Promise<Response>
127
- }
153
+ readonly app: MountableApp<Env>
128
154
  readonly stripPrefix?: boolean
155
+ readonly priority?: number
156
+ readonly fallbackOn?: 404
157
+ readonly opaque?: string
129
158
  }>
130
159
  /** Secret for **draft / preview mode** (see `enableDraft`). When set, a request carrying a valid
131
160
  * signed `__nifra_draft` cookie gets `ctx.draft === true` in loaders/actions (else always `false`).
@@ -152,14 +181,19 @@ export interface CreateWebAppOptions<Env = unknown> {
152
181
  */
153
182
  readonly cssLoading?: CssLoadingMode
154
183
  /** SSG: the prerendered-path set (e.g. `enumerateStaticRoutes(routes).paths` or the build's
155
- * `prerendered.json`). Injected as `window.__NIFRA_PRERENDERED__` on every page so a client soft-nav
156
- * into a prerendered route fetches its static `_data.json` instead of hitting the worker. */
184
+ * `prerendered.json`). Handed over as `window.__NIFRA_PRERENDERED__` on every page so a client soft-nav
185
+ * into a prerendered route fetches its static `_data.json` instead of hitting the worker. A set over
186
+ * 4 KB of JSON is served once from `/__nifra/prerendered.json?v=<version>`, cacheable for good, and
187
+ * pages hand over only that URL; `prerenderRoutes` writes the file into a static output. */
157
188
  readonly prerenderedPaths?: readonly string[]
158
189
  /** Publish the project's `AGENTS.md` inside `/llms.txt` and `/llms-full.txt`. **Off by default**:
159
190
  * those endpoints are public and unauthenticated, while `AGENTS.md` is a repo file written for the
160
191
  * team - unreleased feature names, internal hostnames, and "don't touch X yet" notes live there
161
192
  * routinely. Turn it on only for a repo whose guidelines you would publish as a page. */
162
193
  readonly publishLocalGuidelines?: boolean
194
+ /** Serve `/llms.txt` and `/llms-full.txt` (default `true`): the page routes, and the backend routes
195
+ * when the backend is mounted over HTTP at {@link apiPrefix}. `false` registers neither path. */
196
+ readonly llmsTxt?: boolean
163
197
  /** SSG: per dynamic route pattern, its `getStaticPaths` `fallback` (from `enumerateStaticRoutes` or
164
198
  * the build's `prerendered.json`). A route mapped to `"404"` rejects any path NOT in
165
199
  * `prerenderedPaths` with the 404 page - the unlisted path simply doesn't exist. `"ssr"` (the
@@ -184,14 +218,6 @@ export interface CreateWebAppOptions<Env = unknown> {
184
218
  ) => void
185
219
  }
186
220
 
187
- /** The handler context fields createWebApp uses - a structural subset of nifra's `Context`. */
188
- interface RouteContext<Env = unknown> {
189
- readonly params: Record<string, string>
190
- readonly req: Request
191
- /** Platform bindings (Workers env), forwarded to each route's loader/action as `args.env`. */
192
- readonly env: Env
193
- }
194
-
195
221
  /**
196
222
  * Re-issue `request` with `prefix` removed from its pathname.
197
223
  *
@@ -205,32 +231,47 @@ interface RouteContext<Env = unknown> {
205
231
  * The `Request` is rebuilt rather than mutated (`url` is read-only), preserving method, headers, body,
206
232
  * and duplex streaming - the body is passed through unread, so a large or streamed upload is untouched.
207
233
  */
208
- function stripMountPrefix(request: Request, prefix: string): Request {
209
- const url = new URL(request.url)
210
- const rest = url.pathname.slice(prefix.length)
211
- url.pathname = rest === "" ? "/" : rest
212
- // `url.href`, not the `URL`: this type set only declares the string and Request overloads.
213
- return new Request(url.href, request)
214
- }
215
-
216
234
  /**
217
235
  * Resolve the explicit symbol-keyed backend mount interface. The symbol seam forwards platform
218
236
  * context without making web depend on client.
219
237
  */
220
- function backendMountOf<Env>(api: unknown): BackendMountHandler<Env> | undefined {
238
+ function backendMountOf<Env>(api: unknown): MountableApp<Env> | undefined {
221
239
  if ((typeof api !== "object" && typeof api !== "function") || api === null) return undefined
222
240
  const explicit = (api as Partial<BackendMount<Env>>)[NIFRA_BACKEND_MOUNT]
223
241
  if (typeof explicit !== "function") return undefined
224
- return (request, platform) => explicit.call(api, request, platform)
242
+ const websocket = (api as Partial<BackendMount<Env>>)[NIFRA_BACKEND_WS_MOUNT]
243
+ const runtime = (api as Partial<BackendMount<Env>>)[NIFRA_BACKEND_WS_RUNTIME]
244
+ const evidence = (api as Partial<BackendMount<Env>>)[NIFRA_BACKEND_EVIDENCE]
245
+ const mount: {
246
+ fetch: MountableApp<Env>["fetch"]
247
+ resolveWebSocketUpgrade?: BackendWebSocketMountHandler<Env>
248
+ [NIFRA_BACKEND_WS_RUNTIME]?: NonNullable<BackendMount<Env>[typeof NIFRA_BACKEND_WS_RUNTIME]>
249
+ [NIFRA_BACKEND_EVIDENCE]?: BackendEvidenceProvider
250
+ } = {
251
+ fetch: (request, platform) => explicit.call(api, request, platform),
252
+ }
253
+ if (typeof websocket === "function") {
254
+ mount.resolveWebSocketUpgrade = (request, platform) => websocket.call(api, request, platform)
255
+ }
256
+ if (typeof runtime === "function") mount[NIFRA_BACKEND_WS_RUNTIME] = runtime
257
+ if (typeof evidence === "function") mount[NIFRA_BACKEND_EVIDENCE] = evidence
258
+ return mount
225
259
  }
226
260
 
227
- function requestPathOf(request: Request): string {
228
- try {
229
- const parts = urlPartsFor(request)
230
- return parts.pathname + parts.search
231
- } catch {
232
- return "/"
233
- }
261
+ function evidenceProviderOf(value: unknown): BackendEvidenceProvider | undefined {
262
+ if ((typeof value !== "object" && typeof value !== "function") || value === null) return undefined
263
+ const provider = (value as { [NIFRA_BACKEND_EVIDENCE]?: unknown })[NIFRA_BACKEND_EVIDENCE]
264
+ return typeof provider === "function" ? (provider as BackendEvidenceProvider) : undefined
265
+ }
266
+
267
+ function mountPathPrefix(path: string): string {
268
+ const withoutWildcard = path.endsWith("/*") ? path.slice(0, -2) : path
269
+ if (withoutWildcard === "" || withoutWildcard === "/") return ""
270
+ // Index-scan instead of `/\/+$/`: mount paths can be library input, and the
271
+ // unanchored-start trailing-run replacement can backtrack quadratically.
272
+ let end = withoutWildcard.length
273
+ while (end > 0 && withoutWildcard.charCodeAt(end - 1) === 47 /* '/' */) end--
274
+ return withoutWildcard.slice(0, end)
234
275
  }
235
276
 
236
277
  /**
@@ -251,6 +292,12 @@ export function createWebApp<Env = unknown>(
251
292
  options: CreateWebAppOptions<Env>,
252
293
  ): ReturnType<typeof server<Env>> {
253
294
  const { adapter, manifest, clientEntry, title, api } = options
295
+ if (options.csp !== undefined && options.nonce !== undefined) {
296
+ throw new TypeError("[nifra/web] createWebApp takes `csp` or `nonce`, not both")
297
+ }
298
+ if (options.csp !== undefined && !isCspPolicy(options.csp)) {
299
+ throw new TypeError("[nifra/web] createWebApp `csp` must come from createCspPolicy()")
300
+ }
254
301
  // Seed the context with the declared `Env` so `app.fetch(req, { env })` / `toFetchHandler(app)` type
255
302
  // the platform bindings (see the `createWebApp` doc). The runtime `env` still arrives per-request via
256
303
  // `app.fetch(req, { env })`; this is a compile-time-only seed (`server<Env>()` casts, doesn't store).
@@ -259,43 +306,60 @@ export function createWebApp<Env = unknown>(
259
306
  // `beforeHandle`, assurance evidence) actually bind to them - see the `use` option. A caller
260
307
  // cannot do this after the fact: by the time this function returns, the routes are declared.
261
308
  options.use?.(app)
309
+ // A resolver created by `createNonceResolver({ header })` carries a paired response hook. Register it
310
+ // after caller middleware so its request-specific CSP value is the last default applied to a page.
311
+ const nonceResponse = options.nonce?.onResponse
312
+ if (nonceResponse !== undefined) app.onResponse(nonceResponse)
313
+ // How this app's documents meet a CSP, for wrappers that cache them (`withISR` warns on "nonce").
314
+ const documentPolicy =
315
+ options.csp !== undefined ? "hash" : options.nonce !== undefined ? "nonce" : undefined
316
+ if (documentPolicy !== undefined) {
317
+ Object.defineProperty(app, DOCUMENT_POLICY, { value: documentPolicy })
318
+ }
319
+ const cacheChannel = { enabled: false }
320
+ Object.defineProperty(app, CACHE_CHANNEL, {
321
+ value: () => {
322
+ cacheChannel.enabled = true
323
+ },
324
+ })
262
325
  // Auto-mount the in-process backend over HTTP at `apiPrefix` (default `/api`), BEFORE page routing.
263
- // `onRequest` runs on the raw request ahead of the router, so this wins over the page wildcard `/*`
264
- // for every method (POST/GET/PUT/…) - not just the GET page routes - and a backend 404 (`/api/none`)
265
- // surfaces as the backend's own response, never the page 404. We dispatch the SAME `Request` object
266
- // (its body unread) so streamed/large bodies pass through untouched; the backend defines its routes
267
- // at the full prefixed path (`server().post("/api/sync", …)`), matching the `inProcessClient` call
268
- // sites. `apiPrefix: ""` opts out; a non-mountable `api` (no symbol mount) is left pages-only. The hook
269
- // is registered ONLY when both hold, so a pages-only app keeps core's synchronous no-hook fast path.
326
+ // Core's pre-route mount seam runs before the page wildcard `/*`; parent request hooks run first,
327
+ // child request hooks run inside the mount, and parent response hooks still wrap the result.
328
+ // The mount handles every HTTP method and preserves backend 404 responses instead of letting the
329
+ // page catch-all render them. We dispatch the same `Request` object (its body unread), so
330
+ // streamed/large bodies pass through untouched. The backend defines full prefixed paths
331
+ // (`server().post("/api/sync", …)`) unless `apiStrip` is enabled. `apiPrefix: ""` opts out;
332
+ // a non-mountable `api` remains loader-only and pages-only apps keep the no-mount fast path.
270
333
  const apiPrefix = options.apiPrefix ?? "/api"
271
334
  const apiStrip = options.apiStrip === true
272
335
  const mountedApi = backendMountOf<Env>(api)
273
336
  // Longest path first, so a more specific mount (`/api/auth`) is tried before a broader one (`/api`)
274
337
  // regardless of the order they were declared in.
275
- const mounts = [...(options.mounts ?? [])].sort((a, b) => b.path.length - a.path.length)
276
- const underPrefix = (pathname: string, prefix: string): boolean =>
277
- pathname === prefix || pathname.startsWith(`${prefix}/`)
278
-
279
- if (mounts.length > 0 || (apiPrefix !== "" && mountedApi !== undefined)) {
280
- app.onRequest((req, platform) => {
281
- const { pathname } = urlPartsFor(req)
282
- // Sub-app mounts win over the backend prefix: they are the more specific declaration, and an
283
- // auth handler mounted at `/api/auth` must not be swallowed by the backend mounted at `/api`.
284
- for (const mount of mounts) {
285
- if (!underPrefix(pathname, mount.path)) continue
286
- return mount.app.fetch(
287
- mount.stripPrefix === true ? stripMountPrefix(req, mount.path) : req,
288
- platform,
289
- )
290
- }
291
- // Exactly the prefix (`/api`) or a sub-path (`/api/…`) - NOT a sibling like `/apixyz` that merely
292
- // shares the prefix as a string head. Dispatch the original `req` (body intact) to the backend.
293
- if (mountedApi !== undefined && apiPrefix !== "" && underPrefix(pathname, apiPrefix)) {
294
- return mountedApi(apiStrip ? stripMountPrefix(req, apiPrefix) : req, platform)
295
- }
296
- return undefined // not an API path → continue to page routing
297
- })
338
+ const configuredMounts: readonly MountOptions<Env>[] = Object.freeze(
339
+ (options.mounts ?? []).map(
340
+ (mount): MountOptions<Env> => ({
341
+ path: mount.path,
342
+ app: mount.app,
343
+ ...(mount.stripPrefix === undefined ? {} : { stripPrefix: mount.stripPrefix }),
344
+ ...(mount.priority === undefined ? {} : { priority: mount.priority }),
345
+ ...(mount.fallbackOn === undefined ? {} : { fallbackOn: mount.fallbackOn }),
346
+ ...(mount.opaque === undefined ? {} : { opaque: mount.opaque }),
347
+ }),
348
+ ),
349
+ )
350
+ const mounts: Array<MountOptions<Env>> = [...configuredMounts]
351
+ if (mountedApi !== undefined && apiPrefix !== "") {
352
+ mounts.push({ path: apiPrefix, app: mountedApi, stripPrefix: apiStrip })
298
353
  }
354
+ for (const mount of mounts) app.mount(mount)
355
+ // Read back the server's own mount table, so a mount added in `use` counts as well: a page under any
356
+ // pre-route mount is unreachable, and serving it as a silent 404 hides that.
357
+ const shadowed = shadowedPages(manifest, preRouteMountPaths(app))
358
+ if (shadowed.length > 0) throw new Error(`[nifra/web] ${formatShadowedPages(shadowed)}`)
359
+ const ownPatterns = new Set(manifest.routes.map((route) => route.pattern))
360
+ const prerenderedList = ownPatterns.has(PRERENDERED_LIST_PATH)
361
+ ? undefined
362
+ : servedPrerenderedList(options.prerenderedPaths)
299
363
  const pageExecutor = createPageRequestExecutor<Env>({
300
364
  adapter,
301
365
  manifest,
@@ -310,38 +374,152 @@ export function createWebApp<Env = unknown>(
310
374
  ...(options.prerenderedPaths === undefined
311
375
  ? {}
312
376
  : { prerenderedPaths: options.prerenderedPaths }),
377
+ ...(prerenderedList === undefined ? {} : { prerenderedListUrl: prerenderedList.url }),
313
378
  ...(options.staticFallbacks === undefined ? {} : { staticFallbacks: options.staticFallbacks }),
314
379
  ...(options.staticBoundaryCache === undefined
315
380
  ? {}
316
381
  : { staticBoundaryCache: options.staticBoundaryCache }),
317
382
  ...(options.nonce === undefined ? {} : { nonce: options.nonce }),
383
+ ...(options.csp === undefined
384
+ ? {}
385
+ : { nonce: cspNonceResolver(options.csp) as NonceResolver<Env>, csp: options.csp }),
318
386
  ...(options.onLoaderError === undefined ? {} : { onLoaderError: options.onLoaderError }),
387
+ cacheChannel,
319
388
  })
320
389
 
321
390
  for (const route of manifest.routes) {
322
391
  app.register("GET", route.pattern, undefined, pageExecutor.get(route))
323
392
  app.register("POST", route.pattern, undefined, pageExecutor.post(route))
324
393
  }
325
- // Register llms.txt & llms-full.txt
326
- const llmsOptions = { includeLocalGuidelines: options.publishLocalGuidelines === true }
327
- app.register("GET", "/llms.txt", undefined, async () => {
328
- const text = await generateLlmsTxt(false, manifest.routes, api, llmsOptions)
329
- return new Response(text, {
330
- headers: { "content-type": "text/plain; charset=utf-8" },
331
- })
332
- })
394
+ // llms.txt & llms-full.txt describe what a visitor can reach: the pages, and the backend only when it
395
+ // is served over HTTP - a backend the loaders call in-process has no public surface to describe. An
396
+ // app route at either path takes precedence, and each text is built once per app.
397
+ if (options.llmsTxt !== false) {
398
+ const llmsOptions = { includeLocalGuidelines: options.publishLocalGuidelines === true }
399
+ const publicApi = mountedApi !== undefined && apiPrefix !== "" ? api : undefined
400
+ for (const [path, full] of [
401
+ ["/llms.txt", false],
402
+ ["/llms-full.txt", true],
403
+ ] as const) {
404
+ if (ownPatterns.has(path)) continue
405
+ let text: Promise<string> | undefined
406
+ app.register("GET", path, undefined, async () => {
407
+ text ??= generateLlmsTxt(full, manifest.routes, publicApi, llmsOptions).catch((error) => {
408
+ text = undefined
409
+ throw error
410
+ })
411
+ return new Response(await text, {
412
+ headers: { "content-type": "text/plain; charset=utf-8" },
413
+ })
414
+ })
415
+ }
416
+ }
333
417
 
334
- app.register("GET", "/llms-full.txt", undefined, async () => {
335
- const text = await generateLlmsTxt(true, manifest.routes, api, llmsOptions)
336
- return new Response(text, {
337
- headers: { "content-type": "text/plain; charset=utf-8" },
338
- })
339
- })
418
+ if (prerenderedList !== undefined) {
419
+ const { body, version } = prerenderedList
420
+ app.register(
421
+ "GET",
422
+ PRERENDERED_LIST_PATH,
423
+ undefined,
424
+ (c: { readonly req: Request }) =>
425
+ new Response(body, {
426
+ headers: {
427
+ "content-type": "application/json",
428
+ // Only the versioned URL pages hand over is immutable; a bare request gets today's list.
429
+ "cache-control":
430
+ new URL(c.req.url).searchParams.get("v") === version
431
+ ? "public, max-age=31536000, immutable"
432
+ : "no-cache",
433
+ },
434
+ }),
435
+ )
436
+ }
340
437
 
341
- // Wildcard catch-all: unmatched paths render `_404` (404), or a plain text 404 if absent.
342
- app.register("GET", "/*", undefined, (c: RouteContext<Env>) =>
343
- pageExecutor.notFound(c.req, c.env, requestPathOf(c.req)),
344
- )
438
+ // Wildcard catch-all: unmatched paths render the nearest `_404` (404), or a plain text 404 if
439
+ // absent.
440
+ app.register("GET", "/*", undefined, pageExecutor.fallback)
441
+
442
+ let evidenceComposing = false
443
+ const evidenceProvider: BackendEvidenceProvider = async () => {
444
+ if (evidenceComposing) {
445
+ throw new Error("[nifra/web] cyclic composed assurance evidence mount")
446
+ }
447
+ evidenceComposing = true
448
+ try {
449
+ const { composeProjectEvidence, snapshotProjectEvidence } = await import(
450
+ "@nifrajs/core/evidence"
451
+ )
452
+ const parts: Array<{
453
+ readonly evidence: import("@nifrajs/core/evidence").ProjectEvidenceSnapshot
454
+ readonly pathPrefix?: string
455
+ }> = [{ evidence: snapshotProjectEvidence(app) }]
456
+ for (const mount of configuredMounts) {
457
+ const provider = evidenceProviderOf(mount.app)
458
+ if (provider === undefined) {
459
+ // A declared opaque mount reaches the snapshot as a known gap, through this app's own
460
+ // mount reflection above.
461
+ if (mount.opaque !== undefined) continue
462
+ throw new Error(
463
+ `[nifra/web] cannot compose assurance evidence: mount "${mount.path}" has no token-only evidence provider - declare why it is not analyzed with { opaque: "<reason>" }`,
464
+ )
465
+ }
466
+ parts.push({
467
+ evidence: await provider.call(mount.app),
468
+ ...(mount.stripPrefix === true ? { pathPrefix: mountPathPrefix(mount.path) } : {}),
469
+ })
470
+ }
471
+ if (mountedApi !== undefined && apiPrefix !== "") {
472
+ const provider = evidenceProviderOf(api)
473
+ if (provider === undefined) {
474
+ throw new Error(
475
+ `[nifra/web] cannot compose assurance evidence: auto-mounted API at "${apiPrefix}" has no token-only evidence provider`,
476
+ )
477
+ }
478
+ parts.push({
479
+ evidence: await provider.call(api),
480
+ ...(apiStrip ? { pathPrefix: mountPathPrefix(apiPrefix) } : {}),
481
+ })
482
+ }
483
+ return composeProjectEvidence(parts)
484
+ } finally {
485
+ evidenceComposing = false
486
+ }
487
+ }
488
+ Object.defineProperty(app, NIFRA_BACKEND_EVIDENCE, {
489
+ configurable: false,
490
+ enumerable: false,
491
+ value: evidenceProvider,
492
+ writable: false,
493
+ })
345
494
 
346
495
  return app
347
496
  }
497
+
498
+ /** Resolve one composed, token-only evidence snapshot for a web app and its mounted surfaces. */
499
+ export async function webProjectEvidence(
500
+ source: unknown,
501
+ ): Promise<import("@nifrajs/core/evidence").ProjectEvidenceSnapshot> {
502
+ const provider = evidenceProviderOf(source)
503
+ if (provider === undefined) {
504
+ throw new TypeError("webProjectEvidence(): expected an app created by createWebApp")
505
+ }
506
+ return provider.call(source)
507
+ }
508
+
509
+ // Up to this many characters of JSON, the prerendered set rides inline in each page's handover.
510
+ const INLINE_PRERENDERED_CHARS = 4096
511
+
512
+ /** The prerendered set as a served, content-versioned list, when it is too large to inline. */
513
+ function servedPrerenderedList(
514
+ paths: readonly string[] | undefined,
515
+ ): { readonly body: string; readonly version: string; readonly url: string } | undefined {
516
+ if (paths === undefined || paths.length === 0) return undefined
517
+ const body = JSON.stringify(paths)
518
+ if (body.length <= INLINE_PRERENDERED_CHARS) return undefined
519
+ // FNV-1a: a version that changes with the list is all the URL needs; a stale cached list only
520
+ // costs a fallback to the dynamic data request.
521
+ let hash = 0x811c9dc5
522
+ for (let i = 0; i < body.length; i++) hash = Math.imul(hash ^ body.charCodeAt(i), 0x01000193)
523
+ const version = (hash >>> 0).toString(16).padStart(8, "0")
524
+ return { body, version, url: `${PRERENDERED_LIST_PATH}?v=${version}` }
525
+ }