@nifrajs/web 3.5.0 → 4.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (390) hide show
  1. package/dist/backend-only.d.ts +15 -0
  2. package/dist/backend-only.d.ts.map +1 -0
  3. package/dist/backend-only.js +16 -0
  4. package/dist/backend-only.js.map +1 -0
  5. package/dist/boundary.d.ts +5 -0
  6. package/dist/boundary.d.ts.map +1 -1
  7. package/dist/boundary.js +11 -2
  8. package/dist/boundary.js.map +1 -1
  9. package/dist/build-plan.d.ts +18 -6
  10. package/dist/build-plan.d.ts.map +1 -1
  11. package/dist/build-plan.js +82 -34
  12. package/dist/build-plan.js.map +1 -1
  13. package/dist/build-vite.d.ts +4 -0
  14. package/dist/build-vite.d.ts.map +1 -1
  15. package/dist/build-vite.js +68 -13
  16. package/dist/build-vite.js.map +1 -1
  17. package/dist/build.d.ts +57 -23
  18. package/dist/build.d.ts.map +1 -1
  19. package/dist/build.js +356 -85
  20. package/dist/build.js.map +1 -1
  21. package/dist/bun-dev-entry.d.ts +1 -1
  22. package/dist/bun-dev-entry.js +1 -1
  23. package/dist/cdn.d.ts +74 -0
  24. package/dist/cdn.d.ts.map +1 -0
  25. package/dist/cdn.js +132 -0
  26. package/dist/cdn.js.map +1 -0
  27. package/dist/client.d.ts +3 -1
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +136 -35
  30. package/dist/client.js.map +1 -1
  31. package/dist/conformance.d.ts +4 -3
  32. package/dist/conformance.d.ts.map +1 -1
  33. package/dist/conformance.js +12 -3
  34. package/dist/conformance.js.map +1 -1
  35. package/dist/csp.d.ts +93 -0
  36. package/dist/csp.d.ts.map +1 -0
  37. package/dist/csp.js +122 -0
  38. package/dist/csp.js.map +1 -0
  39. package/dist/dev-client.d.ts +76 -0
  40. package/dist/dev-client.d.ts.map +1 -0
  41. package/dist/dev-client.js +280 -0
  42. package/dist/dev-client.js.map +1 -0
  43. package/dist/dev-diagnostics.d.ts +26 -2
  44. package/dist/dev-diagnostics.d.ts.map +1 -1
  45. package/dist/dev-diagnostics.js +38 -7
  46. package/dist/dev-diagnostics.js.map +1 -1
  47. package/dist/dev-error.d.ts +4 -13
  48. package/dist/dev-error.d.ts.map +1 -1
  49. package/dist/dev-error.js +46 -4
  50. package/dist/dev-error.js.map +1 -1
  51. package/dist/dev-feed.d.ts +298 -0
  52. package/dist/dev-feed.d.ts.map +1 -0
  53. package/dist/dev-feed.js +797 -0
  54. package/dist/dev-feed.js.map +1 -0
  55. package/dist/dev-indicator.d.ts +38 -0
  56. package/dist/dev-indicator.d.ts.map +1 -0
  57. package/dist/dev-indicator.js +93 -0
  58. package/dist/dev-indicator.js.map +1 -0
  59. package/dist/dev-session.d.ts +77 -0
  60. package/dist/dev-session.d.ts.map +1 -0
  61. package/dist/dev-session.js +580 -0
  62. package/dist/dev-session.js.map +1 -0
  63. package/dist/dev-sourcemap.d.ts +27 -0
  64. package/dist/dev-sourcemap.d.ts.map +1 -0
  65. package/dist/dev-sourcemap.js +235 -0
  66. package/dist/dev-sourcemap.js.map +1 -0
  67. package/dist/dev-ssr-graph.d.ts.map +1 -1
  68. package/dist/dev-ssr-graph.js +13 -5
  69. package/dist/dev-ssr-graph.js.map +1 -1
  70. package/dist/dev.d.ts +15 -2
  71. package/dist/dev.d.ts.map +1 -1
  72. package/dist/dev.js +69 -44
  73. package/dist/dev.js.map +1 -1
  74. package/dist/diagnostic-catalog.d.ts +35 -0
  75. package/dist/diagnostic-catalog.d.ts.map +1 -0
  76. package/dist/diagnostic-catalog.js +203 -0
  77. package/dist/diagnostic-catalog.js.map +1 -0
  78. package/dist/diagnostic-prompt.d.ts +49 -0
  79. package/dist/diagnostic-prompt.d.ts.map +1 -0
  80. package/dist/diagnostic-prompt.js +303 -0
  81. package/dist/diagnostic-prompt.js.map +1 -0
  82. package/dist/diagnostic.d.ts +17 -23
  83. package/dist/diagnostic.d.ts.map +1 -1
  84. package/dist/diagnostic.js +31 -39
  85. package/dist/diagnostic.js.map +1 -1
  86. package/dist/draft.d.ts.map +1 -1
  87. package/dist/draft.js +4 -1
  88. package/dist/draft.js.map +1 -1
  89. package/dist/fn.d.ts +5 -0
  90. package/dist/fn.d.ts.map +1 -1
  91. package/dist/fn.js +13 -3
  92. package/dist/fn.js.map +1 -1
  93. package/dist/fs.d.ts.map +1 -1
  94. package/dist/fs.js +4 -2
  95. package/dist/fs.js.map +1 -1
  96. package/dist/index.d.ts +13 -15
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +11 -12
  99. package/dist/index.js.map +1 -1
  100. package/dist/internal/cdn/cloudflare.d.ts +48 -0
  101. package/dist/internal/cdn/cloudflare.d.ts.map +1 -0
  102. package/dist/internal/cdn/cloudflare.js +94 -0
  103. package/dist/internal/cdn/cloudflare.js.map +1 -0
  104. package/dist/internal/cdn/core.d.ts +91 -0
  105. package/dist/internal/cdn/core.d.ts.map +1 -0
  106. package/dist/internal/cdn/core.js +220 -0
  107. package/dist/internal/cdn/core.js.map +1 -0
  108. package/dist/internal/cdn/fastly.d.ts +17 -0
  109. package/dist/internal/cdn/fastly.d.ts.map +1 -0
  110. package/dist/internal/cdn/fastly.js +43 -0
  111. package/dist/internal/cdn/fastly.js.map +1 -0
  112. package/dist/internal/cdn/vercel.d.ts +31 -0
  113. package/dist/internal/cdn/vercel.d.ts.map +1 -0
  114. package/dist/internal/cdn/vercel.js +56 -0
  115. package/dist/internal/cdn/vercel.js.map +1 -0
  116. package/dist/internal/code-unit-order.d.ts +4 -0
  117. package/dist/internal/code-unit-order.d.ts.map +1 -0
  118. package/dist/internal/code-unit-order.js +6 -0
  119. package/dist/internal/code-unit-order.js.map +1 -0
  120. package/dist/internal/codegen.d.ts.map +1 -1
  121. package/dist/internal/codegen.js +141 -25
  122. package/dist/internal/codegen.js.map +1 -1
  123. package/dist/internal/dev-csp.d.ts +24 -0
  124. package/dist/internal/dev-csp.d.ts.map +1 -0
  125. package/dist/internal/dev-csp.js +160 -0
  126. package/dist/internal/dev-csp.js.map +1 -0
  127. package/dist/internal/dev-reserved.d.ts +25 -0
  128. package/dist/internal/dev-reserved.d.ts.map +1 -0
  129. package/dist/internal/dev-reserved.js +39 -0
  130. package/dist/internal/dev-reserved.js.map +1 -0
  131. package/dist/internal/html-spans.d.ts +10 -0
  132. package/dist/internal/html-spans.d.ts.map +1 -0
  133. package/dist/internal/html-spans.js +46 -0
  134. package/dist/internal/html-spans.js.map +1 -0
  135. package/dist/internal/loading-runtime.d.ts +44 -0
  136. package/dist/internal/loading-runtime.d.ts.map +1 -0
  137. package/dist/internal/loading-runtime.js +206 -0
  138. package/dist/internal/loading-runtime.js.map +1 -0
  139. package/dist/internal/matches-runtime.d.ts +8 -0
  140. package/dist/internal/matches-runtime.d.ts.map +1 -0
  141. package/dist/internal/matches-runtime.js +61 -0
  142. package/dist/internal/matches-runtime.js.map +1 -0
  143. package/dist/internal/mount-shadow.d.ts +36 -0
  144. package/dist/internal/mount-shadow.d.ts.map +1 -0
  145. package/dist/internal/mount-shadow.js +65 -0
  146. package/dist/internal/mount-shadow.js.map +1 -0
  147. package/dist/internal/node-builtins.d.ts +3 -0
  148. package/dist/internal/node-builtins.d.ts.map +1 -0
  149. package/dist/internal/node-builtins.js +79 -0
  150. package/dist/internal/node-builtins.js.map +1 -0
  151. package/dist/internal/output-guard.d.ts +65 -0
  152. package/dist/internal/output-guard.d.ts.map +1 -0
  153. package/dist/internal/output-guard.js +630 -0
  154. package/dist/internal/output-guard.js.map +1 -0
  155. package/dist/internal/page-execution.d.ts +20 -8
  156. package/dist/internal/page-execution.d.ts.map +1 -1
  157. package/dist/internal/page-execution.js +571 -105
  158. package/dist/internal/page-execution.js.map +1 -1
  159. package/dist/internal/parity.d.ts +21 -0
  160. package/dist/internal/parity.d.ts.map +1 -1
  161. package/dist/internal/parity.js +104 -22
  162. package/dist/internal/parity.js.map +1 -1
  163. package/dist/internal/private-env.d.ts +12 -0
  164. package/dist/internal/private-env.d.ts.map +1 -0
  165. package/dist/internal/private-env.js +335 -0
  166. package/dist/internal/private-env.js.map +1 -0
  167. package/dist/internal/public-url.d.ts +3 -0
  168. package/dist/internal/public-url.d.ts.map +1 -0
  169. package/dist/internal/public-url.js +18 -0
  170. package/dist/internal/public-url.js.map +1 -0
  171. package/dist/internal/render-document.d.ts +25 -19
  172. package/dist/internal/render-document.d.ts.map +1 -1
  173. package/dist/internal/render-document.js +77 -61
  174. package/dist/internal/render-document.js.map +1 -1
  175. package/dist/internal/response-controls.d.ts +75 -0
  176. package/dist/internal/response-controls.d.ts.map +1 -0
  177. package/dist/internal/response-controls.js +256 -0
  178. package/dist/internal/response-controls.js.map +1 -0
  179. package/dist/internal/runtime-contract.d.ts +11 -16
  180. package/dist/internal/runtime-contract.d.ts.map +1 -1
  181. package/dist/internal/runtime-contract.js.map +1 -1
  182. package/dist/internal/secret-scan.d.ts +104 -0
  183. package/dist/internal/secret-scan.d.ts.map +1 -0
  184. package/dist/internal/secret-scan.js +503 -0
  185. package/dist/internal/secret-scan.js.map +1 -0
  186. package/dist/internal/server-boundary.d.ts +6 -29
  187. package/dist/internal/server-boundary.d.ts.map +1 -1
  188. package/dist/internal/server-boundary.js +6 -66
  189. package/dist/internal/server-boundary.js.map +1 -1
  190. package/dist/internal/source-map.d.ts +47 -0
  191. package/dist/internal/source-map.d.ts.map +1 -0
  192. package/dist/internal/source-map.js +225 -0
  193. package/dist/internal/source-map.js.map +1 -0
  194. package/dist/internal/target-compat.d.ts +27 -0
  195. package/dist/internal/target-compat.d.ts.map +1 -0
  196. package/dist/internal/target-compat.js +77 -0
  197. package/dist/internal/target-compat.js.map +1 -0
  198. package/dist/internal/timing-safe-equal.d.ts +6 -0
  199. package/dist/internal/timing-safe-equal.d.ts.map +1 -1
  200. package/dist/internal/timing-safe-equal.js +10 -0
  201. package/dist/internal/timing-safe-equal.js.map +1 -1
  202. package/dist/internal/vitals-runtime.d.ts +18 -0
  203. package/dist/internal/vitals-runtime.d.ts.map +1 -0
  204. package/dist/internal/vitals-runtime.js +38 -0
  205. package/dist/internal/vitals-runtime.js.map +1 -0
  206. package/dist/internal/web-app.d.ts +38 -9
  207. package/dist/internal/web-app.d.ts.map +1 -1
  208. package/dist/internal/web-app.js +199 -59
  209. package/dist/internal/web-app.js.map +1 -1
  210. package/dist/internal/zone-graph.d.ts +89 -0
  211. package/dist/internal/zone-graph.d.ts.map +1 -0
  212. package/dist/internal/zone-graph.js +492 -0
  213. package/dist/internal/zone-graph.js.map +1 -0
  214. package/dist/isr.d.ts +83 -6
  215. package/dist/isr.d.ts.map +1 -1
  216. package/dist/isr.js +263 -36
  217. package/dist/isr.js.map +1 -1
  218. package/dist/llms-txt.d.ts.map +1 -1
  219. package/dist/llms-txt.js +31 -6
  220. package/dist/llms-txt.js.map +1 -1
  221. package/dist/manifest.d.ts +220 -13
  222. package/dist/manifest.d.ts.map +1 -1
  223. package/dist/manifest.js +394 -9
  224. package/dist/manifest.js.map +1 -1
  225. package/dist/module-graph.d.ts +26 -5
  226. package/dist/module-graph.d.ts.map +1 -1
  227. package/dist/module-graph.js +17 -7
  228. package/dist/module-graph.js.map +1 -1
  229. package/dist/navigation.d.ts +17 -3
  230. package/dist/navigation.d.ts.map +1 -1
  231. package/dist/navigation.js +11 -5
  232. package/dist/navigation.js.map +1 -1
  233. package/dist/nonce.d.ts +47 -0
  234. package/dist/nonce.d.ts.map +1 -0
  235. package/dist/nonce.js +63 -0
  236. package/dist/nonce.js.map +1 -0
  237. package/dist/plugins/css-modules.d.ts.map +1 -1
  238. package/dist/plugins/css-modules.js +4 -3
  239. package/dist/plugins/css-modules.js.map +1 -1
  240. package/dist/plugins/kit.d.ts +9 -0
  241. package/dist/plugins/kit.d.ts.map +1 -1
  242. package/dist/plugins/kit.js +18 -0
  243. package/dist/plugins/kit.js.map +1 -1
  244. package/dist/plugins/svg.d.ts +22 -3
  245. package/dist/plugins/svg.d.ts.map +1 -1
  246. package/dist/plugins/svg.js +112 -1
  247. package/dist/plugins/svg.js.map +1 -1
  248. package/dist/plugins/vite-leak-guard.d.ts +107 -3
  249. package/dist/plugins/vite-leak-guard.d.ts.map +1 -1
  250. package/dist/plugins/vite-leak-guard.js +378 -17
  251. package/dist/plugins/vite-leak-guard.js.map +1 -1
  252. package/dist/plugins/vite-zone-guard.d.ts +61 -0
  253. package/dist/plugins/vite-zone-guard.d.ts.map +1 -0
  254. package/dist/plugins/vite-zone-guard.js +208 -0
  255. package/dist/plugins/vite-zone-guard.js.map +1 -0
  256. package/dist/plugins/zone-guard.d.ts +27 -0
  257. package/dist/plugins/zone-guard.d.ts.map +1 -0
  258. package/dist/plugins/zone-guard.js +80 -0
  259. package/dist/plugins/zone-guard.js.map +1 -0
  260. package/dist/prerender.d.ts +10 -0
  261. package/dist/prerender.d.ts.map +1 -1
  262. package/dist/prerender.js +35 -3
  263. package/dist/prerender.js.map +1 -1
  264. package/dist/public-dir.d.ts +9 -0
  265. package/dist/public-dir.d.ts.map +1 -1
  266. package/dist/public-dir.js +67 -9
  267. package/dist/public-dir.js.map +1 -1
  268. package/dist/pwa-manifest.d.ts +68 -0
  269. package/dist/pwa-manifest.d.ts.map +1 -0
  270. package/dist/pwa-manifest.js +178 -0
  271. package/dist/pwa-manifest.js.map +1 -0
  272. package/dist/query.js +4 -4
  273. package/dist/query.js.map +1 -1
  274. package/dist/render-seam.d.ts +42 -2
  275. package/dist/render-seam.d.ts.map +1 -1
  276. package/dist/render-seam.js +6 -0
  277. package/dist/render-seam.js.map +1 -1
  278. package/dist/route-manifest.d.ts +3 -0
  279. package/dist/route-manifest.d.ts.map +1 -1
  280. package/dist/route-manifest.js +26 -3
  281. package/dist/route-manifest.js.map +1 -1
  282. package/dist/route-types.d.ts +24 -0
  283. package/dist/route-types.d.ts.map +1 -0
  284. package/dist/route-types.js +195 -0
  285. package/dist/route-types.js.map +1 -0
  286. package/dist/router.d.ts +28 -6
  287. package/dist/router.d.ts.map +1 -1
  288. package/dist/router.js +129 -40
  289. package/dist/router.js.map +1 -1
  290. package/dist/vitals.d.ts +33 -0
  291. package/dist/vitals.d.ts.map +1 -0
  292. package/dist/vitals.js +16 -0
  293. package/dist/vitals.js.map +1 -0
  294. package/dist/vite.d.ts +16 -1
  295. package/dist/vite.d.ts.map +1 -1
  296. package/dist/vite.js +248 -43
  297. package/dist/vite.js.map +1 -1
  298. package/dist/zones.d.ts +70 -0
  299. package/dist/zones.d.ts.map +1 -0
  300. package/dist/zones.js +365 -0
  301. package/dist/zones.js.map +1 -0
  302. package/package.json +67 -14
  303. package/src/backend-only.ts +16 -0
  304. package/src/boundary.ts +18 -5
  305. package/src/build-plan.ts +90 -35
  306. package/src/build-vite.ts +77 -11
  307. package/src/build.ts +466 -96
  308. package/src/bun-dev-entry.ts +1 -1
  309. package/src/cdn.ts +216 -0
  310. package/src/client.ts +135 -36
  311. package/src/conformance.ts +16 -3
  312. package/src/csp.ts +194 -0
  313. package/src/dev-client.ts +349 -0
  314. package/src/dev-diagnostics.ts +60 -7
  315. package/src/dev-error.ts +49 -5
  316. package/src/dev-feed.ts +1140 -0
  317. package/src/dev-indicator.ts +113 -0
  318. package/src/dev-session.ts +729 -0
  319. package/src/dev-sourcemap.ts +281 -0
  320. package/src/dev-ssr-graph.ts +16 -5
  321. package/src/dev.ts +86 -43
  322. package/src/diagnostic-catalog.ts +243 -0
  323. package/src/diagnostic-prompt.ts +355 -0
  324. package/src/diagnostic.ts +63 -66
  325. package/src/draft.ts +4 -1
  326. package/src/fn.ts +19 -3
  327. package/src/fs.ts +13 -2
  328. package/src/index.ts +42 -10
  329. package/src/internal/cdn/cloudflare.ts +146 -0
  330. package/src/internal/cdn/core.ts +316 -0
  331. package/src/internal/cdn/fastly.ts +62 -0
  332. package/src/internal/cdn/vercel.ts +95 -0
  333. package/src/internal/code-unit-order.ts +5 -0
  334. package/src/internal/codegen.ts +161 -24
  335. package/src/internal/dev-csp.ts +176 -0
  336. package/src/internal/dev-reserved.ts +52 -0
  337. package/src/internal/html-spans.ts +50 -0
  338. package/src/internal/loading-runtime.ts +244 -0
  339. package/src/internal/matches-runtime.ts +62 -0
  340. package/src/internal/mount-shadow.ts +88 -0
  341. package/src/internal/node-builtins.ts +79 -0
  342. package/src/internal/output-guard.ts +701 -0
  343. package/src/internal/page-execution.ts +795 -185
  344. package/src/internal/parity.ts +150 -36
  345. package/src/internal/private-env.ts +334 -0
  346. package/src/internal/public-url.ts +22 -0
  347. package/src/internal/render-document.ts +114 -67
  348. package/src/internal/response-controls.ts +284 -0
  349. package/src/internal/runtime-contract.ts +10 -15
  350. package/src/internal/secret-scan.ts +638 -0
  351. package/src/internal/server-boundary.ts +6 -69
  352. package/src/internal/source-map.ts +256 -0
  353. package/src/internal/target-compat.ts +92 -0
  354. package/src/internal/timing-safe-equal.ts +13 -0
  355. package/src/internal/vitals-runtime.ts +53 -0
  356. package/src/internal/web-app.ts +262 -84
  357. package/src/internal/zone-graph.ts +593 -0
  358. package/src/isr.ts +325 -37
  359. package/src/llms-txt.ts +28 -4
  360. package/src/manifest.ts +682 -16
  361. package/src/module-graph.ts +53 -9
  362. package/src/navigation.ts +29 -6
  363. package/src/nonce.ts +104 -0
  364. package/src/plugins/css-modules.ts +5 -3
  365. package/src/plugins/kit.ts +24 -0
  366. package/src/plugins/svg.ts +135 -4
  367. package/src/plugins/vite-leak-guard.ts +529 -16
  368. package/src/plugins/vite-zone-guard.ts +263 -0
  369. package/src/plugins/zone-guard.ts +119 -0
  370. package/src/prerender.ts +49 -3
  371. package/src/public-dir.ts +69 -8
  372. package/src/pwa-manifest.ts +226 -0
  373. package/src/query.ts +4 -4
  374. package/src/render-seam.ts +46 -1
  375. package/src/route-manifest.ts +15 -3
  376. package/src/route-types.ts +264 -0
  377. package/src/router.ts +163 -63
  378. package/src/vitals.ts +44 -0
  379. package/src/vite.ts +257 -42
  380. package/src/zones.ts +430 -0
  381. package/dist/plugins/vite-server-only.d.ts +0 -49
  382. package/dist/plugins/vite-server-only.d.ts.map +0 -1
  383. package/dist/plugins/vite-server-only.js +0 -57
  384. package/dist/plugins/vite-server-only.js.map +0 -1
  385. package/dist/server-only.d.ts +0 -21
  386. package/dist/server-only.d.ts.map +0 -1
  387. package/dist/server-only.js +0 -25
  388. package/dist/server-only.js.map +0 -1
  389. package/src/plugins/vite-server-only.ts +0 -75
  390. package/src/server-only.ts +0 -25
