@nifrajs/web 3.3.0 → 4.0.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 (407) 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 +35 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +89 -35
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +18 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +97 -17
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +72 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +411 -98
  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 +21 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +226 -15
  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/css-contract.d.ts +21 -0
  40. package/dist/css-contract.d.ts.map +1 -0
  41. package/dist/css-contract.js +28 -0
  42. package/dist/css-contract.js.map +1 -0
  43. package/dist/dev-client.d.ts +76 -0
  44. package/dist/dev-client.d.ts.map +1 -0
  45. package/dist/dev-client.js +280 -0
  46. package/dist/dev-client.js.map +1 -0
  47. package/dist/dev-diagnostics.d.ts +26 -2
  48. package/dist/dev-diagnostics.d.ts.map +1 -1
  49. package/dist/dev-diagnostics.js +38 -7
  50. package/dist/dev-diagnostics.js.map +1 -1
  51. package/dist/dev-error.d.ts +4 -13
  52. package/dist/dev-error.d.ts.map +1 -1
  53. package/dist/dev-error.js +46 -4
  54. package/dist/dev-error.js.map +1 -1
  55. package/dist/dev-feed.d.ts +298 -0
  56. package/dist/dev-feed.d.ts.map +1 -0
  57. package/dist/dev-feed.js +797 -0
  58. package/dist/dev-feed.js.map +1 -0
  59. package/dist/dev-indicator.d.ts +38 -0
  60. package/dist/dev-indicator.d.ts.map +1 -0
  61. package/dist/dev-indicator.js +93 -0
  62. package/dist/dev-indicator.js.map +1 -0
  63. package/dist/dev-session.d.ts +77 -0
  64. package/dist/dev-session.d.ts.map +1 -0
  65. package/dist/dev-session.js +580 -0
  66. package/dist/dev-session.js.map +1 -0
  67. package/dist/dev-sourcemap.d.ts +27 -0
  68. package/dist/dev-sourcemap.d.ts.map +1 -0
  69. package/dist/dev-sourcemap.js +235 -0
  70. package/dist/dev-sourcemap.js.map +1 -0
  71. package/dist/dev-ssr-graph.d.ts.map +1 -1
  72. package/dist/dev-ssr-graph.js +13 -5
  73. package/dist/dev-ssr-graph.js.map +1 -1
  74. package/dist/dev.d.ts +15 -2
  75. package/dist/dev.d.ts.map +1 -1
  76. package/dist/dev.js +69 -44
  77. package/dist/dev.js.map +1 -1
  78. package/dist/diagnostic-catalog.d.ts +35 -0
  79. package/dist/diagnostic-catalog.d.ts.map +1 -0
  80. package/dist/diagnostic-catalog.js +203 -0
  81. package/dist/diagnostic-catalog.js.map +1 -0
  82. package/dist/diagnostic-prompt.d.ts +49 -0
  83. package/dist/diagnostic-prompt.d.ts.map +1 -0
  84. package/dist/diagnostic-prompt.js +303 -0
  85. package/dist/diagnostic-prompt.js.map +1 -0
  86. package/dist/diagnostic.d.ts +17 -23
  87. package/dist/diagnostic.d.ts.map +1 -1
  88. package/dist/diagnostic.js +31 -39
  89. package/dist/diagnostic.js.map +1 -1
  90. package/dist/draft.d.ts.map +1 -1
  91. package/dist/draft.js +4 -1
  92. package/dist/draft.js.map +1 -1
  93. package/dist/fn.d.ts +5 -0
  94. package/dist/fn.d.ts.map +1 -1
  95. package/dist/fn.js +13 -3
  96. package/dist/fn.js.map +1 -1
  97. package/dist/fs.d.ts.map +1 -1
  98. package/dist/fs.js +4 -2
  99. package/dist/fs.js.map +1 -1
  100. package/dist/index.d.ts +15 -15
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +13 -12
  103. package/dist/index.js.map +1 -1
  104. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  105. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  106. package/dist/internal/cdn/cloudflare.js +94 -0
  107. package/dist/internal/cdn/cloudflare.js.map +1 -0
  108. package/dist/internal/cdn/core.d.ts +91 -0
  109. package/dist/internal/cdn/core.d.ts.map +1 -0
  110. package/dist/internal/cdn/core.js +220 -0
  111. package/dist/internal/cdn/core.js.map +1 -0
  112. package/dist/internal/cdn/fastly.d.ts +17 -0
  113. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  114. package/dist/internal/cdn/fastly.js +43 -0
  115. package/dist/internal/cdn/fastly.js.map +1 -0
  116. package/dist/internal/cdn/vercel.d.ts +31 -0
  117. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  118. package/dist/internal/cdn/vercel.js +56 -0
  119. package/dist/internal/cdn/vercel.js.map +1 -0
  120. package/dist/internal/code-unit-order.d.ts +4 -0
  121. package/dist/internal/code-unit-order.d.ts.map +1 -0
  122. package/dist/internal/code-unit-order.js +6 -0
  123. package/dist/internal/code-unit-order.js.map +1 -0
  124. package/dist/internal/codegen.d.ts +3 -0
  125. package/dist/internal/codegen.d.ts.map +1 -1
  126. package/dist/internal/codegen.js +151 -30
  127. package/dist/internal/codegen.js.map +1 -1
  128. package/dist/internal/dev-csp.d.ts +24 -0
  129. package/dist/internal/dev-csp.d.ts.map +1 -0
  130. package/dist/internal/dev-csp.js +160 -0
  131. package/dist/internal/dev-csp.js.map +1 -0
  132. package/dist/internal/dev-reserved.d.ts +25 -0
  133. package/dist/internal/dev-reserved.d.ts.map +1 -0
  134. package/dist/internal/dev-reserved.js +39 -0
  135. package/dist/internal/dev-reserved.js.map +1 -0
  136. package/dist/internal/html-spans.d.ts +10 -0
  137. package/dist/internal/html-spans.d.ts.map +1 -0
  138. package/dist/internal/html-spans.js +46 -0
  139. package/dist/internal/html-spans.js.map +1 -0
  140. package/dist/internal/loading-runtime.d.ts +44 -0
  141. package/dist/internal/loading-runtime.d.ts.map +1 -0
  142. package/dist/internal/loading-runtime.js +206 -0
  143. package/dist/internal/loading-runtime.js.map +1 -0
  144. package/dist/internal/matches-runtime.d.ts +8 -0
  145. package/dist/internal/matches-runtime.d.ts.map +1 -0
  146. package/dist/internal/matches-runtime.js +61 -0
  147. package/dist/internal/matches-runtime.js.map +1 -0
  148. package/dist/internal/mount-shadow.d.ts +36 -0
  149. package/dist/internal/mount-shadow.d.ts.map +1 -0
  150. package/dist/internal/mount-shadow.js +65 -0
  151. package/dist/internal/mount-shadow.js.map +1 -0
  152. package/dist/internal/node-builtins.d.ts +3 -0
  153. package/dist/internal/node-builtins.d.ts.map +1 -0
  154. package/dist/internal/node-builtins.js +79 -0
  155. package/dist/internal/node-builtins.js.map +1 -0
  156. package/dist/internal/output-guard.d.ts +65 -0
  157. package/dist/internal/output-guard.d.ts.map +1 -0
  158. package/dist/internal/output-guard.js +630 -0
  159. package/dist/internal/output-guard.js.map +1 -0
  160. package/dist/internal/page-execution.d.ts +69 -0
  161. package/dist/internal/page-execution.d.ts.map +1 -0
  162. package/dist/internal/page-execution.js +1021 -0
  163. package/dist/internal/page-execution.js.map +1 -0
  164. package/dist/internal/parity.d.ts +21 -0
  165. package/dist/internal/parity.d.ts.map +1 -1
  166. package/dist/internal/parity.js +104 -22
  167. package/dist/internal/parity.js.map +1 -1
  168. package/dist/internal/private-env.d.ts +12 -0
  169. package/dist/internal/private-env.d.ts.map +1 -0
  170. package/dist/internal/private-env.js +335 -0
  171. package/dist/internal/private-env.js.map +1 -0
  172. package/dist/internal/public-url.d.ts +3 -0
  173. package/dist/internal/public-url.d.ts.map +1 -0
  174. package/dist/internal/public-url.js +18 -0
  175. package/dist/internal/public-url.js.map +1 -0
  176. package/dist/internal/render-document.d.ts +37 -21
  177. package/dist/internal/render-document.d.ts.map +1 -1
  178. package/dist/internal/render-document.js +104 -65
  179. package/dist/internal/render-document.js.map +1 -1
  180. package/dist/internal/request-url.d.ts +3 -0
  181. package/dist/internal/request-url.d.ts.map +1 -0
  182. package/dist/internal/request-url.js +14 -0
  183. package/dist/internal/request-url.js.map +1 -0
  184. package/dist/internal/response-controls.d.ts +75 -0
  185. package/dist/internal/response-controls.d.ts.map +1 -0
  186. package/dist/internal/response-controls.js +256 -0
  187. package/dist/internal/response-controls.js.map +1 -0
  188. package/dist/internal/runtime-contract.d.ts +11 -16
  189. package/dist/internal/runtime-contract.d.ts.map +1 -1
  190. package/dist/internal/runtime-contract.js.map +1 -1
  191. package/dist/internal/secret-scan.d.ts +104 -0
  192. package/dist/internal/secret-scan.d.ts.map +1 -0
  193. package/dist/internal/secret-scan.js +485 -0
  194. package/dist/internal/secret-scan.js.map +1 -0
  195. package/dist/internal/server-boundary.d.ts +6 -29
  196. package/dist/internal/server-boundary.d.ts.map +1 -1
  197. package/dist/internal/server-boundary.js +6 -66
  198. package/dist/internal/server-boundary.js.map +1 -1
  199. package/dist/internal/source-map.d.ts +47 -0
  200. package/dist/internal/source-map.d.ts.map +1 -0
  201. package/dist/internal/source-map.js +225 -0
  202. package/dist/internal/source-map.js.map +1 -0
  203. package/dist/internal/target-compat.d.ts +27 -0
  204. package/dist/internal/target-compat.d.ts.map +1 -0
  205. package/dist/internal/target-compat.js +77 -0
  206. package/dist/internal/target-compat.js.map +1 -0
  207. package/dist/internal/timing-safe-equal.d.ts +6 -0
  208. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  209. package/dist/internal/timing-safe-equal.js +10 -0
  210. package/dist/internal/timing-safe-equal.js.map +1 -1
  211. package/dist/internal/vitals-runtime.d.ts +18 -0
  212. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  213. package/dist/internal/vitals-runtime.js +38 -0
  214. package/dist/internal/vitals-runtime.js.map +1 -0
  215. package/dist/internal/web-app.d.ts +56 -8
  216. package/dist/internal/web-app.d.ts.map +1 -1
  217. package/dist/internal/web-app.js +215 -772
  218. package/dist/internal/web-app.js.map +1 -1
  219. package/dist/internal/zone-graph.d.ts +89 -0
  220. package/dist/internal/zone-graph.d.ts.map +1 -0
  221. package/dist/internal/zone-graph.js +492 -0
  222. package/dist/internal/zone-graph.js.map +1 -0
  223. package/dist/isr.d.ts +83 -6
  224. package/dist/isr.d.ts.map +1 -1
  225. package/dist/isr.js +301 -39
  226. package/dist/isr.js.map +1 -1
  227. package/dist/llms-txt.d.ts +3 -0
  228. package/dist/llms-txt.d.ts.map +1 -1
  229. package/dist/llms-txt.js +37 -9
  230. package/dist/llms-txt.js.map +1 -1
  231. package/dist/manifest.d.ts +221 -14
  232. package/dist/manifest.d.ts.map +1 -1
  233. package/dist/manifest.js +386 -9
  234. package/dist/manifest.js.map +1 -1
  235. package/dist/module-graph.d.ts +26 -5
  236. package/dist/module-graph.d.ts.map +1 -1
  237. package/dist/module-graph.js +17 -7
  238. package/dist/module-graph.js.map +1 -1
  239. package/dist/navigation.d.ts +17 -3
  240. package/dist/navigation.d.ts.map +1 -1
  241. package/dist/navigation.js +11 -5
  242. package/dist/navigation.js.map +1 -1
  243. package/dist/nonce.d.ts +47 -0
  244. package/dist/nonce.d.ts.map +1 -0
  245. package/dist/nonce.js +63 -0
  246. package/dist/nonce.js.map +1 -0
  247. package/dist/plugins/css-modules.d.ts.map +1 -1
  248. package/dist/plugins/css-modules.js +4 -3
  249. package/dist/plugins/css-modules.js.map +1 -1
  250. package/dist/plugins/kit.d.ts +9 -0
  251. package/dist/plugins/kit.d.ts.map +1 -1
  252. package/dist/plugins/kit.js +18 -0
  253. package/dist/plugins/kit.js.map +1 -1
  254. package/dist/plugins/svg.d.ts +22 -3
  255. package/dist/plugins/svg.d.ts.map +1 -1
  256. package/dist/plugins/svg.js +112 -1
  257. package/dist/plugins/svg.js.map +1 -1
  258. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  259. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  260. package/dist/plugins/vite-leak-guard.js +378 -17
  261. package/dist/plugins/vite-leak-guard.js.map +1 -1
  262. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  263. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  264. package/dist/plugins/vite-zone-guard.js +208 -0
  265. package/dist/plugins/vite-zone-guard.js.map +1 -0
  266. package/dist/plugins/zone-guard.d.ts +27 -0
  267. package/dist/plugins/zone-guard.d.ts.map +1 -0
  268. package/dist/plugins/zone-guard.js +72 -0
  269. package/dist/plugins/zone-guard.js.map +1 -0
  270. package/dist/prerender.d.ts +10 -0
  271. package/dist/prerender.d.ts.map +1 -1
  272. package/dist/prerender.js +35 -3
  273. package/dist/prerender.js.map +1 -1
  274. package/dist/public-dir.d.ts +9 -0
  275. package/dist/public-dir.d.ts.map +1 -1
  276. package/dist/public-dir.js +131 -48
  277. package/dist/public-dir.js.map +1 -1
  278. package/dist/pwa-manifest.d.ts +68 -0
  279. package/dist/pwa-manifest.d.ts.map +1 -0
  280. package/dist/pwa-manifest.js +178 -0
  281. package/dist/pwa-manifest.js.map +1 -0
  282. package/dist/query.js +4 -4
  283. package/dist/query.js.map +1 -1
  284. package/dist/render-seam.d.ts +42 -2
  285. package/dist/render-seam.d.ts.map +1 -1
  286. package/dist/render-seam.js +6 -0
  287. package/dist/render-seam.js.map +1 -1
  288. package/dist/route-manifest.d.ts +3 -0
  289. package/dist/route-manifest.d.ts.map +1 -1
  290. package/dist/route-manifest.js +26 -3
  291. package/dist/route-manifest.js.map +1 -1
  292. package/dist/route-types.d.ts +24 -0
  293. package/dist/route-types.d.ts.map +1 -0
  294. package/dist/route-types.js +195 -0
  295. package/dist/route-types.js.map +1 -0
  296. package/dist/router.d.ts +28 -6
  297. package/dist/router.d.ts.map +1 -1
  298. package/dist/router.js +129 -40
  299. package/dist/router.js.map +1 -1
  300. package/dist/trusted-html.d.ts +34 -0
  301. package/dist/trusted-html.d.ts.map +1 -0
  302. package/dist/trusted-html.js +35 -0
  303. package/dist/trusted-html.js.map +1 -0
  304. package/dist/vitals.d.ts +33 -0
  305. package/dist/vitals.d.ts.map +1 -0
  306. package/dist/vitals.js +16 -0
  307. package/dist/vitals.js.map +1 -0
  308. package/dist/vite.d.ts +16 -1
  309. package/dist/vite.d.ts.map +1 -1
  310. package/dist/vite.js +187 -36
  311. package/dist/vite.js.map +1 -1
  312. package/dist/zones.d.ts +70 -0
  313. package/dist/zones.d.ts.map +1 -0
  314. package/dist/zones.js +365 -0
  315. package/dist/zones.js.map +1 -0
  316. package/package.json +67 -14
  317. package/src/backend-only.ts +16 -0
  318. package/src/boundary.ts +18 -5
  319. package/src/build-plan.ts +113 -36
  320. package/src/build-vite.ts +124 -15
  321. package/src/build.ts +535 -102
  322. package/src/bun-dev-entry.ts +1 -1
  323. package/src/cdn.ts +216 -0
  324. package/src/client.ts +251 -18
  325. package/src/conformance.ts +16 -3
  326. package/src/csp.ts +194 -0
  327. package/src/css-contract.ts +44 -0
  328. package/src/dev-client.ts +349 -0
  329. package/src/dev-diagnostics.ts +60 -7
  330. package/src/dev-error.ts +49 -5
  331. package/src/dev-feed.ts +1140 -0
  332. package/src/dev-indicator.ts +113 -0
  333. package/src/dev-session.ts +729 -0
  334. package/src/dev-sourcemap.ts +281 -0
  335. package/src/dev-ssr-graph.ts +16 -5
  336. package/src/dev.ts +86 -43
  337. package/src/diagnostic-catalog.ts +243 -0
  338. package/src/diagnostic-prompt.ts +355 -0
  339. package/src/diagnostic.ts +63 -66
  340. package/src/draft.ts +4 -1
  341. package/src/fn.ts +19 -3
  342. package/src/fs.ts +13 -2
  343. package/src/index.ts +57 -10
  344. package/src/internal/cdn/cloudflare.ts +146 -0
  345. package/src/internal/cdn/core.ts +316 -0
  346. package/src/internal/cdn/fastly.ts +62 -0
  347. package/src/internal/cdn/vercel.ts +95 -0
  348. package/src/internal/code-unit-order.ts +5 -0
  349. package/src/internal/codegen.ts +180 -29
  350. package/src/internal/dev-csp.ts +176 -0
  351. package/src/internal/dev-reserved.ts +52 -0
  352. package/src/internal/html-spans.ts +50 -0
  353. package/src/internal/loading-runtime.ts +244 -0
  354. package/src/internal/matches-runtime.ts +62 -0
  355. package/src/internal/mount-shadow.ts +88 -0
  356. package/src/internal/node-builtins.ts +79 -0
  357. package/src/internal/output-guard.ts +701 -0
  358. package/src/internal/page-execution.ts +1371 -0
  359. package/src/internal/parity.ts +150 -36
  360. package/src/internal/private-env.ts +334 -0
  361. package/src/internal/public-url.ts +22 -0
  362. package/src/internal/render-document.ts +153 -73
  363. package/src/internal/request-url.ts +14 -0
  364. package/src/internal/response-controls.ts +284 -0
  365. package/src/internal/runtime-contract.ts +10 -15
  366. package/src/internal/secret-scan.ts +623 -0
  367. package/src/internal/server-boundary.ts +6 -69
  368. package/src/internal/source-map.ts +256 -0
  369. package/src/internal/target-compat.ts +92 -0
  370. package/src/internal/timing-safe-equal.ts +13 -0
  371. package/src/internal/vitals-runtime.ts +53 -0
  372. package/src/internal/web-app.ts +290 -879
  373. package/src/internal/zone-graph.ts +593 -0
  374. package/src/isr.ts +366 -39
  375. package/src/llms-txt.ts +44 -8
  376. package/src/manifest.ts +671 -17
  377. package/src/module-graph.ts +53 -9
  378. package/src/navigation.ts +29 -6
  379. package/src/nonce.ts +104 -0
  380. package/src/plugins/css-modules.ts +5 -3
  381. package/src/plugins/kit.ts +24 -0
  382. package/src/plugins/svg.ts +135 -4
  383. package/src/plugins/vite-leak-guard.ts +529 -16
  384. package/src/plugins/vite-zone-guard.ts +263 -0
  385. package/src/plugins/zone-guard.ts +110 -0
  386. package/src/prerender.ts +49 -3
  387. package/src/public-dir.ts +135 -49
  388. package/src/pwa-manifest.ts +226 -0
  389. package/src/query.ts +4 -4
  390. package/src/render-seam.ts +46 -1
  391. package/src/route-manifest.ts +15 -3
  392. package/src/route-types.ts +264 -0
  393. package/src/router.ts +163 -63
  394. package/src/trusted-html.ts +44 -0
  395. package/src/vitals.ts +44 -0
  396. package/src/vite.ts +207 -36
  397. package/src/zones.ts +430 -0
  398. package/dist/plugins/vite-server-only.d.ts +0 -49
  399. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  400. package/dist/plugins/vite-server-only.js +0 -57
  401. package/dist/plugins/vite-server-only.js.map +0 -1
  402. package/dist/server-only.d.ts +0 -21
  403. package/dist/server-only.d.ts.map +0 -1
  404. package/dist/server-only.js +0 -25
  405. package/dist/server-only.js.map +0 -1
  406. package/src/plugins/vite-server-only.ts +0 -75
  407. package/src/server-only.ts +0 -25
