@nifrajs/web 4.0.2 → 5.1.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 (426) hide show
  1. package/README.md +19 -0
  2. package/dist/build-vite.d.ts +19 -0
  3. package/dist/build-vite.d.ts.map +1 -1
  4. package/dist/build-vite.js +31 -10
  5. package/dist/build-vite.js.map +1 -1
  6. package/dist/build.d.ts +8 -10
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/build.js +90 -31
  9. package/dist/build.js.map +1 -1
  10. package/dist/cdn.d.ts +8 -0
  11. package/dist/cdn.d.ts.map +1 -1
  12. package/dist/cdn.js +14 -15
  13. package/dist/cdn.js.map +1 -1
  14. package/dist/client.d.ts +6 -6
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +46 -18
  17. package/dist/client.js.map +1 -1
  18. package/dist/deferred.d.ts +2 -2
  19. package/dist/deferred.d.ts.map +1 -1
  20. package/dist/deferred.js +28 -7
  21. package/dist/deferred.js.map +1 -1
  22. package/dist/dev-feed.d.ts.map +1 -1
  23. package/dist/dev-feed.js +24 -5
  24. package/dist/dev-feed.js.map +1 -1
  25. package/dist/dev-port.d.ts +3 -2
  26. package/dist/dev-port.d.ts.map +1 -1
  27. package/dist/dev-port.js +2 -6
  28. package/dist/dev-port.js.map +1 -1
  29. package/dist/dev-session.d.ts.map +1 -1
  30. package/dist/dev-session.js +4 -14
  31. package/dist/dev-session.js.map +1 -1
  32. package/dist/dev.d.ts +7 -17
  33. package/dist/dev.d.ts.map +1 -1
  34. package/dist/dev.js +13 -1
  35. package/dist/dev.js.map +1 -1
  36. package/dist/draft.d.ts +8 -5
  37. package/dist/draft.d.ts.map +1 -1
  38. package/dist/draft.js +30 -13
  39. package/dist/draft.js.map +1 -1
  40. package/dist/fonts-google.d.ts.map +1 -1
  41. package/dist/fonts-google.js +7 -0
  42. package/dist/fonts-google.js.map +1 -1
  43. package/dist/index.d.ts +5 -4
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +4 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/internal/codegen.d.ts +1 -0
  48. package/dist/internal/codegen.d.ts.map +1 -1
  49. package/dist/internal/codegen.js +56 -29
  50. package/dist/internal/codegen.js.map +1 -1
  51. package/dist/internal/disposable-dir.d.ts +8 -0
  52. package/dist/internal/disposable-dir.d.ts.map +1 -0
  53. package/dist/internal/disposable-dir.js +25 -0
  54. package/dist/internal/disposable-dir.js.map +1 -0
  55. package/dist/internal/entry-chunks.d.ts +9 -0
  56. package/dist/internal/entry-chunks.d.ts.map +1 -0
  57. package/dist/internal/entry-chunks.js +15 -0
  58. package/dist/internal/entry-chunks.js.map +1 -0
  59. package/dist/internal/evidence-traversal.d.ts +12 -0
  60. package/dist/internal/evidence-traversal.d.ts.map +1 -0
  61. package/dist/internal/evidence-traversal.js +48 -0
  62. package/dist/internal/evidence-traversal.js.map +1 -0
  63. package/dist/internal/head-merge.d.ts +6 -0
  64. package/dist/internal/head-merge.d.ts.map +1 -1
  65. package/dist/internal/head-merge.js +9 -0
  66. package/dist/internal/head-merge.js.map +1 -1
  67. package/dist/internal/js-string.d.ts +2 -0
  68. package/dist/internal/js-string.d.ts.map +1 -1
  69. package/dist/internal/js-string.js +5 -1
  70. package/dist/internal/js-string.js.map +1 -1
  71. package/dist/internal/mount-shadow.js +1 -1
  72. package/dist/internal/mount-shadow.js.map +1 -1
  73. package/dist/internal/output-guard.d.ts +7 -0
  74. package/dist/internal/output-guard.d.ts.map +1 -1
  75. package/dist/internal/output-guard.js +80 -8
  76. package/dist/internal/output-guard.js.map +1 -1
  77. package/dist/internal/package-entries.d.ts +34 -0
  78. package/dist/internal/package-entries.d.ts.map +1 -0
  79. package/dist/internal/package-entries.js +324 -0
  80. package/dist/internal/package-entries.js.map +1 -0
  81. package/dist/internal/page-execution.d.ts +6 -0
  82. package/dist/internal/page-execution.d.ts.map +1 -1
  83. package/dist/internal/page-execution.js +437 -114
  84. package/dist/internal/page-execution.js.map +1 -1
  85. package/dist/internal/parity.d.ts.map +1 -1
  86. package/dist/internal/parity.js +15 -2
  87. package/dist/internal/parity.js.map +1 -1
  88. package/dist/internal/private-env.d.ts +8 -0
  89. package/dist/internal/private-env.d.ts.map +1 -1
  90. package/dist/internal/private-env.js +25 -10
  91. package/dist/internal/private-env.js.map +1 -1
  92. package/dist/internal/progressive-cookies.d.ts +14 -0
  93. package/dist/internal/progressive-cookies.d.ts.map +1 -0
  94. package/dist/internal/progressive-cookies.js +43 -0
  95. package/dist/internal/progressive-cookies.js.map +1 -0
  96. package/dist/internal/public-env.d.ts +2 -0
  97. package/dist/internal/public-env.d.ts.map +1 -0
  98. package/dist/internal/public-env.js +13 -0
  99. package/dist/internal/public-env.js.map +1 -0
  100. package/dist/internal/render-document.d.ts +5 -44
  101. package/dist/internal/render-document.d.ts.map +1 -1
  102. package/dist/internal/render-document.js +51 -63
  103. package/dist/internal/render-document.js.map +1 -1
  104. package/dist/internal/response-controls.d.ts +2 -0
  105. package/dist/internal/response-controls.d.ts.map +1 -1
  106. package/dist/internal/response-controls.js +26 -16
  107. package/dist/internal/response-controls.js.map +1 -1
  108. package/dist/internal/revive-dates.d.ts +16 -0
  109. package/dist/internal/revive-dates.d.ts.map +1 -0
  110. package/dist/internal/revive-dates.js +32 -0
  111. package/dist/internal/revive-dates.js.map +1 -0
  112. package/dist/internal/route-path.d.ts +14 -0
  113. package/dist/internal/route-path.d.ts.map +1 -0
  114. package/dist/internal/route-path.js +19 -0
  115. package/dist/internal/route-path.js.map +1 -0
  116. package/dist/internal/secret-scan.d.ts.map +1 -1
  117. package/dist/internal/secret-scan.js +190 -20
  118. package/dist/internal/secret-scan.js.map +1 -1
  119. package/dist/internal/server-boundary.d.ts.map +1 -1
  120. package/dist/internal/server-boundary.js +33 -5
  121. package/dist/internal/server-boundary.js.map +1 -1
  122. package/dist/internal/web-app.d.ts +25 -18
  123. package/dist/internal/web-app.d.ts.map +1 -1
  124. package/dist/internal/web-app.js +81 -24
  125. package/dist/internal/web-app.js.map +1 -1
  126. package/dist/internal/wire-date.d.ts +3 -0
  127. package/dist/internal/wire-date.d.ts.map +1 -0
  128. package/dist/internal/wire-date.js +14 -0
  129. package/dist/internal/wire-date.js.map +1 -0
  130. package/dist/internal/without-query.d.ts +4 -0
  131. package/dist/internal/without-query.d.ts.map +1 -0
  132. package/dist/internal/without-query.js +9 -0
  133. package/dist/internal/without-query.js.map +1 -0
  134. package/dist/internal/zone-graph.d.ts +4 -0
  135. package/dist/internal/zone-graph.d.ts.map +1 -1
  136. package/dist/internal/zone-graph.js +71 -20
  137. package/dist/internal/zone-graph.js.map +1 -1
  138. package/dist/isr.d.ts +59 -3
  139. package/dist/isr.d.ts.map +1 -1
  140. package/dist/isr.js +311 -41
  141. package/dist/isr.js.map +1 -1
  142. package/dist/llms-txt.d.ts +2 -0
  143. package/dist/llms-txt.d.ts.map +1 -1
  144. package/dist/llms-txt.js +29 -3
  145. package/dist/llms-txt.js.map +1 -1
  146. package/dist/manifest.d.ts +37 -4
  147. package/dist/manifest.d.ts.map +1 -1
  148. package/dist/manifest.js +4 -0
  149. package/dist/manifest.js.map +1 -1
  150. package/dist/open-resolved.d.ts +18 -0
  151. package/dist/open-resolved.d.ts.map +1 -0
  152. package/dist/open-resolved.js +62 -0
  153. package/dist/open-resolved.js.map +1 -0
  154. package/dist/plugins/kit.d.ts +1 -0
  155. package/dist/plugins/kit.d.ts.map +1 -1
  156. package/dist/plugins/kit.js +26 -11
  157. package/dist/plugins/kit.js.map +1 -1
  158. package/dist/plugins/vite-leak-guard.d.ts +9 -1
  159. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  160. package/dist/plugins/vite-leak-guard.js +84 -7
  161. package/dist/plugins/vite-leak-guard.js.map +1 -1
  162. package/dist/plugins/vite-zone-guard.d.ts.map +1 -1
  163. package/dist/plugins/vite-zone-guard.js +17 -21
  164. package/dist/plugins/vite-zone-guard.js.map +1 -1
  165. package/dist/plugins/zone-guard.d.ts.map +1 -1
  166. package/dist/plugins/zone-guard.js +44 -3
  167. package/dist/plugins/zone-guard.js.map +1 -1
  168. package/dist/prerender.d.ts +5 -1
  169. package/dist/prerender.d.ts.map +1 -1
  170. package/dist/prerender.js +41 -19
  171. package/dist/prerender.js.map +1 -1
  172. package/dist/public-dir.d.ts +8 -1
  173. package/dist/public-dir.d.ts.map +1 -1
  174. package/dist/public-dir.js +17 -6
  175. package/dist/public-dir.js.map +1 -1
  176. package/dist/query.d.ts +13 -4
  177. package/dist/query.d.ts.map +1 -1
  178. package/dist/query.js +89 -26
  179. package/dist/query.js.map +1 -1
  180. package/dist/render-seam.d.ts +25 -2
  181. package/dist/render-seam.d.ts.map +1 -1
  182. package/dist/render-seam.js +1 -0
  183. package/dist/render-seam.js.map +1 -1
  184. package/dist/route-types.d.ts.map +1 -1
  185. package/dist/route-types.js +20 -9
  186. package/dist/route-types.js.map +1 -1
  187. package/dist/router.d.ts +9 -2
  188. package/dist/router.d.ts.map +1 -1
  189. package/dist/router.js +47 -50
  190. package/dist/router.js.map +1 -1
  191. package/dist/seo.d.ts +49 -0
  192. package/dist/seo.d.ts.map +1 -0
  193. package/dist/seo.js +48 -0
  194. package/dist/seo.js.map +1 -0
  195. package/dist/server-components/build-info.d.ts +3 -0
  196. package/dist/server-components/build-info.d.ts.map +1 -0
  197. package/dist/server-components/build-info.js +7 -0
  198. package/dist/server-components/build-info.js.map +1 -0
  199. package/dist/server-components/build-target.d.ts +12 -0
  200. package/dist/server-components/build-target.d.ts.map +1 -0
  201. package/dist/server-components/build-target.js +153 -0
  202. package/dist/server-components/build-target.js.map +1 -0
  203. package/dist/server-components/build.d.ts +24 -0
  204. package/dist/server-components/build.d.ts.map +1 -0
  205. package/dist/server-components/build.js +366 -0
  206. package/dist/server-components/build.js.map +1 -0
  207. package/dist/server-components/bundle-proof.d.ts +3 -0
  208. package/dist/server-components/bundle-proof.d.ts.map +1 -0
  209. package/dist/server-components/bundle-proof.js +23 -0
  210. package/dist/server-components/bundle-proof.js.map +1 -0
  211. package/dist/server-components/client.d.ts +46 -0
  212. package/dist/server-components/client.d.ts.map +1 -0
  213. package/dist/server-components/client.js +594 -0
  214. package/dist/server-components/client.js.map +1 -0
  215. package/dist/server-components/definition.d.ts +11 -0
  216. package/dist/server-components/definition.d.ts.map +1 -0
  217. package/dist/server-components/definition.js +94 -0
  218. package/dist/server-components/definition.js.map +1 -0
  219. package/dist/server-components/dev-http.d.ts +5 -0
  220. package/dist/server-components/dev-http.d.ts.map +1 -0
  221. package/dist/server-components/dev-http.js +71 -0
  222. package/dist/server-components/dev-http.js.map +1 -0
  223. package/dist/server-components/error.d.ts +9 -0
  224. package/dist/server-components/error.d.ts.map +1 -0
  225. package/dist/server-components/error.js +24 -0
  226. package/dist/server-components/error.js.map +1 -0
  227. package/dist/server-components/frames.d.ts +7 -0
  228. package/dist/server-components/frames.d.ts.map +1 -0
  229. package/dist/server-components/frames.js +169 -0
  230. package/dist/server-components/frames.js.map +1 -0
  231. package/dist/server-components/html-policy.d.ts +6 -0
  232. package/dist/server-components/html-policy.d.ts.map +1 -0
  233. package/dist/server-components/html-policy.js +75 -0
  234. package/dist/server-components/html-policy.js.map +1 -0
  235. package/dist/server-components/html.d.ts +4 -0
  236. package/dist/server-components/html.d.ts.map +1 -0
  237. package/dist/server-components/html.js +67 -0
  238. package/dist/server-components/html.js.map +1 -0
  239. package/dist/server-components/islands.d.ts +44 -0
  240. package/dist/server-components/islands.d.ts.map +1 -0
  241. package/dist/server-components/islands.js +42 -0
  242. package/dist/server-components/islands.js.map +1 -0
  243. package/dist/server-components/native.d.ts +4 -0
  244. package/dist/server-components/native.d.ts.map +1 -0
  245. package/dist/server-components/native.js +81 -0
  246. package/dist/server-components/native.js.map +1 -0
  247. package/dist/server-components/portal.d.ts +6 -0
  248. package/dist/server-components/portal.d.ts.map +1 -0
  249. package/dist/server-components/portal.js +35 -0
  250. package/dist/server-components/portal.js.map +1 -0
  251. package/dist/server-components/props.d.ts +3 -0
  252. package/dist/server-components/props.d.ts.map +1 -0
  253. package/dist/server-components/props.js +14 -0
  254. package/dist/server-components/props.js.map +1 -0
  255. package/dist/server-components/protocol.d.ts +44 -0
  256. package/dist/server-components/protocol.d.ts.map +1 -0
  257. package/dist/server-components/protocol.js +139 -0
  258. package/dist/server-components/protocol.js.map +1 -0
  259. package/dist/server-components/range.d.ts +24 -0
  260. package/dist/server-components/range.d.ts.map +1 -0
  261. package/dist/server-components/range.js +263 -0
  262. package/dist/server-components/range.js.map +1 -0
  263. package/dist/server-components/registry.d.ts +6 -0
  264. package/dist/server-components/registry.d.ts.map +1 -0
  265. package/dist/server-components/registry.js +30 -0
  266. package/dist/server-components/registry.js.map +1 -0
  267. package/dist/server-components/response.d.ts +4 -0
  268. package/dist/server-components/response.d.ts.map +1 -0
  269. package/dist/server-components/response.js +32 -0
  270. package/dist/server-components/response.js.map +1 -0
  271. package/dist/server-components/router-integration.d.ts +9 -0
  272. package/dist/server-components/router-integration.d.ts.map +1 -0
  273. package/dist/server-components/router-integration.js +29 -0
  274. package/dist/server-components/router-integration.js.map +1 -0
  275. package/dist/server-components/runtime.d.ts +14 -0
  276. package/dist/server-components/runtime.d.ts.map +1 -0
  277. package/dist/server-components/runtime.js +276 -0
  278. package/dist/server-components/runtime.js.map +1 -0
  279. package/dist/server-components/scheduler.d.ts +14 -0
  280. package/dist/server-components/scheduler.d.ts.map +1 -0
  281. package/dist/server-components/scheduler.js +74 -0
  282. package/dist/server-components/scheduler.js.map +1 -0
  283. package/dist/server-components/schema.d.ts +4 -0
  284. package/dist/server-components/schema.d.ts.map +1 -0
  285. package/dist/server-components/schema.js +77 -0
  286. package/dist/server-components/schema.js.map +1 -0
  287. package/dist/server-components/server-entry.d.ts +12 -0
  288. package/dist/server-components/server-entry.d.ts.map +1 -0
  289. package/dist/server-components/server-entry.js +90 -0
  290. package/dist/server-components/server-entry.js.map +1 -0
  291. package/dist/server-components/source.d.ts +15 -0
  292. package/dist/server-components/source.d.ts.map +1 -0
  293. package/dist/server-components/source.js +269 -0
  294. package/dist/server-components/source.js.map +1 -0
  295. package/dist/server-components/state.d.ts +4 -0
  296. package/dist/server-components/state.d.ts.map +1 -0
  297. package/dist/server-components/state.js +123 -0
  298. package/dist/server-components/state.js.map +1 -0
  299. package/dist/server-components/stream.d.ts +6 -0
  300. package/dist/server-components/stream.d.ts.map +1 -0
  301. package/dist/server-components/stream.js +70 -0
  302. package/dist/server-components/stream.js.map +1 -0
  303. package/dist/server-components/trusted-html.d.ts +3 -0
  304. package/dist/server-components/trusted-html.d.ts.map +1 -0
  305. package/dist/server-components/trusted-html.js +14 -0
  306. package/dist/server-components/trusted-html.js.map +1 -0
  307. package/dist/server-components/types.d.ts +107 -0
  308. package/dist/server-components/types.d.ts.map +1 -0
  309. package/dist/server-components/types.js +2 -0
  310. package/dist/server-components/types.js.map +1 -0
  311. package/dist/server-components/typescript.d.ts +11 -0
  312. package/dist/server-components/typescript.d.ts.map +1 -0
  313. package/dist/server-components/typescript.js +28 -0
  314. package/dist/server-components/typescript.js.map +1 -0
  315. package/dist/server-components/value.d.ts +5 -0
  316. package/dist/server-components/value.d.ts.map +1 -0
  317. package/dist/server-components/value.js +77 -0
  318. package/dist/server-components/value.js.map +1 -0
  319. package/dist/server-components/vite.d.ts +12 -0
  320. package/dist/server-components/vite.d.ts.map +1 -0
  321. package/dist/server-components/vite.js +197 -0
  322. package/dist/server-components/vite.js.map +1 -0
  323. package/dist/server-components.d.ts +5 -0
  324. package/dist/server-components.d.ts.map +1 -0
  325. package/dist/server-components.js +4 -0
  326. package/dist/server-components.js.map +1 -0
  327. package/dist/service-worker.d.ts +8 -1
  328. package/dist/service-worker.d.ts.map +1 -1
  329. package/dist/service-worker.js +20 -6
  330. package/dist/service-worker.js.map +1 -1
  331. package/dist/vite.d.ts +12 -0
  332. package/dist/vite.d.ts.map +1 -1
  333. package/dist/vite.js +7 -1
  334. package/dist/vite.js.map +1 -1
  335. package/dist/zones.d.ts +64 -2
  336. package/dist/zones.d.ts.map +1 -1
  337. package/dist/zones.js +331 -28
  338. package/dist/zones.js.map +1 -1
  339. package/package.json +102 -4
  340. package/src/build-vite.ts +31 -9
  341. package/src/build.ts +109 -31
  342. package/src/cdn.ts +18 -12
  343. package/src/client.ts +48 -17
  344. package/src/deferred.ts +27 -9
  345. package/src/dev-feed.ts +24 -5
  346. package/src/dev-port.ts +4 -10
  347. package/src/dev-session.ts +4 -13
  348. package/src/dev.ts +29 -1
  349. package/src/draft.ts +35 -14
  350. package/src/fonts-google.ts +10 -0
  351. package/src/index.ts +5 -6
  352. package/src/internal/codegen.ts +60 -29
  353. package/src/internal/disposable-dir.ts +24 -0
  354. package/src/internal/entry-chunks.ts +18 -0
  355. package/src/internal/evidence-traversal.ts +57 -0
  356. package/src/internal/head-merge.ts +16 -0
  357. package/src/internal/js-string.ts +6 -1
  358. package/src/internal/mount-shadow.ts +1 -1
  359. package/src/internal/output-guard.ts +88 -7
  360. package/src/internal/package-entries.ts +331 -0
  361. package/src/internal/page-execution.ts +573 -114
  362. package/src/internal/parity.ts +15 -2
  363. package/src/internal/private-env.ts +27 -13
  364. package/src/internal/progressive-cookies.ts +60 -0
  365. package/src/internal/public-env.ts +14 -0
  366. package/src/internal/render-document.ts +66 -89
  367. package/src/internal/response-controls.ts +31 -14
  368. package/src/internal/revive-dates.ts +35 -0
  369. package/src/internal/route-path.ts +24 -0
  370. package/src/internal/secret-scan.ts +190 -18
  371. package/src/internal/server-boundary.ts +29 -5
  372. package/src/internal/web-app.ts +126 -42
  373. package/src/internal/wire-date.ts +16 -0
  374. package/src/internal/without-query.ts +8 -0
  375. package/src/internal/zone-graph.ts +78 -25
  376. package/src/isr.ts +364 -45
  377. package/src/llms-txt.ts +32 -3
  378. package/src/manifest.ts +38 -4
  379. package/src/open-resolved.ts +64 -0
  380. package/src/plugins/kit.ts +31 -11
  381. package/src/plugins/vite-leak-guard.ts +104 -8
  382. package/src/plugins/vite-zone-guard.ts +21 -19
  383. package/src/plugins/zone-guard.ts +41 -2
  384. package/src/prerender.ts +39 -18
  385. package/src/public-dir.ts +27 -8
  386. package/src/query.ts +107 -26
  387. package/src/render-seam.ts +30 -2
  388. package/src/route-types.ts +34 -7
  389. package/src/router.ts +66 -59
  390. package/src/seo.ts +68 -0
  391. package/src/server-components/build-info.ts +11 -0
  392. package/src/server-components/build-target.ts +194 -0
  393. package/src/server-components/build.ts +419 -0
  394. package/src/server-components/bundle-proof.ts +33 -0
  395. package/src/server-components/client.ts +663 -0
  396. package/src/server-components/definition.ts +125 -0
  397. package/src/server-components/dev-http.ts +75 -0
  398. package/src/server-components/error.ts +24 -0
  399. package/src/server-components/frames.ts +181 -0
  400. package/src/server-components/html-policy.ts +94 -0
  401. package/src/server-components/html.ts +81 -0
  402. package/src/server-components/islands.ts +111 -0
  403. package/src/server-components/native.ts +101 -0
  404. package/src/server-components/portal.ts +42 -0
  405. package/src/server-components/props.ts +19 -0
  406. package/src/server-components/protocol.ts +167 -0
  407. package/src/server-components/range.ts +313 -0
  408. package/src/server-components/registry.ts +34 -0
  409. package/src/server-components/response.ts +44 -0
  410. package/src/server-components/router-integration.ts +40 -0
  411. package/src/server-components/runtime.ts +376 -0
  412. package/src/server-components/scheduler.ts +84 -0
  413. package/src/server-components/schema.ts +69 -0
  414. package/src/server-components/server-entry.ts +98 -0
  415. package/src/server-components/source.ts +324 -0
  416. package/src/server-components/state.ts +143 -0
  417. package/src/server-components/stream.ts +71 -0
  418. package/src/server-components/trusted-html.ts +21 -0
  419. package/src/server-components/types.ts +138 -0
  420. package/src/server-components/typescript.ts +39 -0
  421. package/src/server-components/value.ts +82 -0
  422. package/src/server-components/vite.ts +242 -0
  423. package/src/server-components.ts +32 -0
  424. package/src/service-worker.ts +24 -7
  425. package/src/vite.ts +8 -1
  426. package/src/zones.ts +418 -32