@@ -1,12 +1,101 @@
1
+ import { NIFRA_BACKEND_BIND_PLATFORM, NIFRA_PLATFORM_CLIENT_IP_DERIVED, } from "@nifrajs/core/mount";
2
+ import { status as statusResult } from "@nifrajs/core/server";
1
3
  import { boundaryDescriptors, resolveStaticBoundaries, startDynamicBoundaries, } from "../boundary.js";
2
4
  import { DEFAULT_CSS_LOADING, normalizeCssLoading } from "../css-contract.js";
3
5
  import { defer, ndjsonStream, prepareDeferred } from "../deferred.js";
4
6
  import { isDraftEnabled } from "../draft.js";
5
- import { createMatcher, DATA_HEADER, NAV_FROM_HEADER, RETAIN_HEADER, REVALIDATE_HEADER, STATUS_HEADER, } from "../router.js";
7
+ import { routeTags } from "../isr.js";
8
+ import { createMatcher, DATA_HEADER, NAV_FROM_HEADER, REDIRECT_HEADER, RETAIN_HEADER, REVALIDATE_HEADER, STATUS_HEADER, } from "../router.js";
6
9
  import { searchOf, searchOfChain } from "../search.js";
7
10
  import { mergeHeads, resolveMeta } from "./head-merge.js";