@@ -1,13 +1,11 @@
1
- import { NIFRA_BACKEND_MOUNT, } from "@nifrajs/core/mount";
2
- import { server, urlPartsOf } from "@nifrajs/core/server";
3
- import { boundaryDescriptors, resolveStaticBoundaries, startDynamicBoundaries, } from "../boundary.js";
4
- import { defer, ndjsonStream, prepareDeferred } from "../deferred.js";
5
- import { isDraftEnabled } from "../draft.js";
1
+ import { NIFRA_BACKEND_EVIDENCE, NIFRA_BACKEND_MOUNT, NIFRA_BACKEND_WS_MOUNT, NIFRA_BACKEND_WS_RUNTIME, preRouteMountPaths, } from "@nifrajs/core/mount";
2
+ import { server } from "@nifrajs/core/server";
3
+ import { cspNonceResolver, DOCUMENT_POLICY, isCspPolicy } from "../csp.js";
4
+ import { CACHE_CHANNEL } from "../isr.js";
6
5
  import { generateLlmsTxt } from "../llms-txt.js";
7
- import { createMatcher, DATA_HEADER, NAV_FROM_HEADER, RETAIN_HEADER, REVALIDATE_HEADER, STATUS_HEADER, } from "../router.js";
8
- import { searchOf, searchOfChain } from "../search.js";
9
- import { mergeHeads, resolveMeta } from "./head-merge.js";
10
- import { actionResponse, EMPTY_RETAIN, isControlFlow, isStatusSignal, layoutErrorId, renderPageResult, STATUS_SIGNAL, STATUS_TEXT, scopeParams, tagLayoutError, withDuplicateInstanceHint, } from "./render-document.js";
6
+ import { PRERENDERED_LIST_PATH } from "../router.js";
7
+ import { formatShadowedPages, shadowedPages } from "./mount-shadow.js";
8
+ import { createPageRequestExecutor } from "./page-execution.js";
11
9
  /**
12
10
  * Re-issue `request` with `prefix` removed from its pathname.
13
11
  *
@@ -21,13 +19,6 @@ import { actionResponse, EMPTY_RETAIN, isControlFlow, isStatusSignal, layoutErro
21
19
  * The `Request` is rebuilt rather than mutated (`url` is read-only), preserving method, headers, body,
22
20
  * and duplex streaming - the body is passed through unread, so a large or streamed upload is untouched.
23
21
  */
