@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
@@ -0,0 +1,1371 @@
1
+ import {
2
+ type BackendPlatformBinder,
3
+ NIFRA_BACKEND_BIND_PLATFORM,
4
+ NIFRA_PLATFORM_CLIENT_IP_DERIVED,
5
+ } from "@nifrajs/core/mount"
6
+ import { type Platform, type ResponseResult, status as statusResult } from "@nifrajs/core/server"
7
+ import {
8
+ type BoundaryRequestCtx,
9
+ type BoundaryStates,
10
+ boundaryDescriptors,
11
+ resolveStaticBoundaries,
12
+ type StaticBoundaryCache,
13
+ startDynamicBoundaries,
14
+ } from "../boundary.ts"
15
+ import type { CspPolicy } from "../csp.ts"
16
+ import { type CssLoadingMode, DEFAULT_CSS_LOADING, normalizeCssLoading } from "../css-contract.ts"
17
+ import { defer, ndjsonStream, prepareDeferred } from "../deferred.ts"
18
+ import { isDraftEnabled } from "../draft.ts"
19
+ import { routeTags } from "../isr.ts"
20
+ import type {
21
+ LayoutEntry,
22
+ LoaderContext,
23
+ Manifest,
24
+ Meta,
25
+ MetaArgs,
26
+ NotFoundEntry,
27
+ RouteEntry,
28
+ RouteModule,
29
+ ShouldRevalidate,
30
+ } from "../manifest.ts"
31
+ import type { NonceResolver } from "../nonce.ts"
32
+ import type { MatchChain, RenderAdapter } from "../render-seam.ts"
33
+ import {
34
+ createMatcher,
35
+ DATA_HEADER,
36
+ NAV_FROM_HEADER,
37
+ REDIRECT_HEADER,
38
+ RETAIN_HEADER,
39
+ REVALIDATE_HEADER,
40
+ STATUS_HEADER,
41
+ } from "../router.ts"
42
+ import { searchOf, searchOfChain } from "../search.ts"
43
+ import { mergeHeads, resolveMeta } from "./head-merge.ts"
44
+ import {
45
+ EMPTY_RETAIN,
46
+ type HeadersLike,
47
+ isControlFlow,
48
+ isResponseResult,
49
+ isStatusSignal,
50
+ type LoadedLayoutModules,
51
+ layoutErrorId,
52
+ type RenderAssemblyCache,
53
+ type RenderedPage,
54
+ type RevalidateResult,
55
+ renderPageResult,
56
+ STATUS_SIGNAL,
57
+ STATUS_TEXT,
58
+ type StatusSignal,
59
+ scopeParams,
60
+ tagLayoutError,
61
+ withDuplicateInstanceHint,
62
+ } from "./render-document.ts"
63
+ import { urlPartsFor } from "./request-url.ts"
64
+ import {
65
+ ACTION_SCOPE,
66
+ type CoreResponseControls,
67
+ DATA_RESPONSE_HEADERS,
68
+ MIDDLEWARE_SCOPE,
69
+ PAGE_SCOPE,
70
+ PageResponseControls,
71
+ PRIVATE_NO_STORE,
72
+ PRIVATE_PAGE_HEADERS,
73
+ privateOutcome,
74
+ varyOnDataHeader,
75
+ } from "./response-controls.ts"
76
+
77
+ export type { NonceResolver } from "../nonce.ts"
78
+
79
+ /** Structural context supplied by the core server to one page route handler. */
80
+ export interface PageRouteContext<Env = unknown> {
81
+ readonly params: Record<string, string>
82
+ readonly req: Request
83
+ readonly env: Env
84
+ /** The serving app's response controls; a loader's `ctx.set` queues its cookies through them. */
85
+ readonly set: CoreResponseControls
86
+ /** The visitor's IP as the serving app derived it; forwarded to a request-bound `ctx.api`. */
87
+ readonly clientIp?: string | undefined
88
+ readonly waitUntil?: (promise: Promise<unknown>) => void
89
+ }
90
+
91
+ /** The page slot of an `ssr = false` route with no `HydrateFallback`: a component that renders nothing. */
92
+ const EMPTY_LEAF = (): null => null
93
+
94
+ /** The request-scoping seam of an in-process `api` (see `inProcessClient`), if it carries one. */
95
+ function platformBinderOf(api: unknown): BackendPlatformBinder | undefined {
96
+ if ((typeof api !== "object" && typeof api !== "function") || api === null) return undefined
97
+ const bind = (api as { [NIFRA_BACKEND_BIND_PLATFORM]?: unknown })[NIFRA_BACKEND_BIND_PLATFORM]
98
+ return typeof bind === "function" ? (bind as BackendPlatformBinder) : undefined
99
+ }
100
+
101
+ /**
102
+ * The page request's platform identity for loader calls: `clientIp` (read lazily, so a backend that
103
+ * never reads `c.clientIp` never pays the socket lookup), `env` and `waitUntil` - and nothing else. The
104
+ * page request's headers (cookie, authorization) never travel; a loader passes them explicitly if it
105
+ * wants an authenticated call. The prototype marker tells the backend its `clientIp` is already derived,
106
+ * so its own trust declaration does not re-read forwarding headers the synthesized request does not
107
+ * carry; a spread copy drops both the getter and the marker, so it falls back to re-deriving.
108
+ */
109
+ class LoaderPlatform {
110
+ readonly env: unknown
111
+ readonly waitUntil: ((promise: Promise<unknown>) => void) | undefined
112
+ readonly #c: PageRouteContext<unknown>
113
+ constructor(c: PageRouteContext<unknown>) {
114
+ this.#c = c
115
+ this.env = c.env
116
+ this.waitUntil = c.waitUntil
117
+ }
118
+ get clientIp(): string | undefined {
119
+ return this.#c.clientIp
120
+ }
121
+ }
122
+ ;(LoaderPlatform.prototype as { [NIFRA_PLATFORM_CLIENT_IP_DERIVED]?: true })[
123
+ NIFRA_PLATFORM_CLIENT_IP_DERIVED
124
+ ] = true
125
+
126
+ /**
127
+ * A redirect answering a data request (a client navigation or form submit), as the client router reads
128
+ * one: fetch would follow a 3xx and hand the router another page's data, so the target rides
129
+ * `x-nifra-redirect` on a 204 that keeps the redirect's own headers. `undefined` for any other outcome
130
+ * or request.
131
+ */
132
+ function dataRedirect(req: Request, outcome: unknown): Response | ResponseResult | undefined {
133
+ if (req.headers.get(DATA_HEADER) === null) return undefined
134
+ if (isResponseResult(outcome)) {
135
+ const plain = outcome.plain
136
+ if (plain === undefined) return dataRedirect(req, outcome.toResponse())
137
+ if (plain.status < 300 || plain.status >= 400) return undefined
138
+ const { location, ...own } = plain.headers ?? {}
139
+ if (location === undefined) return undefined
140
+ return statusResult(204, undefined, {
141
+ headers: {
142
+ ...own,
143
+ [REDIRECT_HEADER]: appTarget(location, req.url),
144
+ ...DATA_RESPONSE_HEADERS,
145
+ },
146
+ })
147
+ }
148
+ if (!(outcome instanceof Response) || outcome.status < 300 || outcome.status >= 400) {
149
+ return undefined
150
+ }
151
+ const location = outcome.headers.get("location")
152
+ if (location === null) return undefined
153
+ const headers = new Headers(outcome.headers)
154
+ headers.delete("location")
155
+ headers.delete("content-length")
156
+ headers.set(REDIRECT_HEADER, appTarget(location, req.url))
157
+ for (const [name, value] of Object.entries(DATA_RESPONSE_HEADERS)) headers.set(name, value)
158
+ return new Response(null, { status: 204, headers })
159
+ }
160
+
161
+ /** A same-origin target as a path, which the client router can follow without loading a document. */
162
+ function appTarget(location: string, requestUrl: string): string {
163
+ let target: URL
164
+ try {
165
+ target = new URL(location, requestUrl)
166
+ } catch {
167
+ return location // the browser fails on it the same way when it loads it
168
+ }
169
+ return target.origin === new URL(requestUrl).origin
170
+ ? target.pathname + target.search + target.hash
171
+ : location
172
+ }
173
+
174
+ export interface PageExecutionOptions<Env = unknown> {
175
+ readonly adapter: RenderAdapter
176
+ readonly manifest: Manifest
177
+ readonly clientEntry: string
178
+ readonly title?: string
179
+ readonly api?: unknown
180
+ readonly draftSecret?: string
181
+ readonly routePreload?: Readonly<Record<string, readonly string[]>>
182
+ readonly styles?: readonly string[]
183
+ readonly routeStyles?: Readonly<Record<string, readonly string[]>>
184
+ readonly cssLoading?: CssLoadingMode
185
+ readonly prerenderedPaths?: readonly string[]
186
+ /** Where pages fetch {@link prerenderedPaths} from when the list is too large to inline. */
187
+ readonly prerenderedListUrl?: string
188
+ readonly staticFallbacks?: Readonly<Record<string, "ssr" | "404">>
189
+ readonly staticBoundaryCache?: StaticBoundaryCache
190
+ /** Per-request CSP nonce for framework-owned executable document scripts. */
191
+ readonly nonce?: NonceResolver<Env>
192
+ /** A hash-based CSP policy: every document is rendered under it, carrying `nonce` only when needed. */
193
+ readonly csp?: CspPolicy
194
+ /** Set once a cache wrapper attaches; until then a route's freshness and tags stay off the response. */
195
+ readonly cacheChannel?: { readonly enabled: boolean }
196
+ readonly onLoaderError?: (
197
+ error: unknown,
198
+ ctx: {
199
+ readonly request: Request
200
+ readonly params: Readonly<Record<string, string>>
201
+ readonly route: string
202
+ },
203
+ ) => void
204
+ }
205
+
206
+ type PageResponse = Response | ResponseResult | RenderedPage
207
+
208
+ export interface PageRequestExecutor<Env = unknown> {
209
+ get(route: RouteEntry): (ctx: PageRouteContext<Env>) => Promise<PageResponse>
210
+ post(route: RouteEntry): (ctx: PageRouteContext<Env>) => Promise<PageResponse>
211
+ /** Answer a URL no route matched: the nearest nested `_404` for it, else the root one. */
212
+ fallback(ctx: PageRouteContext<Env>): Promise<PageResponse>
213
+ }
214
+
215
+ /** One URL pattern a nested `_404` answers, as the executor indexes it. */
216
+ interface NotFoundTarget {
217
+ readonly id: string
218
+ readonly page: NotFoundEntry
219
+ /** The pattern's param names in order; the index holds them by position. */
220
+ readonly names: readonly string[]
221
+ /** The page's layout chain as a route, so layouts, gates and boundaries run as they do for one. */
222
+ readonly route: RouteEntry
223
+ }
224
+
225
+ /** The browser's side of a client navigation: the page it is leaving and the layout slots it holds. */
226
+ type RetainContext = {
227
+ readonly requested: ReadonlySet<number>
228
+ readonly from: RouteEntry
229
+ readonly fromParams: Readonly<Record<string, string>>
230
+ readonly fromUrl: URL
231
+ readonly toUrl: URL
232
+ }
233
+
234
+ type LayoutRun = {
235
+ readonly modules: LoadedLayoutModules
236
+ readonly layoutData: readonly unknown[] | undefined
237
+ readonly retained: readonly number[]
238
+ readonly pending: Promise<unknown> | undefined
239
+ }
240
+
241
+ /**
242
+ * Own the page request program behind one private seam.
243
+ *
244
+ * `createWebApp` remains responsible for app construction, mount ordering, and route registration.
245
+ * This module owns the page-specific contract: context, layout execution, control-flow precedence,
246
+ * data/document assembly, and error/status rendering. GET and POST retain their distinct ordering
247
+ * (ordinary layout data may run after an action; a gate must run before it) while sharing the same
248
+ * outcome and render decisions.
249
+ */
250
+ export function createPageRequestExecutor<Env = unknown>(
251
+ options: PageExecutionOptions<Env>,
252
+ ): PageRequestExecutor<Env> {
253
+ const { adapter, manifest, clientEntry, api } = options
254
+ // Bound once per render, so every loader, action and boundary in that render shares one view of the
255
+ // in-process client carrying the visitor's platform. An `api` without the seam passes through as-is.
256
+ const bindApi = platformBinderOf(api)
257
+ // One view per render, shared by every loader, action and boundary it runs.
258
+ const apiFor = (c: PageRouteContext<Env>): unknown =>
259
+ bindApi === undefined ? api : bindApi.call(api, new LoaderPlatform(c) as Platform)
260
+ const cssLoading = normalizeCssLoading(options.cssLoading ?? DEFAULT_CSS_LOADING)
261
+ const titleOption = options.title === undefined ? {} : { title: options.title }
262
+ const cspOption = options.csp === undefined ? {} : { csp: options.csp }
263
+ const prerenderedSet = new Set(options.prerenderedPaths ?? [])
264
+ const prerenderedHandover =
265
+ options.prerenderedListUrl === undefined
266
+ ? { prerenderedPaths: options.prerenderedPaths ?? [] }
267
+ : { prerenderedListUrl: options.prerenderedListUrl }
268
+ const matchManifestRoute = createMatcher(
269
+ manifest.routes.map((route) => ({ routeId: route.id, pattern: route.pattern })),
270
+ )
271
+ const routeById = new Map(manifest.routes.map((route) => [route.id, route]))
272
+
273
+ // A nested `_404` answers an unmatched URL from inside the catch-all, never as a route of its own:
274
+ // a registered `/admin/*` would take URLs from a route the router only reaches by backtracking.
275
+ // Param names are indexed by position, so two directories that name one segment differently share
276
+ // the index; each target keeps its own names to read a match back.
277
+ const notFoundTargets: NotFoundTarget[] = []
278
+ const notFoundPatterns: Array<{ routeId: string; pattern: string }> = []
279
+ for (const [id, page] of Object.entries(manifest.notFounds ?? {})) {
280
+ for (const scope of page.scopes) {
281
+ const names: string[] = []
282
+ const pattern = scope.pattern.replace(
283
+ /([:*])([A-Za-z_][A-Za-z0-9_]*)/g,
284
+ (_match, sigil: string, name: string) => `${sigil}p${names.push(name) - 1}`,
285
+ )
286
+ notFoundPatterns.push({ routeId: String(notFoundTargets.length), pattern })
287
+ notFoundTargets.push({
288
+ id,
289
+ page,
290
+ names,
291
+ route: {
292
+ id,
293
+ pattern: scope.pattern,
294
+ layoutIds: page.layoutIds,
295
+ layoutParams: scope.layoutParams,
296
+ ...(page.middlewareIds === undefined
297
+ ? {}
298
+ : {
299
+ middlewareIds: page.middlewareIds,
300
+ ...(scope.middlewareParams === undefined
301
+ ? {}
302
+ : { middlewareParams: scope.middlewareParams }),
303
+ }),
304
+ errorIds: page.errorIds,
305
+ file: page.file,
306
+ load: page.load,
307
+ },
308
+ })
309
+ }
310
+ }
311
+ const matchNotFoundScope =
312
+ notFoundPatterns.length === 0 ? undefined : createMatcher(notFoundPatterns)
313
+
314
+ const draftFlag = (req: Request): Promise<boolean> =>
315
+ options.draftSecret === undefined
316
+ ? Promise.resolve(false)
317
+ : isDraftEnabled(req, options.draftSecret)
318
+
319
+ const preloadOf = (id: string): { preload?: readonly string[] } => {
320
+ const chunks = options.routePreload?.[id]
321
+ return chunks ? { preload: chunks } : {}
322
+ }
323
+
324
+ const resolveNonce = (
325
+ request: Request,
326
+ env: Env,
327
+ ): string | undefined | Promise<string | undefined> =>
328
+ options.nonce === undefined ? undefined : options.nonce({ request, env })
329
+
330
+ const stylesOf = (id: string): { styles?: readonly string[]; cssLoading?: CssLoadingMode } => {
331
+ const perRoute = options.routeStyles?.[id]
332
+ const styles = perRoute ?? options.styles
333
+ if (styles === undefined || styles.length === 0) return {}
334
+ return {
335
+ styles,
336
+ ...(cssLoading === "deferred" ? { cssLoading } : {}),
337
+ }
338
+ }
339
+
340
+ // Load a route's layout modules once for the current execution. The render path needs each whole
341
+ // module for its component + meta, while the data path also needs loader/gate/boundary declarations.
342
+ const loadLayoutModules = async (route: RouteEntry): Promise<LoadedLayoutModules> => {
343
+ const modules = await Promise.all(
344
+ route.layoutIds.map((id) => (manifest.layouts[id] as LayoutEntry).load()),
345
+ )
346
+ for (let i = 0; i < modules.length; i++) {
347
+ if ((modules[i] as { action?: unknown }).action === undefined) continue
348
+ const id = route.layoutIds[i] as string
349
+ throw new Error(
350
+ `[nifra/web] "${(manifest.layouts[id] as LayoutEntry).file}" exports an \`action\`, but layouts do not run one - only route files do. Move the mutation into the route that submits to it.`,
351
+ )
352
+ }
353
+ return modules
354
+ }
355
+
356
+ // No modules, data, or per-request state: reuse the settled result for page-only routes.
357
+ const emptyLayoutRun: LayoutRun = {
358
+ modules: [],
359
+ layoutData: undefined,
360
+ retained: [],
361
+ pending: undefined,
362
+ }
363
+
364
+ /** Run layout loaders, awaiting gates before starting the page loader and retaining only safe data. */
365
+ const runLayoutChain = async (
366
+ route: RouteEntry,
367
+ ctx: LoaderContext,
368
+ controls: PageResponseControls,
369
+ retain?: RetainContext,
370
+ ): Promise<LayoutRun> => {
371
+ if (route.layoutIds.length === 0) return emptyLayoutRun
372
+ const modules = await loadLayoutModules(route)
373
+ if (!modules.some((m) => m.loader !== undefined)) {
374
+ return { modules, layoutData: undefined, retained: [], pending: undefined }
375
+ }
376
+ const results: unknown[] = new Array(modules.length).fill(null)
377
+ const retained: number[] = []
378
+ const pending: Array<Promise<void>> = []
379
+ for (let i = 0; i < modules.length; i++) {
380
+ const layout = modules[i] as LoadedLayoutModules[number]
381
+ const loader = layout.loader
382
+ if (loader === undefined) continue
383
+ if (retain !== undefined && layout.gate !== true) {
384
+ let keep: boolean
385
+ try {
386
+ keep = keepsLayoutData(route, i, layout, ctx.params, retain)
387
+ } catch (err) {
388
+ throw tagLayoutError(err, route.layoutIds[i] as string)
389
+ }
390
+ if (keep) {
391
+ retained.push(i)
392
+ continue
393
+ }
394
+ }
395
+ const scoped: LoaderContext = {
396
+ ...ctx,
397
+ params: scopeParams(ctx.params, route.layoutParams?.[i]),
398
+ set: controls.scope(i),
399
+ }
400
+ if (modules[i]?.gate === true) {
401
+ try {
402
+ results[i] = await loader(scoped)
403
+ } catch (err) {
404
+ throw tagLayoutError(err, route.layoutIds[i] as string)
405
+ }
406
+ continue
407
+ }
408
+ const layoutId = route.layoutIds[i] as string
409
+ const index = i
410
+ try {
411
+ const value = loader(scoped)
412
+ // Scalars settle synchronously. Objects keep await's intrinsic promise/thenable
413
+ // assimilation: property-presence and prototype probes are unsafe for proxies.
414
+ if (value !== null && (typeof value === "object" || typeof value === "function")) {
415
+ pending.push(
416
+ (async () => {
417
+ try {
418
+ results[index] = await value
419
+ } catch (err) {
420
+ throw tagLayoutError(err, layoutId)
421
+ }
422
+ })(),
423
+ )
424
+ } else results[index] = value
425
+ } catch (err) {
426
+ // Preserve a synchronous throw as a pending layout failure, just like the async lane.
427
+ pending.push(Promise.reject(tagLayoutError(err, layoutId)))
428
+ }
429
+ }
430
+ return {
431
+ modules,
432
+ layoutData: results,
433
+ retained,
434
+ pending:
435
+ pending.length === 0 ? undefined : pending.length === 1 ? pending[0] : Promise.all(pending),
436
+ }
437
+ }
438
+
439
+ /** Run only authorization gates before an action; ordinary layout data runs after a native action. */
440
+ const runLayoutGates = async (
441
+ route: RouteEntry,
442
+ ctx: LoaderContext,
443
+ controls: PageResponseControls,
444
+ ): Promise<LoadedLayoutModules> => {
445
+ const modules = await loadLayoutModules(route)
446
+ for (let i = 0; i < modules.length; i++) {
447
+ const mod = modules[i]
448
+ if (mod?.gate !== true || mod.loader === undefined) continue
449
+ try {
450
+ await mod.loader({
451
+ ...ctx,
452
+ params: scopeParams(ctx.params, route.layoutParams?.[i]),
453
+ set: controls.scope(i),
454
+ })
455
+ } catch (err) {
456
+ throw tagLayoutError(err, route.layoutIds[i] as string)
457
+ }
458
+ }
459
+ return modules
460
+ }
461
+
462
+ /** Outermost first; a failure carries its directory so the nearest `_error` above it renders. */
463
+ const runMiddleware = async (
464
+ route: RouteEntry,
465
+ ctx: LoaderContext,
466
+ controls: PageResponseControls,
467
+ ): Promise<void> => {
468
+ const ids = route.middlewareIds ?? []
469
+ for (let i = 0; i < ids.length; i++) {
470
+ const id = ids[i] as string
471
+ const entry = manifest.middlewares?.[id] as LayoutEntry
472
+ const dirTag = `${id.slice(0, id.length - "_middleware".length)}_layout`
473
+ let outcome: unknown
474
+ try {
475
+ const middleware = (await entry.load()).default
476
+ // A `_layout.backend.ts` that exports no middleware is a layout data module, nothing more.
477
+ if (middleware === undefined) continue
478
+ if (typeof middleware !== "function") {
479
+ throw new Error(
480
+ `[nifra/web] "${entry.file}" exports a middleware that is not a function.`,
481
+ )
482
+ }
483
+ outcome = await middleware({
484
+ ...ctx,
485
+ params: scopeParams(ctx.params, route.middlewareParams?.[i]),
486
+ set: controls.scope(MIDDLEWARE_SCOPE + i),
487
+ })
488
+ } catch (err) {
489
+ throw tagLayoutError(err, dirTag)
490
+ }
491
+ if (outcome === undefined) continue
492
+ throw tagLayoutError(
493
+ isControlFlow(outcome)
494
+ ? outcome
495
+ : new Error(
496
+ `[nifra/web] the middleware in "${entry.file}" returned a value. Middleware returns nothing to let the request through, or a redirect(), a status such as notFound(), or a Response to answer it.`,
497
+ ),
498
+ dirTag,
499
+ )
500
+ }
501
+ }
502
+
503
+ const retainedIndices = (header: string | null): ReadonlySet<number> => {
504
+ if (header === null || header === "") return EMPTY_RETAIN
505
+ const out = new Set<number>()
506
+ for (const part of header.split(",")) {
507
+ if (!/^\d+$/.test(part)) continue
508
+ out.add(Number(part))
509
+ }
510
+ return out
511
+ }
512
+
513
+ /**
514
+ * The browser's side of a client navigation: the page it is leaving and the layout slots it asked
515
+ * to keep. Undefined unless this is a data request whose `from` is same-origin and routable.
516
+ */
517
+ const retainContextOf = (req: Request): RetainContext | undefined => {
518
+ if (req.headers.get(DATA_HEADER) === null) return undefined
519
+ const requested = retainedIndices(req.headers.get(RETAIN_HEADER))
520
+ const from = req.headers.get(NAV_FROM_HEADER)
521
+ if (requested.size === 0 || from === null) return undefined
522
+ let toUrl: URL
523
+ let fromUrl: URL
524
+ try {
525
+ toUrl = new URL(req.url)
526
+ fromUrl = new URL(from, toUrl)
527
+ } catch {
528
+ return undefined
529
+ }
530
+ if (fromUrl.origin !== toUrl.origin) return undefined
531
+ const previousMatch = matchManifestRoute(fromUrl.pathname + fromUrl.search)
532
+ if (previousMatch === null) return undefined
533
+ const previous = routeById.get(previousMatch.routeId)
534
+ if (previous === undefined) return undefined
535
+ return { requested, from: previous, fromParams: previousMatch.params, fromUrl, toUrl }
536
+ }
537
+
538
+ /** A query change re-runs layout loaders by default: any can read it, and none declares keys. */
539
+ const keepsLayoutData = (
540
+ route: RouteEntry,
541
+ index: number,
542
+ layout: LoadedLayoutModules[number],
543
+ params: Readonly<Record<string, string>>,
544
+ retain: RetainContext,
545
+ ): boolean => {
546
+ if (!retain.requested.has(index)) return false
547
+ const previous = retain.from
548
+ if (route.layoutIds[index] !== previous.layoutIds[index]) return false
549
+ const owned = route.layoutParams?.[index] ?? []
550
+ const previouslyOwned = previous.layoutParams?.[index] ?? []
551
+ const defaultShouldRevalidate =
552
+ retain.fromUrl.search !== retain.toUrl.search ||
553
+ owned.length !== previouslyOwned.length ||
554
+ owned.some((name, i) => name !== previouslyOwned[i]) ||
555
+ owned.some((name) => params[name] !== retain.fromParams[name])
556
+ const decide = layout.shouldRevalidate
557
+ if (typeof decide !== "function") return !defaultShouldRevalidate
558
+ // Copies, so a hook that mutates its arguments cannot reach the request's params or the next
559
+ // layout's view of the navigation.
560
+ const answer = (decide as ShouldRevalidate)({
561
+ currentUrl: new URL(retain.fromUrl),
562
+ nextUrl: new URL(retain.toUrl),
563
+ currentParams: { ...retain.fromParams },
564
+ nextParams: { ...params },
565
+ defaultShouldRevalidate,
566
+ })
567
+ return answer === false
568
+ }
569
+
570
+ const loaderSearch = (searchSchema: RouteModule["searchSchema"], request: Request) =>
571
+ searchOf(searchSchema, urlPartsFor(request).search)
572
+
573
+ const searchChainOf = (
574
+ layoutMods: LoadedLayoutModules,
575
+ mod: RouteModule,
576
+ request: Request,
577
+ ): Record<string, unknown> =>
578
+ searchOfChain(
579
+ [...layoutMods.map((m) => m.searchSchema), mod.searchSchema],
580
+ urlPartsFor(request).search,
581
+ )
582
+
583
+ // `ssr = false`: the slot holds `HydrateFallback`, which the browser hydrates before the page.
584
+ const serverLeafOf = (route: RouteEntry, mod: RouteModule): unknown => {
585
+ if (mod.ssr !== false) return mod.default
586
+ if (mod.hydrate === false) {
587
+ throw new Error(
588
+ `[nifra/web] route "${route.id}" sets both \`ssr = false\` and \`hydrate = false\`: the ` +
589
+ "server skips its component and no client would ever render it. Remove one of the two.",
590
+ )
591
+ }
592
+ return mod.HydrateFallback ?? EMPTY_LEAF
593
+ }
594
+
595
+ const resolveChainAndHead = (
596
+ layoutModules: LoadedLayoutModules,
597
+ page: RouteModule,
598
+ metaArgs: MetaArgs,
599
+ leaf: unknown = page.default,
600
+ ): { chain: unknown[]; head: Meta } => {
601
+ const chain = [...layoutModules.map((m) => m.default), leaf]
602
+ const heads = [
603
+ ...layoutModules.map((m) => resolveMeta(m.meta, metaArgs)),
604
+ resolveMeta(page.meta, metaArgs),
605
+ ]
606
+ return { chain, head: mergeHeads(heads) }
607
+ }
608
+
609
+ /** What `useMatches` reports for a chain: the layouts' ids and `handle`s, then the page's. */
610
+ const matchChainOf = (
611
+ layoutIds: readonly string[],
612
+ layoutModules: LoadedLayoutModules,
613
+ pageId: string,
614
+ page: { readonly handle?: unknown },
615
+ ): MatchChain => ({
616
+ ids: [...layoutIds, pageId],
617
+ handles: [...layoutModules.map((layout) => layout.handle), page.handle],
618
+ })
619
+
620
+ const dirOfId = (id: string, suffix: string): string =>
621
+ id === suffix ? "" : id.slice(0, id.length - suffix.length - 1)
622
+
623
+ const boundaryFor = (route: RouteEntry, err: unknown): string | undefined => {
624
+ const ids = route.errorIds ?? []
625
+ const failing = layoutErrorId(err)
626
+ if (failing === undefined) return ids.at(-1)
627
+ const layoutDir = dirOfId(failing, "_layout")
628
+ for (let i = ids.length - 1; i >= 0; i--) {
629
+ const errDir = dirOfId(ids[i] as string, "_error")
630
+ if (errDir === "" || errDir === layoutDir || layoutDir.startsWith(`${errDir}/`)) return ids[i]
631
+ }
632
+ return undefined
633
+ }
634
+
635
+ const originOf = (req: Request): string => {
636
+ try {
637
+ return new URL(req.url).origin
638
+ } catch {
639
+ return ""
640
+ }
641
+ }
642
+
643
+ const pathOf = (req: Request): string => {
644
+ try {
645
+ const parts = urlPartsFor(req)
646
+ return parts.pathname + parts.search
647
+ } catch {
648
+ return "/"
649
+ }
650
+ }
651
+
652
+ const renderError = async (
653
+ route: RouteEntry,
654
+ errorId: string,
655
+ err: unknown,
656
+ personalized: boolean,
657
+ nonce?: string,
658
+ ): Promise<PageResponse> => {
659
+ const errDir = dirOfId(errorId, "_error")
660
+ const keptLayoutIds = route.layoutIds.filter((id) => {
661
+ const ld = dirOfId(id, "_layout")
662
+ return ld === "" || ld === errDir || errDir.startsWith(`${ld}/`)
663
+ })
664
+ const layouts = await Promise.all(
665
+ keptLayoutIds.map((id) =>
666
+ (manifest.layouts[id] as LayoutEntry).load().then((m) => m.default),
667
+ ),
668
+ )
669
+ const { default: errComp } = await (manifest.errors?.[errorId] as LayoutEntry).load()
670
+ const e = err instanceof Error ? err : new Error(String(err))
671
+ // An error's text can carry a connection string or a query, so a production page gets neither.
672
+ const production = typeof process === "undefined" || process.env.NODE_ENV === "production"
673
+ return renderPageResult({
674
+ adapter,
675
+ chain: [...layouts, errComp],
676
+ data: production
677
+ ? { name: "Error", message: "Internal Server Error" }
678
+ : { name: e.name, message: e.message },
679
+ clientEntry,
680
+ routeId: errorId,
681
+ status: 500,
682
+ hydrate: false,
683
+ ...(personalized ? { headers: PRIVATE_PAGE_HEADERS } : {}),
684
+ ...(nonce === undefined ? {} : { nonce }),
685
+ ...cspOption,
686
+ ...titleOption,
687
+ })
688
+ }
689
+
690
+ const renderTerminalStatus = async (
691
+ status: number,
692
+ path?: string,
693
+ extraHeaders?: HeadersLike,
694
+ nonce?: string,
695
+ ): Promise<PageResponse> => {
696
+ const page = manifest.statusPages?.[String(status)] ?? manifest.notFound
697
+ if (page === undefined) {
698
+ const headers = new Headers(extraHeaders)
699
+ headers.set("content-type", "text/plain; charset=utf-8")
700
+ return new Response(STATUS_TEXT[status] ?? "Error", { status, headers })
701
+ }
702
+ const routeId = manifest.statusPages?.[String(status)] !== undefined ? `_${status}` : "_404"
703
+ const loaded = (await page.load()) as {
704
+ readonly default: unknown
705
+ readonly searchSchema?: RouteModule["searchSchema"]
706
+ readonly handle?: unknown
707
+ }
708
+ return renderPageResult({
709
+ adapter,
710
+ chain: [loaded.default],
711
+ data: null,
712
+ clientEntry,
713
+ routeId,
714
+ matchChain: { ids: [routeId], handles: [loaded.handle] },
715
+ ...(path !== undefined
716
+ ? {
717
+ search: searchOf(
718
+ loaded.searchSchema,
719
+ path.includes("?") ? path.slice(path.indexOf("?")) : "",
720
+ ),
721
+ }
722
+ : {}),
723
+ ...(path !== undefined ? { path } : {}),
724
+ status,
725
+ ...(extraHeaders !== undefined ? { headers: extraHeaders } : {}),
726
+ ...(nonce === undefined ? {} : { nonce }),
727
+ ...cspOption,
728
+ ...preloadOf(routeId),
729
+ ...stylesOf(routeId),
730
+ ...prerenderedHandover,
731
+ ...titleOption,
732
+ })
733
+ }
734
+
735
+ const renderStatusSignal = async (
736
+ req: Request,
737
+ env: Env,
738
+ signal: StatusSignal,
739
+ personalized: boolean,
740
+ ): Promise<PageResponse> => {
741
+ const { status, headers } = signal[STATUS_SIGNAL]
742
+ if (req.headers.get(DATA_HEADER) !== null) {
743
+ // Same URL as the status page, so it is never storable: a cache keyed on the URL alone would
744
+ // otherwise answer a document request with this empty body.
745
+ const responseHeaders = new Headers(headers)
746
+ responseHeaders.set(STATUS_HEADER, String(status))
747
+ responseHeaders.set("cache-control", PRIVATE_NO_STORE)
748
+ responseHeaders.set("vary", varyOnDataHeader(responseHeaders.get("vary")))
749
+ return new Response(null, { status, headers: responseHeaders })
750
+ }
751
+ const nonce = await resolveNonce(req, env)
752
+ if (headers === undefined && !personalized) {
753
+ return renderTerminalStatus(status, pathOf(req), undefined, nonce)
754
+ }
755
+ const documentHeaders = new Headers(headers)
756
+ if (headers !== undefined) {
757
+ documentHeaders.set("vary", varyOnDataHeader(documentHeaders.get("vary")))
758
+ }
759
+ // A cookie queued before the signal makes the status page specific to this visitor.
760
+ if (personalized) documentHeaders.set("cache-control", PRIVATE_NO_STORE)
761
+ return renderTerminalStatus(status, pathOf(req), documentHeaders, nonce)
762
+ }
763
+
764
+ const reportLoaderError = (
765
+ route: RouteEntry,
766
+ req: Request,
767
+ params: Record<string, string>,
768
+ err: unknown,
769
+ ): void => {
770
+ if (options.onLoaderError === undefined) return
771
+ try {
772
+ options.onLoaderError(err, { request: req, params, route: route.pattern })
773
+ } catch {
774
+ // A faulty reporter must never break error rendering.
775
+ }
776
+ }
777
+
778
+ const handleDataOrDocument = async (
779
+ req: Request,
780
+ env: Env,
781
+ route: RouteEntry,
782
+ params: Readonly<Record<string, string>>,
783
+ mod: RouteModule,
784
+ data: unknown,
785
+ layoutData: readonly unknown[] | undefined,
786
+ layoutModules: LoadedLayoutModules,
787
+ layoutRetained: readonly number[],
788
+ boundaryStates: BoundaryStates | undefined,
789
+ assemblyCache: RenderAssemblyCache | undefined,
790
+ responseHeaders: Record<string, string> | undefined,
791
+ personalized: boolean,
792
+ ): Promise<PageResponse> => {
793
+ if (isStatusSignal(data)) return renderStatusSignal(req, env, data, personalized)
794
+ if (isControlFlow(data)) return data
795
+ if (req.headers.get(DATA_HEADER) !== null) {
796
+ const pageSplit = prepareDeferred(data)
797
+ const layoutSplits: Array<ReturnType<typeof prepareDeferred>> = []
798
+ let offset = pageSplit.deferred.length
799
+ for (const entry of layoutData ?? []) {
800
+ const split = prepareDeferred(entry, offset)
801
+ layoutSplits.push(split)
802
+ offset += split.deferred.length
803
+ }
804
+ const deferred = [...pageSplit.deferred, ...layoutSplits.flatMap((split) => split.deferred)]
805
+ const boundarySplit =
806
+ boundaryStates === undefined ? undefined : prepareDeferred(boundaryStates, deferred.length)
807
+ const allDeferred = [
808
+ ...deferred,
809
+ ...(boundarySplit === undefined ? [] : boundarySplit.deferred),
810
+ ]
811
+ const payload =
812
+ layoutData === undefined && boundarySplit === undefined
813
+ ? pageSplit.forClient
814
+ : {
815
+ v: 1 as const,
816
+ data: pageSplit.forClient,
817
+ layoutData: layoutSplits.map((split) => split.forClient),
818
+ retained: layoutRetained,
819
+ ...(boundarySplit === undefined ? {} : { boundaries: boundarySplit.forClient }),
820
+ }
821
+ if (allDeferred.length === 0) {
822
+ return Response.json(payload ?? null, { headers: DATA_RESPONSE_HEADERS })
823
+ }
824
+ return new Response(ndjsonStream(payload, allDeferred), {
825
+ headers: {
826
+ "content-type": "application/x-ndjson; charset=utf-8",
827
+ ...DATA_RESPONSE_HEADERS,
828
+ },
829
+ })
830
+ }
831
+
832
+ const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env)
833
+ const { chain, head } = resolveChainAndHead(
834
+ layoutModules,
835
+ mod,
836
+ {
837
+ data,
838
+ params,
839
+ origin: originOf(req),
840
+ ...(nonce === undefined ? {} : { nonce }),
841
+ },
842
+ serverLeafOf(route, mod),
843
+ )
844
+ try {
845
+ return await renderPageResult({
846
+ adapter,
847
+ chain,
848
+ data,
849
+ head,
850
+ clientEntry,
851
+ routeId: route.id,
852
+ params,
853
+ path: pathOf(req),
854
+ search: searchChainOf(layoutModules, mod, req),
855
+ matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
856
+ hydrate: mod.hydrate !== false,
857
+ ...(layoutData !== undefined ? { layoutData } : {}),
858
+ ...(boundaryStates !== undefined ? { boundaries: boundaryStates } : {}),
859
+ ...preloadOf(route.id),
860
+ ...stylesOf(route.id),
861
+ ...prerenderedHandover,
862
+ ...(responseHeaders === undefined ? {} : { headers: responseHeaders }),
863
+ // A response that sets a cookie is one visitor's: it never advertises ISR freshness.
864
+ ...(mod.revalidate !== undefined && !personalized && options.cacheChannel?.enabled === true
865
+ ? { revalidate: mod.revalidate }
866
+ : {}),
867
+ ...(mod.revalidateTags !== undefined &&
868
+ !personalized &&
869
+ options.cacheChannel?.enabled === true
870
+ ? {
871
+ revalidateTags: routeTags(
872
+ mod.revalidateTags,
873
+ { params, url: new URL(req.url) },
874
+ route.id,
875
+ ),
876
+ }
877
+ : {}),
878
+ ...(mod.islandScripts !== undefined ? { islandScripts: mod.islandScripts } : {}),
879
+ ...(nonce === undefined ? {} : { nonce }),
880
+ ...cspOption,
881
+ ...titleOption,
882
+ ...(assemblyCache === undefined ? {} : { assemblyCache }),
883
+ })
884
+ } catch (err) {
885
+ if (isStatusSignal(err)) return renderStatusSignal(req, env, err, personalized)
886
+ if (isControlFlow(err)) throw err
887
+ reportLoaderError(route, req, { ...params }, err)
888
+ const errorId = boundaryFor(route, err)
889
+ if (errorId === undefined) throw err
890
+ return renderError(route, errorId, withDuplicateInstanceHint(err), personalized, nonce)
891
+ }
892
+ }
893
+
894
+ /** What a failed loader answers with, once a status signal has been ruled out. */
895
+ const renderLoaderFailure = async (
896
+ route: RouteEntry,
897
+ req: Request,
898
+ env: Env,
899
+ params: Record<string, string>,
900
+ err: unknown,
901
+ personalized: boolean,
902
+ ): Promise<PageResponse> => {
903
+ if (isControlFlow(err)) throw err
904
+ reportLoaderError(route, req, params, err)
905
+ const errorId = boundaryFor(route, err)
906
+ if (errorId === undefined) throw err
907
+ if (req.headers.get(DATA_HEADER) !== null) {
908
+ return new Response("Internal Server Error", {
909
+ status: 500,
910
+ headers: { "content-type": "text/plain; charset=utf-8" },
911
+ })
912
+ }
913
+ return renderError(
914
+ route,
915
+ errorId,
916
+ withDuplicateInstanceHint(err),
917
+ personalized,
918
+ options.nonce === undefined ? undefined : await resolveNonce(req, env),
919
+ )
920
+ }
921
+
922
+ /**
923
+ * Render a nested `_404` inside its layouts. `layoutModules` and `layoutData` are exactly the
924
+ * layouts at or above the page, their gates already passed.
925
+ */
926
+ const renderNestedNotFound = async (
927
+ req: Request,
928
+ env: Env,
929
+ id: string,
930
+ page: NotFoundEntry,
931
+ params: Readonly<Record<string, string>>,
932
+ layoutModules: LoadedLayoutModules,
933
+ layoutData: readonly unknown[] | undefined,
934
+ personalized: boolean,
935
+ signalHeaders?: HeadersLike,
936
+ ): Promise<PageResponse> => {
937
+ // The page can show what a layout loaded for this visitor, and a 404 is storable by default.
938
+ const isPrivate = personalized || layoutData !== undefined
939
+ if (req.headers.get(DATA_HEADER) !== null) {
940
+ // No status header: the page has no client chunk, so the navigation falls back to loading the
941
+ // URL as a document, which renders it here.
942
+ const headers = new Headers(signalHeaders)
943
+ headers.set("cache-control", PRIVATE_NO_STORE)
944
+ headers.set("vary", varyOnDataHeader(headers.get("vary")))
945
+ return new Response(null, { status: 404, headers })
946
+ }
947
+ const mod = await page.load()
948
+ const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env)
949
+ const { chain, head } = resolveChainAndHead(layoutModules, mod, {
950
+ data: null,
951
+ params,
952
+ origin: originOf(req),
953
+ ...(nonce === undefined ? {} : { nonce }),
954
+ })
955
+ let headers: Headers | undefined
956
+ if (signalHeaders !== undefined || isPrivate) {
957
+ headers = new Headers(signalHeaders)
958
+ if (signalHeaders !== undefined) headers.set("vary", varyOnDataHeader(headers.get("vary")))
959
+ if (isPrivate) headers.set("cache-control", PRIVATE_NO_STORE)
960
+ }
961
+ return renderPageResult({
962
+ adapter,
963
+ chain,
964
+ data: null,
965
+ head,
966
+ clientEntry,
967
+ routeId: id,
968
+ params,
969
+ path: pathOf(req),
970
+ search: searchChainOf(layoutModules, mod, req),
971
+ matchChain: matchChainOf(page.layoutIds, layoutModules, id, mod),
972
+ status: 404,
973
+ hydrate: false,
974
+ ...(layoutData !== undefined ? { layoutData } : {}),
975
+ ...stylesOf(id),
976
+ ...(headers === undefined ? {} : { headers }),
977
+ ...(nonce === undefined ? {} : { nonce }),
978
+ ...cspOption,
979
+ ...titleOption,
980
+ })
981
+ }
982
+
983
+ /** The nearest nested `_404` for the route's own `notFound()`, else `undefined` (root page). */
984
+ const renderRouteNotFound = async (
985
+ req: Request,
986
+ env: Env,
987
+ route: RouteEntry,
988
+ params: Readonly<Record<string, string>>,
989
+ signal: StatusSignal,
990
+ run: LayoutRun | undefined,
991
+ personalized: boolean,
992
+ ): Promise<PageResponse | undefined> => {
993
+ const id = route.notFoundIds?.at(-1)
994
+ if (id === undefined || run === undefined) return undefined
995
+ if (signal[STATUS_SIGNAL].status !== 404 || layoutErrorId(signal) !== undefined) {
996
+ return undefined
997
+ }
998
+ const page = manifest.notFounds?.[id]
999
+ if (page === undefined) return undefined
1000
+ try {
1001
+ await run.pending
1002
+ } catch {
1003
+ return undefined
1004
+ }
1005
+ // The page's layouts are the leading ones of the route's: its directory contains the route.
1006
+ const kept = page.layoutIds.length
1007
+ return renderNestedNotFound(
1008
+ req,
1009
+ env,
1010
+ id,
1011
+ page,
1012
+ params,
1013
+ run.modules.slice(0, kept),
1014
+ run.layoutData?.slice(0, kept),
1015
+ personalized,
1016
+ signal[STATUS_SIGNAL].headers,
1017
+ )
1018
+ }
1019
+
1020
+ const renderNotFound = async (
1021
+ request: Request,
1022
+ env: Env,
1023
+ path?: string,
1024
+ ): Promise<PageResponse> => {
1025
+ const page = manifest.statusPages?.["404"] ?? manifest.notFound
1026
+ if (page === undefined) return renderTerminalStatus(404, path)
1027
+ return renderTerminalStatus(
1028
+ 404,
1029
+ path,
1030
+ undefined,
1031
+ options.nonce === undefined ? undefined : await resolveNonce(request, env),
1032
+ )
1033
+ }
1034
+
1035
+ /**
1036
+ * Run a page handler with the request's `ctx.set`, sealed once the handler settles - whatever it
1037
+ * answered with - so a write that arrives after the response was decided throws. A request that
1038
+ * queued a cookie answers with a response no shared cache may store, returned or thrown. A redirect
1039
+ * answering a data request, returned or thrown, reaches the client router intact.
1040
+ */
1041
+ const withResponseControls =
1042
+ (run: (c: PageRouteContext<Env>, controls: PageResponseControls) => Promise<PageResponse>) =>
1043
+ async (c: PageRouteContext<Env>): Promise<PageResponse> => {
1044
+ const controls = new PageResponseControls(c)
1045
+ try {
1046
+ const outcome = await run(c, controls)
1047
+ const answer = dataRedirect(c.req, outcome) ?? outcome
1048
+ return controls.personalized ? (privateOutcome(answer) as PageResponse) : answer
1049
+ } catch (err) {
1050
+ if (!isControlFlow(err)) throw err
1051
+ const answer = dataRedirect(c.req, err)
1052
+ // Returned rather than rethrown: the serving app sends a thrown `Response` exactly as it is,
1053
+ // without the cookies the request queued.
1054
+ if (controls.personalized) return privateOutcome(answer ?? err) as PageResponse
1055
+ if (answer !== undefined) return answer
1056
+ throw err
1057
+ } finally {
1058
+ controls.seal()
1059
+ }
1060
+ }
1061
+
1062
+ return {
1063
+ get: (route) => {
1064
+ const is404Fallback = options.staticFallbacks?.[route.pattern] === "404"
1065
+ let assemblySlot: RenderAssemblyCache | undefined
1066
+ let assemblySlotMod: unknown
1067
+ let assemblySlotLayouts: LoadedLayoutModules | undefined
1068
+ const assemblyCacheFor = (
1069
+ mod: RouteModule,
1070
+ layouts: LoadedLayoutModules,
1071
+ ): RenderAssemblyCache | undefined => {
1072
+ if (typeof mod.meta === "function") return undefined
1073
+ for (const layout of layouts) if (typeof layout.meta === "function") return undefined
1074
+ const layoutsUnchanged =
1075
+ assemblySlotLayouts !== undefined &&
1076
+ assemblySlotLayouts.length === layouts.length &&
1077
+ assemblySlotLayouts.every((layout, index) => layout === layouts[index])
1078
+ if (assemblySlotMod !== mod || !layoutsUnchanged) {
1079
+ assemblySlot = {}
1080
+ assemblySlotMod = mod
1081
+ assemblySlotLayouts = layouts
1082
+ }
1083
+ return assemblySlot
1084
+ }
1085
+
1086
+ return withResponseControls(async (c, controls) => {
1087
+ if (is404Fallback && !prerenderedSet.has(urlPartsFor(c.req).pathname)) {
1088
+ return renderNotFound(c.req, c.env, pathOf(c.req))
1089
+ }
1090
+ const mod = await route.load()
1091
+ const draft = await draftFlag(c.req)
1092
+ // Unpublished content is the editor's alone: whatever cache-control a loader sets, no cache keeps it.
1093
+ if (draft) controls.personalize()
1094
+ let data: unknown
1095
+ let layoutData: readonly unknown[] | undefined
1096
+ let boundaryStates: BoundaryStates | undefined
1097
+ let layoutModules: LoadedLayoutModules | undefined
1098
+ let layoutRetained: readonly number[] = []
1099
+ let responseHeaders: Record<string, string> | undefined
1100
+ const requestApi = apiFor(c)
1101
+ let run: LayoutRun | undefined
1102
+ try {
1103
+ const ctx: LoaderContext = {
1104
+ params: c.params,
1105
+ request: c.req,
1106
+ req: c.req,
1107
+ api: requestApi,
1108
+ env: c.env,
1109
+ draft,
1110
+ search: loaderSearch(mod.searchSchema, c.req),
1111
+ set: controls.scope(PAGE_SCOPE),
1112
+ }
1113
+ if (route.middlewareIds !== undefined) await runMiddleware(route, ctx, controls)
1114
+ run = await runLayoutChain(route, ctx, controls, retainContextOf(c.req))
1115
+ layoutModules = run.modules
1116
+ layoutRetained = run.retained
1117
+ const boundaryDefinitions = [
1118
+ ...run.modules.flatMap((layout) => layout.boundaries ?? []),
1119
+ ...(mod.boundaries ?? []),
1120
+ ]
1121
+ if (boundaryDefinitions.length > 0) boundaryDescriptors(boundaryDefinitions)
1122
+ const effectiveSearch = searchChainOf(run.modules, mod, c.req)
1123
+ const boundaryBatch =
1124
+ boundaryDefinitions.length === 0
1125
+ ? undefined
1126
+ : startDynamicBoundaries(boundaryDefinitions, {
1127
+ request: c.req,
1128
+ params: c.params,
1129
+ api: requestApi,
1130
+ env: c.env,
1131
+ draft,
1132
+ search: effectiveSearch,
1133
+ signal: c.req.signal,
1134
+ } satisfies BoundaryRequestCtx)
1135
+ const staticBoundaryPromise =
1136
+ boundaryDefinitions.length === 0
1137
+ ? undefined
1138
+ : resolveStaticBoundaries(
1139
+ boundaryDefinitions,
1140
+ { phase: "build" },
1141
+ options.staticBoundaryCache,
1142
+ )
1143
+ const pageResult = mod.loader ? mod.loader({ ...ctx, search: effectiveSearch }) : null
1144
+ const [pageData, , staticBoundaries] =
1145
+ run.pending === undefined && staticBoundaryPromise === undefined
1146
+ ? ([await pageResult, undefined, undefined] as const)
1147
+ : staticBoundaryPromise === undefined
1148
+ ? await Promise.all([pageResult, run.pending])
1149
+ : await Promise.all([pageResult, run.pending, staticBoundaryPromise])
1150
+ data = pageData
1151
+ layoutData = run.layoutData
1152
+ if (boundaryBatch !== undefined) {
1153
+ const streamed = { ...boundaryBatch.initial, ...(staticBoundaries ?? {}) }
1154
+ for (const entry of boundaryBatch.pending) {
1155
+ const initial = streamed[entry.name]
1156
+ if (initial !== undefined)
1157
+ streamed[entry.name] = { ...initial, data: defer(entry.promise) }
1158
+ }
1159
+ boundaryStates = streamed
1160
+ } else if (staticBoundaries !== undefined) {
1161
+ boundaryStates = staticBoundaries
1162
+ }
1163
+ // Inside the loader `try`: a header the response cannot carry is a loader error, rendered
1164
+ // by the same boundary as one.
1165
+ responseHeaders = controls.documentHeaders()
1166
+ } catch (err) {
1167
+ if (isStatusSignal(err)) {
1168
+ return (
1169
+ (await renderRouteNotFound(
1170
+ c.req,
1171
+ c.env,
1172
+ route,
1173
+ c.params,
1174
+ err,
1175
+ run,
1176
+ controls.personalized,
1177
+ )) ?? renderStatusSignal(c.req, c.env, err, controls.personalized)
1178
+ )
1179
+ }
1180
+ return renderLoaderFailure(route, c.req, c.env, c.params, err, controls.personalized)
1181
+ }
1182
+ if (isStatusSignal(data)) {
1183
+ const nested = await renderRouteNotFound(
1184
+ c.req,
1185
+ c.env,
1186
+ route,
1187
+ c.params,
1188
+ data,
1189
+ run,
1190
+ controls.personalized,
1191
+ )
1192
+ if (nested !== undefined) return nested
1193
+ }
1194
+ return handleDataOrDocument(
1195
+ c.req,
1196
+ c.env,
1197
+ route,
1198
+ c.params,
1199
+ mod,
1200
+ data,
1201
+ layoutData,
1202
+ layoutModules ?? (await loadLayoutModules(route)),
1203
+ layoutRetained,
1204
+ boundaryStates,
1205
+ layoutModules === undefined ? undefined : assemblyCacheFor(mod, layoutModules),
1206
+ responseHeaders,
1207
+ controls.personalized,
1208
+ )
1209
+ })
1210
+ },
1211
+
1212
+ post: (route) =>
1213
+ withResponseControls(async (c, controls) => {
1214
+ const mod = await route.load()
1215
+ const draft = await draftFlag(c.req)
1216
+ if (draft) controls.personalize()
1217
+ if (mod.action === undefined) {
1218
+ return new Response("Method Not Allowed", {
1219
+ status: 405,
1220
+ headers: { allow: "GET", "content-type": "text/plain; charset=utf-8" },
1221
+ })
1222
+ }
1223
+ const requestApi = apiFor(c)
1224
+ const actionContext: LoaderContext = {
1225
+ params: c.params,
1226
+ request: c.req,
1227
+ req: c.req,
1228
+ api: requestApi,
1229
+ env: c.env,
1230
+ draft,
1231
+ search: loaderSearch(mod.searchSchema, c.req),
1232
+ set: controls.scope(ACTION_SCOPE),
1233
+ }
1234
+ const isDataRequest = c.req.headers.get(DATA_HEADER) !== null
1235
+ let layoutModules: LoadedLayoutModules
1236
+ let result: unknown
1237
+ try {
1238
+ if (route.middlewareIds !== undefined) await runMiddleware(route, actionContext, controls)
1239
+ layoutModules = await runLayoutGates(route, actionContext, controls)
1240
+ result = await mod.action(actionContext)
1241
+ } catch (err) {
1242
+ if (isControlFlow(err)) return err
1243
+ throw err
1244
+ }
1245
+ const isRevalidate =
1246
+ result !== null && typeof result === "object" && "__nifraRevalidate" in result
1247
+ const actionResult = isRevalidate ? (result as RevalidateResult<unknown>).data : result
1248
+ const revalidateHeader: Record<string, string> = isRevalidate
1249
+ ? {
1250
+ [REVALIDATE_HEADER]: (result as RevalidateResult<unknown>).__nifraRevalidate.join(
1251
+ ",",
1252
+ ),
1253
+ }
1254
+ : {}
1255
+ if (isControlFlow(actionResult)) return actionResult
1256
+ if (isDataRequest) {
1257
+ // Headers shape the document only, but a bad one fails here too, so a navigation and a full
1258
+ // page load agree on whether the action's response is valid.
1259
+ controls.commit()
1260
+ const { forClient, deferred } = prepareDeferred(actionResult)
1261
+ if (deferred.length === 0) {
1262
+ return Response.json(actionResult ?? null, {
1263
+ headers: { ...DATA_RESPONSE_HEADERS, ...revalidateHeader },
1264
+ })
1265
+ }
1266
+ return new Response(ndjsonStream(forClient, deferred), {
1267
+ headers: {
1268
+ "content-type": "application/x-ndjson; charset=utf-8",
1269
+ ...DATA_RESPONSE_HEADERS,
1270
+ ...revalidateHeader,
1271
+ },
1272
+ })
1273
+ }
1274
+ const search = searchChainOf(layoutModules, mod, c.req)
1275
+ const data = mod.loader
1276
+ ? await mod.loader({
1277
+ params: c.params,
1278
+ request: c.req,
1279
+ req: c.req,
1280
+ api: requestApi,
1281
+ env: c.env,
1282
+ draft,
1283
+ search,
1284
+ set: controls.scope(PAGE_SCOPE),
1285
+ })
1286
+ : null
1287
+ const responseHeaders = controls.documentHeaders()
1288
+ const nonce = options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env)
1289
+ const { chain, head } = resolveChainAndHead(
1290
+ layoutModules,
1291
+ mod,
1292
+ {
1293
+ data,
1294
+ params: c.params,
1295
+ origin: originOf(c.req),
1296
+ ...(nonce === undefined ? {} : { nonce }),
1297
+ },
1298
+ serverLeafOf(route, mod),
1299
+ )
1300
+ return renderPageResult({
1301
+ adapter,
1302
+ chain,
1303
+ data,
1304
+ actionData: actionResult,
1305
+ head,
1306
+ clientEntry,
1307
+ routeId: route.id,
1308
+ params: c.params,
1309
+ path: pathOf(c.req),
1310
+ search,
1311
+ matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
1312
+ hydrate: mod.hydrate !== false,
1313
+ ...preloadOf(route.id),
1314
+ ...stylesOf(route.id),
1315
+ ...prerenderedHandover,
1316
+ ...(responseHeaders === undefined ? {} : { headers: responseHeaders }),
1317
+ ...(nonce === undefined ? {} : { nonce }),
1318
+ ...cspOption,
1319
+ ...titleOption,
1320
+ })
1321
+ }),
1322
+
1323
+ fallback: withResponseControls(async (c, controls) => {
1324
+ const match = matchNotFoundScope?.(pathOf(c.req)) ?? null
1325
+ const target = match === null ? undefined : notFoundTargets[Number(match.routeId)]
1326
+ if (match === null || target === undefined) {
1327
+ return renderNotFound(c.req, c.env, pathOf(c.req))
1328
+ }
1329
+ const params: Record<string, string> = {}
1330
+ for (let i = 0; i < target.names.length; i++) {
1331
+ params[target.names[i] as string] = match.params[`p${i}`] as string
1332
+ }
1333
+ const mod = await target.page.load()
1334
+ const draft = await draftFlag(c.req)
1335
+ if (draft) controls.personalize()
1336
+ let run: LayoutRun
1337
+ try {
1338
+ const ctx: LoaderContext = {
1339
+ params,
1340
+ request: c.req,
1341
+ req: c.req,
1342
+ api: apiFor(c),
1343
+ env: c.env,
1344
+ draft,
1345
+ search: loaderSearch(mod.searchSchema, c.req),
1346
+ set: controls.scope(PAGE_SCOPE),
1347
+ }
1348
+ if (target.route.middlewareIds !== undefined) {
1349
+ await runMiddleware(target.route, ctx, controls)
1350
+ }
1351
+ run = await runLayoutChain(target.route, ctx, controls)
1352
+ await run.pending
1353
+ } catch (err) {
1354
+ if (isStatusSignal(err)) {
1355
+ return renderStatusSignal(c.req, c.env, err, controls.personalized)
1356
+ }
1357
+ return renderLoaderFailure(target.route, c.req, c.env, params, err, controls.personalized)
1358
+ }
1359
+ return renderNestedNotFound(
1360
+ c.req,
1361
+ c.env,
1362
+ target.id,
1363
+ target.page,
1364
+ params,
1365
+ run.modules,
1366
+ run.layoutData,
1367
+ controls.personalized,
1368
+ )
1369
+ }),
1370
+ }
1371
+ }