8
- import { actionResponse, EMPTY_RETAIN, isControlFlow, isStatusSignal, layoutErrorId, renderPageResult, STATUS_SIGNAL, STATUS_TEXT, scopeParams, tagLayoutError, withDuplicateInstanceHint, } from "./render-document.js";
11
+ import { EMPTY_RETAIN, isControlFlow, isResponseResult, isStatusSignal, layoutErrorId, renderPageResult, STATUS_SIGNAL, STATUS_TEXT, scopeParams, tagLayoutError, withDuplicateInstanceHint, } from "./render-document.js";
9
12
  import { urlPartsFor } from "./request-url.js";
13
+ import { ACTION_SCOPE, DATA_RESPONSE_HEADERS, MIDDLEWARE_SCOPE, PAGE_SCOPE, PageResponseControls, PRIVATE_NO_STORE, PRIVATE_PAGE_HEADERS, privateOutcome, varyOnDataHeader, } from "./response-controls.js";
14
+ /** The page slot of an `ssr = false` route with no `HydrateFallback`: a component that renders nothing. */
15
+ const EMPTY_LEAF = () => null;
16
+ /** The request-scoping seam of an in-process `api` (see `inProcessClient`), if it carries one. */
17
+ function platformBinderOf(api) {
18
+ if ((typeof api !== "object" && typeof api !== "function") || api === null)
19
+ return undefined;
20
+ const bind = api[NIFRA_BACKEND_BIND_PLATFORM];
21
+ return typeof bind === "function" ? bind : undefined;
22
+ }
23
+ /**
24
+ * The page request's platform identity for loader calls: `clientIp` (read lazily, so a backend that
25
+ * never reads `c.clientIp` never pays the socket lookup), `env` and `waitUntil` - and nothing else. The
26
+ * page request's headers (cookie, authorization) never travel; a loader passes them explicitly if it
27
+ * wants an authenticated call. The prototype marker tells the backend its `clientIp` is already derived,
28
+ * so its own trust declaration does not re-read forwarding headers the synthesized request does not
29
+ * carry; a spread copy drops both the getter and the marker, so it falls back to re-deriving.
30
+ */
31
+ class LoaderPlatform {
32
+ env;
33
+ waitUntil;
34
+ #c;
35
+ constructor(c) {
36
+ this.#c = c;
37
+ this.env = c.env;
38
+ this.waitUntil = c.waitUntil;
39
+ }
40
+ get clientIp() {
41
+ return this.#c.clientIp;
42
+ }
43
+ }
44
+ ;
45
+ LoaderPlatform.prototype[NIFRA_PLATFORM_CLIENT_IP_DERIVED] = true;
46
+ /**
47
+ * A redirect answering a data request (a client navigation or form submit), as the client router reads
48
+ * one: fetch would follow a 3xx and hand the router another page's data, so the target rides
49
+ * `x-nifra-redirect` on a 204 that keeps the redirect's own headers. `undefined` for any other outcome
50
+ * or request.
51
+ */
52
+ function dataRedirect(req, outcome) {
53
+ if (req.headers.get(DATA_HEADER) === null)
54
+ return undefined;
55
+ if (isResponseResult(outcome)) {
56
+ const plain = outcome.plain;
57
+ if (plain === undefined)
58
+ return dataRedirect(req, outcome.toResponse());
59
+ if (plain.status < 300 || plain.status >= 400)
60
+ return undefined;
61
+ const { location, ...own } = plain.headers ?? {};
62
+ if (location === undefined)
63
+ return undefined;
64
+ return statusResult(204, undefined, {
65
+ headers: {
66
+ ...own,
67
+ [REDIRECT_HEADER]: appTarget(location, req.url),
68
+ ...DATA_RESPONSE_HEADERS,
69
+ },
70
+ });
71
+ }
72
+ if (!(outcome instanceof Response) || outcome.status < 300 || outcome.status >= 400) {
73
+ return undefined;
74
+ }
75
+ const location = outcome.headers.get("location");
76
+ if (location === null)
77
+ return undefined;
78
+ const headers = new Headers(outcome.headers);
79
+ headers.delete("location");
80
+ headers.delete("content-length");
81
+ headers.set(REDIRECT_HEADER, appTarget(location, req.url));
82
+ for (const [name, value] of Object.entries(DATA_RESPONSE_HEADERS))
83
+ headers.set(name, value);
84
+ return new Response(null, { status: 204, headers });
85
+ }
86
+ /** A same-origin target as a path, which the client router can follow without loading a document. */
87
+ function appTarget(location, requestUrl) {
88
+ let target;
89
+ try {
90
+ target = new URL(location, requestUrl);
91
+ }
92
+ catch {
93
+ return location; // the browser fails on it the same way when it loads it
94
+ }
95
+ return target.origin === new URL(requestUrl).origin
96
+ ? target.pathname + target.search + target.hash
97
+ : location;
98
+ }
10
99
  /**
11
100
  * Own the page request program behind one private seam.
12
101
  *
@@ -18,11 +107,56 @@ import { urlPartsFor } from "./request-url.js";
18
107
  */