24
- function stripMountPrefix(request, prefix) {
25
- const url = new URL(request.url);
26
- const rest = url.pathname.slice(prefix.length);
27
- url.pathname = rest === "" ? "/" : rest;
28
- // `url.href`, not the `URL`: this type set only declares the string and Request overloads.
29
- return new Request(url.href, request);
30
- }
31
22
  /**
32
23
  * Resolve the explicit symbol-keyed backend mount interface. The symbol seam forwards platform
33
24
  * context without making web depend on client.
@@ -38,20 +29,38 @@ function backendMountOf(api) {
38
29
  const explicit = api[NIFRA_BACKEND_MOUNT];
39
30
  if (typeof explicit !== "function")
40
31
  return undefined;
41
- return (request, platform) => explicit.call(api, request, platform);
32
+ const websocket = api[NIFRA_BACKEND_WS_MOUNT];
33
+ const runtime = api[NIFRA_BACKEND_WS_RUNTIME];
34
+ const evidence = api[NIFRA_BACKEND_EVIDENCE];
35
+ const mount = {
36
+ fetch: (request, platform) => explicit.call(api, request, platform),
37
+ };
38
+ if (typeof websocket === "function") {
39
+ mount.resolveWebSocketUpgrade = (request, platform) => websocket.call(api, request, platform);
40
+ }
41
+ if (typeof runtime === "function")
42
+ mount[NIFRA_BACKEND_WS_RUNTIME] = runtime;
43
+ if (typeof evidence === "function")
44
+ mount[NIFRA_BACKEND_EVIDENCE] = evidence;
45
+ return mount;
46
+ }
47
+ function evidenceProviderOf(value) {
48
+ if ((typeof value !== "object" && typeof value !== "function") || value === null)
49
+ return undefined;
50
+ const provider = value[NIFRA_BACKEND_EVIDENCE];
51
+ return typeof provider === "function" ? provider : undefined;
52
+ }
53
+ function mountPathPrefix(path) {
54
+ const withoutWildcard = path.endsWith("/*") ? path.slice(0, -2) : path;
55
+ if (withoutWildcard === "" || withoutWildcard === "/")
56
+ return "";
57
+ // Index-scan instead of `/\/+$/`: mount paths can be library input, and the
58
+ // unanchored-start trailing-run replacement can backtrack quadratically.
59
+ let end = withoutWildcard.length;
60
+ while (end > 0 && withoutWildcard.charCodeAt(end - 1) === 47 /* '/' */)
61
+ end--;
62
+ return withoutWildcard.slice(0, end);
42
63
  }
43
- // `createWebApp` reaches the same Request object through the mount hook and the page route. Cache the
44
- // allocation-light core split once so API mounting, path hydration, search parsing, and SSG fallback
45
- // checks do not each rescan the absolute URL. WeakMap keeps this bounded to requests that are alive.
46
- const REQUEST_URL_PARTS = new WeakMap();
47
- const urlPartsFor = (request) => {
48
- const cached = REQUEST_URL_PARTS.get(request);
49
- if (cached !== undefined)
50
- return cached;
51
- const parts = urlPartsOf(request.url);
52
- REQUEST_URL_PARTS.set(request, parts);
53
- return parts;
54
- };
55
64
  /**
56
65
  * Build a nifra app from a route manifest: every route SSRs its layout chain via `renderPage`,
57
66
  * and a wildcard catch-all renders `_404` (or a plain 404). Reuses @nifrajs/core's router +
@@ -68,26 +77,12 @@ const urlPartsFor = (request) => {
68
77
  */