@@ -3,7 +3,12 @@ import {
3
3
  NIFRA_BACKEND_BIND_PLATFORM,
4
4
  NIFRA_PLATFORM_CLIENT_IP_DERIVED,
5
5
  } from "@nifrajs/core/mount"
6
- import { type Platform, type ResponseResult, status as statusResult } from "@nifrajs/core/server"
6
+ import {
7
+ isSameOriginForm,
8
+ type Platform,
9
+ type ResponseResult,
10
+ status as statusResult,
11
+ } from "@nifrajs/core/server"
7
12
  import {
8
13
  type BoundaryRequestCtx,
9
14
  type BoundaryStates,
@@ -40,7 +45,7 @@ import {
40
45
  STATUS_HEADER,
41
46
  } from "../router.ts"
42
47
  import { searchOf, searchOfChain } from "../search.ts"
43
- import { mergeHeads, resolveMeta } from "./head-merge.ts"
48
+ import { mergeHeads, resolveChainMeta } from "./head-merge.ts"
44
49
  import {
45
50
  EMPTY_RETAIN,
46
51
  type HeadersLike,
@@ -78,6 +83,7 @@ export type { NonceResolver } from "../nonce.ts"
78
83
 
79
84
  /** Structural context supplied by the core server to one page route handler. */
80
85
  export interface PageRouteContext<Env = unknown> {
86
+ readonly signal?: AbortSignal
81
87
  readonly params: Record<string, string>
82
88
  readonly req: Request
83
89
  readonly env: Env
@@ -166,12 +172,19 @@ function appTarget(location: string, requestUrl: string): string {
166
172
  } catch {
167
173
  return location // the browser fails on it the same way when it loads it
168
174
  }
169
- return target.origin === new URL(requestUrl).origin
170
- ? target.pathname + target.search + target.hash
171
- : location
175
+ if (target.origin !== new URL(requestUrl).origin) return location
176
+ // A path starting `//` (from `/.//host`) is a protocol-relative URL to whoever resolves it next.
177
+ return target.pathname.startsWith("//")
178
+ ? target.href
179
+ : target.pathname + target.search + target.hash
172
180
  }
173
181
 
174
182
  export interface PageExecutionOptions<Env = unknown> {
183
+ readonly serverComponents?: import("../server-components/types.ts").ServerComponentRuntime
184
+ readonly rerenderOwnerGET?: (
185
+ request: Request,
186
+ context: PageRouteContext<Env>,
187
+ ) => Promise<Response>
175
188
  readonly adapter: RenderAdapter
176
189
  readonly manifest: Manifest
177
190
  readonly clientEntry: string
@@ -201,6 +214,9 @@ export interface PageExecutionOptions<Env = unknown> {
201
214
  readonly route: string
202
215
  },
203
216
  ) => void
217
+ /** Logs a failure an `_error` boundary answered. Without a boundary the error propagates and the
218
+ * server logs it, so this runs only where the server never sees the throw. */
219
+ readonly logBoundaryError?: (error: unknown, request: Request) => void
204
220
  }
205
221
 
206
222
  type PageResponse = Response | ResponseResult | RenderedPage
@@ -238,6 +254,24 @@ type LayoutRun = {
238
254
  readonly pending: Promise<unknown> | undefined
239
255
  }
240
256
 
257
+ /** A route's layouts after only their gates ran: `gates[i]` is gate `i`'s value. */
258
+ type GatedLayouts = {
259
+ readonly modules: LoadedLayoutModules
260
+ readonly gates: readonly unknown[]
261
+ }
262
+
263
+ /** Layouts whose loaders settled for this request: what a status or error page renders in. */
264
+ type LayoutChain = {
265
+ readonly modules: LoadedLayoutModules
266
+ readonly layoutData: readonly unknown[] | undefined
267
+ }
268
+
269
+ /**
270
+ * Not called for a page that exports `inLayouts = false`, so such a page runs no layout loader.
271
+ * `undefined` renders the page alone.
272
+ */
273
+ type LayoutChainSource = (page: RouteModule, count: number) => Promise<LayoutChain | undefined>
274
+
241
275
  /**
242
276
  * Own the page request program behind one private seam.
243
277
  *
@@ -251,6 +285,8 @@ export function createPageRequestExecutor<Env = unknown>(
251
285
  options: PageExecutionOptions<Env>,
252
286
  ): PageRequestExecutor<Env> {
253
287
  const { adapter, manifest, clientEntry, api } = options
288
+ // The action result is request-local; a browser cannot manufacture this handoff.
289
+ const progressiveActionData = new WeakMap<Request, unknown>()
254
290
  // Bound once per render, so every loader, action and boundary in that render shares one view of the
255
291
  // in-process client carrying the visitor's platform. An `api` without the seam passes through as-is.
256
292
  const bindApi = platformBinderOf(api)
@@ -327,9 +363,9 @@ export function createPageRequestExecutor<Env = unknown>(
327
363
  ): string | undefined | Promise<string | undefined> =>
328
364
  options.nonce === undefined ? undefined : options.nonce({ request, env })
329
365
 
330
- const stylesOf = (id: string): { styles?: readonly string[]; cssLoading?: CssLoadingMode } => {
331
- const perRoute = options.routeStyles?.[id]
332
- const styles = perRoute ?? options.styles
366
+ const stylesFrom = (
367
+ styles: readonly string[] | undefined,
368
+ ): { styles?: readonly string[]; cssLoading?: CssLoadingMode } => {
333
369
  if (styles === undefined || styles.length === 0) return {}
334
370
  return {
335
371
  styles,
@@ -337,6 +373,9 @@ export function createPageRequestExecutor<Env = unknown>(
337
373
  }
338
374
  }
339
375
 
376
+ const stylesOf = (id: string): { styles?: readonly string[]; cssLoading?: CssLoadingMode } =>
377
+ stylesFrom(options.routeStyles?.[id] ?? options.styles)
378
+
340
379
  // Load a route's layout modules once for the current execution. The render path needs each whole
341
380
  // module for its component + meta, while the data path also needs loader/gate/boundary declarations.
342
381
  const loadLayoutModules = async (route: RouteEntry): Promise<LoadedLayoutModules> => {
@@ -361,15 +400,60 @@ export function createPageRequestExecutor<Env = unknown>(
361
400
  pending: undefined,
362
401
  }
363
402
 
364
- /** Run layout loaders, awaiting gates before starting the page loader and retaining only safe data. */
403
+ /**
404
+ * Await one gate. A returned redirect, status or Response refuses the request exactly as a thrown
405
+ * one does: stored as layout data it would let the page loader and the action run.
406
+ */
407
+ const runGate = async (
408
+ route: RouteEntry,
409
+ index: number,
410
+ loader: NonNullable<LoadedLayoutModules[number]["loader"]>,
411
+ ctx: LoaderContext,
412
+ ): Promise<unknown> => {
413
+ let value: unknown
414
+ try {
415
+ value = await loader(ctx)
416
+ } catch (err) {
417
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The loaded layout array is derived from these same manifest IDs in the same order.
418
+ throw tagLayoutError(err, route.layoutIds[index] as string)
419
+ }
420
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The loaded layout array is derived from these same manifest IDs in the same order.
421
+ if (isControlFlow(value)) throw tagLayoutError(value, route.layoutIds[index] as string)
422
+ return value
423
+ }
424
+
425
+ // Development only. A layout loader that refuses access reads as a guard, and is not one: a client
426
+ // navigation can skip it and an action below it runs without it.
427
+ const warnsUngatedRefusal =
428
+ typeof process !== "undefined" && process.env?.NODE_ENV === "development"
429
+ const warnedUngated = new Set<string>()
430
+ const warnUngatedRefusal = (layoutId: string, outcome: unknown): void => {
431
+ if (warnedUngated.has(layoutId)) return
432
+ // notFound() and gone() say the record is missing, which a layout may decide without guarding.
433
+ const refusesAccess = isStatusSignal(outcome)
434
+ ? outcome.status === 401 || outcome.status === 403
435
+ : isControlFlow(outcome)
436
+ if (!refusesAccess) return
437
+ warnedUngated.add(layoutId)
438
+ const file = manifest.layouts?.[layoutId]?.file ?? layoutId
439
+ console.warn(
440
+ `[nifra/web] the loader in "${file}" refused a request, but that layout is not a gate. A client navigation that keeps the layout on screen can skip its loader, and an action below it runs without it, so it does not protect the pages under it. Export \`gate = true\` from that file, or move the check into middleware. (Shown in development only.)`,
441
+ )
442
+ }
443
+
444
+ /**
445
+ * Run layout loaders, awaiting gates before starting the page loader and retaining only safe data.
446
+ * `gated` carries gates that already ran for this request, so none runs twice.
447
+ */
365
448
  const runLayoutChain = async (
366
449
  route: RouteEntry,
367
450
  ctx: LoaderContext,
368
451
  controls: PageResponseControls,
369
452
  retain?: RetainContext,
453
+ gated?: GatedLayouts,
370
454
  ): Promise<LayoutRun> => {
371
455
  if (route.layoutIds.length === 0) return emptyLayoutRun
372
- const modules = await loadLayoutModules(route)
456
+ const modules = gated?.modules ?? (await loadLayoutModules(route))
373
457
  if (!modules.some((m) => m.loader !== undefined)) {
374
458
  return { modules, layoutData: undefined, retained: [], pending: undefined }
375
459
  }
@@ -385,6 +469,7 @@ export function createPageRequestExecutor<Env = unknown>(
385
469
  try {
386
470
  keep = keepsLayoutData(route, i, layout, ctx.params, retain)
387
471
  } catch (err) {
472
+ // biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: The loaded layout array is derived from these same manifest IDs in the same order.
388
473
  throw tagLayoutError(err, route.layoutIds[i] as string)
389
474
  }
390
475
  if (keep) {
@@ -398,11 +483,7 @@ export function createPageRequestExecutor<Env = unknown>(
398
483
  set: controls.scope(i),
399
484
  }
400
485
  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
- }
486
+ results[i] = gated === undefined ? await runGate(route, i, loader, scoped) : gated.gates[i]
406
487
  continue
407
488
  }
408
489
  const layoutId = route.layoutIds[i] as string
@@ -414,15 +495,25 @@ export function createPageRequestExecutor<Env = unknown>(
414
495
  if (value !== null && (typeof value === "object" || typeof value === "function")) {
415
496
  pending.push(
416
497
  (async () => {
498
+ let settled: unknown
417
499
  try {
418
- results[index] = await value
500
+ settled = await value
419
501
  } catch (err) {
502
+ if (warnsUngatedRefusal) warnUngatedRefusal(layoutId, err)
420
503
  throw tagLayoutError(err, layoutId)
421
504
  }
505
+ // A returned redirect or Response refuses exactly as a thrown one: kept as layout data,
506
+ // it would ship beside the page loader's data.
507
+ if (isControlFlow(settled)) {
508
+ if (warnsUngatedRefusal) warnUngatedRefusal(layoutId, settled)
509
+ throw tagLayoutError(settled, layoutId)
510
+ }
511
+ results[index] = settled
422
512
  })(),
423
513
  )
424
514
  } else results[index] = value
425
515
  } catch (err) {
516
+ if (warnsUngatedRefusal) warnUngatedRefusal(layoutId, err)
426
517
  // Preserve a synchronous throw as a pending layout failure, just like the async lane.
427
518
  pending.push(Promise.reject(tagLayoutError(err, layoutId)))
428
519
  }
@@ -441,22 +532,19 @@ export function createPageRequestExecutor<Env = unknown>(
441
532
  route: RouteEntry,
442
533
  ctx: LoaderContext,
443
534
  controls: PageResponseControls,
444
- ): Promise<LoadedLayoutModules> => {
535
+ ): Promise<GatedLayouts> => {
445
536
  const modules = await loadLayoutModules(route)
537
+ const gates: unknown[] = []
446
538
  for (let i = 0; i < modules.length; i++) {
447
539
  const mod = modules[i]
448
540
  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
- }
541
+ gates[i] = await runGate(route, i, mod.loader, {
542
+ ...ctx,
543
+ params: scopeParams(ctx.params, route.layoutParams?.[i]),
544
+ set: controls.scope(i),
545
+ })
458
546
  }
459
- return modules
547
+ return { modules, gates }
460
548
  }
461
549
 
462
550
  /** Outermost first; a failure carries its directory so the nearest `_error` above it renders. */
@@ -594,16 +682,14 @@ export function createPageRequestExecutor<Env = unknown>(
594
682
 
595
683
  const resolveChainAndHead = (
596
684
  layoutModules: LoadedLayoutModules,
685
+ layoutData: readonly unknown[] | undefined,
597
686
  page: RouteModule,
598
687
  metaArgs: MetaArgs,
599
688
  leaf: unknown = page.default,
600
689
  ): { chain: unknown[]; head: Meta } => {
601
690
  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) }
691
+ const metas = [...layoutModules.map((m) => m.meta), page.meta]
692
+ return { chain, head: mergeHeads(resolveChainMeta(metas, layoutData, metaArgs)) }
607
693
  }