19
108
  export function createPageRequestExecutor(options) {
20
109
  const { adapter, manifest, clientEntry, api } = options;
110
+ // Bound once per render, so every loader, action and boundary in that render shares one view of the
111
+ // in-process client carrying the visitor's platform. An `api` without the seam passes through as-is.
112
+ const bindApi = platformBinderOf(api);
113
+ // One view per render, shared by every loader, action and boundary it runs.
114
+ const apiFor = (c) => bindApi === undefined ? api : bindApi.call(api, new LoaderPlatform(c));
21
115
  const cssLoading = normalizeCssLoading(options.cssLoading ?? DEFAULT_CSS_LOADING);
22
116
  const titleOption = options.title === undefined ? {} : { title: options.title };
117
+ const cspOption = options.csp === undefined ? {} : { csp: options.csp };
23
118
  const prerenderedSet = new Set(options.prerenderedPaths ?? []);
119
+ const prerenderedHandover = options.prerenderedListUrl === undefined
120
+ ? { prerenderedPaths: options.prerenderedPaths ?? [] }
121
+ : { prerenderedListUrl: options.prerenderedListUrl };
24
122
  const matchManifestRoute = createMatcher(manifest.routes.map((route) => ({ routeId: route.id, pattern: route.pattern })));
25
123
  const routeById = new Map(manifest.routes.map((route) => [route.id, route]));
124
+ // A nested `_404` answers an unmatched URL from inside the catch-all, never as a route of its own:
125
+ // a registered `/admin/*` would take URLs from a route the router only reaches by backtracking.
126
+ // Param names are indexed by position, so two directories that name one segment differently share
127
+ // the index; each target keeps its own names to read a match back.
128
+ const notFoundTargets = [];
129
+ const notFoundPatterns = [];
130
+ for (const [id, page] of Object.entries(manifest.notFounds ?? {})) {
131
+ for (const scope of page.scopes) {
132
+ const names = [];
133
+ const pattern = scope.pattern.replace(/([:*])([A-Za-z_][A-Za-z0-9_]*)/g, (_match, sigil, name) => `${sigil}p${names.push(name) - 1}`);
134
+ notFoundPatterns.push({ routeId: String(notFoundTargets.length), pattern });
135
+ notFoundTargets.push({
136
+ id,
137
+ page,
138
+ names,
139
+ route: {
140
+ id,
141
+ pattern: scope.pattern,
142
+ layoutIds: page.layoutIds,
143
+ layoutParams: scope.layoutParams,
144
+ ...(page.middlewareIds === undefined
145
+ ? {}
146
+ : {
147
+ middlewareIds: page.middlewareIds,
148
+ ...(scope.middlewareParams === undefined
149
+ ? {}
150
+ : { middlewareParams: scope.middlewareParams }),
151
+ }),
152
+ errorIds: page.errorIds,
153
+ file: page.file,
154
+ load: page.load,
155
+ },
156
+ });
157
+ }
158
+ }
159
+ const matchNotFoundScope = notFoundPatterns.length === 0 ? undefined : createMatcher(notFoundPatterns);
26
160
  const draftFlag = (req) => options.draftSecret === undefined
27
161
  ? Promise.resolve(false)
28
162
  : isDraftEnabled(req, options.draftSecret);
@@ -53,26 +187,46 @@ export function createPageRequestExecutor(options) {
53
187
  }
54
188
  return modules;
55
189
  };
190
+ // No modules, data, or per-request state: reuse the settled result for page-only routes.
191
+ const emptyLayoutRun = {
192
+ modules: [],
193
+ layoutData: undefined,
194
+ retained: [],
195
+ pending: undefined,
196
+ };
56
197
  /** Run layout loaders, awaiting gates before starting the page loader and retaining only safe data. */