69
78
  export function createWebApp(options) {
70
79
  const { adapter, manifest, clientEntry, title, api } = options;
71
- const titleOption = title === undefined ? {} : { title };
72
- // Draft/preview: when a `draftSecret` is configured, each request's signed `__nifra_draft` cookie is
73
- // verified once and surfaced to loaders/actions as `ctx.draft`. No secret ⇒ always `false` (sync, free).
74
- const draftFlag = (req) => options.draftSecret === undefined
75
- ? Promise.resolve(false)
76
- : isDraftEnabled(req, options.draftSecret);
77
- // Per-route preload chunks (spread into renderPage; omitted when unmapped, for exactOptionalPropertyTypes).
78
- const preloadOf = (id) => {
79
- const chunks = options.routePreload?.[id];
80
- return chunks ? { preload: chunks } : {};
81
- };
82
- // The matched route's stylesheet links (spread into renderPage). Per-route when the build mapped it
83
- // (`routeStyles[id]` - only the chain's CSS; an empty array ⇒ no `<link>`), else the aggregate
84
- // `styles`. Omitted entirely when CSS-free (for exactOptionalPropertyTypes).
85
- const stylesOf = (id) => {
86
- const perRoute = options.routeStyles?.[id];
87
- if (perRoute !== undefined)
88
- return { styles: perRoute };
89
- return options.styles && options.styles.length > 0 ? { styles: options.styles } : {};
90
- };
80
+ if (options.csp !== undefined && options.nonce !== undefined) {
81
+ throw new TypeError("[nifra/web] createWebApp takes `csp` or `nonce`, not both");
82
+ }
83
+ if (options.csp !== undefined && !isCspPolicy(options.csp)) {
84
+ throw new TypeError("[nifra/web] createWebApp `csp` must come from createCspPolicy()");
85
+ }
91
86
  // Seed the context with the declared `Env` so `app.fetch(req, { env })` / `toFetchHandler(app)` type
92
87
  // the platform bindings (see the `createWebApp` doc). The runtime `env` still arrives per-request via
93
88
  // `app.fetch(req, { env })`; this is a compile-time-only seed (`server<Env>()` casts, doesn't store).
@@ -96,749 +91,197 @@ export function createWebApp(options) {
96
91
  // `beforeHandle`, assurance evidence) actually bind to them - see the `use` option. A caller
97
92
  // cannot do this after the fact: by the time this function returns, the routes are declared.
98
93
  options.use?.(app);
94
+ // A resolver created by `createNonceResolver({ header })` carries a paired response hook. Register it
95
+ // after caller middleware so its request-specific CSP value is the last default applied to a page.
96
+ const nonceResponse = options.nonce?.onResponse;
97
+ if (nonceResponse !== undefined)
98
+ app.onResponse(nonceResponse);
99
+ // How this app's documents meet a CSP, for wrappers that cache them (`withISR` warns on "nonce").
100
+ const documentPolicy = options.csp !== undefined ? "hash" : options.nonce !== undefined ? "nonce" : undefined;
101
+ if (documentPolicy !== undefined) {
102
+ Object.defineProperty(app, DOCUMENT_POLICY, { value: documentPolicy });
103
+ }
104
+ const cacheChannel = { enabled: false };
105
+ Object.defineProperty(app, CACHE_CHANNEL, {
106
+ value: () => {
107
+ cacheChannel.enabled = true;
108
+ },
109
+ });
99
110
  // Auto-mount the in-process backend over HTTP at `apiPrefix` (default `/api`), BEFORE page routing.
100
- // `onRequest` runs on the raw request ahead of the router, so this wins over the page wildcard `/*`
101
- // for every method (POST/GET/PUT/…) - not just the GET page routes - and a backend 404 (`/api/none`)
102
- // surfaces as the backend's own response, never the page 404. We dispatch the SAME `Request` object
103
- // (its body unread) so streamed/large bodies pass through untouched; the backend defines its routes
104
- // at the full prefixed path (`server().post("/api/sync", …)`), matching the `inProcessClient` call
105
- // sites. `apiPrefix: ""` opts out; a non-mountable `api` (no symbol mount) is left pages-only. The hook
106
- // is registered ONLY when both hold, so a pages-only app keeps core's synchronous no-hook fast path.
111
+ // Core's pre-route mount seam runs before the page wildcard `/*`; parent request hooks run first,
112
+ // child request hooks run inside the mount, and parent response hooks still wrap the result.
113
+ // The mount handles every HTTP method and preserves backend 404 responses instead of letting the
114
+ // page catch-all render them. We dispatch the same `Request` object (its body unread), so
115
+ // streamed/large bodies pass through untouched. The backend defines full prefixed paths
116
+ // (`server().post("/api/sync", …)`) unless `apiStrip` is enabled. `apiPrefix: ""` opts out;
117
+ // a non-mountable `api` remains loader-only and pages-only apps keep the no-mount fast path.
107
118
  const apiPrefix = options.apiPrefix ?? "/api";
108
119
  const apiStrip = options.apiStrip === true;
109
120
  const mountedApi = backendMountOf(api);
110
121
  // Longest path first, so a more specific mount (`/api/auth`) is tried before a broader one (`/api`)
111
122
  // regardless of the order they were declared in.
112
- const mounts = [...(options.mounts ?? [])].sort((a, b) => b.path.length - a.path.length);
113
- const underPrefix = (pathname, prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`);
114
- if (mounts.length > 0 || (apiPrefix !== "" && mountedApi !== undefined)) {
115
- app.onRequest((req, platform) => {
116
- const { pathname } = urlPartsFor(req);
117
- // Sub-app mounts win over the backend prefix: they are the more specific declaration, and an
118
- // auth handler mounted at `/api/auth` must not be swallowed by the backend mounted at `/api`.
119
- for (const mount of mounts) {
120
- if (!underPrefix(pathname, mount.path))
121
- continue;
122
- return mount.app.fetch(mount.stripPrefix === true ? stripMountPrefix(req, mount.path) : req);
123
- }
124
- // Exactly the prefix (`/api`) or a sub-path (`/api/…`) - NOT a sibling like `/apixyz` that merely
125
- // shares the prefix as a string head. Dispatch the original `req` (body intact) to the backend.
126
- if (mountedApi !== undefined && apiPrefix !== "" && underPrefix(pathname, apiPrefix)) {
127
- return mountedApi(apiStrip ? stripMountPrefix(req, apiPrefix) : req, platform);
128
- }
129
- return undefined; // not an API path → continue to page routing
130
- });
123
+ const configuredMounts = Object.freeze((options.mounts ?? []).map((mount) => ({
124
+ path: mount.path,
125
+ app: mount.app,
126
+ ...(mount.stripPrefix === undefined ? {} : { stripPrefix: mount.stripPrefix }),
127
+ ...(mount.priority === undefined ? {} : { priority: mount.priority }),
128
+ ...(mount.fallbackOn === undefined ? {} : { fallbackOn: mount.fallbackOn }),
129
+ ...(mount.opaque === undefined ? {} : { opaque: mount.opaque }),
130
+ })));
131
+ const mounts = [...configuredMounts];
132
+ if (mountedApi !== undefined && apiPrefix !== "") {
133
+ mounts.push({ path: apiPrefix, app: mountedApi, stripPrefix: apiStrip });
134
+ }
135
+ for (const mount of mounts)
136
+ app.mount(mount);
137
+ // Read back the server's own mount table, so a mount added in `use` counts as well: a page under any
138
+ // pre-route mount is unreachable, and serving it as a silent 404 hides that.
139
+ const shadowed = shadowedPages(manifest, preRouteMountPaths(app));
140
+ if (shadowed.length > 0)
141
+ throw new Error(`[nifra/web] ${formatShadowedPages(shadowed)}`);
142
+ const ownPatterns = new Set(manifest.routes.map((route) => route.pattern));
143
+ const prerenderedList = ownPatterns.has(PRERENDERED_LIST_PATH)
144
+ ? undefined
145
+ : servedPrerenderedList(options.prerenderedPaths);
146
+ const pageExecutor = createPageRequestExecutor({
147
+ adapter,
148
+ manifest,
149
+ clientEntry,
150
+ ...(title === undefined ? {} : { title }),
151
+ ...(api === undefined ? {} : { api }),
152
+ ...(options.draftSecret === undefined ? {} : { draftSecret: options.draftSecret }),
153
+ ...(options.routePreload === undefined ? {} : { routePreload: options.routePreload }),
154
+ ...(options.styles === undefined ? {} : { styles: options.styles }),
155
+ ...(options.routeStyles === undefined ? {} : { routeStyles: options.routeStyles }),
156
+ ...(options.cssLoading === undefined ? {} : { cssLoading: options.cssLoading }),
157
+ ...(options.prerenderedPaths === undefined
158
+ ? {}
159
+ : { prerenderedPaths: options.prerenderedPaths }),
160
+ ...(prerenderedList === undefined ? {} : { prerenderedListUrl: prerenderedList.url }),
161
+ ...(options.staticFallbacks === undefined ? {} : { staticFallbacks: options.staticFallbacks }),
162
+ ...(options.staticBoundaryCache === undefined
163
+ ? {}
164
+ : { staticBoundaryCache: options.staticBoundaryCache }),
165
+ ...(options.nonce === undefined ? {} : { nonce: options.nonce }),
166
+ ...(options.csp === undefined
167
+ ? {}
168
+ : { nonce: cspNonceResolver(options.csp), csp: options.csp }),
169
+ ...(options.onLoaderError === undefined ? {} : { onLoaderError: options.onLoaderError }),
170
+ cacheChannel,
171
+ });
172
+ for (const route of manifest.routes) {
173
+ app.register("GET", route.pattern, undefined, pageExecutor.get(route));
174
+ app.register("POST", route.pattern, undefined, pageExecutor.post(route));
131
175
  }
132
- // SSG `fallback: "404"`: the set of concrete paths that actually exist for those routes. An unlisted
133
- // path under a `"404"` route is rejected (it isn't a static file, and the route declared it shouldn't
134
- // SSR on demand). O(1) membership via a Set.
135
- const prerenderedSet = new Set(options.prerenderedPaths ?? []);
136
- const matchManifestRoute = createMatcher(manifest.routes.map((route) => ({ routeId: route.id, pattern: route.pattern })));
137
- const routeById = new Map(manifest.routes.map((route) => [route.id, route]));
138
- // Load a route's layout modules (outermost layout → innermost), keeping each module whole so the
139
- // render path can take both its `default` (the component chain) AND its `meta` (the sitewide head it
140
- // contributes). Loaded lazily - the data-only and 405 branches return before any layout is needed.
141
- const loadLayoutModules = async (route) => {
142
- const modules = await Promise.all(
143
- // layoutIds only reference layouts present in the manifest (buildManifest invariant).
144
- route.layoutIds.map((id) => manifest.layouts[id].load()));
145
- // A layout is not a form target, so an `action` there is a mistake with no meaning to give it.
146
- // A `loader`, by contrast, now runs (see `runLayoutChain`).
147
- for (let i = 0; i < modules.length; i++) {
148
- if (modules[i].action === undefined)
176
+ // llms.txt & llms-full.txt describe what a visitor can reach: the pages, and the backend only when it
177
+ // is served over HTTP - a backend the loaders call in-process has no public surface to describe. An
178
+ // app route at either path takes precedence, and each text is built once per app.
179
+ if (options.llmsTxt !== false) {
180
+ const llmsOptions = { includeLocalGuidelines: options.publishLocalGuidelines === true };
181
+ const publicApi = mountedApi !== undefined && apiPrefix !== "" ? api : undefined;
182
+ for (const [path, full] of [
183
+ ["/llms.txt", false],
184
+ ["/llms-full.txt", true],
185
+ ]) {
186
+ if (ownPatterns.has(path))
149
187
  continue;
150
- const id = route.layoutIds[i];
151
- throw new Error(`[nifra/web] "${manifest.layouts[id].file}" exports an \`action\`, but layouts do not run one - only route files do. Move the mutation into the route that submits to it.`);
188
+ let text;
189
+ app.register("GET", path, undefined, async () => {
190
+ text ??= generateLlmsTxt(full, manifest.routes, publicApi, llmsOptions).catch((error) => {
191
+ text = undefined;
192
+ throw error;
193
+ });
194
+ return new Response(await text, {
195
+ headers: { "content-type": "text/plain; charset=utf-8" },
196
+ });
197
+ });
152
198
  }
153
- return modules;
154
- };
155
- /**
156
- * Run a route's layout-chain loaders, honouring declared gates.
157
- *
158
- * Returns once every **gate** has resolved, with the non-gate loaders still in flight, so the caller
159
- * can start the page loader at exactly the right moment: after anything that might reject the
160
- * request, alongside anything that merely fetches.
161
- *
162
- * The ordering is the security-relevant part. Parallel loaders are NOT an authorization boundary -
163
- * a page loader running concurrently with a guard has already queried by the time the guard says no.
164
- * Remix documents that as a footgun; here it is a declaration instead. `export const gate = true`
165
- * makes a layout blocking for everything beneath it, and nothing beneath a rejected gate runs.
166
- *
167
- * Each layout receives only the params it OWNS ({@link RouteEntry.layoutParams}), not the route's
168
- * full set, so a layout at `orgs/[org]/` cannot read a param belonging to a route below it.
169
- */
170
- /**
171
- * Layout indices a client navigation asked to keep, from {@link RETAIN_HEADER}.
172
- *
173
- * The CLIENT decides retainability: it knows each layout's scoped params (shipped in the bootstrap)
174
- * and both the old and new match, so it can tell whether a layout's own prefix changed - without the
175
- * server re-matching a path the client already matched.
176
- *
177
- * It is a HINT, not an instruction. The client is asking to keep data it already holds, which is
178
- * harmless for data and unacceptable for a guard, so `runLayoutChain` refuses to skip a gate no
179
- * matter what arrives here. Anything unparseable is ignored rather than rejected: the worst case of
180
- * a bad hint is a loader that runs when it need not.
181
- */
182
- const retainedIndices = (header) => {
183
- if (header === null || header === "")
184
- return EMPTY_RETAIN;
185
- const out = new Set();
186
- for (const part of header.split(",")) {
187
- // Digits only. `Number("")` is 0 and `Number(" ")` is 0, so a stray comma would otherwise read
188
- // as "retain index 0" and silently skip the outermost layout.
189
- if (!/^\d+$/.test(part))
190
- continue;
191
- out.add(Number(part));
199
+ }
200
+ if (prerenderedList !== undefined) {
201
+ const { body, version } = prerenderedList;
202
+ app.register("GET", PRERENDERED_LIST_PATH, undefined, (c) => new Response(body, {
203
+ headers: {
204
+ "content-type": "application/json",
205
+ // Only the versioned URL pages hand over is immutable; a bare request gets today's list.
206
+ "cache-control": new URL(c.req.url).searchParams.get("v") === version
207
+ ? "public, max-age=31536000, immutable"
208
+ : "no-cache",
209
+ },
210
+ }));
211
+ }
212
+ // Wildcard catch-all: unmatched paths render the nearest `_404` (404), or a plain text 404 if
213
+ // absent.
214
+ app.register("GET", "/*", undefined, pageExecutor.fallback);
215
+ let evidenceComposing = false;
216
+ const evidenceProvider = async () => {
217
+ if (evidenceComposing) {
218
+ throw new Error("[nifra/web] cyclic composed assurance evidence mount");
192
219
  }
193
- return out;
194
- };
195
- /**
196
- * Validate a data-navigation's retain request against the manifest and its declared source path.
197
- * A client may ask to retain every slot it currently has; the server only honours indices whose
198
- * layout identity and owned params are unchanged. Hard document GETs never retain anything.
199
- */
200
- const validatedRetainedIndices = (req, route, params) => {
201
- if (req.headers.get(DATA_HEADER) === null)
202
- return EMPTY_RETAIN;
203
- const requested = retainedIndices(req.headers.get(RETAIN_HEADER));
204
- const from = req.headers.get(NAV_FROM_HEADER);
205
- if (requested.size === 0 || from === null)
206
- return EMPTY_RETAIN;
207
- let fromPath;
220
+ evidenceComposing = true;
208
221
  try {
209
- const current = new URL(req.url);
210
- const source = new URL(from, current);
211
- if (source.origin !== current.origin)
212
- return EMPTY_RETAIN;
213
- fromPath = source.pathname + source.search;
214
- }
215
- catch {
216
- return EMPTY_RETAIN;
217
- }
218
- const previousMatch = matchManifestRoute(fromPath);
219
- if (previousMatch === null)
220
- return EMPTY_RETAIN;
221
- const previous = routeById.get(previousMatch.routeId);
222
- if (previous === undefined)
223
- return EMPTY_RETAIN;
224
- const valid = new Set();
225
- for (const index of requested) {
226
- if (route.layoutIds[index] !== previous.layoutIds[index])
227
- continue;
228
- const owned = route.layoutParams?.[index] ?? [];
229
- const previouslyOwned = previous.layoutParams?.[index] ?? [];
230
- if (owned.length !== previouslyOwned.length ||
231
- owned.some((name, i) => name !== previouslyOwned[i]) ||
232
- owned.some((name) => params[name] !== previousMatch.params[name])) {
233
- continue;
234
- }
235
- valid.add(index);
236
- }
237
- return valid;
238
- };
239
- // The validated (or raw-parsed) search a route's loader context receives; `searchOf` fails closed to
240
- // the schema's defaults on hostile input, so this never throws on an attacker-picked query.
241
- const loaderSearch = (searchSchema, request) => searchOf(searchSchema, urlPartsFor(request).search);
242
- // The route's EFFECTIVE search once its layout chain is loaded: the layout schemas merged with the
243
- // page's (page-wins), the same chain the client mount builds for `useSearch`. Used for the page loader's
244
- // `ctx.search` and the render's `RenderProps.search`, so both agree with the client and with each other.
245
- const searchChainOf = (layoutMods, mod, request) => searchOfChain([...layoutMods.map((m) => m.searchSchema), mod.searchSchema], urlPartsFor(request).search);
246
- const runLayoutChain = async (route, ctx, retain = new Set()) => {
247
- const modules = await loadLayoutModules(route);
248
- if (!modules.some((m) => m.loader !== undefined)) {
249
- // Nothing to run: no array is produced, so a page-only app serializes nothing extra and its
250
- // HTML is byte-identical to before this feature.
251
- return { modules, layoutData: undefined, retained: [], pending: Promise.resolve() };
252
- }
253
- const results = new Array(modules.length).fill(null);
254
- const retained = [];
255
- const pending = [];
256
- for (let i = 0; i < modules.length; i++) {
257
- const loader = modules[i]?.loader;
258
- if (loader === undefined)
259
- continue;
260
- // Unchanged scope: the client keeps what it has. A GATE is never skipped - it is a guard, and a
261
- // guard that only runs when the URL prefix changed is not a guard.
262
- if (retain.has(i) && modules[i]?.gate !== true) {
263
- retained.push(i);
264
- continue;
265
- }
266
- const scoped = {
267
- ...ctx,
268
- params: scopeParams(ctx.params, route.layoutParams?.[i]),
269
- };
270
- if (modules[i]?.gate === true) {
271
- // Blocking: awaited here, so a throw propagates before anything deeper is started.
272
- try {
273
- results[i] = await loader(scoped);
222
+ const { composeProjectEvidence, snapshotProjectEvidence } = await import("@nifrajs/core/evidence");
223
+ const parts = [{ evidence: snapshotProjectEvidence(app) }];
224
+ for (const mount of configuredMounts) {
225
+ const provider = evidenceProviderOf(mount.app);
226
+ if (provider === undefined) {
227
+ // A declared opaque mount reaches the snapshot as a known gap, through this app's own
228
+ // mount reflection above.
229
+ if (mount.opaque !== undefined)
230
+ continue;
231
+ throw new Error(`[nifra/web] cannot compose assurance evidence: mount "${mount.path}" has no token-only evidence provider - declare why it is not analyzed with { opaque: "<reason>" }`);
274
232
  }
275
- catch (err) {
276
- throw tagLayoutError(err, route.layoutIds[i]);
277
- }
278
- continue;
233
+ parts.push({
234
+ evidence: await provider.call(mount.app),
235
+ ...(mount.stripPrefix === true ? { pathPrefix: mountPathPrefix(mount.path) } : {}),
236
+ });
279
237
  }
280
- const layoutId = route.layoutIds[i];
281
- const index = i;
282
- // The loader is invoked INSIDE the async function, not before it. `Promise.resolve(loader(x))`
283
- // would let a synchronously-throwing loader escape past the rejection handler entirely - the
284
- // exception happens while evaluating the argument, before any promise exists to reject.
285
- pending.push((async () => {
286
- try {
287
- results[index] = await loader(scoped);
238
+ if (mountedApi !== undefined && apiPrefix !== "") {
239
+ const provider = evidenceProviderOf(api);
240
+ if (provider === undefined) {
241
+ throw new Error(`[nifra/web] cannot compose assurance evidence: auto-mounted API at "${apiPrefix}" has no token-only evidence provider`);
288
242
  }
289
- catch (err) {
290
- throw tagLayoutError(err, layoutId);
291
- }
292
- })());
293
- }
294
- return { modules, layoutData: results, retained, pending: Promise.all(pending) };
295
- };
296
- /** Run only blocking layout gates before a route action. Non-gate loaders fetch render data and
297
- * therefore run after the mutation when a native POST needs a fresh document. */
298
- const runLayoutGates = async (route, ctx) => {
299
- const modules = await loadLayoutModules(route);
300
- for (let i = 0; i < modules.length; i++) {
301
- const mod = modules[i];
302
- if (mod?.gate !== true || mod.loader === undefined)
303
- continue;
304
- try {
305
- await mod.loader({
306
- ...ctx,
307
- params: scopeParams(ctx.params, route.layoutParams?.[i]),
243
+ parts.push({
244
+ evidence: await provider.call(api),
245
+ ...(apiStrip ? { pathPrefix: mountPathPrefix(apiPrefix) } : {}),
308
246
  });
309
247
  }
310
- catch (err) {
311
- throw tagLayoutError(err, route.layoutIds[i]);
312
- }
313
- }
314
- return modules;
315
- };
316
- // Build a route's render chain + its merged `<head>` from the already-loaded layout modules + the
317
- // page module. The chain is `[…layout components, page]`; the head merges each layout's `meta`
318
- // export (outermost→innermost) with the page's (last), so a `head`/`meta` on `_layout.tsx`
319
- // contributes sitewide tags. `metaArgs` (loader data + params) feed function-form `meta`s.
320
- const resolveChainAndHead = (layoutModules, page, metaArgs) => {
321
- const chain = [...layoutModules.map((m) => m.default), page.default];
322
- const heads = [
323
- ...layoutModules.map((m) => resolveMeta(m.meta, metaArgs)),
324
- resolveMeta(page.meta, metaArgs),
325
- ];
326
- return { chain, head: mergeHeads(heads) };
327
- };
328
- // Dir a special-file id lives in: `_error`→"" , `a/b/_error`→"a/b" (and likewise for `_layout`).
329
- const dirOfId = (id, suffix) => id === suffix ? "" : id.slice(0, id.length - suffix.length - 1);
330
- /**
331
- * Choose the `_error` boundary for a failure, respecting where a LAYOUT loader failed.
332
- *
333
- * A route error uses the nearest boundary, as it always has. A layout error must not: the nearest
334
- * boundary can sit BELOW the layout that threw, and rendering there would wrap the boundary in that
335
- * very layout - the one whose loader just failed and whose data therefore never arrived.
336
- */
337
- const boundaryFor = (route, err) => {
338
- const ids = route.errorIds ?? [];
339
- const failing = layoutErrorId(err);
340
- if (failing === undefined)
341
- return ids.at(-1);
342
- const layoutDir = dirOfId(failing, "_layout");
343
- // Deepest boundary at or ABOVE the failing layout's directory.
344
- for (let i = ids.length - 1; i >= 0; i--) {
345
- const errDir = dirOfId(ids[i], "_error");
346
- if (errDir === "" || errDir === layoutDir || layoutDir.startsWith(`${errDir}/`))
347
- return ids[i];
248
+ return composeProjectEvidence(parts);
348
249
  }
349
- return undefined;
350
- };
351
- // The request's origin (scheme + host + port, NO trailing slash) - the one server fact `meta()` needs
352
- // for absolute `canonical`/`og:url`/`og:image` but can't otherwise see (it also runs on the client).
353
- // `URL.origin` is exactly that shape; it matches the browser's `location.origin` for the same URL, so
354
- // a soft-nav's `applyHead` resolves an identical `<head>` (no hydration drift). A malformed `req.url`
355
- // (shouldn't happen on a real request) degrades to `""` - the documented unknown-origin default -
356
- // rather than throwing during render.
357
- const originOf = (req) => {
358
- try {
359
- return new URL(req.url).origin;
360
- }
361
- catch {
362
- return "";
363
- }
364
- };
365
- // The request's `pathname + search` - threaded into `renderPage` so an adapter's `useLocation`/
366
- // `useSearchParams` render the current URL server-side and hydrate against the client's initial state
367
- // (which the client entry seeds from `location.pathname + location.search`). No hash: it never reaches
368
- // the server. A malformed `req.url` degrades to "/" rather than throwing during render.
369
- const pathOf = (req) => {
370
- try {
371
- const parts = urlPartsFor(req);
372
- return parts.pathname + parts.search;
373
- }
374
- catch {
375
- return "/";
376
- }
377
- };
378
- /**
379
- * Render the **nearest `_error` boundary** when a route's loader throws (the agnostic, server-side
380
- * half of error UI - works on every adapter, no client takeover). The boundary renders in place of
381
- * the page, wrapped by the layouts **at or above** its segment (deeper layouts are dropped). It's
382
- * served **non-hydrated** at status 500: a terminal page, and rendering a boundary (not the page the
383
- * client maps this route to) would otherwise hydrate-mismatch. The component receives `{ name,
384
- * message }` - never the stack (no internals leak into HTML).
385
- */
386
- const renderError = async (route, errorId, err) => {
387
- const errDir = dirOfId(errorId, "_error");
388
- const keptLayoutIds = route.layoutIds.filter((id) => {
389
- const ld = dirOfId(id, "_layout");
390
- return ld === "" || ld === errDir || errDir.startsWith(`${ld}/`);
391
- });
392
- const layouts = await Promise.all(keptLayoutIds.map((id) => manifest.layouts[id].load().then((m) => m.default)));
393
- const { default: errComp } = await (manifest.errors?.[errorId]).load();
394
- const e = err instanceof Error ? err : new Error(String(err));
395
- return renderPageResult({
396
- adapter,
397
- chain: [...layouts, errComp],
398
- data: { name: e.name, message: e.message },
399
- clientEntry,
400
- routeId: errorId,
401
- status: 500,
402
- hydrate: false,
403
- ...titleOption,
404
- });
405
- };
406
- // The 404 response - the `_404` page (status 404) or a plain-text fallback. Shared by the wildcard
407
- // catch-all (unmatched paths) and the `fallback: "404"` enforcement (unlisted paths under a route
408
- // that opted out of on-demand SSR).
409
- const renderNotFound = (path) => renderTerminalStatus(404, path);
410
- /**
411
- * Render a terminal-status page: the `_<status>` page if the app authored one, else `_404`, else
412
- * plain text. Shared by the wildcard catch-all, `fallback: "404"` enforcement, and a loader's
413
- * `notFound()` / `gone()` / `statusPage()`, so all of them resolve the page and set the status
414
- * through one path rather than forking.
415
- *
416
- * Hydrates, following the `_404` precedent rather than `_error`'s non-hydrated 500: these are
417
- * ordinary pages the client router can own, and a 404 arrived at by clicking a link should behave
418
- * like any other route.
419
- */
420
- const renderTerminalStatus = async (status, path, extraHeaders) => {
421
- const page = manifest.statusPages?.[String(status)] ?? manifest.notFound;
422
- if (page === undefined) {
423
- const headers = new Headers(extraHeaders);
424
- headers.set("content-type", "text/plain; charset=utf-8");
425
- return new Response(STATUS_TEXT[status] ?? "Error", { status, headers });
426
- }
427
- // The routeId decides which preload/style bundle and which client component this maps to, so it
428
- // must name the page actually rendered - `_410` when `_410.tsx` exists, `_404` when it fell back.
429
- const routeId = manifest.statusPages?.[String(status)] !== undefined ? `_${status}` : "_404";
430
- const loaded = (await page.load());
431
- const component = loaded.default;
432
- return renderPageResult({
433
- adapter,
434
- chain: [component],
435
- data: null,
436
- clientEntry,
437
- routeId,
438
- // The page hydrates, so besides `path` its SSR `search` must equal the client mount's derivation for
439
- // this routeId (`searchOf(searchSchema, url.search)`), or a status page reading `useSearch` drifts.
440
- // Only when `path` is present (a real URL); a schema-less status page then gets the raw parsed query.
441
- ...(path !== undefined
442
- ? {
443
- search: searchOf(loaded.searchSchema, path.includes("?") ? path.slice(path.indexOf("?")) : ""),
444
- }
445
- : {}),
446
- // The page hydrates, so its SSR `path` must equal the client's initial
447
- // `location.pathname+search` (an unmatched path yields no params → `{}`), or a `useLocation`
448
- // on it would drift.
449
- ...(path !== undefined ? { path } : {}),
450
- status,
451
- ...(extraHeaders !== undefined ? { headers: extraHeaders } : {}),
452
- ...preloadOf(routeId),
453
- ...stylesOf(routeId),
454
- prerenderedPaths: options.prerenderedPaths ?? [],
455
- ...titleOption,
456
- });
457
- };
458
- /**
459
- * Answer a loader's terminal-status signal, in whichever form the caller asked for.
460
- *
461
- * A hard navigation wants the document, so it gets the rendered boundary. A client-side navigation
462
- * fetched DATA, not a document, so it gets an empty body plus the status on `X-Nifra-Status`, and
463
- * the client router renders the same boundary from its own manifest. Sending the HTML back to a
464
- * soft-nav would be answering a different question than the one asked - and skipping the header
465
- * entirely is what leaves a crawler seeing a correct 404 while a user who clicked a link sees a
466
- * blank page.
467
- */
468
- const renderStatusSignal = (req, signal) => {
469
- const { status, headers } = signal[STATUS_SIGNAL];
470
- if (req.headers.get(DATA_HEADER) !== null) {
471
- const responseHeaders = new Headers(headers);
472
- responseHeaders.set(STATUS_HEADER, String(status));
473
- return new Response(null, { status, headers: responseHeaders });
250
+ finally {
251
+ evidenceComposing = false;
474
252
  }
475
- return renderTerminalStatus(status, pathOf(req), headers);
476
253
  };
477
- for (const route of manifest.routes) {
478
- // A dynamic route whose `getStaticPaths` declared `fallback: "404"` - only its prerendered paths
479
- // exist; anything else 404s (computed once per route, not per request).
480
- const is404Fallback = options.staticFallbacks?.[route.pattern] === "404";
481
- // Per-route document-assembly cache (see RenderAssemblyCache): filled on the first GET render and
482
- // reused while the route + layout MODULE IDENTITIES are unchanged. Attached only when every meta
483
- // in the chain is static - a `meta(data)` function makes the head per-request, so those routes
484
- // always assemble fresh. A module reload (dev HMR) yields new module objects, dropping the slot.
485
- let assemblySlot;
486
- let assemblySlotMod;
487
- let assemblySlotLayouts;
488
- const assemblyCacheFor = (mod, lms) => {
489
- if (typeof mod.meta === "function")
490
- return {};
491
- for (const m of lms)
492
- if (typeof m.meta === "function")
493
- return {};
494
- const layoutsUnchanged = assemblySlotLayouts !== undefined &&
495
- assemblySlotLayouts.length === lms.length &&
496
- assemblySlotLayouts.every((m, i) => m === lms[i]);
497
- if (assemblySlotMod !== mod || !layoutsUnchanged) {
498
- assemblySlot = {};
499
- assemblySlotMod = mod;
500
- assemblySlotLayouts = lms;
501
- }
502
- return { assemblyCache: assemblySlot };
503
- };
504
- app.register("GET", route.pattern, undefined, async (c) => {
505
- // Enforce `fallback: "404"` before any work: an unlisted path under this route doesn't exist.
506
- // Covers hard navigation directly; a client soft-nav's data fetch gets the 404, throws, and the
507
- // history layer falls back to a full-page navigation (which lands here again, as a document).
508
- if (is404Fallback && !prerenderedSet.has(urlPartsFor(c.req).pathname)) {
509
- return renderNotFound(pathOf(c.req));
510
- }
511
- const mod = await route.load();
512
- const draft = await draftFlag(c.req);
513
- let data;
514
- let layoutData;
515
- let boundaryStates;
516
- let layoutModules;
517
- let layoutRetained = [];
518
- try {
519
- const ctx = {
520
- params: c.params,
521
- request: c.req,
522
- req: c.req,
523
- api,
524
- env: c.env,
525
- draft,
526
- search: loaderSearch(mod.searchSchema, c.req),
527
- };
528
- // Layout loaders run for BOTH the document and the data-only request. A gate that only ran on
529
- // the document path would be bypassed by adding the data header, which is exactly the request
530
- // a client navigation makes - a guard has to hold on the path an attacker can pick.
531
- //
532
- // `runLayoutChain` returns once every GATE has resolved, with the non-gate loaders still in
533
- // flight. So the page loader starts as late as correctness requires and as early as speed
534
- // allows, and both are awaited together below.
535
- const run = await runLayoutChain(route, ctx, validatedRetainedIndices(c.req, route, c.params));
536
- layoutModules = run.modules;
537
- layoutRetained = run.retained;
538
- const boundaryDefinitions = [
539
- ...run.modules.flatMap((layout) => layout.boundaries ?? []),
540
- ...(mod.boundaries ?? []),
541
- ];
542
- if (boundaryDefinitions.length > 0)
543
- boundaryDescriptors(boundaryDefinitions);
544
- const effectiveSearch = searchChainOf(run.modules, mod, c.req);
545
- const boundaryBatch = boundaryDefinitions.length === 0
546
- ? undefined
547
- : startDynamicBoundaries(boundaryDefinitions, {
548
- request: c.req,
549
- params: c.params,
550
- api,
551
- env: c.env,
552
- draft,
553
- search: effectiveSearch,
554
- signal: c.req.signal,
555
- });
556
- const staticBoundaryPromise = boundaryDefinitions.length === 0
557
- ? Promise.resolve(undefined)
558
- : resolveStaticBoundaries(boundaryDefinitions, { phase: "build", origin: originOf(c.req) }, options.staticBoundaryCache);
559
- // Boundary data is attached as one Deferred marker per slot. The shell can render
560
- // immediately with each slot pending, while the existing deferred protocol emits the first
561
- // completed sibling independently instead of waiting for the slowest boundary.
562
- // The page loader gets the EFFECTIVE search (layout chain + page, page-wins), now that the layout
563
- // modules are loaded - so `ctx.search` includes a layout's shared keys (`?org`), matching `useSearch`.
564
- const [pageData, , staticBoundaries] = await Promise.all([
565
- mod.loader ? mod.loader({ ...ctx, search: effectiveSearch }) : null,
566
- run.pending, // a non-gate layout loader that rejects must still surface, not go unhandled
567
- staticBoundaryPromise,
568
- ]);
569
- data = pageData;
570
- layoutData = run.layoutData;
571
- if (boundaryBatch !== undefined) {
572
- const streamed = { ...boundaryBatch.initial, ...(staticBoundaries ?? {}) };
573
- for (const entry of boundaryBatch.pending) {
574
- const initial = streamed[entry.name];
575
- if (initial !== undefined)
576
- streamed[entry.name] = { ...initial, data: defer(entry.promise) };
577
- }
578
- boundaryStates = streamed;
579
- }
580
- else if (staticBoundaries !== undefined) {
581
- boundaryStates = staticBoundaries;
582
- }
583
- }
584
- catch (err) {
585
- // A terminal-status signal (`notFound()` / `gone()` / `statusPage(n)`) renders a boundary at
586
- // that status. Checked BEFORE the pass-through below: a signal IS a `Response`, so the order
587
- // is what keeps a hand-rolled `throw new Response(...)` served verbatim, as it always was.
588
- if (isStatusSignal(err))
589
- return renderStatusSignal(c.req, err);
590
- // A thrown control-flow signal (a guard's `redirect(...)`, an explicit error response) - let
591
- // it propagate to core, which renders it as-is, whether it is a plain render or a
592
- // hand-rolled `Response`. Real errors render the nearest `_error` boundary, if any; with
593
- // none, rethrow (unchanged 500 behavior).
594
- if (isControlFlow(err))
595
- throw err;
596
- // Let reporting plugins observe the data-layer failure before it's rendered/rethrown/500'd.
597
- if (options.onLoaderError !== undefined) {
598
- try {
599
- options.onLoaderError(err, { request: c.req, params: c.params, route: route.pattern });
600
- }
601
- catch {
602
- // A faulty reporter must never break error rendering.
603
- }
604
- }
605
- const errorId = boundaryFor(route, err);
606
- if (errorId === undefined)
607
- throw err;
608
- // A soft-nav data fetch can't render a boundary - 500 so the client falls back to a full-page
609
- // navigation, which lands here as a document and renders the `_error` page.
610
- if (c.req.headers.get(DATA_HEADER) !== null) {
611
- return new Response("Internal Server Error", {
612
- status: 500,
613
- headers: { "content-type": "text/plain; charset=utf-8" },
614
- });
615
- }
616
- return renderError(route, errorId, withDuplicateInstanceHint(err));
617
- }
618
- // A loader may RETURN its control-flow signal instead of throwing it - `return redirect(...)`
619
- // reads naturally and must not silently serialize the signal as loader data. Mirror the catch
620
- // above exactly: a status signal renders its boundary, any other signal (a redirect, a
621
- // hand-rolled response) passes through to core verbatim - so return and throw are
622
- // interchangeable.
623
- if (isStatusSignal(data))
624
- return renderStatusSignal(c.req, data);
625
- if (isControlFlow(data))
626
- return data;
627
- // Client-side navigation asks (via the X-Nifra-Data header) for just the loader data - no full
628
- // document, no layout chain. A route with deferred data streams NDJSON (critical data first,
629
- // then each deferred value as it settles); otherwise one JSON (the fast path). Same loader,
630
- // same auth - only the transport differs.
631
- if (c.req.headers.get(DATA_HEADER) !== null) {
632
- const pageSplit = prepareDeferred(data);
633
- const layoutSplits = [];
634
- let offset = pageSplit.deferred.length;
635
- for (const entry of layoutData ?? []) {
636
- const split = prepareDeferred(entry, offset);
637
- layoutSplits.push(split);
638
- offset += split.deferred.length;
639
- }
640
- const deferred = [...pageSplit.deferred, ...layoutSplits.flatMap((split) => split.deferred)];
641
- const boundarySplit = boundaryStates === undefined
642
- ? undefined
643
- : prepareDeferred(boundaryStates, deferred.length);
644
- const allDeferred = [
645
- ...deferred,
646
- ...(boundarySplit === undefined ? [] : boundarySplit.deferred),
647
- ];
648
- const payload = layoutData === undefined && boundarySplit === undefined
649
- ? pageSplit.forClient
650
- : {
651
- v: 1,
652
- data: pageSplit.forClient,
653
- layoutData: layoutSplits.map((split) => split.forClient),
654
- retained: layoutRetained,
655
- ...(boundarySplit === undefined ? {} : { boundaries: boundarySplit.forClient }),
656
- };
657
- // Bare value when there is no layout data - the pre-envelope shape, so a client running older
658
- // code against a newer server keeps working.
659
- if (allDeferred.length === 0) {
660
- return Response.json(payload ?? null);
661
- }
662
- return new Response(ndjsonStream(payload, allDeferred), {
663
- headers: { "content-type": "application/x-ndjson; charset=utf-8" },
664
- });
665
- }
666
- const chainLayoutModules = layoutModules ?? (await loadLayoutModules(route));
667
- const { chain, head } = resolveChainAndHead(chainLayoutModules, mod, {
668
- data,
669
- params: c.params,
670
- origin: originOf(c.req),
671
- });
672
- const hydrateRoute = mod.hydrate !== false;
673
- try {
674
- // `await` so a shell-render throw (renderToStream rejects before any byte) is caught here and
675
- // can render the `_error` page - not just a loader throw. Mid-stream (post-shell) throws can't
676
- // be recovered to a full page; the per-adapter client boundary catches client render errors.
677
- return await renderPageResult({
678
- adapter,
679
- chain,
680
- data,
681
- head,
682
- clientEntry,
683
- routeId: route.id,
684
- params: c.params,
685
- path: pathOf(c.req),
686
- // The effective (layout chain + page) search, matching the page loader's `ctx.search` above and
687
- // the client mount - so `useSearch` renders SSR-correct and hydrates with no drift. `layoutModules`
688
- // is loaded by now (the loader try succeeded to reach here).
689
- search: searchChainOf(layoutModules ?? [], mod, c.req),
690
- hydrate: hydrateRoute,
691
- ...(layoutData !== undefined ? { layoutData } : {}),
692
- ...(boundaryStates !== undefined ? { boundaries: boundaryStates } : {}),
693
- ...preloadOf(route.id),
694
- ...stylesOf(route.id),
695
- prerenderedPaths: options.prerenderedPaths ?? [],
696
- ...(mod.revalidate !== undefined ? { revalidate: mod.revalidate } : {}),
697
- ...(mod.revalidateTags !== undefined ? { revalidateTags: mod.revalidateTags } : {}),
698
- ...(mod.islandScripts !== undefined ? { islandScripts: mod.islandScripts } : {}),
699
- ...titleOption,
700
- ...assemblyCacheFor(mod, chainLayoutModules),
701
- });
702
- }
703
- catch (err) {
704
- // Same precedence as the loader catch above - a `meta()` may signal too.
705
- if (isStatusSignal(err))
706
- return renderStatusSignal(c.req, err);
707
- if (isControlFlow(err))
708
- throw err;
709
- // Let reporting plugins observe the data-layer failure before it's rendered/rethrown/500'd.
710
- if (options.onLoaderError !== undefined) {
711
- try {
712
- options.onLoaderError(err, { request: c.req, params: c.params, route: route.pattern });
713
- }
714
- catch {
715
- // A faulty reporter must never break error rendering.
716
- }
717
- }
718
- const errorId = boundaryFor(route, err);
719
- if (errorId === undefined)
720
- throw err;
721
- return renderError(route, errorId, withDuplicateInstanceHint(err));
722
- }
723
- });
724
- // POST runs the route's `action` (mutation). A control-flow return (e.g. a `redirect(...)`)
725
- // passes straight through; a data return re-renders the page (the loader re-runs for fresh
726
- // data) with `actionData`. Routes without an action reject POST with 405 - not a stray 404.
727
- app.register("POST", route.pattern, undefined, async (c) => {
728
- const mod = await route.load();
729
- const draft = await draftFlag(c.req);
730
- if (mod.action === undefined) {
731
- return new Response("Method Not Allowed", {
732
- status: 405,
733
- headers: { allow: "GET", "content-type": "text/plain; charset=utf-8" },
734
- });
735
- }
736
- const actionContext = {
737
- params: c.params,
738
- request: c.req,
739
- req: c.req,
740
- api,
741
- env: c.env,
742
- draft,
743
- search: loaderSearch(mod.searchSchema, c.req),
744
- };
745
- const isDataRequest = c.req.headers.get(DATA_HEADER) !== null;
746
- let layoutModules;
747
- let result;
748
- try {
749
- // A gate is an authorization boundary for everything beneath its layout, mutations included.
750
- // Run only gates before the action; ordinary layout data loaders run after a native mutation.
751
- layoutModules = await runLayoutGates(route, actionContext);
752
- result = await mod.action(actionContext);
753
- }
754
- catch (err) {
755
- // A THROWN signal is the same control flow as a returned one (`throw redirect()` in a gate
756
- // or an action) - route it through the same conversion as the returned branch below, so
757
- // throw and return are interchangeable on the mutation path too.
758
- if (isControlFlow(err))
759
- return actionResponse(err, isDataRequest);
760
- throw err;
761
- }
762
- // An action may wrap its data in `revalidate(paths, data)` to declare which routes it changed.
763
- // Unwrap to the inner data; the paths ride the `X-Nifra-Revalidate` header on the data-mode
764
- // responses (the client acts on them - a full-page POST re-runs loaders inline, so no header).
765
- const isRevalidate = result !== null && typeof result === "object" && "__nifraRevalidate" in result;
766
- const actionResult = isRevalidate ? result.data : result;
767
- const revalidateHeader = isRevalidate
768
- ? { [REVALIDATE_HEADER]: result.__nifraRevalidate.join(",") }
769
- : {};
770
- if (isControlFlow(actionResult))
771
- return actionResponse(actionResult, isDataRequest);
772
- // Client submit wants just the action's data (it revalidates the loader itself); a native
773
- // form POST re-renders the full page (loader re-runs) with the action data.
774
- if (isDataRequest) {
775
- // An action may `defer()` slow parts of its result - stream them (critical data first, then
776
- // each deferred as it settles) exactly like a loader; a non-deferred action returns one JSON.
777
- const { forClient, deferred } = prepareDeferred(actionResult);
778
- if (deferred.length === 0)
779
- return Response.json(actionResult ?? null, { headers: revalidateHeader });
780
- return new Response(ndjsonStream(forClient, deferred), {
781
- headers: { "content-type": "application/x-ndjson; charset=utf-8", ...revalidateHeader },
782
- });
783
- }
784
- // Derived once and shared: the loader's `ctx.search` and the render's `RenderProps.search` are the
785
- // SAME effective (layout chain + page) value, and the client mount rebuilds it identically. The
786
- // layout modules are already loaded (the action ran through their gates above).
787
- const search = searchChainOf(layoutModules, mod, c.req);
788
- const data = mod.loader
789
- ? await mod.loader({
790
- params: c.params,
791
- request: c.req,
792
- req: c.req,
793
- api,
794
- env: c.env,
795
- draft,
796
- search,
797
- })
798
- : null;
799
- const { chain, head } = resolveChainAndHead(layoutModules, mod, {
800
- data,
801
- params: c.params,
802
- origin: originOf(c.req),
803
- });
804
- const hydrateRoute = mod.hydrate !== false;
805
- // A full-page POST streams the action's `defer()`'d parts mid-document behind `<Await>` too -
806
- // `renderPage` splits `actionData` like loader data (works with JS off; hydrates after).
807
- return renderPageResult({
808
- adapter,
809
- chain,
810
- data,
811
- actionData: actionResult,
812
- head,
813
- clientEntry,
814
- routeId: route.id,
815
- params: c.params,
816
- path: pathOf(c.req),
817
- search,
818
- hydrate: hydrateRoute,
819
- ...preloadOf(route.id),
820
- ...stylesOf(route.id),
821
- prerenderedPaths: options.prerenderedPaths ?? [],
822
- ...titleOption,
823
- });
824
- });
825
- }
826
- // Register llms.txt & llms-full.txt
827
- const llmsOptions = { includeLocalGuidelines: options.publishLocalGuidelines === true };
828
- app.register("GET", "/llms.txt", undefined, async () => {
829
- const text = await generateLlmsTxt(false, manifest.routes, api, llmsOptions);
830
- return new Response(text, {
831
- headers: { "content-type": "text/plain; charset=utf-8" },
832
- });
833
- });
834
- app.register("GET", "/llms-full.txt", undefined, async () => {
835
- const text = await generateLlmsTxt(true, manifest.routes, api, llmsOptions);
836
- return new Response(text, {
837
- headers: { "content-type": "text/plain; charset=utf-8" },
838
- });
254
+ Object.defineProperty(app, NIFRA_BACKEND_EVIDENCE, {
255
+ configurable: false,
256
+ enumerable: false,
257
+ value: evidenceProvider,
258
+ writable: false,
839
259
  });
840
- // Wildcard catch-all: unmatched paths render `_404` (404), or a plain text 404 if absent.
841
- app.register("GET", "/*", undefined, (c) => renderNotFound(pathOf(c.req)));
842
260
  return app;
843
261
  }
262
+ /** Resolve one composed, token-only evidence snapshot for a web app and its mounted surfaces. */
263
+ export async function webProjectEvidence(source) {
264
+ const provider = evidenceProviderOf(source);
265
+ if (provider === undefined) {
266
+ throw new TypeError("webProjectEvidence(): expected an app created by createWebApp");
267
+ }
268
+ return provider.call(source);
269
+ }
270
+ // Up to this many characters of JSON, the prerendered set rides inline in each page's handover.
271
+ const INLINE_PRERENDERED_CHARS = 4096;
272
+ /** The prerendered set as a served, content-versioned list, when it is too large to inline. */
273
+ function servedPrerenderedList(paths) {
274
+ if (paths === undefined || paths.length === 0)
275
+ return undefined;
276
+ const body = JSON.stringify(paths);
277
+ if (body.length <= INLINE_PRERENDERED_CHARS)
278
+ return undefined;
279
+ // FNV-1a: a version that changes with the list is all the URL needs; a stale cached list only
280
+ // costs a fallback to the dynamic data request.
281
+ let hash = 0x811c9dc5;
282
+ for (let i = 0; i < body.length; i++)
283
+ hash = Math.imul(hash ^ body.charCodeAt(i), 0x01000193);
284
+ const version = (hash >>> 0).toString(16).padStart(8, "0");
285
+ return { body, version, url: `${PRERENDERED_LIST_PATH}?v=${version}` };
286
+ }
844
287
  //# sourceMappingURL=web-app.js.map