608
694
 
609
695
  /** What `useMatches` reports for a chain: the layouts' ids and `handle`s, then the page's. */
@@ -649,49 +735,160 @@ export function createPageRequestExecutor<Env = unknown>(
649
735
  }
650
736
  }
651
737
 
738
+ /** What a status or error page's `meta` receives: no route params, and its own data. */
739
+ const statusMetaArgs = (req: Request, data: unknown, nonce: string | undefined): MetaArgs =>
740
+ nonce === undefined
741
+ ? { data, pageData: data, params: {}, origin: originOf(req) }
742
+ : { data, pageData: data, params: {}, origin: originOf(req), nonce }
743
+
652
744
  const renderError = async (
745
+ req: Request,
653
746
  route: RouteEntry,
654
747
  errorId: string,
655
748
  err: unknown,
656
749
  personalized: boolean,
657
750
  nonce?: string,
751
+ layouts?: LayoutChainSource,
658
752
  ): Promise<PageResponse> => {
659
753
  const errDir = dirOfId(errorId, "_error")
660
754
  const keptLayoutIds = route.layoutIds.filter((id) => {
661
755
  const ld = dirOfId(id, "_layout")
662
756
  return ld === "" || ld === errDir || errDir.startsWith(`${ld}/`)
663
757
  })
664
- const layouts = await Promise.all(
758
+ const errorModule = await (manifest.errors?.[errorId] as LayoutEntry).load()
759
+ const e = err instanceof Error ? err : new Error(String(err))
760
+ // An error's text can carry a connection string or a query, so only a run that says it is
761
+ // development or test shows it; an unset NODE_ENV (an unbuilt `bun app.ts`) counts as production.
762
+ const nodeEnv = typeof process === "undefined" ? undefined : process.env.NODE_ENV
763
+ const data =
764
+ nodeEnv === "development" || nodeEnv === "test"
765
+ ? { name: e.name, message: e.message }
766
+ : { name: "Error", message: "Internal Server Error" }
767
+ const inLayouts = errorModule.inLayouts !== false
768
+ const chain =
769
+ inLayouts && layouts !== undefined && keptLayoutIds.length > 0
770
+ ? await layouts(errorModule, keptLayoutIds.length)
771
+ : undefined
772
+ const render = (components: unknown[], wrapped?: { head: Meta; chain: LayoutChain }) => {
773
+ const layoutData = wrapped?.chain.layoutData
774
+ return renderPageResult({
775
+ adapter,
776
+ chain: components,
777
+ data,
778
+ ...(wrapped === undefined ? {} : { head: wrapped.head }),
779
+ clientEntry,
780
+ routeId: errorId,
781
+ status: 500,
782
+ hydrate: false,
783
+ ...(layoutData === undefined ? {} : { layoutData }),
784
+ ...(personalized || layoutData !== undefined ? { headers: PRIVATE_PAGE_HEADERS } : {}),
785
+ ...(nonce === undefined ? {} : { nonce }),
786
+ ...cspOption,
787
+ ...titleOption,
788
+ })
789
+ }
790
+ if (chain !== undefined) {
791
+ const resolved = resolveChainAndHead(
792
+ chain.modules,
793
+ chain.layoutData,
794
+ errorModule,
795
+ statusMetaArgs(req, data, nonce),
796
+ )
797
+ try {
798
+ return await render(resolved.chain, { head: resolved.head, chain })
799
+ } catch {
800
+ // A layout that fails to render leaves the error page alone, never a second error.
801
+ return render([errorModule.default])
802
+ }
803
+ }
804
+ // Layouts whose data did not settle still frame the error page, without their data.
805
+ const layoutComponents = await Promise.all(
665
806
  keptLayoutIds.map((id) =>
666
807
  (manifest.layouts[id] as LayoutEntry).load().then((m) => m.default),
667
808
  ),
668
809
  )
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
- })
810
+ return render([...layoutComponents, errorModule.default])
811
+ }
812
+
813
+ /** The leading `count` layouts of `modules`, unless one of them answered with a status or redirect. */
814
+ const settledLayouts = (
815
+ modules: LoadedLayoutModules,
816
+ layoutData: readonly unknown[] | undefined,
817
+ count: number,
818
+ ): LayoutChain | undefined => {
819
+ if (modules.length < count) return undefined
820
+ const data = layoutData?.slice(0, count)
821
+ if (data?.some((value) => isControlFlow(value) || isStatusSignal(value))) return undefined
822
+ return { modules: modules.slice(0, count), layoutData: data }
688
823
  }