57
- const runLayoutChain = async (route, ctx, retain = new Set()) => {
198
+ const runLayoutChain = async (route, ctx, controls, retain) => {
199
+ if (route.layoutIds.length === 0)
200
+ return emptyLayoutRun;
58
201
  const modules = await loadLayoutModules(route);
59
202
  if (!modules.some((m) => m.loader !== undefined)) {
60
- return { modules, layoutData: undefined, retained: [], pending: Promise.resolve() };
203
+ return { modules, layoutData: undefined, retained: [], pending: undefined };
61
204
  }
62
205
  const results = new Array(modules.length).fill(null);
63
206
  const retained = [];
64
207
  const pending = [];
65
208
  for (let i = 0; i < modules.length; i++) {
66
- const loader = modules[i]?.loader;
209
+ const layout = modules[i];
210
+ const loader = layout.loader;
67
211
  if (loader === undefined)
68
212
  continue;
69
- if (retain.has(i) && modules[i]?.gate !== true) {
70
- retained.push(i);
71
- continue;
213
+ if (retain !== undefined && layout.gate !== true) {
214
+ let keep;
215
+ try {
216
+ keep = keepsLayoutData(route, i, layout, ctx.params, retain);
217
+ }
218
+ catch (err) {
219
+ throw tagLayoutError(err, route.layoutIds[i]);
220
+ }
221
+ if (keep) {
222
+ retained.push(i);
223
+ continue;
224
+ }
72
225
  }
73
226
  const scoped = {
74
227
  ...ctx,
75
228
  params: scopeParams(ctx.params, route.layoutParams?.[i]),
229
+ set: controls.scope(i),
76
230
  };
77
231
  if (modules[i]?.gate === true) {
78
232
  try {
@@ -85,19 +239,37 @@ export function createPageRequestExecutor(options) {
85
239
  }
86
240
  const layoutId = route.layoutIds[i];
87
241
  const index = i;
88
- pending.push((async () => {
89
- try {
90
- results[index] = await loader(scoped);
91
- }
92
- catch (err) {
93
- throw tagLayoutError(err, layoutId);
242
+ try {
243
+ const value = loader(scoped);
244
+ // Scalars settle synchronously. Objects keep await's intrinsic promise/thenable
245
+ // assimilation: property-presence and prototype probes are unsafe for proxies.
246
+ if (value !== null && (typeof value === "object" || typeof value === "function")) {
247
+ pending.push((async () => {
248
+ try {
249
+ results[index] = await value;
250
+ }
251
+ catch (err) {
252
+ throw tagLayoutError(err, layoutId);
253
+ }
254
+ })());
94
255
  }
95
- })());
256
+ else
257
+ results[index] = value;
258
+ }
259
+ catch (err) {
260
+ // Preserve a synchronous throw as a pending layout failure, just like the async lane.
261
+ pending.push(Promise.reject(tagLayoutError(err, layoutId)));
262
+ }
96
263
  }
97
- return { modules, layoutData: results, retained, pending: Promise.all(pending) };
264
+ return {
265
+ modules,
266
+ layoutData: results,
267
+ retained,
268
+ pending: pending.length === 0 ? undefined : pending.length === 1 ? pending[0] : Promise.all(pending),
269
+ };
98
270
  };
99
271
  /** Run only authorization gates before an action; ordinary layout data runs after a native action. */
100
- const runLayoutGates = async (route, ctx) => {
272
+ const runLayoutGates = async (route, ctx, controls) => {
101
273
  const modules = await loadLayoutModules(route);
102
274
  for (let i = 0; i < modules.length; i++) {
103
275
  const mod = modules[i];
@@ -107,6 +279,7 @@ export function createPageRequestExecutor(options) {
107
279
  await mod.loader({
108
280
  ...ctx,
109
281
  params: scopeParams(ctx.params, route.layoutParams?.[i]),
282
+ set: controls.scope(i),
110
283
  });
111
284
  }
112
285
  catch (err) {
@@ -115,6 +288,38 @@ export function createPageRequestExecutor(options) {
115
288
  }
116
289
  return modules;
117
290
  };
291
+ /** Outermost first; a failure carries its directory so the nearest `_error` above it renders. */
292
+ const runMiddleware = async (route, ctx, controls) => {
293
+ const ids = route.middlewareIds ?? [];
294
+ for (let i = 0; i < ids.length; i++) {
295
+ const id = ids[i];
296
+ const entry = manifest.middlewares?.[id];
297
+ const dirTag = `${id.slice(0, id.length - "_middleware".length)}_layout`;
298
+ let outcome;
299
+ try {
300
+ const middleware = (await entry.load()).default;
301
+ // A `_layout.backend.ts` that exports no middleware is a layout data module, nothing more.
302
+ if (middleware === undefined)
303
+ continue;
304
+ if (typeof middleware !== "function") {
305
+ throw new Error(`[nifra/web] "${entry.file}" exports a middleware that is not a function.`);
306
+ }
307
+ outcome = await middleware({
308
+ ...ctx,
309
+ params: scopeParams(ctx.params, route.middlewareParams?.[i]),
310
+ set: controls.scope(MIDDLEWARE_SCOPE + i),
311
+ });
312
+ }
313
+ catch (err) {
314
+ throw tagLayoutError(err, dirTag);
315
+ }
316
+ if (outcome === undefined)
317
+ continue;
318
+ throw tagLayoutError(isControlFlow(outcome)
319
+ ? outcome
320
+ : new Error(`[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.`), dirTag);
321
+ }
322
+ };
118
323
  const retainedIndices = (header) => {
119
324
  if (header === null || header === "")
120
325
  return EMPTY_RETAIN;
@@ -126,55 +331,88 @@ export function createPageRequestExecutor(options) {
126
331
  }
127
332
  return out;
128
333
  };
129
- const validatedRetainedIndices = (req, route, params) => {
334
+ /**
335
+ * The browser's side of a client navigation: the page it is leaving and the layout slots it asked
336
+ * to keep. Undefined unless this is a data request whose `from` is same-origin and routable.
337
+ */
338
+ const retainContextOf = (req) => {
130
339
  if (req.headers.get(DATA_HEADER) === null)
131
- return EMPTY_RETAIN;
340
+ return undefined;
132
341
  const requested = retainedIndices(req.headers.get(RETAIN_HEADER));
133
342
  const from = req.headers.get(NAV_FROM_HEADER);
134
343
  if (requested.size === 0 || from === null)
135
- return EMPTY_RETAIN;
136
- let fromPath;
344
+ return undefined;
345
+ let toUrl;
346
+ let fromUrl;
137
347
  try {
138
- const current = new URL(req.url);
139
- const source = new URL(from, current);
140
- if (source.origin !== current.origin)
141
- return EMPTY_RETAIN;
142
- fromPath = source.pathname + source.search;
348
+ toUrl = new URL(req.url);
349
+ fromUrl = new URL(from, toUrl);
143
350
  }
144
351
  catch {
145
- return EMPTY_RETAIN;
352
+ return undefined;
146
353
  }
147
- const previousMatch = matchManifestRoute(fromPath);
354
+ if (fromUrl.origin !== toUrl.origin)
355
+ return undefined;
356
+ const previousMatch = matchManifestRoute(fromUrl.pathname + fromUrl.search);
148
357
  if (previousMatch === null)
149
- return EMPTY_RETAIN;
358
+ return undefined;
150
359
  const previous = routeById.get(previousMatch.routeId);
151
360
  if (previous === undefined)
152
- return EMPTY_RETAIN;
153
- const valid = new Set();
154
- for (const index of requested) {
155
- if (route.layoutIds[index] !== previous.layoutIds[index])
156
- continue;
157
- const owned = route.layoutParams?.[index] ?? [];
158
- const previouslyOwned = previous.layoutParams?.[index] ?? [];
159
- if (owned.length !== previouslyOwned.length ||
160
- owned.some((name, i) => name !== previouslyOwned[i]) ||
161
- owned.some((name) => params[name] !== previousMatch.params[name])) {
162
- continue;
163
- }
164
- valid.add(index);
165
- }
166
- return valid;
361
+ return undefined;
362
+ return { requested, from: previous, fromParams: previousMatch.params, fromUrl, toUrl };
363
+ };
364
+ /** A query change re-runs layout loaders by default: any can read it, and none declares keys. */
365
+ const keepsLayoutData = (route, index, layout, params, retain) => {
366
+ if (!retain.requested.has(index))
367
+ return false;
368
+ const previous = retain.from;
369
+ if (route.layoutIds[index] !== previous.layoutIds[index])
370
+ return false;
371
+ const owned = route.layoutParams?.[index] ?? [];
372
+ const previouslyOwned = previous.layoutParams?.[index] ?? [];
373
+ const defaultShouldRevalidate = retain.fromUrl.search !== retain.toUrl.search ||
374
+ owned.length !== previouslyOwned.length ||
375
+ owned.some((name, i) => name !== previouslyOwned[i]) ||
376
+ owned.some((name) => params[name] !== retain.fromParams[name]);
377
+ const decide = layout.shouldRevalidate;
378
+ if (typeof decide !== "function")
379
+ return !defaultShouldRevalidate;
380
+ // Copies, so a hook that mutates its arguments cannot reach the request's params or the next
381
+ // layout's view of the navigation.
382
+ const answer = decide({
383
+ currentUrl: new URL(retain.fromUrl),
384
+ nextUrl: new URL(retain.toUrl),
385
+ currentParams: { ...retain.fromParams },
386
+ nextParams: { ...params },
387
+ defaultShouldRevalidate,
388
+ });
389
+ return answer === false;
167
390
  };
168
391
  const loaderSearch = (searchSchema, request) => searchOf(searchSchema, urlPartsFor(request).search);
169
392
  const searchChainOf = (layoutMods, mod, request) => searchOfChain([...layoutMods.map((m) => m.searchSchema), mod.searchSchema], urlPartsFor(request).search);
170
- const resolveChainAndHead = (layoutModules, page, metaArgs) => {
171
- const chain = [...layoutModules.map((m) => m.default), page.default];
393
+ // `ssr = false`: the slot holds `HydrateFallback`, which the browser hydrates before the page.
394
+ const serverLeafOf = (route, mod) => {
395
+ if (mod.ssr !== false)
396
+ return mod.default;
397
+ if (mod.hydrate === false) {
398
+ throw new Error(`[nifra/web] route "${route.id}" sets both \`ssr = false\` and \`hydrate = false\`: the ` +
399
+ "server skips its component and no client would ever render it. Remove one of the two.");
400
+ }
401
+ return mod.HydrateFallback ?? EMPTY_LEAF;
402
+ };
403
+ const resolveChainAndHead = (layoutModules, page, metaArgs, leaf = page.default) => {
404
+ const chain = [...layoutModules.map((m) => m.default), leaf];
172
405
  const heads = [
173
406
  ...layoutModules.map((m) => resolveMeta(m.meta, metaArgs)),
174
407
  resolveMeta(page.meta, metaArgs),
175
408
  ];
176
409
  return { chain, head: mergeHeads(heads) };
177
410
  };
411
+ /** What `useMatches` reports for a chain: the layouts' ids and `handle`s, then the page's. */
412
+ const matchChainOf = (layoutIds, layoutModules, pageId, page) => ({
413
+ ids: [...layoutIds, pageId],
414
+ handles: [...layoutModules.map((layout) => layout.handle), page.handle],
415
+ });
178
416
  const dirOfId = (id, suffix) => id === suffix ? "" : id.slice(0, id.length - suffix.length - 1);
179
417
  const boundaryFor = (route, err) => {
180
418
  const ids = route.errorIds ?? [];
@@ -206,7 +444,7 @@ export function createPageRequestExecutor(options) {
206
444
  return "/";
207
445
  }
208
446
  };
209
- const renderError = async (route, errorId, err, nonce) => {
447
+ const renderError = async (route, errorId, err, personalized, nonce) => {
210
448
  const errDir = dirOfId(errorId, "_error");
211
449
  const keptLayoutIds = route.layoutIds.filter((id) => {
212
450
  const ld = dirOfId(id, "_layout");
@@ -215,15 +453,21 @@ export function createPageRequestExecutor(options) {
215
453
  const layouts = await Promise.all(keptLayoutIds.map((id) => manifest.layouts[id].load().then((m) => m.default)));
216
454
  const { default: errComp } = await (manifest.errors?.[errorId]).load();
217
455
  const e = err instanceof Error ? err : new Error(String(err));
456
+ // An error's text can carry a connection string or a query, so a production page gets neither.
457
+ const production = typeof process === "undefined" || process.env.NODE_ENV === "production";
218
458
  return renderPageResult({
219
459
  adapter,
220
460
  chain: [...layouts, errComp],
221
- data: { name: e.name, message: e.message },
461
+ data: production
462
+ ? { name: "Error", message: "Internal Server Error" }
463
+ : { name: e.name, message: e.message },
222
464
  clientEntry,
223
465
  routeId: errorId,
224
466
  status: 500,
225
467
  hydrate: false,
468
+ ...(personalized ? { headers: PRIVATE_PAGE_HEADERS } : {}),
226
469
  ...(nonce === undefined ? {} : { nonce }),
470
+ ...cspOption,
227
471
  ...titleOption,
228
472
  });
229
473
  };
@@ -242,6 +486,7 @@ export function createPageRequestExecutor(options) {
242
486
  data: null,
243
487
  clientEntry,
244
488
  routeId,
489
+ matchChain: { ids: [routeId], handles: [loaded.handle] },
245
490
  ...(path !== undefined
246
491
  ? {
247
492
  search: searchOf(loaded.searchSchema, path.includes("?") ? path.slice(path.indexOf("?")) : ""),
@@ -251,20 +496,36 @@ export function createPageRequestExecutor(options) {
251
496
  status,
252
497
  ...(extraHeaders !== undefined ? { headers: extraHeaders } : {}),
253
498
  ...(nonce === undefined ? {} : { nonce }),
499
+ ...cspOption,
254
500
  ...preloadOf(routeId),
255
501
  ...stylesOf(routeId),
256
- prerenderedPaths: options.prerenderedPaths ?? [],
502
+ ...prerenderedHandover,
257
503
  ...titleOption,
258
504
  });
259
505
  };
260
- const renderStatusSignal = async (req, env, signal) => {
506
+ const renderStatusSignal = async (req, env, signal, personalized) => {
261
507
  const { status, headers } = signal[STATUS_SIGNAL];
262
508
  if (req.headers.get(DATA_HEADER) !== null) {
509
+ // Same URL as the status page, so it is never storable: a cache keyed on the URL alone would
510
+ // otherwise answer a document request with this empty body.
263
511
  const responseHeaders = new Headers(headers);
264
512
  responseHeaders.set(STATUS_HEADER, String(status));
513
+ responseHeaders.set("cache-control", PRIVATE_NO_STORE);
514
+ responseHeaders.set("vary", varyOnDataHeader(responseHeaders.get("vary")));
265
515
  return new Response(null, { status, headers: responseHeaders });
266
516
  }
267
- return renderTerminalStatus(status, pathOf(req), headers, await resolveNonce(req, env));
517
+ const nonce = await resolveNonce(req, env);
518
+ if (headers === undefined && !personalized) {
519
+ return renderTerminalStatus(status, pathOf(req), undefined, nonce);
520
+ }
521
+ const documentHeaders = new Headers(headers);
522
+ if (headers !== undefined) {
523
+ documentHeaders.set("vary", varyOnDataHeader(documentHeaders.get("vary")));
524
+ }
525
+ // A cookie queued before the signal makes the status page specific to this visitor.
526
+ if (personalized)
527
+ documentHeaders.set("cache-control", PRIVATE_NO_STORE);
528
+ return renderTerminalStatus(status, pathOf(req), documentHeaders, nonce);
268
529
  };
269
530
  const reportLoaderError = (route, req, params, err) => {
270
531
  if (options.onLoaderError === undefined)
@@ -276,9 +537,9 @@ export function createPageRequestExecutor(options) {
276
537
  // A faulty reporter must never break error rendering.
277
538
  }
278
539
  };
279
- const handleDataOrDocument = async (req, env, route, params, mod, data, layoutData, layoutModules, layoutRetained, boundaryStates, assemblyCache) => {
540
+ const handleDataOrDocument = async (req, env, route, params, mod, data, layoutData, layoutModules, layoutRetained, boundaryStates, assemblyCache, responseHeaders, personalized) => {
280
541
  if (isStatusSignal(data))
281
- return renderStatusSignal(req, env, data);
542
+ return renderStatusSignal(req, env, data, personalized);
282
543
  if (isControlFlow(data))
283
544
  return data;
284
545
  if (req.headers.get(DATA_HEADER) !== null) {
@@ -305,18 +566,23 @@ export function createPageRequestExecutor(options) {
305
566
  retained: layoutRetained,
306
567
  ...(boundarySplit === undefined ? {} : { boundaries: boundarySplit.forClient }),
307
568
  };
308
- if (allDeferred.length === 0)
309
- return Response.json(payload ?? null);
569
+ if (allDeferred.length === 0) {
570
+ return Response.json(payload ?? null, { headers: DATA_RESPONSE_HEADERS });
571
+ }
310
572
  return new Response(ndjsonStream(payload, allDeferred), {
311
- headers: { "content-type": "application/x-ndjson; charset=utf-8" },
573
+ headers: {
574
+ "content-type": "application/x-ndjson; charset=utf-8",
575
+ ...DATA_RESPONSE_HEADERS,
576
+ },
312
577
  });
313
578
  }
579
+ const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env);
314
580
  const { chain, head } = resolveChainAndHead(layoutModules, mod, {
315
581
  data,
316
582
  params,
317
583
  origin: originOf(req),
318
- });
319
- const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env);
584
+ ...(nonce === undefined ? {} : { nonce }),
585
+ }, serverLeafOf(route, mod));
320
586
  try {
321
587
  return await renderPageResult({
322
588
  adapter,
@@ -328,38 +594,168 @@ export function createPageRequestExecutor(options) {
328
594
  params,
329
595
  path: pathOf(req),
330
596
  search: searchChainOf(layoutModules, mod, req),
597
+ matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
331
598
  hydrate: mod.hydrate !== false,
332
599
  ...(layoutData !== undefined ? { layoutData } : {}),
333
600
  ...(boundaryStates !== undefined ? { boundaries: boundaryStates } : {}),
334
601
  ...preloadOf(route.id),
335
602
  ...stylesOf(route.id),
336
- prerenderedPaths: options.prerenderedPaths ?? [],
337
- ...(mod.revalidate !== undefined ? { revalidate: mod.revalidate } : {}),
338
- ...(mod.revalidateTags !== undefined ? { revalidateTags: mod.revalidateTags } : {}),
603
+ ...prerenderedHandover,
604
+ ...(responseHeaders === undefined ? {} : { headers: responseHeaders }),
605
+ // A response that sets a cookie is one visitor's: it never advertises ISR freshness.
606
+ ...(mod.revalidate !== undefined && !personalized && options.cacheChannel?.enabled === true
607
+ ? { revalidate: mod.revalidate }
608
+ : {}),
609
+ ...(mod.revalidateTags !== undefined &&
610
+ !personalized &&
611
+ options.cacheChannel?.enabled === true
612
+ ? {
613
+ revalidateTags: routeTags(mod.revalidateTags, { params, url: new URL(req.url) }, route.id),
614
+ }
615
+ : {}),
339
616
  ...(mod.islandScripts !== undefined ? { islandScripts: mod.islandScripts } : {}),
340
617
  ...(nonce === undefined ? {} : { nonce }),
618
+ ...cspOption,
341
619
  ...titleOption,
342
620
  ...(assemblyCache === undefined ? {} : { assemblyCache }),
343
621
  });
344
622
  }
345
623
  catch (err) {
346
624
  if (isStatusSignal(err))
347
- return renderStatusSignal(req, env, err);
625
+ return renderStatusSignal(req, env, err, personalized);
348
626
  if (isControlFlow(err))
349
627
  throw err;
350
628
  reportLoaderError(route, req, { ...params }, err);
351
629
  const errorId = boundaryFor(route, err);
352
630
  if (errorId === undefined)
353
631
  throw err;
354
- return renderError(route, errorId, withDuplicateInstanceHint(err), nonce);
632
+ return renderError(route, errorId, withDuplicateInstanceHint(err), personalized, nonce);
355
633
  }
356
634
  };
635
+ /** What a failed loader answers with, once a status signal has been ruled out. */
636
+ const renderLoaderFailure = async (route, req, env, params, err, personalized) => {
637
+ if (isControlFlow(err))
638
+ throw err;
639
+ reportLoaderError(route, req, params, err);
640
+ const errorId = boundaryFor(route, err);
641
+ if (errorId === undefined)
642
+ throw err;
643
+ if (req.headers.get(DATA_HEADER) !== null) {
644
+ return new Response("Internal Server Error", {
645
+ status: 500,
646
+ headers: { "content-type": "text/plain; charset=utf-8" },
647
+ });
648
+ }
649
+ return renderError(route, errorId, withDuplicateInstanceHint(err), personalized, options.nonce === undefined ? undefined : await resolveNonce(req, env));
650
+ };
651
+ /**
652
+ * Render a nested `_404` inside its layouts. `layoutModules` and `layoutData` are exactly the
653
+ * layouts at or above the page, their gates already passed.
654
+ */
655
+ const renderNestedNotFound = async (req, env, id, page, params, layoutModules, layoutData, personalized, signalHeaders) => {
656
+ // The page can show what a layout loaded for this visitor, and a 404 is storable by default.
657
+ const isPrivate = personalized || layoutData !== undefined;
658
+ if (req.headers.get(DATA_HEADER) !== null) {
659
+ // No status header: the page has no client chunk, so the navigation falls back to loading the
660
+ // URL as a document, which renders it here.
661
+ const headers = new Headers(signalHeaders);
662
+ headers.set("cache-control", PRIVATE_NO_STORE);
663
+ headers.set("vary", varyOnDataHeader(headers.get("vary")));
664
+ return new Response(null, { status: 404, headers });
665
+ }
666
+ const mod = await page.load();
667
+ const nonce = options.nonce === undefined ? undefined : await resolveNonce(req, env);
668
+ const { chain, head } = resolveChainAndHead(layoutModules, mod, {
669
+ data: null,
670
+ params,
671
+ origin: originOf(req),
672
+ ...(nonce === undefined ? {} : { nonce }),
673
+ });
674
+ let headers;
675
+ if (signalHeaders !== undefined || isPrivate) {
676
+ headers = new Headers(signalHeaders);
677
+ if (signalHeaders !== undefined)
678
+ headers.set("vary", varyOnDataHeader(headers.get("vary")));
679
+ if (isPrivate)
680
+ headers.set("cache-control", PRIVATE_NO_STORE);
681
+ }
682
+ return renderPageResult({
683
+ adapter,
684
+ chain,
685
+ data: null,
686
+ head,
687
+ clientEntry,
688
+ routeId: id,
689
+ params,
690
+ path: pathOf(req),
691
+ search: searchChainOf(layoutModules, mod, req),
692
+ matchChain: matchChainOf(page.layoutIds, layoutModules, id, mod),
693
+ status: 404,
694
+ hydrate: false,
695
+ ...(layoutData !== undefined ? { layoutData } : {}),
696
+ ...stylesOf(id),
697
+ ...(headers === undefined ? {} : { headers }),
698
+ ...(nonce === undefined ? {} : { nonce }),
699
+ ...cspOption,
700
+ ...titleOption,
701
+ });
702
+ };
703
+ /** The nearest nested `_404` for the route's own `notFound()`, else `undefined` (root page). */
704
+ const renderRouteNotFound = async (req, env, route, params, signal, run, personalized) => {
705
+ const id = route.notFoundIds?.at(-1);
706
+ if (id === undefined || run === undefined)
707
+ return undefined;
708
+ if (signal[STATUS_SIGNAL].status !== 404 || layoutErrorId(signal) !== undefined) {
709
+ return undefined;
710
+ }
711
+ const page = manifest.notFounds?.[id];
712
+ if (page === undefined)
713
+ return undefined;
714
+ try {
715
+ await run.pending;
716
+ }
717
+ catch {
718
+ return undefined;
719
+ }
720
+ // The page's layouts are the leading ones of the route's: its directory contains the route.
721
+ const kept = page.layoutIds.length;
722
+ return renderNestedNotFound(req, env, id, page, params, run.modules.slice(0, kept), run.layoutData?.slice(0, kept), personalized, signal[STATUS_SIGNAL].headers);
723
+ };
357
724
  const renderNotFound = async (request, env, path) => {
358
725
  const page = manifest.statusPages?.["404"] ?? manifest.notFound;
359
726
  if (page === undefined)
360
727
  return renderTerminalStatus(404, path);
361
728
  return renderTerminalStatus(404, path, undefined, options.nonce === undefined ? undefined : await resolveNonce(request, env));
362
729
  };
730
+ /**
731
+ * Run a page handler with the request's `ctx.set`, sealed once the handler settles - whatever it
732
+ * answered with - so a write that arrives after the response was decided throws. A request that
733
+ * queued a cookie answers with a response no shared cache may store, returned or thrown. A redirect
734
+ * answering a data request, returned or thrown, reaches the client router intact.
735
+ */
736
+ const withResponseControls = (run) => async (c) => {
737
+ const controls = new PageResponseControls(c);
738
+ try {
739
+ const outcome = await run(c, controls);
740
+ const answer = dataRedirect(c.req, outcome) ?? outcome;
741
+ return controls.personalized ? privateOutcome(answer) : answer;
742
+ }
743
+ catch (err) {
744
+ if (!isControlFlow(err))
745
+ throw err;
746
+ const answer = dataRedirect(c.req, err);
747
+ // Returned rather than rethrown: the serving app sends a thrown `Response` exactly as it is,
748
+ // without the cookies the request queued.
749
+ if (controls.personalized)
750
+ return privateOutcome(answer ?? err);
751
+ if (answer !== undefined)
752
+ return answer;
753
+ throw err;
754
+ }
755
+ finally {
756
+ controls.seal();
757
+ }
758
+ };
363
759
  return {
364
760
  get: (route) => {
365
761
  const is404Fallback = options.staticFallbacks?.[route.pattern] === "404";
@@ -382,28 +778,37 @@ export function createPageRequestExecutor(options) {
382
778
  }
383
779
  return assemblySlot;
384
780
  };
385
- return async (c) => {
781
+ return withResponseControls(async (c, controls) => {
386
782
  if (is404Fallback && !prerenderedSet.has(urlPartsFor(c.req).pathname)) {
387
783
  return renderNotFound(c.req, c.env, pathOf(c.req));
388
784
  }
389
785
  const mod = await route.load();
390
786
  const draft = await draftFlag(c.req);
787
+ // Unpublished content is the editor's alone: whatever cache-control a loader sets, no cache keeps it.
788
+ if (draft)
789
+ controls.personalize();
391
790
  let data;
392
791
  let layoutData;
393
792
  let boundaryStates;
394
793
  let layoutModules;
395
794
  let layoutRetained = [];
795
+ let responseHeaders;
796
+ const requestApi = apiFor(c);
797
+ let run;
396
798
  try {
397
799
  const ctx = {
398
800
  params: c.params,
399
801
  request: c.req,
400
802
  req: c.req,
401
- api,
803
+ api: requestApi,
402
804
  env: c.env,
403
805
  draft,
404
806
  search: loaderSearch(mod.searchSchema, c.req),
807
+ set: controls.scope(PAGE_SCOPE),
405
808
  };
406
- const run = await runLayoutChain(route, ctx, validatedRetainedIndices(c.req, route, c.params));
809
+ if (route.middlewareIds !== undefined)
810
+ await runMiddleware(route, ctx, controls);
811
+ run = await runLayoutChain(route, ctx, controls, retainContextOf(c.req));
407
812
  layoutModules = run.modules;
408
813
  layoutRetained = run.retained;
409
814
  const boundaryDefinitions = [
@@ -418,20 +823,21 @@ export function createPageRequestExecutor(options) {
418
823
  : startDynamicBoundaries(boundaryDefinitions, {
419
824
  request: c.req,
420
825
  params: c.params,
421
- api,
826
+ api: requestApi,
422
827
  env: c.env,
423
828
  draft,
424
829
  search: effectiveSearch,
425
830
  signal: c.req.signal,
426
831
  });
427
832
  const staticBoundaryPromise = boundaryDefinitions.length === 0
428
- ? Promise.resolve(undefined)
429
- : resolveStaticBoundaries(boundaryDefinitions, { phase: "build", origin: originOf(c.req) }, options.staticBoundaryCache);
430
- const [pageData, , staticBoundaries] = await Promise.all([
431
- mod.loader ? mod.loader({ ...ctx, search: effectiveSearch }) : null,
432
- run.pending,
433
- staticBoundaryPromise,
434
- ]);
833
+ ? undefined
834
+ : resolveStaticBoundaries(boundaryDefinitions, { phase: "build" }, options.staticBoundaryCache);
835
+ const pageResult = mod.loader ? mod.loader({ ...ctx, search: effectiveSearch }) : null;
836
+ const [pageData, , staticBoundaries] = run.pending === undefined && staticBoundaryPromise === undefined
837
+ ? [await pageResult, undefined, undefined]
838
+ : staticBoundaryPromise === undefined
839
+ ? await Promise.all([pageResult, run.pending])
840
+ : await Promise.all([pageResult, run.pending, staticBoundaryPromise]);
435
841
  data = pageData;
436
842
  layoutData = run.layoutData;
437
843
  if (boundaryBatch !== undefined) {
@@ -446,70 +852,85 @@ export function createPageRequestExecutor(options) {
446
852
  else if (staticBoundaries !== undefined) {
447
853
  boundaryStates = staticBoundaries;
448
854
  }
855
+ // Inside the loader `try`: a header the response cannot carry is a loader error, rendered
856
+ // by the same boundary as one.
857
+ responseHeaders = controls.documentHeaders();
449
858
  }
450
859
  catch (err) {
451
- if (isStatusSignal(err))
452
- return renderStatusSignal(c.req, c.env, err);
453
- if (isControlFlow(err))
454
- throw err;
455
- reportLoaderError(route, c.req, c.params, err);
456
- const errorId = boundaryFor(route, err);
457
- if (errorId === undefined)
458
- throw err;
459
- if (c.req.headers.get(DATA_HEADER) !== null) {
460
- return new Response("Internal Server Error", {
461
- status: 500,
462
- headers: { "content-type": "text/plain; charset=utf-8" },
463
- });
860
+ if (isStatusSignal(err)) {
861
+ return ((await renderRouteNotFound(c.req, c.env, route, c.params, err, run, controls.personalized)) ?? renderStatusSignal(c.req, c.env, err, controls.personalized));
464
862
  }
465
- return renderError(route, errorId, withDuplicateInstanceHint(err), options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env));
863
+ return renderLoaderFailure(route, c.req, c.env, c.params, err, controls.personalized);
466
864
  }
467
- return handleDataOrDocument(c.req, c.env, route, c.params, mod, data, layoutData, layoutModules ?? (await loadLayoutModules(route)), layoutRetained, boundaryStates, layoutModules === undefined ? undefined : assemblyCacheFor(mod, layoutModules));
468
- };
865
+ if (isStatusSignal(data)) {
866
+ const nested = await renderRouteNotFound(c.req, c.env, route, c.params, data, run, controls.personalized);
867
+ if (nested !== undefined)
868
+ return nested;
869
+ }
870
+ return handleDataOrDocument(c.req, c.env, route, c.params, mod, data, layoutData, layoutModules ?? (await loadLayoutModules(route)), layoutRetained, boundaryStates, layoutModules === undefined ? undefined : assemblyCacheFor(mod, layoutModules), responseHeaders, controls.personalized);
871
+ });
469
872
  },
470
- post: (route) => async (c) => {
873
+ post: (route) => withResponseControls(async (c, controls) => {
471
874
  const mod = await route.load();
472
875
  const draft = await draftFlag(c.req);
876
+ if (draft)
877
+ controls.personalize();
473
878
  if (mod.action === undefined) {
474
879
  return new Response("Method Not Allowed", {
475
880
  status: 405,
476
881
  headers: { allow: "GET", "content-type": "text/plain; charset=utf-8" },
477
882
  });
478
883
  }
884
+ const requestApi = apiFor(c);
479
885
  const actionContext = {
480
886
  params: c.params,
481
887
  request: c.req,
482
888
  req: c.req,
483
- api,
889
+ api: requestApi,
484
890
  env: c.env,
485
891
  draft,
486
892
  search: loaderSearch(mod.searchSchema, c.req),
893
+ set: controls.scope(ACTION_SCOPE),
487
894
  };
488
895
  const isDataRequest = c.req.headers.get(DATA_HEADER) !== null;
489
896
  let layoutModules;
490
897
  let result;
491
898
  try {
492
- layoutModules = await runLayoutGates(route, actionContext);
899
+ if (route.middlewareIds !== undefined)
900
+ await runMiddleware(route, actionContext, controls);
901
+ layoutModules = await runLayoutGates(route, actionContext, controls);
493
902
  result = await mod.action(actionContext);
494
903
  }
495
904
  catch (err) {
496
905
  if (isControlFlow(err))
497
- return actionResponse(err, isDataRequest);
906
+ return err;
498
907
  throw err;
499
908
  }
500
909
  const isRevalidate = result !== null && typeof result === "object" && "__nifraRevalidate" in result;
501
910
  const actionResult = isRevalidate ? result.data : result;
502
911
  const revalidateHeader = isRevalidate
503
- ? { [REVALIDATE_HEADER]: result.__nifraRevalidate.join(",") }
912
+ ? {
913
+ [REVALIDATE_HEADER]: result.__nifraRevalidate.join(","),
914
+ }
504
915
  : {};
505
916
  if (isControlFlow(actionResult))
506
- return actionResponse(actionResult, isDataRequest);
917
+ return actionResult;
507
918
  if (isDataRequest) {
919
+ // Headers shape the document only, but a bad one fails here too, so a navigation and a full
920
+ // page load agree on whether the action's response is valid.
921
+ controls.commit();
508
922
  const { forClient, deferred } = prepareDeferred(actionResult);
509
- if (deferred.length === 0)
510
- return Response.json(actionResult ?? null, { headers: revalidateHeader });
923
+ if (deferred.length === 0) {
924
+ return Response.json(actionResult ?? null, {
925
+ headers: { ...DATA_RESPONSE_HEADERS, ...revalidateHeader },
926
+ });
927
+ }
511
928
  return new Response(ndjsonStream(forClient, deferred), {
512
- headers: { "content-type": "application/x-ndjson; charset=utf-8", ...revalidateHeader },
929
+ headers: {
930
+ "content-type": "application/x-ndjson; charset=utf-8",
931
+ ...DATA_RESPONSE_HEADERS,
932
+ ...revalidateHeader,
933
+ },
513
934
  });
514
935
  }
515
936
  const search = searchChainOf(layoutModules, mod, c.req);
@@ -518,18 +939,21 @@ export function createPageRequestExecutor(options) {
518
939
  params: c.params,
519
940
  request: c.req,
520
941
  req: c.req,
521
- api,
942
+ api: requestApi,
522
943
  env: c.env,
523
944
  draft,
524
945
  search,
946
+ set: controls.scope(PAGE_SCOPE),
525
947
  })
526
948
  : null;
949
+ const responseHeaders = controls.documentHeaders();
950
+ const nonce = options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env);
527
951
  const { chain, head } = resolveChainAndHead(layoutModules, mod, {
528
952
  data,
529
953
  params: c.params,
530
954
  origin: originOf(c.req),
531
- });
532
- const nonce = options.nonce === undefined ? undefined : await resolveNonce(c.req, c.env);
955
+ ...(nonce === undefined ? {} : { nonce }),
956
+ }, serverLeafOf(route, mod));
533
957
  return renderPageResult({
534
958
  adapter,
535
959
  chain,
@@ -541,15 +965,57 @@ export function createPageRequestExecutor(options) {
541
965
  params: c.params,
542
966
  path: pathOf(c.req),
543
967
  search,
968
+ matchChain: matchChainOf(route.layoutIds, layoutModules, route.id, mod),
544
969
  hydrate: mod.hydrate !== false,
545
970
  ...preloadOf(route.id),
546
971
  ...stylesOf(route.id),
547
- prerenderedPaths: options.prerenderedPaths ?? [],
972
+ ...prerenderedHandover,
973
+ ...(responseHeaders === undefined ? {} : { headers: responseHeaders }),
548
974
  ...(nonce === undefined ? {} : { nonce }),
975
+ ...cspOption,
549
976
  ...titleOption,
550
977
  });
551
- },
552
- notFound: renderNotFound,
978
+ }),
979
+ fallback: withResponseControls(async (c, controls) => {
980
+ const match = matchNotFoundScope?.(pathOf(c.req)) ?? null;
981
+ const target = match === null ? undefined : notFoundTargets[Number(match.routeId)];
982
+ if (match === null || target === undefined) {
983
+ return renderNotFound(c.req, c.env, pathOf(c.req));
984
+ }
985
+ const params = {};
986
+ for (let i = 0; i < target.names.length; i++) {
987
+ params[target.names[i]] = match.params[`p${i}`];
988
+ }
989
+ const mod = await target.page.load();
990
+ const draft = await draftFlag(c.req);
991
+ if (draft)
992
+ controls.personalize();
993
+ let run;
994
+ try {
995
+ const ctx = {
996
+ params,
997
+ request: c.req,
998
+ req: c.req,
999
+ api: apiFor(c),
1000
+ env: c.env,
1001
+ draft,
1002
+ search: loaderSearch(mod.searchSchema, c.req),
1003
+ set: controls.scope(PAGE_SCOPE),
1004
+ };
1005
+ if (target.route.middlewareIds !== undefined) {
1006
+ await runMiddleware(target.route, ctx, controls);
1007
+ }
1008
+ run = await runLayoutChain(target.route, ctx, controls);
1009
+ await run.pending;
1010
+ }
1011
+ catch (err) {
1012
+ if (isStatusSignal(err)) {
1013
+ return renderStatusSignal(c.req, c.env, err, controls.personalized);
1014
+ }
1015
+ return renderLoaderFailure(target.route, c.req, c.env, params, err, controls.personalized);
1016
+ }
1017
+ return renderNestedNotFound(c.req, c.env, target.id, target.page, params, run.modules, run.layoutData, controls.personalized);
1018
+ }),
553
1019
  };
554
1020
  }
555
1021
  //# sourceMappingURL=page-execution.js.map