689
824
 
825
+ /** A run's leading layouts once all of its loaders settle - never when a layout or middleware refused. */
826
+ const leadingLayouts =
827
+ (run: LayoutRun | undefined, err?: unknown): LayoutChainSource =>
828
+ async (_page, count) => {
829
+ if (run === undefined || (err !== undefined && layoutErrorId(err) !== undefined)) {
830
+ return undefined
831
+ }
832
+ try {
833
+ await run.pending
834
+ } catch {
835
+ return undefined
836
+ }
837
+ return settledLayouts(run.modules, run.layoutData, count)
838
+ }
839
+
840
+ const rootLayoutIds: readonly string[] = manifest.layouts._layout === undefined ? [] : ["_layout"]
841
+ const rootMiddlewareIds: readonly string[] =
842
+ manifest.middlewares?._middleware === undefined ? [] : ["_middleware"]
843
+ /**
844
+ * Run the root middleware and layouts for a request no route ran them for (an unmatched URL), as a
845
+ * nested `_404` runs its own. Any refusal or failure renders the page alone.
846
+ */
847
+ const rootLayoutsFor =
848
+ (c: PageRouteContext<Env>, controls: PageResponseControls): LayoutChainSource =>
849
+ async (page) => {
850
+ const entry = manifest.statusPages?.["404"] ?? manifest.notFound
851
+ if (rootLayoutIds.length === 0 || entry === undefined) return undefined
852
+ const layoutsOnly: RouteEntry = {
853
+ id: "_404",
854
+ pattern: "/*",
855
+ layoutIds: rootLayoutIds,
856
+ errorIds: [],
857
+ file: entry.file,
858
+ load: entry.load,
859
+ }
860
+ const route: RouteEntry =
861
+ rootMiddlewareIds.length === 0
862
+ ? layoutsOnly
863
+ : { ...layoutsOnly, middlewareIds: rootMiddlewareIds }
864
+ try {
865
+ const draft = await draftFlag(c.req)
866
+ if (draft) controls.personalize()
867
+ const ctx: LoaderContext = {
868
+ params: {},
869
+ request: c.req,
870
+ req: c.req,
871
+ api: apiFor(c),
872
+ env: c.env,
873
+ draft,
874
+ search: loaderSearch(page.searchSchema, c.req),
875
+ set: controls.scope(PAGE_SCOPE),
876
+ }
877
+ if (route.middlewareIds !== undefined) await runMiddleware(route, ctx, controls)
878
+ const run = await runLayoutChain(route, ctx, controls)
879
+ await run.pending
880
+ return settledLayouts(run.modules, run.layoutData, rootLayoutIds.length)
881
+ } catch {
882
+ return undefined
883
+ }
884
+ }
885
+
690
886
  const renderTerminalStatus = async (
691
887
  status: number,
692
- path?: string,
888
+ req: Request,
693
889
  extraHeaders?: HeadersLike,
694
890
  nonce?: string,
891
+ layouts?: LayoutChainSource,
695
892
  ): Promise<PageResponse> => {
696
893
  const page = manifest.statusPages?.[String(status)] ?? manifest.notFound
697
894
  if (page === undefined) {
@@ -700,36 +897,57 @@ export function createPageRequestExecutor<Env = unknown>(
700
897
  return new Response(STATUS_TEXT[status] ?? "Error", { status, headers })
701
898
  }
702
899
  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
900
+ const loaded = await page.load()
901
+ const wraps = loaded.inLayouts !== false && rootLayoutIds.length > 0
902
+ const chain =
903
+ wraps && layouts !== undefined ? await layouts(loaded, rootLayoutIds.length) : undefined
904
+ const modules = chain?.modules ?? []
905
+ const layoutIds = chain === undefined ? [] : rootLayoutIds
906
+ const layoutData = chain?.layoutData
907
+ const { chain: components, head } = resolveChainAndHead(
908
+ modules,
909
+ layoutData,
910
+ loaded,
911
+ statusMetaArgs(req, null, nonce),
912
+ )
913
+ let headers = extraHeaders
914
+ if (layoutData !== undefined) {
915
+ // The page now shows what a layout loaded for this visitor.
916
+ const privateHeaders = new Headers(extraHeaders)
917
+ privateHeaders.set("cache-control", PRIVATE_NO_STORE)
918
+ headers = privateHeaders
707
919
  }
708
- return renderPageResult({
920
+ const statusPage = {
709
921
  adapter,
710
- chain: [loaded.default],
922
+ chain: components,
711
923
  data: null,
924
+ head,
712
925
  clientEntry,
713
926
  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 } : {}),
927
+ matchChain: matchChainOf(layoutIds, modules, routeId, loaded),
928
+ search: searchChainOf(modules, loaded, req),
929
+ path: pathOf(req),
724
930
  status,
725
- ...(extraHeaders !== undefined ? { headers: extraHeaders } : {}),
931
+ // The browser renders this page inside the root layout, so a page that had to render alone
932
+ // stays server-only, as a nested `_404` does.
933
+ ...(wraps && chain === undefined ? { hydrate: false } : {}),
934
+ ...(layoutData === undefined ? {} : { layoutData }),
935
+ ...(headers !== undefined ? { headers } : {}),
726
936
  ...(nonce === undefined ? {} : { nonce }),
727
937
  ...cspOption,
728
938
  ...preloadOf(routeId),
729
- ...stylesOf(routeId),
939
+ // The build keys stylesheets by route, so a page inside the root layout links the whole-app one.
940
+ ...(chain === undefined ? stylesOf(routeId) : stylesFrom(options.styles)),
730
941
  ...prerenderedHandover,
731
942
  ...titleOption,
732
- })
943
+ }
944
+ if (chain === undefined) return renderPageResult(statusPage)
945
+ try {
946
+ return await renderPageResult(statusPage)
947
+ } catch {
948
+ // A layout that fails to render leaves the status page alone, never a second error.
949
+ return renderTerminalStatus(status, req, extraHeaders, nonce)
950
+ }
733
951
  }
734
952
 
735
953
  const renderStatusSignal = async (
@@ -737,6 +955,7 @@ export function createPageRequestExecutor<Env = unknown>(
737
955
  env: Env,
738
956
  signal: StatusSignal,
739
957
  personalized: boolean,
958
+ layouts?: LayoutChainSource,
740
959
  ): Promise<PageResponse> => {
741
960
  const { status, headers } = signal[STATUS_SIGNAL]
742
961
  if (req.headers.get(DATA_HEADER) !== null) {
@@ -750,7 +969,7 @@ export function createPageRequestExecutor<Env = unknown>(
750
969
  }
751
970
  const nonce = await resolveNonce(req, env)
752
971
  if (headers === undefined && !personalized) {
753
- return renderTerminalStatus(status, pathOf(req), undefined, nonce)
972
+ return renderTerminalStatus(status, req, undefined, nonce, layouts)
754
973
  }
755
974
  const documentHeaders = new Headers(headers)
756
975
  if (headers !== undefined) {
@@ -758,7 +977,7 @@ export function createPageRequestExecutor<Env = unknown>(
758
977
  }
759
978
  // A cookie queued before the signal makes the status page specific to this visitor.
760
979
  if (personalized) documentHeaders.set("cache-control", PRIVATE_NO_STORE)
761
- return renderTerminalStatus(status, pathOf(req), documentHeaders, nonce)
980
+ return renderTerminalStatus(status, req, documentHeaders, nonce, layouts)
762
981
  }
763
982
 
764
983
  const reportLoaderError = (
@@ -789,8 +1008,12 @@ export function createPageRequestExecutor<Env = unknown>(
789
1008
  assemblyCache: RenderAssemblyCache | undefined,
790
1009
  responseHeaders: Record<string, string> | undefined,
791
1010
  personalized: boolean,
1011
+ serverComponents?: import("../server-components/types.ts").ComponentState,
792
1012
  ): Promise<PageResponse> => {
793
- if (isStatusSignal(data)) return renderStatusSignal(req, env, data, personalized)
1013
+ // Every layout loader settled before the page rendered, so a status or error page can reuse them.
1014
+ const settled: LayoutChainSource = async (_page, count) =>
1015
+ settledLayouts(layoutModules, layoutData, count)
1016
+ if (isStatusSignal(data)) return renderStatusSignal(req, env, data, personalized, settled)
794
1017
  if (isControlFlow(data)) return data
795
1018
  if (req.headers.get(DATA_HEADER) !== null) {
796
1019
  const pageSplit = prepareDeferred(data)
@@ -809,7 +1032,7 @@ export function createPageRequestExecutor<Env = unknown>(
809
1032
  ...(boundarySplit === undefined ? [] : boundarySplit.deferred),
810
1033
  ]
811
1034
  const payload =
812
- layoutData === undefined && boundarySplit === undefined
1035
+ layoutData === undefined && boundarySplit === undefined && serverComponents === undefined
813
1036
  ? pageSplit.forClient
814
1037
  : {
815
1038
  v: 1 as const,
@@ -817,6 +1040,7 @@ export function createPageRequestExecutor<Env = unknown>(
817
1040
  layoutData: layoutSplits.map((split) => split.forClient),
818
1041
  retained: layoutRetained,
819
1042
  ...(boundarySplit === undefined ? {} : { boundaries: boundarySplit.forClient }),
1043
+ ...(serverComponents === undefined ? {} : { serverComponents }),
820
1044
  }
821
1045
  if (allDeferred.length === 0) {
822
1046
  return Response.json(payload ?? null, { headers: DATA_RESPONSE_HEADERS })
@@ -832,9 +1056,11 @@ export function createPageRequestExecutor<Env = unknown>(
832
1056
  const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env)
833
1057
  const { chain, head } = resolveChainAndHead(
834
1058
  layoutModules,
1059
+ layoutData,
835
1060
  mod,
836
1061
  {
837
1062
  data,
1063
+ pageData: data,
838
1064
  params,
839
1065
  origin: originOf(req),
840
1066
  ...(nonce === undefined ? {} : { nonce }),
@@ -853,6 +1079,8 @@ export function createPageRequestExecutor<Env = unknown>(
853
1079
  path: pathOf(req),
854
1080
  search: searchChainOf(layoutModules, mod, req),
855
1081
  matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
1082
+ ...(serverComponents === undefined ? {} : { serverComponents }),
1083
+ ...(progressiveActionData.has(req) ? { actionData: progressiveActionData.get(req) } : {}),
856
1084
  hydrate: mod.hydrate !== false,
857
1085
  ...(layoutData !== undefined ? { layoutData } : {}),
858
1086
  ...(boundaryStates !== undefined ? { boundaries: boundaryStates } : {}),
@@ -882,12 +1110,21 @@ export function createPageRequestExecutor<Env = unknown>(
882
1110
  ...(assemblyCache === undefined ? {} : { assemblyCache }),
883
1111
  })
884
1112
  } catch (err) {
885
- if (isStatusSignal(err)) return renderStatusSignal(req, env, err, personalized)
1113
+ if (isStatusSignal(err)) return renderStatusSignal(req, env, err, personalized, settled)
886
1114
  if (isControlFlow(err)) throw err
887
1115
  reportLoaderError(route, req, { ...params }, err)
888
1116
  const errorId = boundaryFor(route, err)
889
1117
  if (errorId === undefined) throw err
890
- return renderError(route, errorId, withDuplicateInstanceHint(err), personalized, nonce)
1118
+ options.logBoundaryError?.(err, req)
1119
+ return renderError(
1120
+ req,
1121
+ route,
1122
+ errorId,
1123
+ withDuplicateInstanceHint(err),
1124
+ personalized,
1125
+ nonce,
1126
+ settled,
1127
+ )
891
1128
  }
892
1129
  }
893
1130
 
@@ -899,11 +1136,13 @@ export function createPageRequestExecutor<Env = unknown>(
899
1136
  params: Record<string, string>,
900
1137
  err: unknown,
901
1138
  personalized: boolean,
1139
+ layouts?: LayoutChainSource,
902
1140
  ): Promise<PageResponse> => {
903
1141
  if (isControlFlow(err)) throw err
904
1142
  reportLoaderError(route, req, params, err)
905
1143
  const errorId = boundaryFor(route, err)
906
1144
  if (errorId === undefined) throw err
1145
+ options.logBoundaryError?.(err, req)
907
1146
  if (req.headers.get(DATA_HEADER) !== null) {
908
1147
  return new Response("Internal Server Error", {
909
1148
  status: 500,
@@ -911,11 +1150,13 @@ export function createPageRequestExecutor<Env = unknown>(
911
1150
  })
912
1151
  }
913
1152
  return renderError(
1153
+ req,
914
1154
  route,
915
1155
  errorId,
916
1156
  withDuplicateInstanceHint(err),
917
1157
  personalized,
918
1158
  options.nonce === undefined ? undefined : await resolveNonce(req, env),
1159
+ layouts,
919
1160
  )
920
1161
  }
921
1162
 
@@ -946,8 +1187,9 @@ export function createPageRequestExecutor<Env = unknown>(
946
1187
  }
947
1188
  const mod = await page.load()
948
1189
  const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env)
949
- const { chain, head } = resolveChainAndHead(layoutModules, mod, {
1190
+ const { chain, head } = resolveChainAndHead(layoutModules, layoutData, mod, {
950
1191
  data: null,
1192
+ pageData: null,
951
1193
  params,
952
1194
  origin: originOf(req),
953
1195
  ...(nonce === undefined ? {} : { nonce }),
@@ -1018,17 +1260,17 @@ export function createPageRequestExecutor<Env = unknown>(
1018
1260
  }
1019
1261
 
1020
1262
  const renderNotFound = async (
1021
- request: Request,
1022
- env: Env,
1023
- path?: string,
1263
+ c: PageRouteContext<Env>,
1264
+ controls: PageResponseControls,
1024
1265
  ): Promise<PageResponse> => {
1025
1266
  const page = manifest.statusPages?.["404"] ?? manifest.notFound
1026
- if (page === undefined) return renderTerminalStatus(404, path)
1267
+ if (page === undefined) return renderTerminalStatus(404, c.req)
1027
1268
  return renderTerminalStatus(
1028
1269
  404,
1029
- path,
1270
+ c.req,
1030
1271
  undefined,
1031
- options.nonce === undefined ? undefined : await resolveNonce(request, env),
1272
+ options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env),
1273
+ rootLayoutsFor(c, controls),
1032
1274
  )
1033
1275
  }
1034
1276
 
@@ -1084,8 +1326,19 @@ export function createPageRequestExecutor<Env = unknown>(
1084
1326
  }
1085
1327
 
1086
1328
  return withResponseControls(async (c, controls) => {
1087
- if (is404Fallback && !prerenderedSet.has(urlPartsFor(c.req).pathname)) {
1088
- return renderNotFound(c.req, c.env, pathOf(c.req))
1329
+ const componentRuntime = options.serverComponents
1330
+ const componentOwner = componentRuntime?.owns(route.id) === true
1331
+ let componentRequest:
1332
+ | import("../server-components/protocol.ts").ComponentRequest
1333
+ | undefined
1334
+ try {
1335
+ componentRequest = componentRuntime?.classify(c.req, route.id)
1336
+ } catch (error) {
1337
+ return componentRuntime!.failure(error)
1338
+ }
1339
+ if (componentOwner || componentRequest !== undefined) controls.personalize()
1340
+ if (!componentOwner && is404Fallback && !prerenderedSet.has(urlPartsFor(c.req).pathname)) {
1341
+ return renderNotFound(c, controls)
1089
1342
  }
1090
1343
  const mod = await route.load()
1091
1344
  const draft = await draftFlag(c.req)
@@ -1097,6 +1350,7 @@ export function createPageRequestExecutor<Env = unknown>(
1097
1350
  let layoutModules: LoadedLayoutModules | undefined
1098
1351
  let layoutRetained: readonly number[] = []
1099
1352
  let responseHeaders: Record<string, string> | undefined
1353
+ let componentState: import("../server-components/types.ts").ComponentState | undefined
1100
1354
  const requestApi = apiFor(c)
1101
1355
  let run: LayoutRun | undefined
1102
1356
  try {
@@ -1111,13 +1365,21 @@ export function createPageRequestExecutor<Env = unknown>(
1111
1365
  set: controls.scope(PAGE_SCOPE),
1112
1366
  }
1113
1367
  if (route.middlewareIds !== undefined) await runMiddleware(route, ctx, controls)
1114
- run = await runLayoutChain(route, ctx, controls, retainContextOf(c.req))
1368
+ run = await runLayoutChain(
1369
+ route,
1370
+ ctx,
1371
+ controls,
1372
+ componentOwner ? undefined : retainContextOf(c.req),
1373
+ )
1115
1374
  layoutModules = run.modules
1116
1375
  layoutRetained = run.retained
1117
- const boundaryDefinitions = [
1118
- ...run.modules.flatMap((layout) => layout.boundaries ?? []),
1119
- ...(mod.boundaries ?? []),
1120
- ]
1376
+ const boundaryDefinitions =
1377
+ componentRequest === undefined
1378
+ ? [
1379
+ ...run.modules.flatMap((layout) => layout.boundaries ?? []),
1380
+ ...(mod.boundaries ?? []),
1381
+ ]
1382
+ : []
1121
1383
  if (boundaryDefinitions.length > 0) boundaryDescriptors(boundaryDefinitions)
1122
1384
  const effectiveSearch = searchChainOf(run.modules, mod, c.req)
1123
1385
  const boundaryBatch =
@@ -1149,6 +1411,34 @@ export function createPageRequestExecutor<Env = unknown>(
1149
1411
  : await Promise.all([pageResult, run.pending, staticBoundaryPromise])
1150
1412
  data = pageData
1151
1413
  layoutData = run.layoutData
1414
+ if (componentOwner) {
1415
+ for (let i = 0; i < (layoutData?.length ?? 0); i++) {
1416
+ const outcome = layoutData?.[i]
1417
+ if (isControlFlow(outcome) || isStatusSignal(outcome))
1418
+ throw tagLayoutError(outcome, route.layoutIds[i] as string)
1419
+ }
1420
+ }
1421
+ if (componentRequest !== undefined && (isControlFlow(data) || isStatusSignal(data)))
1422
+ return componentRuntime!.failure(data)
1423
+ if (componentOwner && !isControlFlow(data) && !isStatusSignal(data)) {
1424
+ const componentContext = Object.create(
1425
+ c,
1426
+ Object.getOwnPropertyDescriptors({
1427
+ ...ctx,
1428
+ search: effectiveSearch,
1429
+ signal:
1430
+ c.signal === undefined ? c.req.signal : AbortSignal.any([c.signal, c.req.signal]),
1431
+ data,
1432
+ layoutData: layoutData ?? [],
1433
+ }),
1434
+ )
1435
+ componentState = await componentRuntime!.execute(
1436
+ route.id,
1437
+ componentContext,
1438
+ componentRequest,
1439
+ options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env),
1440
+ )
1441
+ }
1152
1442
  if (boundaryBatch !== undefined) {
1153
1443
  const streamed = { ...boundaryBatch.initial, ...(staticBoundaries ?? {}) }
1154
1444
  for (const entry of boundaryBatch.pending) {
@@ -1164,6 +1454,7 @@ export function createPageRequestExecutor<Env = unknown>(
1164
1454
  // by the same boundary as one.
1165
1455
  responseHeaders = controls.documentHeaders()
1166
1456
  } catch (err) {
1457
+ if (componentRequest !== undefined) return componentRuntime!.failure(err)
1167
1458
  if (isStatusSignal(err)) {
1168
1459
  return (
1169
1460
  (await renderRouteNotFound(
@@ -1174,10 +1465,27 @@ export function createPageRequestExecutor<Env = unknown>(
1174
1465
  err,
1175
1466
  run,
1176
1467
  controls.personalized,
1177
- )) ?? renderStatusSignal(c.req, c.env, err, controls.personalized)
1468
+ )) ??
1469
+ renderStatusSignal(c.req, c.env, err, controls.personalized, leadingLayouts(run, err))
1178
1470
  )
1179
1471
  }
1180
- return renderLoaderFailure(route, c.req, c.env, c.params, err, controls.personalized)
1472
+ return renderLoaderFailure(
1473
+ route,
1474
+ c.req,
1475
+ c.env,
1476
+ c.params,
1477
+ err,
1478
+ controls.personalized,
1479
+ leadingLayouts(run, err),
1480
+ )
1481
+ }
1482
+ if (componentRequest !== undefined && componentState !== undefined) {
1483
+ const answer = componentRuntime!.response(componentState, componentRequest)
1484
+ const headers = new Headers(answer.headers)
1485
+ for (const [name, value] of Object.entries(responseHeaders ?? {}))
1486
+ if (name !== "cache-control" && name !== "content-type" && name !== "vary")
1487
+ headers.set(name, value)
1488
+ return new Response(answer.body, { status: answer.status, headers })
1181
1489
  }
1182
1490
  if (isStatusSignal(data)) {
1183
1491
  const nested = await renderRouteNotFound(
@@ -1205,6 +1513,7 @@ export function createPageRequestExecutor<Env = unknown>(
1205
1513
  layoutModules === undefined ? undefined : assemblyCacheFor(mod, layoutModules),
1206
1514
  responseHeaders,
1207
1515
  controls.personalized,
1516
+ componentState,
1208
1517
  )
1209
1518
  })
1210
1519
  },
@@ -1220,6 +1529,32 @@ export function createPageRequestExecutor<Env = unknown>(
1220
1529
  headers: { allow: "GET", "content-type": "text/plain; charset=utf-8" },
1221
1530
  })
1222
1531
  }
1532
+ const isDataRequest = c.req.headers.get(DATA_HEADER) !== null
1533
+ // Before anything runs: a form posts cross-site without a preflight and carries the visitor's
1534
+ // cookies, so an action is a CSRF target unless its route names the origins it accepts.
1535
+ const origin = c.req.headers.get("origin")
1536
+ const allowedOrigins = mod.allowedOrigins
1537
+ const allowed =
1538
+ allowedOrigins === undefined
1539
+ ? origin === null ||
1540
+ isSameOriginForm(origin, c.req.headers.get("sec-fetch-site"), c.req)
1541
+ : typeof allowedOrigins === "function"
1542
+ ? allowedOrigins(origin) === true
1543
+ : origin !== null && allowedOrigins.includes(origin)
1544
+ if (!allowed) {
1545
+ return isDataRequest
1546
+ ? Response.json(
1547
+ { ok: false, error: "forbidden_origin" },
1548
+ { status: 403, headers: { "cache-control": PRIVATE_NO_STORE } },
1549
+ )
1550
+ : new Response("Forbidden", {
1551
+ status: 403,
1552
+ headers: {
1553
+ "content-type": "text/plain; charset=utf-8",
1554
+ "cache-control": PRIVATE_NO_STORE,
1555
+ },
1556
+ })
1557
+ }
1223
1558
  const requestApi = apiFor(c)
1224
1559
  const actionContext: LoaderContext = {
1225
1560
  params: c.params,
@@ -1231,12 +1566,11 @@ export function createPageRequestExecutor<Env = unknown>(
1231
1566
  search: loaderSearch(mod.searchSchema, c.req),
1232
1567
  set: controls.scope(ACTION_SCOPE),
1233
1568
  }
1234
- const isDataRequest = c.req.headers.get(DATA_HEADER) !== null
1235
- let layoutModules: LoadedLayoutModules
1569
+ let gated: GatedLayouts
1236
1570
  let result: unknown
1237
1571
  try {
1238
1572
  if (route.middlewareIds !== undefined) await runMiddleware(route, actionContext, controls)
1239
- layoutModules = await runLayoutGates(route, actionContext, controls)
1573
+ gated = await runLayoutGates(route, actionContext, controls)
1240
1574
  result = await mod.action(actionContext)
1241
1575
  } catch (err) {
1242
1576
  if (isControlFlow(err)) return err
@@ -1271,26 +1605,136 @@ export function createPageRequestExecutor<Env = unknown>(
1271
1605
  },
1272
1606
  })
1273
1607
  }
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),
1608
+ if (options.serverComponents?.owns(route.id)) {
1609
+ if (options.rerenderOwnerGET === undefined) throw new Error("component_contract")
1610
+ const actionHeaders = controls.documentHeaders()
1611
+ if (controls.progressiveRedirect(c.req)) {
1612
+ // Relative, so a browser behind a TLS terminator stays on its own scheme. A `//` path
1613
+ // would read as another host, so that one keeps the absolute URL.
1614
+ const { pathname, search } = new URL(c.req.url)
1615
+ const location = pathname.startsWith("//") ? c.req.url : pathname + search
1616
+ return new Response(null, {
1617
+ status: 303,
1618
+ headers: { ...actionHeaders, location, "cache-control": PRIVATE_NO_STORE },
1619
+ })
1620
+ }
1621
+ const headers = new Headers(c.req.headers)
1622
+ for (const name of [
1623
+ DATA_HEADER,
1624
+ RETAIN_HEADER,
1625
+ NAV_FROM_HEADER,
1626
+ "x-nifra-component",
1627
+ "content-type",
1628
+ "content-length",
1629
+ "transfer-encoding",
1630
+ ])
1631
+ headers.delete(name)
1632
+ const cookie = controls.progressiveCookies(c.req)
1633
+ if (cookie) headers.set("cookie", cookie)
1634
+ else headers.delete("cookie")
1635
+ const request = new Request(c.req.url, {
1636
+ method: "GET",
1637
+ headers,
1638
+ signal:
1639
+ c.signal === undefined ? c.req.signal : AbortSignal.any([c.signal, c.req.signal]),
1640
+ })
1641
+ progressiveActionData.set(request, actionResult)
1642
+ try {
1643
+ const response = await options.rerenderOwnerGET(request, c)
1644
+ const merged = new Headers(response.headers)
1645
+ for (const [name, value] of Object.entries(actionHeaders ?? {}))
1646
+ if (!merged.has(name)) merged.set(name, value)
1647
+ merged.set("cache-control", PRIVATE_NO_STORE)
1648
+ return new Response(response.body, {
1649
+ status: response.status,
1650
+ statusText: response.statusText,
1651
+ headers: merged,
1285
1652
  })
1286
- : null
1287
- const responseHeaders = controls.documentHeaders()
1653
+ } finally {
1654
+ progressiveActionData.delete(request)
1655
+ }
1656
+ }
1657
+ const layoutModules = gated.modules
1658
+ const search = searchChainOf(layoutModules, mod, c.req)
1659
+ let run: LayoutRun | undefined
1660
+ let data: unknown
1661
+ let responseHeaders: Record<string, string> | undefined
1662
+ // The ordinary layout loaders run now, after the mutation, as a GET of this page would.
1663
+ try {
1664
+ run = await runLayoutChain(route, actionContext, controls, undefined, gated)
1665
+ const pageResult = mod.loader
1666
+ ? mod.loader({
1667
+ params: c.params,
1668
+ request: c.req,
1669
+ req: c.req,
1670
+ api: requestApi,
1671
+ env: c.env,
1672
+ draft,
1673
+ search,
1674
+ set: controls.scope(PAGE_SCOPE),
1675
+ })
1676
+ : null
1677
+ const [pageData] =
1678
+ run.pending === undefined
1679
+ ? ([await pageResult] as const)
1680
+ : await Promise.all([pageResult, run.pending])
1681
+ data = pageData
1682
+ responseHeaders = controls.documentHeaders()
1683
+ } catch (err) {
1684
+ if (isStatusSignal(err)) {
1685
+ return (
1686
+ (await renderRouteNotFound(
1687
+ c.req,
1688
+ c.env,
1689
+ route,
1690
+ c.params,
1691
+ err,
1692
+ run,
1693
+ controls.personalized,
1694
+ )) ??
1695
+ renderStatusSignal(c.req, c.env, err, controls.personalized, leadingLayouts(run, err))
1696
+ )
1697
+ }
1698
+ return renderLoaderFailure(
1699
+ route,
1700
+ c.req,
1701
+ c.env,
1702
+ c.params,
1703
+ err,
1704
+ controls.personalized,
1705
+ leadingLayouts(run, err),
1706
+ )
1707
+ }
1708
+ // The re-render answers a returned redirect or status the way the page's GET does.
1709
+ if (isStatusSignal(data))
1710
+ return (
1711
+ (await renderRouteNotFound(
1712
+ c.req,
1713
+ c.env,
1714
+ route,
1715
+ c.params,
1716
+ data,
1717
+ run,
1718
+ controls.personalized,
1719
+ )) ??
1720
+ renderStatusSignal(c.req, c.env, data, controls.personalized, leadingLayouts(run, data))
1721
+ )
1722
+ if (isControlFlow(data)) return data
1723
+ const layoutData = run.layoutData
1724
+ const layoutProps: { layoutData?: Exclude<typeof layoutData, undefined> } = {}
1725
+ if (layoutData !== undefined) {
1726
+ layoutProps.layoutData = layoutData
1727
+ // The page now shows what a layout loaded for this visitor.
1728
+ responseHeaders = { ...responseHeaders, ...PRIVATE_PAGE_HEADERS }
1729
+ }
1288
1730
  const nonce = options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env)
1289
1731
  const { chain, head } = resolveChainAndHead(
1290
1732
  layoutModules,
1733
+ layoutData,
1291
1734
  mod,
1292
1735
  {
1293
1736
  data,
1737
+ pageData: data,
1294
1738
  params: c.params,
1295
1739
  origin: originOf(c.req),
1296
1740
  ...(nonce === undefined ? {} : { nonce }),
@@ -1310,6 +1754,7 @@ export function createPageRequestExecutor<Env = unknown>(
1310
1754
  search,
1311
1755
  matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
1312
1756
  hydrate: mod.hydrate !== false,
1757
+ ...layoutProps,
1313
1758
  ...preloadOf(route.id),
1314
1759
  ...stylesOf(route.id),
1315
1760
  ...prerenderedHandover,
@@ -1324,7 +1769,7 @@ export function createPageRequestExecutor<Env = unknown>(
1324
1769
  const match = matchNotFoundScope?.(pathOf(c.req)) ?? null
1325
1770
  const target = match === null ? undefined : notFoundTargets[Number(match.routeId)]
1326
1771
  if (match === null || target === undefined) {
1327
- return renderNotFound(c.req, c.env, pathOf(c.req))
1772
+ return renderNotFound(c, controls)
1328
1773
  }
1329
1774
  const params: Record<string, string> = {}
1330
1775
  for (let i = 0; i < target.names.length; i++) {
@@ -1333,7 +1778,7 @@ export function createPageRequestExecutor<Env = unknown>(
1333
1778
  const mod = await target.page.load()
1334
1779
  const draft = await draftFlag(c.req)
1335
1780
  if (draft) controls.personalize()
1336
- let run: LayoutRun
1781
+ let run: LayoutRun | undefined
1337
1782
  try {
1338
1783
  const ctx: LoaderContext = {
1339
1784
  params,
@@ -1352,9 +1797,23 @@ export function createPageRequestExecutor<Env = unknown>(
1352
1797
  await run.pending
1353
1798
  } catch (err) {
1354
1799
  if (isStatusSignal(err)) {
1355
- return renderStatusSignal(c.req, c.env, err, controls.personalized)
1800
+ return renderStatusSignal(
1801
+ c.req,
1802
+ c.env,
1803
+ err,
1804
+ controls.personalized,
1805
+ leadingLayouts(run, err),
1806
+ )
1356
1807
  }
1357
- return renderLoaderFailure(target.route, c.req, c.env, params, err, controls.personalized)
1808
+ return renderLoaderFailure(
1809
+ target.route,
1810
+ c.req,
1811
+ c.env,
1812
+ params,
1813
+ err,
1814
+ controls.personalized,
1815
+ leadingLayouts(run, err),
1816
+ )
1358
1817
  }
1359
1818
  return renderNestedNotFound(
1360
1819
  c.req,