@timber-js/app 0.2.0-alpha.192 → 0.2.0-alpha.194

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 (667) hide show
  1. package/agent-skill.md +2 -0
  2. package/dist/_chunks/{actions-BjbbNRFN.js → actions-CWYtq6ii.js} +3 -3
  3. package/dist/_chunks/actions-CWYtq6ii.js.map +1 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  5. package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -1
  6. package/dist/_chunks/{build-output-helper-C3DrfzZR.js → build-output-helper-9dqCv1pZ.js} +3 -2
  7. package/dist/_chunks/build-output-helper-9dqCv1pZ.js.map +1 -0
  8. package/dist/_chunks/{cache-api-DllJ-Lyw.js → cache-api-CQeYzA5g.js} +6 -8
  9. package/dist/_chunks/cache-api-CQeYzA5g.js.map +1 -0
  10. package/dist/_chunks/chains-h7EO-u3n.js +412 -0
  11. package/dist/_chunks/chains-h7EO-u3n.js.map +1 -0
  12. package/dist/_chunks/{cli-check-DJZDc22E.js → cli-check-BVthpfLS.js} +43 -6
  13. package/dist/_chunks/cli-check-BVthpfLS.js.map +1 -0
  14. package/dist/_chunks/{cli-schema-sync-D4_AZVUb.js → cli-schema-sync-ZwM9u_ob.js} +6 -5
  15. package/dist/_chunks/{cli-schema-sync-D4_AZVUb.js.map → cli-schema-sync-ZwM9u_ob.js.map} +1 -1
  16. package/dist/_chunks/{cloudflare-CGP6BZKO.js → cloudflare-BKJC3SC_.js} +28 -22
  17. package/dist/_chunks/{cloudflare-CGP6BZKO.js.map → cloudflare-BKJC3SC_.js.map} +1 -1
  18. package/dist/_chunks/codec-registry-oOUxugz3.js.map +1 -1
  19. package/dist/_chunks/{convention-lint-CpteIpTm.js → convention-lint-DO10_pVl.js} +93 -7
  20. package/dist/_chunks/convention-lint-DO10_pVl.js.map +1 -0
  21. package/dist/_chunks/dist-DtrJyBbw.js +386 -0
  22. package/dist/_chunks/dist-DtrJyBbw.js.map +1 -0
  23. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -1
  24. package/dist/_chunks/{file-cache-DmX7OqZP.js → file-cache-Dw6BJPG7.js} +5 -1
  25. package/dist/_chunks/{file-cache-DmX7OqZP.js.map → file-cache-Dw6BJPG7.js.map} +1 -1
  26. package/dist/_chunks/{format-CfwjgPz9.js → format-BKclgVbk.js} +3 -3
  27. package/dist/_chunks/{format-CfwjgPz9.js.map → format-BKclgVbk.js.map} +1 -1
  28. package/dist/_chunks/graph-cache-CP4GEmf9.js +304 -0
  29. package/dist/_chunks/graph-cache-CP4GEmf9.js.map +1 -0
  30. package/dist/_chunks/href-validation-CMc5JRls.js.map +1 -1
  31. package/dist/_chunks/live-graph-Bx4HodF1.js +150 -0
  32. package/dist/_chunks/live-graph-Bx4HodF1.js.map +1 -0
  33. package/dist/_chunks/{logger-CH7IcMmg.js → logger-pumCm3Il.js} +25 -7
  34. package/dist/_chunks/{logger-CH7IcMmg.js.map → logger-pumCm3Il.js.map} +1 -1
  35. package/dist/_chunks/{mdx-file-C005ay-P.js → mdx-file-CXyHGUpS.js} +3 -3
  36. package/dist/_chunks/mdx-file-CXyHGUpS.js.map +1 -0
  37. package/dist/_chunks/{plugin-context-rCinWLiE.js → plugin-context-8idfR3UZ.js} +2 -2
  38. package/dist/_chunks/plugin-context-8idfR3UZ.js.map +1 -0
  39. package/dist/_chunks/poison-rules-DoEhbqaY.js +110 -0
  40. package/dist/_chunks/poison-rules-DoEhbqaY.js.map +1 -0
  41. package/dist/_chunks/poison-scan-BAxfTT5L.js +101 -0
  42. package/dist/_chunks/poison-scan-BAxfTT5L.js.map +1 -0
  43. package/dist/_chunks/resolve-schema-CBR6Lm4i.js.map +1 -1
  44. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  45. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -1
  46. package/dist/_chunks/{rsc-media-type-DRqE_lD_.js → rsc-media-type-DDc7duTD.js} +17 -2
  47. package/dist/_chunks/{rsc-media-type-DRqE_lD_.js.map → rsc-media-type-DDc7duTD.js.map} +1 -1
  48. package/dist/_chunks/scan-BoP17W3w.js +140 -0
  49. package/dist/_chunks/scan-BoP17W3w.js.map +1 -0
  50. package/dist/_chunks/{scanner-CAietmj4.js → scanner-BRIOmHE2.js} +43 -4
  51. package/dist/_chunks/scanner-BRIOmHE2.js.map +1 -0
  52. package/dist/_chunks/schema-bridge-C83xa9lT.js.map +1 -1
  53. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
  54. package/dist/_chunks/{segment-keys-BawYuNFO.js → segment-keys-BhqoHiLc.js} +14 -2
  55. package/dist/_chunks/{segment-keys-BawYuNFO.js.map → segment-keys-BhqoHiLc.js.map} +1 -1
  56. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -1
  57. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -1
  58. package/dist/_chunks/use-query-states-I3JMng6J.js.map +1 -1
  59. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -1
  60. package/dist/_chunks/{walkers-BsVLmD1S.js → walkers-uCu3WW6_.js} +9 -6
  61. package/dist/_chunks/{walkers-BsVLmD1S.js.map → walkers-uCu3WW6_.js.map} +1 -1
  62. package/dist/adapters/build-output-helper.d.ts +1 -1
  63. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  64. package/dist/adapters/cloudflare-dev.d.ts.map +1 -1
  65. package/dist/adapters/cloudflare-dev.js +2 -1
  66. package/dist/adapters/cloudflare-dev.js.map +1 -1
  67. package/dist/adapters/cloudflare-kv-cache.d.ts +1 -1
  68. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  69. package/dist/adapters/cloudflare-kv-cache.js +7 -8
  70. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  71. package/dist/adapters/cloudflare.d.ts +1 -1
  72. package/dist/adapters/cloudflare.d.ts.map +1 -1
  73. package/dist/adapters/cloudflare.js +1 -1
  74. package/dist/adapters/nitro-presets.d.ts +37 -0
  75. package/dist/adapters/nitro-presets.d.ts.map +1 -0
  76. package/dist/adapters/nitro-preview.d.ts +32 -0
  77. package/dist/adapters/nitro-preview.d.ts.map +1 -0
  78. package/dist/adapters/nitro.d.ts +28 -51
  79. package/dist/adapters/nitro.d.ts.map +1 -1
  80. package/dist/adapters/nitro.js +224 -171
  81. package/dist/adapters/nitro.js.map +1 -1
  82. package/dist/analyze/chains.d.ts +124 -0
  83. package/dist/analyze/chains.d.ts.map +1 -0
  84. package/dist/analyze/classify.d.ts +150 -0
  85. package/dist/analyze/classify.d.ts.map +1 -0
  86. package/dist/analyze/crawl-entry.d.ts +17 -0
  87. package/dist/analyze/crawl-entry.d.ts.map +1 -0
  88. package/dist/analyze/crawl-entry.js +313 -0
  89. package/dist/analyze/crawl-entry.js.map +1 -0
  90. package/dist/analyze/crawl.d.ts +134 -0
  91. package/dist/analyze/crawl.d.ts.map +1 -0
  92. package/dist/analyze/graph-cache.d.ts +104 -0
  93. package/dist/analyze/graph-cache.d.ts.map +1 -0
  94. package/dist/analyze/graph-command.d.ts +43 -0
  95. package/dist/analyze/graph-command.d.ts.map +1 -0
  96. package/dist/analyze/graph-command.js +472 -0
  97. package/dist/analyze/graph-command.js.map +1 -0
  98. package/dist/analyze/live-graph.d.ts +81 -0
  99. package/dist/analyze/live-graph.d.ts.map +1 -0
  100. package/dist/analyze/poison-rules.d.ts +72 -0
  101. package/dist/analyze/poison-rules.d.ts.map +1 -0
  102. package/dist/analyze/poison-scan.d.ts +48 -0
  103. package/dist/analyze/poison-scan.d.ts.map +1 -0
  104. package/dist/analyze/scan.d.ts +73 -0
  105. package/dist/analyze/scan.d.ts.map +1 -0
  106. package/dist/cache/cache-api.d.ts +2 -2
  107. package/dist/cache/cache-api.d.ts.map +1 -1
  108. package/dist/cache/index.d.ts +8 -8
  109. package/dist/cache/index.d.ts.map +1 -1
  110. package/dist/cache/index.js +1 -1
  111. package/dist/cache/redis-handler.d.ts +1 -1
  112. package/dist/cache/redis-handler.d.ts.map +1 -1
  113. package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
  114. package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
  115. package/dist/cache/stores/cloudflare-kv.js.map +1 -1
  116. package/dist/cache/stores/memory.d.ts +1 -1
  117. package/dist/cache/stores/memory.d.ts.map +1 -1
  118. package/dist/cache/stores/memory.js.map +1 -1
  119. package/dist/cache/stores/redis.d.ts +1 -1
  120. package/dist/cache/stores/redis.d.ts.map +1 -1
  121. package/dist/cache/stores/redis.js.map +1 -1
  122. package/dist/cache/stores/vercel.d.ts +1 -1
  123. package/dist/cache/stores/vercel.d.ts.map +1 -1
  124. package/dist/cache/stores/vercel.js.map +1 -1
  125. package/dist/cache/tag-aware-handler.d.ts +2 -2
  126. package/dist/cache/tag-aware-handler.d.ts.map +1 -1
  127. package/dist/cache/timber-cache.d.ts +1 -1
  128. package/dist/cache/timber-cache.d.ts.map +1 -1
  129. package/dist/cdn/cloudflare-purge.d.ts +1 -1
  130. package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
  131. package/dist/cdn/cloudflare-purge.js.map +1 -1
  132. package/dist/cdn/fastly-purge.d.ts +1 -1
  133. package/dist/cdn/fastly-purge.d.ts.map +1 -1
  134. package/dist/cdn/fastly-purge.js.map +1 -1
  135. package/dist/cdn/index.d.ts +4 -4
  136. package/dist/cdn/index.d.ts.map +1 -1
  137. package/dist/cdn/workers-cache-purge.d.ts +5 -1
  138. package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
  139. package/dist/cdn/workers-cache-purge.js.map +1 -1
  140. package/dist/cli-check.d.ts +10 -0
  141. package/dist/cli-check.d.ts.map +1 -1
  142. package/dist/cli.d.ts +12 -4
  143. package/dist/cli.d.ts.map +1 -1
  144. package/dist/cli.js +30 -6
  145. package/dist/cli.js.map +1 -1
  146. package/dist/client/browser-entry/hydrate.d.ts +2 -2
  147. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  148. package/dist/client/browser-entry/post-hydration.d.ts +2 -2
  149. package/dist/client/browser-entry/router-init.d.ts +2 -2
  150. package/dist/client/browser-entry/rsc-stream.d.ts +1 -1
  151. package/dist/client/form.d.ts +2 -2
  152. package/dist/client/form.d.ts.map +1 -1
  153. package/dist/client/history.d.ts +2 -2
  154. package/dist/client/index.d.ts +18 -18
  155. package/dist/client/index.d.ts.map +1 -1
  156. package/dist/client/index.js.map +1 -1
  157. package/dist/client/internal.d.ts +19 -19
  158. package/dist/client/internal.d.ts.map +1 -1
  159. package/dist/client/internal.js +11 -15
  160. package/dist/client/internal.js.map +1 -1
  161. package/dist/client/link.d.ts +3 -3
  162. package/dist/client/navigation-commit.d.ts +5 -5
  163. package/dist/client/navigation-root.d.ts +1 -1
  164. package/dist/client/navigation-root.d.ts.map +1 -1
  165. package/dist/client/params-context.d.ts +3 -3
  166. package/dist/client/router-pipeline.d.ts +7 -7
  167. package/dist/client/router-ref.d.ts +1 -1
  168. package/dist/client/router-types.d.ts +6 -6
  169. package/dist/client/router-types.d.ts.map +1 -1
  170. package/dist/client/router.d.ts +1 -1
  171. package/dist/client/rsc-fetch.d.ts +4 -4
  172. package/dist/client/rsc-fetch.d.ts.map +1 -1
  173. package/dist/client/segment-cache.d.ts +2 -2
  174. package/dist/client/slot-provider.d.ts +1 -1
  175. package/dist/client/ssr-data.d.ts +2 -2
  176. package/dist/client/state.d.ts +4 -4
  177. package/dist/client/use-query-states.d.ts +1 -1
  178. package/dist/client/use-segment-params.d.ts +3 -3
  179. package/dist/codec.d.ts +2 -2
  180. package/dist/codec.js.map +1 -1
  181. package/dist/config-validation.d.ts +1 -1
  182. package/dist/cookies/define-cookie.d.ts +3 -3
  183. package/dist/cookies/index.d.ts +3 -3
  184. package/dist/cookies/index.js +3 -4
  185. package/dist/cookies/index.js.map +1 -1
  186. package/dist/cookies/json-cookie.d.ts +1 -1
  187. package/dist/dev-tools/404-page.d.ts +2 -2
  188. package/dist/dev-tools/error-page.d.ts +2 -2
  189. package/dist/dev-tools/index.d.ts +12 -12
  190. package/dist/dev-tools/instrumentation.d.ts +1 -1
  191. package/dist/dev-tools/logs.d.ts +1 -1
  192. package/dist/dev-tools/overlay.d.ts +2 -2
  193. package/dist/dev-tools/terminal.d.ts +1 -1
  194. package/dist/fonts/ast.d.ts +4 -4
  195. package/dist/fonts/bundle.d.ts +3 -3
  196. package/dist/fonts/css.d.ts +1 -1
  197. package/dist/fonts/dev-middleware.d.ts +1 -1
  198. package/dist/fonts/google.d.ts +1 -1
  199. package/dist/fonts/local.d.ts +1 -1
  200. package/dist/fonts/pipeline.d.ts +3 -3
  201. package/dist/fonts/transform.d.ts +2 -2
  202. package/dist/fonts/types.d.ts +1 -1
  203. package/dist/fonts/virtual-modules.d.ts +1 -1
  204. package/dist/index.d.ts +3 -3
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +785 -282
  207. package/dist/index.js.map +1 -1
  208. package/dist/params/codec-registry.d.ts +1 -1
  209. package/dist/params/define-schema.d.ts +2 -2
  210. package/dist/params/index.d.ts +4 -4
  211. package/dist/params/index.js.map +1 -1
  212. package/dist/params/resolve-schema.d.ts +1 -1
  213. package/dist/plugin-context.d.ts +9 -9
  214. package/dist/plugin-context.d.ts.map +1 -1
  215. package/dist/plugins/adapter-build.d.ts +2 -2
  216. package/dist/plugins/build-manifest.d.ts +2 -2
  217. package/dist/plugins/build-report.d.ts +5 -5
  218. package/dist/plugins/cache.d.ts +1 -1
  219. package/dist/plugins/callsite-jsx.d.ts +2 -2
  220. package/dist/plugins/chunks.d.ts +1 -1
  221. package/dist/plugins/chunks.d.ts.map +1 -1
  222. package/dist/plugins/cloudflare-wasm.d.ts +1 -1
  223. package/dist/plugins/content.d.ts +1 -1
  224. package/dist/plugins/content.d.ts.map +1 -1
  225. package/dist/plugins/dev-discovery.d.ts +123 -0
  226. package/dist/plugins/dev-discovery.d.ts.map +1 -0
  227. package/dist/plugins/dev-server.d.ts +1 -1
  228. package/dist/plugins/dev-server.d.ts.map +1 -1
  229. package/dist/plugins/entries.d.ts +1 -1
  230. package/dist/plugins/fonts.d.ts +3 -3
  231. package/dist/plugins/fonts.d.ts.map +1 -1
  232. package/dist/plugins/graph-endpoint.d.ts +112 -0
  233. package/dist/plugins/graph-endpoint.d.ts.map +1 -0
  234. package/dist/plugins/mdx.d.ts +1 -1
  235. package/dist/plugins/mdx.d.ts.map +1 -1
  236. package/dist/plugins/prebuilt-capture.d.ts +2 -2
  237. package/dist/plugins/prebuilt-options-analysis.d.ts +1 -1
  238. package/dist/plugins/prebuilt.d.ts +1 -1
  239. package/dist/plugins/prerender-sugar.d.ts +11 -14
  240. package/dist/plugins/prerender-sugar.d.ts.map +1 -1
  241. package/dist/plugins/request-dep/analysis.d.ts +1 -1
  242. package/dist/plugins/request-dep/index.d.ts +5 -5
  243. package/dist/plugins/request-dep/origin.d.ts +1 -1
  244. package/dist/plugins/routing.d.ts +1 -1
  245. package/dist/plugins/routing.d.ts.map +1 -1
  246. package/dist/plugins/server-bundle.d.ts.map +1 -1
  247. package/dist/plugins/shims.d.ts +1 -1
  248. package/dist/plugins/shims.d.ts.map +1 -1
  249. package/dist/plugins/static-build.d.ts +5 -5
  250. package/dist/routing/codegen-shared.d.ts +1 -1
  251. package/dist/routing/codegen-write.d.ts +1 -1
  252. package/dist/routing/codegen.d.ts +1 -1
  253. package/dist/routing/collision-probe.d.ts +1 -1
  254. package/dist/routing/collision-spaces.d.ts +1 -1
  255. package/dist/routing/convention-lint.d.ts +1 -1
  256. package/dist/routing/convention-lint.d.ts.map +1 -1
  257. package/dist/routing/index.d.ts +13 -13
  258. package/dist/routing/index.js +3 -3
  259. package/dist/routing/interception.d.ts +1 -1
  260. package/dist/routing/interception.d.ts.map +1 -1
  261. package/dist/routing/link-codegen.d.ts +1 -1
  262. package/dist/routing/manifest-codegen.d.ts +1 -1
  263. package/dist/routing/scanner.d.ts +14 -1
  264. package/dist/routing/scanner.d.ts.map +1 -1
  265. package/dist/routing/schema-validation.d.ts +1 -1
  266. package/dist/routing/segment-classify.d.ts +1 -1
  267. package/dist/routing/segment-codegen.d.ts +1 -1
  268. package/dist/routing/segment-keys.d.ts +1 -1
  269. package/dist/routing/slot-placement.d.ts +1 -1
  270. package/dist/routing/walkers.d.ts +1 -1
  271. package/dist/rsc-runtime/transforms.d.ts +13 -0
  272. package/dist/rsc-runtime/transforms.d.ts.map +1 -0
  273. package/dist/schema-bridge.d.ts +1 -1
  274. package/dist/search-params/define.d.ts +3 -3
  275. package/dist/search-params/index.d.ts +3 -3
  276. package/dist/search-params/index.js.map +1 -1
  277. package/dist/search-params/wrappers.d.ts +1 -1
  278. package/dist/server/access-gate.d.ts +1 -1
  279. package/dist/server/action-client.d.ts +1 -1
  280. package/dist/server/action-handler.d.ts +5 -5
  281. package/dist/server/actions.d.ts +2 -2
  282. package/dist/server/als-registry.d.ts +8 -8
  283. package/dist/server/asset-headers.d.ts +1 -1
  284. package/dist/server/chain-url-parts.d.ts +3 -3
  285. package/dist/server/children-interception.d.ts +2 -2
  286. package/dist/server/cookie-context.d.ts +1 -1
  287. package/dist/server/cookie-parsing.d.ts +2 -2
  288. package/dist/server/default-logger.d.ts +1 -1
  289. package/dist/server/deny-boundary.d.ts +2 -2
  290. package/dist/server/deny-renderer.d.ts +5 -5
  291. package/dist/server/early-hints.d.ts +1 -1
  292. package/dist/server/error-boundary-wrapper.d.ts +2 -4
  293. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  294. package/dist/server/fallback-error.d.ts +5 -5
  295. package/dist/server/flight-injection-state.d.ts +1 -1
  296. package/dist/server/form-flash.d.ts +1 -1
  297. package/dist/server/html-injector-core.d.ts +2 -2
  298. package/dist/server/html-injectors.d.ts +1 -1
  299. package/dist/server/index.d.ts +18 -18
  300. package/dist/server/index.d.ts.map +1 -1
  301. package/dist/server/index.js +33 -32
  302. package/dist/server/index.js.map +1 -1
  303. package/dist/server/instrumentation.d.ts +1 -1
  304. package/dist/server/internal.d.ts +50 -50
  305. package/dist/server/internal.d.ts.map +1 -1
  306. package/dist/server/internal.js +1023 -1025
  307. package/dist/server/internal.js.map +1 -1
  308. package/dist/server/metadata-collector.d.ts +2 -2
  309. package/dist/server/metadata-platform.d.ts +2 -2
  310. package/dist/server/metadata-render.d.ts +2 -2
  311. package/dist/server/metadata-routes.d.ts +3 -3
  312. package/dist/server/metadata-social.d.ts +2 -2
  313. package/dist/server/metadata.d.ts +2 -2
  314. package/dist/server/middleware-runner.d.ts +1 -1
  315. package/dist/server/node-stream-transforms.d.ts +1 -1
  316. package/dist/server/param-coercion.d.ts +4 -4
  317. package/dist/server/pipeline-helpers.d.ts +3 -3
  318. package/dist/server/pipeline-interception.d.ts +1 -1
  319. package/dist/server/pipeline-metadata.d.ts +1 -1
  320. package/dist/server/pipeline-outcome.d.ts +2 -2
  321. package/dist/server/pipeline-phases.d.ts +3 -3
  322. package/dist/server/pipeline-phases.d.ts.map +1 -1
  323. package/dist/server/pipeline.d.ts +9 -9
  324. package/dist/server/prebuilt/cache-key.d.ts +1 -1
  325. package/dist/server/prebuilt/capture-state.d.ts +1 -1
  326. package/dist/server/prebuilt/key-discipline.d.ts +1 -1
  327. package/dist/server/prebuilt/manifest-tag-index.d.ts +1 -1
  328. package/dist/server/prebuilt/overlay.d.ts +2 -2
  329. package/dist/server/prebuilt/payload-source.d.ts +1 -1
  330. package/dist/server/prebuilt/slots.d.ts +1 -1
  331. package/dist/server/prebuilt/synthetic-store.d.ts +2 -2
  332. package/dist/server/prebuilt-builder.d.ts +2 -2
  333. package/dist/server/primitives.d.ts +2 -2
  334. package/dist/server/publish-params.d.ts +1 -1
  335. package/dist/server/request-context.d.ts +3 -3
  336. package/dist/server/route-element-builder.d.ts +16 -6
  337. package/dist/server/route-element-builder.d.ts.map +1 -1
  338. package/dist/server/route-handler.d.ts +1 -1
  339. package/dist/server/route-matcher.d.ts +4 -4
  340. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  341. package/dist/server/rsc-entry/action-middleware-runner.d.ts +2 -2
  342. package/dist/server/rsc-entry/api-handler.d.ts +1 -1
  343. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  344. package/dist/server/rsc-entry/deny-fallback.d.ts +3 -3
  345. package/dist/server/rsc-entry/error-renderer.d.ts +4 -4
  346. package/dist/server/rsc-entry/helpers.d.ts +6 -6
  347. package/dist/server/rsc-entry/index.d.ts +7 -6
  348. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  349. package/dist/server/rsc-entry/render-route.d.ts +4 -4
  350. package/dist/server/rsc-entry/revalidate-renderer.d.ts +4 -4
  351. package/dist/server/rsc-entry/rsc-payload.d.ts +4 -4
  352. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  353. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -3
  354. package/dist/server/rsc-entry/ssr-bridge.d.ts +1 -1
  355. package/dist/server/rsc-entry/ssr-renderer.d.ts +7 -7
  356. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +5 -5
  357. package/dist/server/sitemap-generator.d.ts +2 -2
  358. package/dist/server/sitemap-handler.d.ts +1 -1
  359. package/dist/server/skippable-prefix.d.ts +3 -3
  360. package/dist/server/slot-interception.d.ts +2 -2
  361. package/dist/server/slot-resolver.d.ts +3 -7
  362. package/dist/server/slot-resolver.d.ts.map +1 -1
  363. package/dist/server/ssr-bridge-types.d.ts +2 -2
  364. package/dist/server/ssr-entry.d.ts +1 -1
  365. package/dist/server/static-generator.d.ts +1 -1
  366. package/dist/server/static-not-found.d.ts +1 -1
  367. package/dist/server/status-code-resolver.d.ts +1 -1
  368. package/dist/server/tracing.d.ts +3 -3
  369. package/dist/server/tree-builder.d.ts +3 -3
  370. package/dist/server/tree-match.d.ts +1 -1
  371. package/dist/server/types.d.ts +2 -2
  372. package/dist/server/utils/mdx-file.d.ts +2 -0
  373. package/dist/server/utils/mdx-file.d.ts.map +1 -1
  374. package/dist/server/workers-cache-bridge.d.ts +1 -1
  375. package/dist/shared/als-slots.d.ts +1 -1
  376. package/dist/shared/payload-root.d.ts +2 -2
  377. package/dist/shared/rsc-media-type.d.ts +12 -0
  378. package/dist/shared/rsc-media-type.d.ts.map +1 -1
  379. package/dist/shared/slot-params.d.ts +1 -1
  380. package/dist/shims/font-google.d.ts +1 -1
  381. package/dist/shims/font-google.js.map +1 -1
  382. package/dist/shims/font-local.d.ts +1 -1
  383. package/dist/shims/font-local.js.map +1 -1
  384. package/dist/shims/navigation-client.d.ts +4 -4
  385. package/dist/shims/navigation.d.ts +2 -2
  386. package/docs/api/36-cli.mdx +23 -3
  387. package/docs/learn/11-error-handling.mdx +11 -0
  388. package/docs/more/01-advanced-routing.mdx +1 -0
  389. package/package.json +17 -16
  390. package/src/adapters/build-output-helper.ts +4 -4
  391. package/src/adapters/cloudflare-dev.ts +7 -1
  392. package/src/adapters/cloudflare-kv-cache.ts +11 -8
  393. package/src/adapters/cloudflare.ts +21 -15
  394. package/src/adapters/compress-module.ts +2 -2
  395. package/src/adapters/nitro-presets.ts +139 -0
  396. package/src/adapters/nitro-preview.ts +340 -0
  397. package/src/adapters/nitro.ts +100 -465
  398. package/src/adapters/shared.ts +1 -1
  399. package/src/analyze/chains.ts +411 -0
  400. package/src/analyze/classify.ts +258 -0
  401. package/src/analyze/crawl-entry.ts +68 -0
  402. package/src/analyze/crawl.ts +500 -0
  403. package/src/analyze/graph-cache.ts +332 -0
  404. package/src/analyze/graph-command.ts +762 -0
  405. package/src/analyze/live-graph.ts +232 -0
  406. package/src/analyze/poison-rules.ts +165 -0
  407. package/src/analyze/poison-scan.ts +162 -0
  408. package/src/analyze/scan.ts +149 -0
  409. package/src/cache/cache-api.ts +7 -7
  410. package/src/cache/index.ts +9 -9
  411. package/src/cache/redis-handler.ts +2 -2
  412. package/src/cache/stores/cloudflare-kv.ts +1 -1
  413. package/src/cache/stores/memory.ts +2 -2
  414. package/src/cache/stores/redis.ts +1 -1
  415. package/src/cache/stores/vercel.ts +1 -1
  416. package/src/cache/tag-aware-handler.ts +2 -2
  417. package/src/cache/timber-cache.ts +8 -8
  418. package/src/cdn/cloudflare-purge.ts +1 -1
  419. package/src/cdn/fastly-purge.ts +1 -1
  420. package/src/cdn/index.ts +10 -4
  421. package/src/cdn/workers-cache-purge.ts +6 -2
  422. package/src/cli-check.ts +53 -4
  423. package/src/cli-schema-sync.ts +3 -3
  424. package/src/cli.ts +50 -11
  425. package/src/client/browser-dev.ts +1 -1
  426. package/src/client/browser-entry/action-dispatch.ts +5 -5
  427. package/src/client/browser-entry/hmr.ts +1 -1
  428. package/src/client/browser-entry/hydrate.ts +9 -9
  429. package/src/client/browser-entry/index.ts +10 -10
  430. package/src/client/browser-entry/post-hydration.ts +5 -5
  431. package/src/client/browser-entry/router-init.ts +9 -9
  432. package/src/client/browser-entry/rsc-stream.ts +3 -3
  433. package/src/client/error-boundary.tsx +4 -4
  434. package/src/client/form.tsx +7 -2
  435. package/src/client/history.ts +2 -2
  436. package/src/client/index.ts +21 -18
  437. package/src/client/internal.ts +19 -19
  438. package/src/client/link.tsx +9 -9
  439. package/src/client/navigation-api.ts +1 -1
  440. package/src/client/navigation-commit.ts +5 -5
  441. package/src/client/navigation-context.ts +1 -1
  442. package/src/client/navigation-root.tsx +1 -1
  443. package/src/client/nuqs-adapter.tsx +2 -2
  444. package/src/client/params-context.ts +4 -4
  445. package/src/client/router-effects.ts +3 -3
  446. package/src/client/router-lifecycle.ts +2 -2
  447. package/src/client/router-pipeline.ts +10 -10
  448. package/src/client/router-ref.ts +2 -2
  449. package/src/client/router-skew.ts +2 -2
  450. package/src/client/router-types.ts +6 -6
  451. package/src/client/router.ts +13 -13
  452. package/src/client/rsc-fetch.ts +11 -16
  453. package/src/client/segment-cache.ts +2 -2
  454. package/src/client/segment-outlet.tsx +1 -1
  455. package/src/client/shallow-url.ts +1 -1
  456. package/src/client/slot-outlet.tsx +1 -1
  457. package/src/client/slot-provider.tsx +1 -1
  458. package/src/client/ssr-data.ts +3 -3
  459. package/src/client/stale-client.ts +1 -1
  460. package/src/client/state.ts +4 -4
  461. package/src/client/top-loader.tsx +1 -1
  462. package/src/client/unload-guard.ts +1 -1
  463. package/src/client/use-cookie.ts +2 -2
  464. package/src/client/use-pathname.ts +2 -2
  465. package/src/client/use-pending-navigation.ts +1 -1
  466. package/src/client/use-query-states.ts +3 -3
  467. package/src/client/use-router.ts +2 -2
  468. package/src/client/use-search-params.ts +3 -3
  469. package/src/client/use-segment-params.ts +6 -6
  470. package/src/client/use-selected-layout-segment.ts +2 -2
  471. package/src/codec.ts +2 -2
  472. package/src/config-validation.ts +1 -1
  473. package/src/cookies/define-cookie.ts +6 -6
  474. package/src/cookies/index.ts +3 -3
  475. package/src/cookies/json-cookie.ts +1 -1
  476. package/src/dev-tools/404-page.ts +4 -4
  477. package/src/dev-tools/dev-page-shell.ts +1 -1
  478. package/src/dev-tools/error-page.ts +4 -4
  479. package/src/dev-tools/index.ts +12 -12
  480. package/src/dev-tools/instrumentation.ts +1 -1
  481. package/src/dev-tools/logs.ts +1 -1
  482. package/src/dev-tools/overlay.ts +3 -3
  483. package/src/dev-tools/terminal.ts +2 -2
  484. package/src/fonts/ast.ts +5 -5
  485. package/src/fonts/bundle.ts +5 -5
  486. package/src/fonts/css.ts +1 -1
  487. package/src/fonts/dev-middleware.ts +2 -2
  488. package/src/fonts/google.ts +2 -2
  489. package/src/fonts/local.ts +4 -4
  490. package/src/fonts/pipeline.ts +8 -8
  491. package/src/fonts/transform.ts +5 -5
  492. package/src/fonts/types.ts +1 -1
  493. package/src/fonts/virtual-modules.ts +1 -1
  494. package/src/index.ts +46 -37
  495. package/src/params/codec-registry.ts +1 -1
  496. package/src/params/define-schema.ts +2 -2
  497. package/src/params/index.ts +4 -4
  498. package/src/params/resolve-schema.ts +2 -2
  499. package/src/plugin-context.ts +10 -10
  500. package/src/plugins/adapter-build.ts +5 -5
  501. package/src/plugins/build-manifest.ts +4 -4
  502. package/src/plugins/build-report.ts +6 -6
  503. package/src/plugins/cache.ts +3 -3
  504. package/src/plugins/callsite-ast.ts +1 -1
  505. package/src/plugins/callsite-jsx.ts +2 -2
  506. package/src/plugins/chunks.ts +1 -1
  507. package/src/plugins/cloudflare-wasm.ts +1 -1
  508. package/src/plugins/content.ts +5 -1
  509. package/src/plugins/dev-discovery.ts +218 -0
  510. package/src/plugins/dev-server.ts +97 -12
  511. package/src/plugins/entries.ts +1 -1
  512. package/src/plugins/fonts.ts +17 -10
  513. package/src/plugins/graph-endpoint.ts +532 -0
  514. package/src/plugins/mdx.ts +4 -3
  515. package/src/plugins/prebuilt-capture.ts +3 -3
  516. package/src/plugins/prebuilt-options-analysis.ts +1 -1
  517. package/src/plugins/prebuilt.ts +7 -7
  518. package/src/plugins/prerender-sugar.ts +11 -19
  519. package/src/plugins/request-dep/analysis.ts +1 -1
  520. package/src/plugins/request-dep/index.ts +8 -8
  521. package/src/plugins/request-dep/origin.ts +1 -1
  522. package/src/plugins/routing.ts +12 -7
  523. package/src/plugins/server-bundle.ts +9 -2
  524. package/src/plugins/shims.ts +16 -31
  525. package/src/plugins/static-build.ts +10 -10
  526. package/src/routing/codegen-shared.ts +1 -1
  527. package/src/routing/codegen-write.ts +3 -3
  528. package/src/routing/codegen.ts +14 -5
  529. package/src/routing/collision-probe.ts +3 -3
  530. package/src/routing/collision-spaces.ts +3 -3
  531. package/src/routing/convention-lint.ts +136 -6
  532. package/src/routing/export-detect.ts +1 -1
  533. package/src/routing/index.ts +13 -13
  534. package/src/routing/interception.ts +41 -4
  535. package/src/routing/link-codegen.ts +3 -3
  536. package/src/routing/manifest-codegen.ts +2 -2
  537. package/src/routing/scanner.ts +33 -9
  538. package/src/routing/schema-validation.ts +4 -4
  539. package/src/routing/segment-classify.ts +2 -2
  540. package/src/routing/segment-codegen.ts +1 -1
  541. package/src/routing/segment-keys.ts +1 -1
  542. package/src/routing/slot-placement.ts +1 -1
  543. package/src/routing/walkers.ts +1 -1
  544. package/src/rsc-runtime/transforms.ts +13 -0
  545. package/src/rsc-runtime/vendor-types.d.ts +12 -0
  546. package/src/schema-bridge.ts +1 -1
  547. package/src/search-params/define.ts +8 -8
  548. package/src/search-params/index.ts +3 -3
  549. package/src/search-params/wrappers.ts +3 -3
  550. package/src/server/access-gate.tsx +6 -6
  551. package/src/server/action-client.ts +5 -5
  552. package/src/server/action-handler.ts +16 -16
  553. package/src/server/actions.ts +5 -5
  554. package/src/server/als-registry.ts +8 -8
  555. package/src/server/asset-headers.ts +2 -2
  556. package/src/server/chain-url-parts.ts +5 -5
  557. package/src/server/children-interception.ts +4 -4
  558. package/src/server/compress.ts +1 -1
  559. package/src/server/cookie-context.ts +4 -4
  560. package/src/server/cookie-parsing.ts +2 -2
  561. package/src/server/default-logger.ts +3 -3
  562. package/src/server/deny-boundary.ts +10 -10
  563. package/src/server/deny-renderer.ts +38 -25
  564. package/src/server/early-hints-sender.ts +2 -2
  565. package/src/server/early-hints.ts +2 -2
  566. package/src/server/error-boundary-wrapper.ts +4 -12
  567. package/src/server/fallback-error.ts +13 -13
  568. package/src/server/flight-injection-state.ts +1 -1
  569. package/src/server/form-flash.ts +2 -2
  570. package/src/server/html-injector-core.ts +4 -4
  571. package/src/server/html-injectors.ts +5 -5
  572. package/src/server/index.ts +18 -18
  573. package/src/server/instrumentation.ts +1 -1
  574. package/src/server/internal.ts +50 -50
  575. package/src/server/logger.ts +4 -4
  576. package/src/server/metadata-collector.ts +7 -7
  577. package/src/server/metadata-platform.ts +2 -2
  578. package/src/server/metadata-render.ts +4 -4
  579. package/src/server/metadata-routes.ts +3 -3
  580. package/src/server/metadata-social.ts +2 -2
  581. package/src/server/metadata.ts +2 -2
  582. package/src/server/middleware-runner.ts +1 -1
  583. package/src/server/node-stream-transforms.ts +4 -4
  584. package/src/server/param-coercion.ts +8 -8
  585. package/src/server/pipeline-helpers.ts +7 -7
  586. package/src/server/pipeline-interception.ts +2 -2
  587. package/src/server/pipeline-metadata.ts +1 -1
  588. package/src/server/pipeline-outcome.ts +8 -8
  589. package/src/server/pipeline-phases.ts +24 -28
  590. package/src/server/pipeline.ts +22 -22
  591. package/src/server/prebuilt/cache-key.ts +1 -1
  592. package/src/server/prebuilt/capture-state.ts +1 -1
  593. package/src/server/prebuilt/key-discipline.ts +1 -1
  594. package/src/server/prebuilt/manifest-tag-index.ts +1 -1
  595. package/src/server/prebuilt/overlay.ts +6 -6
  596. package/src/server/prebuilt/payload-source.ts +2 -2
  597. package/src/server/prebuilt/slots.ts +5 -5
  598. package/src/server/prebuilt/synthetic-store.ts +2 -2
  599. package/src/server/prebuilt-builder.ts +16 -16
  600. package/src/server/prebuilt-runtime.ts +12 -12
  601. package/src/server/primitives.ts +7 -7
  602. package/src/server/publish-params.ts +3 -3
  603. package/src/server/request-context.ts +7 -7
  604. package/src/server/route-element-builder.ts +108 -50
  605. package/src/server/route-handler.ts +4 -4
  606. package/src/server/route-matcher.ts +5 -5
  607. package/src/server/rsc-cache-key-guard.ts +3 -5
  608. package/src/server/rsc-entry/action-middleware-runner.ts +7 -7
  609. package/src/server/rsc-entry/api-handler.ts +18 -9
  610. package/src/server/rsc-entry/deny-fallback.ts +10 -10
  611. package/src/server/rsc-entry/error-renderer.ts +27 -27
  612. package/src/server/rsc-entry/helpers.ts +10 -10
  613. package/src/server/rsc-entry/index.ts +51 -47
  614. package/src/server/rsc-entry/render-route.ts +20 -20
  615. package/src/server/rsc-entry/revalidate-renderer.ts +12 -12
  616. package/src/server/rsc-entry/rsc-payload.ts +19 -15
  617. package/src/server/rsc-entry/rsc-stream.ts +8 -8
  618. package/src/server/rsc-entry/ssr-bridge.ts +3 -3
  619. package/src/server/rsc-entry/ssr-renderer.ts +23 -23
  620. package/src/server/rsc-entry/wrap-action-dispatch.ts +9 -9
  621. package/src/server/rsc-prop-warnings.ts +1 -1
  622. package/src/server/sensitive-fields.ts +1 -1
  623. package/src/server/server-timing.ts +1 -1
  624. package/src/server/sitemap-generator.ts +3 -3
  625. package/src/server/sitemap-handler.ts +3 -3
  626. package/src/server/skippable-prefix.ts +7 -7
  627. package/src/server/slot-interception.ts +5 -5
  628. package/src/server/slot-resolver.ts +20 -28
  629. package/src/server/ssr-bridge-types.ts +2 -2
  630. package/src/server/ssr-entry.ts +18 -18
  631. package/src/server/ssr-render.ts +4 -4
  632. package/src/server/ssr-wrappers.tsx +1 -1
  633. package/src/server/state-tree-diff.ts +1 -1
  634. package/src/server/static-generator.ts +6 -6
  635. package/src/server/static-not-found.ts +3 -3
  636. package/src/server/status-code-resolver.ts +1 -1
  637. package/src/server/tracing.ts +5 -5
  638. package/src/server/tree-builder.ts +4 -4
  639. package/src/server/tree-match.ts +1 -1
  640. package/src/server/types.ts +2 -2
  641. package/src/server/utils/mdx-file.ts +2 -2
  642. package/src/server/waituntil-bridge.ts +1 -1
  643. package/src/server/workers-cache-bridge.ts +2 -2
  644. package/src/shared/als-slots.ts +1 -1
  645. package/src/shared/payload-root.ts +2 -2
  646. package/src/shared/rsc-media-type.ts +16 -0
  647. package/src/shared/slot-params.ts +1 -1
  648. package/src/shims/font-google.ts +1 -1
  649. package/src/shims/font-local.ts +1 -1
  650. package/src/shims/navigation-client.ts +5 -5
  651. package/src/shims/navigation.ts +2 -2
  652. package/LICENSE +0 -8
  653. package/dist/_chunks/actions-BjbbNRFN.js.map +0 -1
  654. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +0 -1
  655. package/dist/_chunks/cache-api-DllJ-Lyw.js.map +0 -1
  656. package/dist/_chunks/cli-check-DJZDc22E.js.map +0 -1
  657. package/dist/_chunks/convention-lint-CpteIpTm.js.map +0 -1
  658. package/dist/_chunks/dist-BA3u1z3W.js +0 -423
  659. package/dist/_chunks/dist-BA3u1z3W.js.map +0 -1
  660. package/dist/_chunks/mdx-file-C005ay-P.js.map +0 -1
  661. package/dist/_chunks/plugin-context-rCinWLiE.js.map +0 -1
  662. package/dist/_chunks/scanner-CAietmj4.js.map +0 -1
  663. package/dist/content/index.d.ts +0 -2
  664. package/dist/content/index.d.ts.map +0 -1
  665. package/dist/content/index.js +0 -0
  666. package/src/content/index.ts +0 -5
  667. package/src/shims/server-only-noop.js +0 -6
@@ -1,13 +1,13 @@
1
- import { n as isRscRequest } from "../_chunks/rsc-media-type-DRqE_lD_.js";
2
- import { a as canonicalize, c as getMetadataRouteAutoLink, l as getMetadataRouteServePath, o as METADATA_ROUTE_CONVENTIONS, s as classifyMetadataRoute, t as computeSegmentTreePaths } from "../_chunks/segment-keys-BawYuNFO.js";
1
+ import { n as isRscContentType, r as isRscRequest } from "../_chunks/rsc-media-type-DDc7duTD.js";
2
+ import { a as canonicalize, c as getMetadataRouteAutoLink, l as getMetadataRouteServePath, o as METADATA_ROUTE_CONVENTIONS, s as classifyMetadataRoute, t as computeSegmentTreePaths } from "../_chunks/segment-keys-BhqoHiLc.js";
3
+ import { n as isMdxFilePath } from "../_chunks/mdx-file-CXyHGUpS.js";
3
4
  import { n as classifyUrlSegment, r as effectiveUrlSegment } from "../_chunks/segment-classify-C539Pa2O.js";
4
5
  import { a as timingAls, r as requestContextAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-C6kcfprT.js";
5
- import { A as setMatchedSegmentPath, C as applyRequestHeaderOverlay, D as getSegmentParams, E as getSearchParams, F as getSetCookieHeaders, G as replaceTraceId, H as getOtelTraceId, I as getWaitUntil, J as withSpan, K as runWithTraceId, L as isDebug, M as setSegmentParams, N as getCookie, O as markResponseFlushed, V as generateTraceId, W as getTraceId, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMutableCookieContext, k as runWithRequestContext, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as setSpanAttribute, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as getHeader } from "../_chunks/logger-CH7IcMmg.js";
6
+ import { A as setMatchedSegmentPath, C as applyRequestHeaderOverlay, D as getSegmentParams, E as getSearchParams, F as getSetCookieHeaders, G as replaceTraceId, H as getOtelTraceId, I as getWaitUntil, J as withSpan, K as runWithTraceId, L as isDebug, M as setSegmentParams, N as getCookie, O as markResponseFlushed, V as generateTraceId, W as getTraceId, _ as RedirectSignal, a as logProxyError, c as logRequestReceived, d as logSwrRefetchFailed, f as logWaitUntilRejected, g as DenySignal, h as swallow, i as logMiddlewareShortCircuit, j as setMutableCookieContext, k as runWithRequestContext, l as logRouteError, m as setLogger, n as logCacheMiss, o as logRenderError, p as logWaitUntilUnsupported, q as setSpanAttribute, r as logMiddlewareError, s as logRequestCompleted, t as getLogger, u as logSlowRequest, v as RenderError, w as getHeader } from "../_chunks/logger-pumCm3Il.js";
6
7
  import { i as isRscCacheKeyShape, n as RSC_KEY_PARAM, s as rscCacheKey, t as RSC_KEY_HEADERS } from "../_chunks/rsc-cache-key-DD0fl_-s.js";
7
- import { t as isMdxFilePath } from "../_chunks/mdx-file-C005ay-P.js";
8
8
  import { i as collectRouteModulepreloads, n as collectRouteCss, r as collectRouteFonts } from "../_chunks/build-manifest-DWppEdLB.js";
9
- import { i as coerce, t as executeAction } from "../_chunks/actions-BjbbNRFN.js";
10
- import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-DllJ-Lyw.js";
9
+ import { i as coerce, t as executeAction } from "../_chunks/actions-CWYtq6ii.js";
10
+ import { a as getManifestEntry, b as wasInvalidatedSince, c as lookupPrebuiltPayload, d as checkVersionSkew, i as storeOverlayEntry, l as setPrebuiltPayloadSource, n as lookupOverlay, o as hasPrebuiltPayloadSource, r as overlayKey, s as isParamIndependent, u as applyReloadHeaders, v as currentInvalidationEpoch, x as createSingleflight, y as lastInvalidationEpochFor } from "../_chunks/cache-api-CQeYzA5g.js";
11
11
  import "../_chunks/error-boundary-D-ODYX41.js";
12
12
  import { n as toBracketKey } from "../_chunks/resolve-schema-CBR6Lm4i.js";
13
13
  import { r as normalizeParamValue } from "../_chunks/segment-context-CjOlyB8Y.js";
@@ -403,44 +403,46 @@ function renderOpenGraph(og, elements) {
403
403
  content
404
404
  }
405
405
  });
406
- if (og.images) if (typeof og.images === "string") elements.push({
407
- tag: "meta",
408
- attrs: {
409
- property: "og:image",
410
- content: og.images
411
- }
412
- });
413
- else {
414
- const imgList = Array.isArray(og.images) ? og.images : [og.images];
415
- for (const img of imgList) {
416
- elements.push({
417
- tag: "meta",
418
- attrs: {
419
- property: "og:image",
420
- content: img.url
421
- }
422
- });
423
- if (img.width) elements.push({
424
- tag: "meta",
425
- attrs: {
426
- property: "og:image:width",
427
- content: String(img.width)
428
- }
429
- });
430
- if (img.height) elements.push({
431
- tag: "meta",
432
- attrs: {
433
- property: "og:image:height",
434
- content: String(img.height)
435
- }
436
- });
437
- if (img.alt) elements.push({
438
- tag: "meta",
439
- attrs: {
440
- property: "og:image:alt",
441
- content: img.alt
442
- }
443
- });
406
+ if (og.images) {
407
+ if (typeof og.images === "string") elements.push({
408
+ tag: "meta",
409
+ attrs: {
410
+ property: "og:image",
411
+ content: og.images
412
+ }
413
+ });
414
+ else {
415
+ const imgList = Array.isArray(og.images) ? og.images : [og.images];
416
+ for (const img of imgList) {
417
+ elements.push({
418
+ tag: "meta",
419
+ attrs: {
420
+ property: "og:image",
421
+ content: img.url
422
+ }
423
+ });
424
+ if (img.width) elements.push({
425
+ tag: "meta",
426
+ attrs: {
427
+ property: "og:image:width",
428
+ content: String(img.width)
429
+ }
430
+ });
431
+ if (img.height) elements.push({
432
+ tag: "meta",
433
+ attrs: {
434
+ property: "og:image:height",
435
+ content: String(img.height)
436
+ }
437
+ });
438
+ if (img.alt) elements.push({
439
+ tag: "meta",
440
+ attrs: {
441
+ property: "og:image:alt",
442
+ content: img.alt
443
+ }
444
+ });
445
+ }
444
446
  }
445
447
  }
446
448
  if (og.videos) for (const video of og.videos) elements.push({
@@ -488,24 +490,26 @@ function renderTwitter(tw, elements) {
488
490
  content
489
491
  }
490
492
  });
491
- if (tw.images) if (typeof tw.images === "string") elements.push({
492
- tag: "meta",
493
- attrs: {
494
- name: "twitter:image",
495
- content: tw.images
496
- }
497
- });
498
- else {
499
- const imgList = Array.isArray(tw.images) ? tw.images : [tw.images];
500
- for (const img of imgList) {
501
- const url = typeof img === "string" ? img : img.url;
502
- elements.push({
503
- tag: "meta",
504
- attrs: {
505
- name: "twitter:image",
506
- content: url
507
- }
508
- });
493
+ if (tw.images) {
494
+ if (typeof tw.images === "string") elements.push({
495
+ tag: "meta",
496
+ attrs: {
497
+ name: "twitter:image",
498
+ content: tw.images
499
+ }
500
+ });
501
+ else {
502
+ const imgList = Array.isArray(tw.images) ? tw.images : [tw.images];
503
+ for (const img of imgList) {
504
+ const url = typeof img === "string" ? img : img.url;
505
+ elements.push({
506
+ tag: "meta",
507
+ attrs: {
508
+ name: "twitter:image",
509
+ content: url
510
+ }
511
+ });
512
+ }
509
513
  }
510
514
  }
511
515
  if (tw.players) for (const player of tw.players) {
@@ -1082,11 +1086,12 @@ function resolveMetadataUrls(metadata) {
1082
1086
  * the user's error component with the proper props.
1083
1087
  */
1084
1088
  function ErrorReconstituter({ error: serialized, digest, reset, component, status, dangerouslyPassData }) {
1089
+ const error = Object.assign(new Error(serialized.message), {
1090
+ name: serialized.name,
1091
+ ...serialized.stack != null ? { stack: serialized.stack } : {}
1092
+ });
1085
1093
  return createElement(component, {
1086
- error: Object.assign(new Error(serialized.message), {
1087
- name: serialized.name,
1088
- ...serialized.stack != null ? { stack: serialized.stack } : {}
1089
- }),
1094
+ error,
1090
1095
  digest,
1091
1096
  reset,
1092
1097
  ...status != null ? {
@@ -1493,527 +1498,154 @@ function buildDeniedFallback(DeniedComponent, slotName, data, createElement) {
1493
1498
  });
1494
1499
  }
1495
1500
  //#endregion
1496
- //#region src/client/slot-context.ts
1501
+ //#region src/server/error-boundary-wrapper.ts
1497
1502
  /**
1498
- * SlotContext — delivers live slot content to SlotOutlet holes inside
1499
- * cached component shells (TIM-1173).
1503
+ * Load a status/error file's component, or `null` if the boundary should be skipped.
1500
1504
  *
1501
- * A `cache.component(Fn, { slots: [...] })` shell is captured with a
1502
- * stable <SlotOutlet slot={name} /> client reference in place of each
1503
- * declared slot prop. At request time the revived shell is wrapped in a
1504
- * SlotsProvider carrying the live slot values; each outlet reads its
1505
- * value from this context by name.
1505
+ * Two ways to get `null`, both deliberate:
1506
+ * - the module fails to load (syntax error in the error page) — building an
1507
+ * error boundary must not itself throw, so the boundary is skipped (TIM-584);
1508
+ * - the default export is not a valid React component type, which would
1509
+ * otherwise crash inside `createElement`.
1510
+ */
1511
+ async function loadBoundaryComponent(file, loadModule) {
1512
+ let mod;
1513
+ try {
1514
+ mod = await loadModule(file);
1515
+ } catch {
1516
+ return null;
1517
+ }
1518
+ return isValidElementType(mod?.default) ? mod.default : null;
1519
+ }
1520
+ /**
1521
+ * Wrap `element` in one error boundary rendering `Component` on failure.
1506
1522
  *
1507
- * Providers MERGE per-key with their parent: an inner SlotsProvider
1508
- * inherits the outer's values and overrides only the keys it declares.
1509
- * Two sibling instances each have their own provider and resolve
1510
- * independently. Layouts use a reserved slot name (LAYOUT_CHILDREN_SLOT)
1511
- * that cannot collide with user-declared slot names (TIM-1191).
1523
+ * MDX status files are server components and cannot be passed as function
1524
+ * props to `TimberErrorBoundary` (a 'use client' component). They are
1525
+ * pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
1512
1526
  *
1513
- * Generalizes the original layout children pattern (TIM-1181) from a
1514
- * single implicit `children` hole on layouts to explicitly declared,
1515
- * named slot props on any cached component. Layouts use this same
1516
- * context with the reserved LAYOUT_CHILDREN_SLOT key (TIM-1191).
1527
+ * @param status boundary status filter — omitted entirely for `error.tsx`
1528
+ * @param fallbackProps props handed to an MDX fallback when it is pre-rendered
1529
+ */
1530
+ function wrapInBoundary(element, Component, file, status, fallbackProps, config) {
1531
+ const props = isMdxFilePath(file.filePath) ? {
1532
+ fallbackElement: config.createElement(Component, fallbackProps),
1533
+ children: element
1534
+ } : {
1535
+ fallbackComponent: Component,
1536
+ children: element
1537
+ };
1538
+ if (status !== void 0) props.status = status;
1539
+ if (config.segmentKey !== void 0) props.segmentKey = config.segmentKey;
1540
+ return config.createElement(config.errorBoundaryComponent, props);
1541
+ }
1542
+ /**
1543
+ * Wrap an element in the error boundaries declared by a route segment.
1517
1544
  *
1518
- * SINGLETON GUARANTEE: globalThis + Symbol.for — the RSC client bundler
1519
- * can duplicate this module across chunks; globalThis guarantees a single
1520
- * context instance.
1545
+ * Wrapping is applied inside-out — the last wrap produces the outermost
1546
+ * boundary — so the processing order below is innermost to outermost:
1521
1547
  *
1522
- * See design/45-cache-lifetimes.md §Slot Components.
1548
+ * 1. Specific status files (e.g. `404.tsx`, `503.tsx`) — highest priority at runtime
1549
+ * 2. Category catch-alls (`4xx.tsx`, `5xx.tsx`)
1550
+ * 3. `error.tsx` — catches anything not matched by a status file
1551
+ *
1552
+ * The order is load-bearing: it decides which boundary catches a given throw.
1553
+ *
1554
+ * See design/10-error-handling.md §"Format-Aware Fallback Chains".
1523
1555
  */
1524
- var CTX_KEY$1 = Symbol.for("__timber_slot_ctx");
1525
- function getOrCreateContext$1() {
1526
- const existing = globalThis[CTX_KEY$1];
1527
- if (existing !== void 0) return existing;
1528
- if (typeof React.createContext === "function") {
1529
- const ctx = React.createContext(null);
1530
- globalThis[CTX_KEY$1] = ctx;
1531
- return ctx;
1556
+ async function wrapSegmentWithErrorBoundaries(segment, element, config) {
1557
+ const { loadModule } = config;
1558
+ if (segment.statusFiles) {
1559
+ for (const [key, file] of Object.entries(segment.statusFiles)) {
1560
+ if (key === "4xx" || key === "5xx") continue;
1561
+ const status = parseInt(key, 10);
1562
+ if (isNaN(status)) continue;
1563
+ const Component = await loadBoundaryComponent(file, loadModule);
1564
+ if (Component) element = wrapInBoundary(element, Component, file, status, { status }, config);
1565
+ }
1566
+ for (const [key, file] of Object.entries(segment.statusFiles)) {
1567
+ if (key !== "4xx" && key !== "5xx") continue;
1568
+ const Component = await loadBoundaryComponent(file, loadModule);
1569
+ if (Component) element = wrapInBoundary(element, Component, file, key === "4xx" ? 400 : 500, {}, config);
1570
+ }
1532
1571
  }
1572
+ if (segment.error) {
1573
+ const Component = await loadBoundaryComponent(segment.error, loadModule);
1574
+ if (Component) element = wrapInBoundary(element, Component, segment.error, void 0, {}, config);
1575
+ }
1576
+ return element;
1533
1577
  }
1534
- var SlotContext = getOrCreateContext$1();
1535
1578
  //#endregion
1536
- //#region src/client/slot-outlet.tsx
1579
+ //#region src/server/param-coercion.ts
1580
+ /** Global schema codecs, set once at startup from virtual:timber-schema. */
1581
+ var _globalCodecs = null;
1537
1582
  /**
1538
- * SlotOutlet — the hole in a cached component shell (TIM-1173).
1583
+ * Run segment param coercion on the matched route's segments.
1539
1584
  *
1540
- * At capture time the prebuilt runtime renders the wrapped component with
1541
- * <SlotOutlet slot={name} /> in place of each declared slot prop. Being a
1542
- * client component, it serializes into the flight payload as a stable,
1543
- * deterministic client reference — the same bytes regardless of what live
1544
- * content will fill the hole. At request time the revived shell sits under
1545
- * a SlotsProvider and each outlet reads its live value from SlotContext.
1585
+ * Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
1586
+ * design/41-global-params.md migration step 9). A route with no schema, or
1587
+ * a param the schema does not declare, keeps its raw string — which is the
1588
+ * honest type for an uninterpreted URL segment.
1546
1589
  *
1547
- * See design/45-cache-lifetimes.md §Slot Components.
1590
+ * Throws ParamCoercionError if any codec fails (→ 404).
1591
+ *
1592
+ * This runs BEFORE middleware, so ctx.segmentParams is already typed.
1593
+ * See design/07-routing.md §"Where Coercion Runs"
1594
+ * See design/41-global-params.md §"Pipeline Integration"
1595
+ *
1596
+ * Stays `async` with nothing to await: it is injected as a
1597
+ * `CoerceSegmentParamsFn` into the revalidation renderer and the action
1598
+ * middleware runner, both of which await it. Narrowing the return type to
1599
+ * `void` would break those contracts and remove a microtask boundary the
1600
+ * pipeline's ordering has always had, for no gain.
1548
1601
  */
1549
- function SlotOutlet({ slot }) {
1550
- const values = useContext(SlotContext);
1551
- return values ? values[slot] ?? null : null;
1602
+ async function coerceSegmentParams(match) {
1603
+ const globalCodecs = _globalCodecs;
1604
+ const mergeTarget = Object.create(null);
1605
+ for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
1606
+ match.segmentParams = mergeTarget;
1607
+ if (globalCodecs) for (const node of match.segments) {
1608
+ const segment = effectiveUrlSegment(node);
1609
+ if (!segment.paramName) continue;
1610
+ const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
1611
+ const codec = globalCodecs[bracketKey];
1612
+ if (!codec) continue;
1613
+ const key = segment.paramName;
1614
+ if (!(key in mergeTarget)) continue;
1615
+ let parsed;
1616
+ try {
1617
+ parsed = codec.parse(mergeTarget[key]);
1618
+ } catch (err) {
1619
+ const message = err instanceof Error ? err.message : String(err);
1620
+ if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
1621
+ throw new ParamCoercionError(message);
1622
+ }
1623
+ mergeTarget[key] = normalizeParamValue(parsed, key);
1624
+ }
1552
1625
  }
1553
1626
  //#endregion
1554
- //#region src/client/slot-provider.tsx
1627
+ //#region src/server/pipeline-interception.ts
1555
1628
  /**
1556
- * SlotsProvider — wraps a revived cached shell to supply live slot content
1557
- * to the SlotOutlet holes inside it (TIM-1173).
1629
+ * Interception route matching for the request pipeline.
1558
1630
  *
1559
- * Values MERGE with the nearest parent provider so that nesting a
1560
- * cache.component SlotsProvider inside a layout SlotsProvider preserves
1561
- * the layout's `children` slot while adding the component's own slots.
1562
- * Inner values override outer ones with the same name (spread order).
1631
+ * Matches target URLs against interception rewrites to support the
1632
+ * modal route pattern (soft navigation intercepts).
1563
1633
  *
1564
- * Tree structure per cached-component instance:
1565
- * SlotsProvider(values={children: <Live />})
1566
- * └── revived shell
1567
- * └── SlotOutlet(slot="children") → reads context → <Live />
1634
+ * Extracted from pipeline.ts to keep files under 500 lines.
1568
1635
  *
1569
- * See design/45-cache-lifetimes.md §Slot Components.
1636
+ * See design/07-routing.md §"Intercepting Routes"
1570
1637
  */
1571
- function SlotsProvider({ values, children }) {
1572
- const parent = useContext(SlotContext);
1573
- const merged = parent ? {
1574
- ...parent,
1575
- ...values
1576
- } : values;
1577
- return createElement(SlotContext.Provider, { value: merged }, children);
1578
- }
1579
- //#endregion
1580
- //#region src/server/prebuilt/cache-key.ts
1581
1638
  /**
1582
- * Component cache-key hashing for prebuilt (cache.component) entries.
1639
+ * Check if an intercepting route applies for this soft navigation.
1583
1640
  *
1584
- * Key: `sha256(canonicalJson({ props, params })).slice(0, 16)` — see
1585
- * design/44-render-at-build-time.md §Cache Key. Both the request-time
1586
- * lookup (prebuilt-runtime.ts) and the post-build capture pass (TIM-1119)
1587
- * MUST use this exact function, or build artifacts become unaddressable.
1641
+ * Two conditions must hold:
1588
1642
  *
1589
- * THE IDENTITY CONTRACT (design/44 §Cache Key): cache identity is **JSON
1590
- * value semantics** — two inputs are the same entry iff they are the same
1591
- * JSON value. This is an ALLOWLIST, not a denylist: a value participates
1592
- * in caching iff it is plain JSON data — null, boolean, finite number,
1593
- * string, dense array, or a plain `Object.prototype` object with only
1594
- * enumerable string-keyed data properties — plus `undefined` and `-0`,
1595
- * which hash as distinct sentinels because flight preserves them. Every
1596
- * value outside that domain (functions, symbols, bigints, accessors,
1597
- * hidden or symbol-keyed properties, non-`Object.prototype` prototypes
1598
- * including null, aliased references, sparse arrays, cycles, non-finite
1599
- * numbers) is UNKEYABLE by definition: `computeComponentCacheKey` returns
1600
- * null and the caller renders live — slower, never wrong.
1601
- *
1602
- * The line between rejected and declared-out-of-domain: values are
1603
- * REJECTED when the JSON projection itself would be ambiguous or lossy
1604
- * (hidden/symbol/non-enumerable properties, accessors, exotic
1605
- * prototypes). Observable state that leaves the projection intact — key
1606
- * order, reference identity, descriptor metadata (writable/configurable,
1607
- * frozen/sealed/extensible), proxy behavior — is DECLARED outside
1608
- * cache.component's supported input domain rather than rejected:
1609
- * key-order insensitivity is required for determinism, and rejecting
1610
- * frozen objects would unkey ubiquitous legitimate inputs
1611
- * (Object.freeze'd config props) to defend against components that
1612
- * render differently based on property attributes.
1613
- *
1614
- * Two hard rules for this walk itself: it must be TOTAL (never throw
1615
- * anything but UnkeyableValueError) and SIDE-EFFECT-FREE (descriptor
1616
- * reads only; never invoke user code such as getters).
1617
- *
1618
- * Object keys sort recursively so `{a, b}` and `{b, a}` hash identically
1619
- * (design/13-security.md checklist #14 — cache key determinism; key order
1620
- * is deliberately NOT part of identity).
1621
- */
1622
- var UnkeyableValueError = class extends Error {};
1623
- function canonicalize$1(value, seen) {
1624
- if (value === null) return "null";
1625
- switch (typeof value) {
1626
- case "string": return JSON.stringify(value);
1627
- case "boolean": return value ? "true" : "false";
1628
- case "number":
1629
- if (!Number.isFinite(value)) throw new UnkeyableValueError("non-finite number");
1630
- return Object.is(value, -0) ? "-0" : String(value);
1631
- case "object": break;
1632
- case "undefined": return "undefined";
1633
- default: throw new UnkeyableValueError(`unsupported type: ${typeof value}`);
1634
- }
1635
- const obj = value;
1636
- if (seen.has(obj)) throw new UnkeyableValueError("repeated object reference (cycle or alias)");
1637
- seen.add(obj);
1638
- if (Object.getOwnPropertySymbols(obj).length > 0) throw new UnkeyableValueError("symbol-keyed property");
1639
- {
1640
- if (Array.isArray(obj)) {
1641
- if (Object.getOwnPropertyNames(obj).length !== obj.length + 1) throw new UnkeyableValueError("non-index array property");
1642
- const parts = [];
1643
- for (let i = 0; i < obj.length; i++) {
1644
- const desc = Object.getOwnPropertyDescriptor(obj, i);
1645
- if (desc === void 0) throw new UnkeyableValueError("sparse array hole");
1646
- if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
1647
- if (!desc.enumerable) throw new UnkeyableValueError("non-enumerable array element");
1648
- parts.push(canonicalize$1(desc.value, seen));
1649
- }
1650
- return "[" + parts.join(",") + "]";
1651
- }
1652
- if (Object.getPrototypeOf(obj) !== Object.prototype) throw new UnkeyableValueError("non-plain object");
1653
- const keys = Object.keys(obj).sort();
1654
- if (keys.length !== Object.getOwnPropertyNames(obj).length) throw new UnkeyableValueError("non-enumerable property");
1655
- const parts = [];
1656
- for (const key of keys) {
1657
- const desc = Object.getOwnPropertyDescriptor(obj, key);
1658
- if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
1659
- parts.push(JSON.stringify(key) + ":" + canonicalize$1(desc.value, seen));
1660
- }
1661
- return "{" + parts.join(",") + "}";
1662
- }
1663
- }
1664
- /**
1665
- * Canonicalize a single value, or null if it is unkeyable. The ONE
1666
- * definition of "faithfully comparable" — every prebuilt code path that
1667
- * needs value equality (cache keys, slot-param divergence) goes through
1668
- * this, so the unkeyable rules (accessors, hidden props, aliases,
1669
- * bigints, …) live in exactly one place.
1670
- */
1671
- function tryCanonicalize(value) {
1672
- try {
1673
- return canonicalize$1(value, /* @__PURE__ */ new Set());
1674
- } catch (error) {
1675
- return toUnkeyable(error);
1676
- }
1677
- }
1678
- /**
1679
- * Totality by construction: ANY throw from the walk means the input is
1680
- * outside the keyable domain. The walk touches only user-controlled
1681
- * values through reflection ops — and a Proxy trap can throw arbitrary
1682
- * errors from any of them (codex review on PR #835) — so converting only
1683
- * UnkeyableValueError would let hostile/exotic inputs abort a render
1684
- * that plain live rendering would have served fine. Unexpected error
1685
- * types are logged for visibility (they would indicate either a proxy
1686
- * trap or a canonicalizer bug) but still degrade to unkeyable.
1687
- */
1688
- function toUnkeyable(error) {
1689
- if (!(error instanceof UnkeyableValueError)) console.error("[timber] cache key canonicalization threw — treating input as unkeyable:", error);
1690
- return null;
1691
- }
1692
- /**
1693
- * Split a props record into data props (participate in the cache key) and
1694
- * slot values (React-element holes, excluded from the key) per the
1695
- * component's declared `slots` option (TIM-1173).
1696
- *
1697
- * Every DECLARED slot lands in `slotValues` — present or not — because the
1698
- * capture render always injects a SlotOutlet placeholder for each declared
1699
- * slot: the cached shell is identical whether the caller passed the slot
1700
- * or omitted it, so slot presence must not vary the key. `dataProps` is
1701
- * everything else and is what gets hashed.
1702
- *
1703
- * Returns null when the props OBJECT itself is outside the identity
1704
- * contract at the top level — accessors, symbol-keyed or non-enumerable
1705
- * properties, or a non-`Object.prototype` prototype. Splitting such an
1706
- * object would normalize away observable state (and reading an accessor
1707
- * would invoke user code, which this walk must never do) BEFORE
1708
- * `computeComponentCacheKey` gets the chance to reject it — a cached
1709
- * shell could then be served for inputs a live render distinguishes.
1710
- * Null means unkeyable: the caller renders live with the original props.
1711
- * The walk is descriptor-reads only; getters are never invoked. Nested
1712
- * data-prop values still face the full allowlist when hashed.
1713
- */
1714
- /** Create an own enumerable data property — safe for keys like `__proto__`. */
1715
- function defineOwn(target, key, value) {
1716
- Object.defineProperty(target, key, {
1717
- value,
1718
- enumerable: true,
1719
- writable: true,
1720
- configurable: true
1721
- });
1722
- }
1723
- function splitSlotProps(props, slots) {
1724
- try {
1725
- if (Object.getPrototypeOf(props) !== Object.prototype) return null;
1726
- if (Object.getOwnPropertySymbols(props).length > 0) return null;
1727
- const slotSet = new Set(slots);
1728
- const dataProps = {};
1729
- const slotValues = {};
1730
- for (const key of Object.getOwnPropertyNames(props)) {
1731
- const desc = Object.getOwnPropertyDescriptor(props, key);
1732
- if (desc === void 0 || desc.get !== void 0 || desc.set !== void 0 || !desc.enumerable) return null;
1733
- defineOwn(slotSet.has(key) ? slotValues : dataProps, key, desc.value);
1734
- }
1735
- for (const slot of slotSet) if (!Object.hasOwn(slotValues, slot)) defineOwn(slotValues, slot, void 0);
1736
- return {
1737
- dataProps,
1738
- slotValues
1739
- };
1740
- } catch (error) {
1741
- return toUnkeyable(error);
1742
- }
1743
- }
1744
- /**
1745
- * Compute the cache-entry hash for a component render:
1746
- * `sha256({props, params}).slice(0, 16)` over canonical JSON.
1747
- *
1748
- * Returns null when props (or params) contain unkeyable values — the
1749
- * caller must fall back to dynamic rendering.
1750
- */
1751
- function computeComponentCacheKey(props, params) {
1752
- let canonical;
1753
- try {
1754
- const seen = /* @__PURE__ */ new Set();
1755
- canonical = "{\"params\":" + canonicalize$1({ ...params }, seen) + ",\"props\":" + canonicalize$1(props, seen) + "}";
1756
- } catch (error) {
1757
- return toUnkeyable(error);
1758
- }
1759
- return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
1760
- }
1761
- /**
1762
- * Is this value the framework-injected layout children slot outlet
1763
- * (`<SlotOutlet slot={LAYOUT_CHILDREN_SLOT} />`)? Layout components
1764
- * receive this as their `children` prop — a stable client reference
1765
- * whose cache key is stripped so layout `cache.component` hits work
1766
- * (design/45 §Layout Propagation).
1767
- *
1768
- * Uses a framework-reserved slot name (LAYOUT_CHILDREN_SLOT) that
1769
- * cannot collide with user-declared slot names, preventing context
1770
- * shadowing when a cached component with its own SlotsProvider is
1771
- * nested inside a layout. See TIM-1191.
1772
- *
1773
- * Total: the probes run against a user-controlled value, and a Proxy
1774
- * trap can throw — that must classify as "not the outlet" (the
1775
- * canonicalizer then rejects the value as unkeyable → live render), not
1776
- * abort the request (codex P2 on PR #890).
1777
- */
1778
- function isChildrenSlotOutletElement(value) {
1779
- try {
1780
- if (value == null || typeof value !== "object") return false;
1781
- if (!("type" in value)) return false;
1782
- const el = value;
1783
- return el.type === SlotOutlet && el.props?.slot === "\0timber:children";
1784
- } catch {
1785
- return false;
1786
- }
1787
- }
1788
- /**
1789
- * Dense array of non-empty strings — no holes, no non-string entries.
1790
- * Total: the reads run against a user-controlled value at module load,
1791
- * and a throwing Proxy trap must classify as invalid, not crash startup.
1792
- */
1793
- function isDenseStringArray(value) {
1794
- try {
1795
- if (!Array.isArray(value)) return false;
1796
- for (let i = 0; i < value.length; i++) {
1797
- if (!Object.hasOwn(value, i)) return false;
1798
- const entry = value[i];
1799
- if (typeof entry !== "string" || entry.length === 0) return false;
1800
- }
1801
- return true;
1802
- } catch {
1803
- return false;
1804
- }
1805
- }
1806
- /**
1807
- * Validate the `slots` option. Both the production and dev wrappers gate
1808
- * on this — slot substitution is active in dev exactly when it would be
1809
- * active in production, so the tree shape a component sees never
1810
- * diverges between the two.
1811
- */
1812
- function resolveSlotNames(id, options) {
1813
- const slots = options.slots;
1814
- if (slots === void 0) return { kind: "none" };
1815
- if (!isDenseStringArray(slots)) {
1816
- console.warn(`[timber] cache.component "${id}": invalid slots declaration — expected an array of non-empty prop-name strings. Slot caching is disabled.`);
1817
- return { kind: "disabled" };
1818
- }
1819
- if (slots.length === 0) return { kind: "none" };
1820
- if (!(options.ttl !== void 0 || options.tags != null)) {
1821
- console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
1822
- return { kind: "disabled" };
1823
- }
1824
- return {
1825
- kind: "active",
1826
- slots
1827
- };
1828
- }
1829
- /**
1830
- * Split the live props into the cacheable shell render (data props plus a
1831
- * deterministic SlotOutlet placeholder per declared slot — identical
1832
- * whether or not the caller passed the slot) and the per-instance live
1833
- * slot values.
1834
- *
1835
- * Null when the props object itself is unkeyable at the top level
1836
- * (accessors, hidden properties, exotic prototype — see splitSlotProps):
1837
- * the caller must render live with the original props.
1838
- */
1839
- function prepareSlotRender(props, slots) {
1840
- const split = splitSlotProps(props ?? {}, slots);
1841
- if (split === null) return null;
1842
- const { dataProps, slotValues } = split;
1843
- const shellProps = { ...dataProps };
1844
- for (const slot of slots) Object.defineProperty(shellProps, slot, {
1845
- value: createElement(SlotOutlet, { slot }),
1846
- enumerable: true,
1847
- writable: true,
1848
- configurable: true
1849
- });
1850
- return {
1851
- shellProps,
1852
- dataProps,
1853
- slotValues
1854
- };
1855
- }
1856
- /**
1857
- * Wrap a rendered shell (revived payload or live element) in a
1858
- * per-instance SlotsProvider feeding the live values to its outlets.
1859
- */
1860
- function wrapInSlotsProvider(shell, slotValues) {
1861
- return createElement(SlotsProvider, {
1862
- values: slotValues,
1863
- children: shell
1864
- });
1865
- }
1866
- //#endregion
1867
- //#region src/server/error-boundary-wrapper.ts
1868
- /**
1869
- * Load a status/error file's component, or `null` if the boundary should be skipped.
1870
- *
1871
- * Two ways to get `null`, both deliberate:
1872
- * - the module fails to load (syntax error in the error page) — building an
1873
- * error boundary must not itself throw, so the boundary is skipped (TIM-584);
1874
- * - the default export is not a valid React component type, which would
1875
- * otherwise crash inside `createElement`.
1876
- */
1877
- async function loadBoundaryComponent(file, loadModule) {
1878
- let mod;
1879
- try {
1880
- mod = await loadModule(file);
1881
- } catch {
1882
- return null;
1883
- }
1884
- return isValidElementType(mod?.default) ? mod.default : null;
1885
- }
1886
- /**
1887
- * Wrap `element` in one error boundary rendering `Component` on failure.
1888
- *
1889
- * MDX status files are server components and cannot be passed as function
1890
- * props to `TimberErrorBoundary` (a 'use client' component). They are
1891
- * pre-rendered as elements and passed as `fallbackElement` instead. See TIM-503.
1892
- *
1893
- * @param status boundary status filter — omitted entirely for `error.tsx`
1894
- * @param fallbackProps props handed to an MDX fallback when it is pre-rendered
1895
- */
1896
- function wrapInBoundary(element, Component, file, status, fallbackProps, config) {
1897
- const props = isMdxFilePath(file.filePath) ? {
1898
- fallbackElement: config.createElement(Component, fallbackProps),
1899
- children: element
1900
- } : {
1901
- fallbackComponent: Component,
1902
- children: element
1903
- };
1904
- if (status !== void 0) props.status = status;
1905
- if (config.segmentKey !== void 0) props.segmentKey = config.segmentKey;
1906
- return config.createElement(config.errorBoundaryComponent, props);
1907
- }
1908
- /**
1909
- * Wrap an element in the error boundaries declared by a route segment.
1910
- *
1911
- * Wrapping is applied inside-out — the last wrap produces the outermost
1912
- * boundary — so the processing order below is innermost to outermost:
1913
- *
1914
- * 1. Specific status files (e.g. `404.tsx`, `503.tsx`) — highest priority at runtime
1915
- * 2. Category catch-alls (`4xx.tsx`, `5xx.tsx`)
1916
- * 3. `error.tsx` — catches anything not matched by a status file
1917
- *
1918
- * The order is load-bearing: it decides which boundary catches a given throw.
1919
- *
1920
- * See design/10-error-handling.md §"Format-Aware Fallback Chains".
1921
- */
1922
- async function wrapSegmentWithErrorBoundaries(segment, element, config) {
1923
- const { loadModule, skipDenyBoundaries = false } = config;
1924
- if (segment.statusFiles) {
1925
- for (const [key, file] of Object.entries(segment.statusFiles)) {
1926
- if (key === "4xx" || key === "5xx") continue;
1927
- const status = parseInt(key, 10);
1928
- if (isNaN(status)) continue;
1929
- if (skipDenyBoundaries && status >= 400 && status <= 499) continue;
1930
- const Component = await loadBoundaryComponent(file, loadModule);
1931
- if (Component) element = wrapInBoundary(element, Component, file, status, { status }, config);
1932
- }
1933
- for (const [key, file] of Object.entries(segment.statusFiles)) {
1934
- if (key !== "4xx" && key !== "5xx") continue;
1935
- if (skipDenyBoundaries && key === "4xx") continue;
1936
- const Component = await loadBoundaryComponent(file, loadModule);
1937
- if (Component) element = wrapInBoundary(element, Component, file, key === "4xx" ? 400 : 500, {}, config);
1938
- }
1939
- }
1940
- if (segment.error) {
1941
- const Component = await loadBoundaryComponent(segment.error, loadModule);
1942
- if (Component) element = wrapInBoundary(element, Component, segment.error, void 0, {}, config);
1943
- }
1944
- return element;
1945
- }
1946
- //#endregion
1947
- //#region src/server/param-coercion.ts
1948
- /** Global schema codecs, set once at startup from virtual:timber-schema. */
1949
- var _globalCodecs = null;
1950
- /**
1951
- * Run segment param coercion on the matched route's segments.
1952
- *
1953
- * Codecs come from `app/schema.ts` and nowhere else (TIM-1342 completed
1954
- * design/41-global-params.md migration step 9). A route with no schema, or
1955
- * a param the schema does not declare, keeps its raw string — which is the
1956
- * honest type for an uninterpreted URL segment.
1957
- *
1958
- * Throws ParamCoercionError if any codec fails (→ 404).
1959
- *
1960
- * This runs BEFORE middleware, so ctx.segmentParams is already typed.
1961
- * See design/07-routing.md §"Where Coercion Runs"
1962
- * See design/41-global-params.md §"Pipeline Integration"
1963
- *
1964
- * Stays `async` with nothing to await: it is injected as a
1965
- * `CoerceSegmentParamsFn` into the revalidation renderer and the action
1966
- * middleware runner, both of which await it. Narrowing the return type to
1967
- * `void` would break those contracts and remove a microtask boundary the
1968
- * pipeline's ordering has always had, for no gain.
1969
- */
1970
- async function coerceSegmentParams(match) {
1971
- const globalCodecs = _globalCodecs;
1972
- const mergeTarget = Object.create(null);
1973
- for (const key of Object.keys(match.segmentParams)) if (key !== "__proto__") mergeTarget[key] = match.segmentParams[key];
1974
- match.segmentParams = mergeTarget;
1975
- if (globalCodecs) for (const node of match.segments) {
1976
- const segment = effectiveUrlSegment(node);
1977
- if (!segment.paramName) continue;
1978
- const bracketKey = toBracketKey(segment.segmentType, segment.paramName);
1979
- const codec = globalCodecs[bracketKey];
1980
- if (!codec) continue;
1981
- const key = segment.paramName;
1982
- if (!(key in mergeTarget)) continue;
1983
- let parsed;
1984
- try {
1985
- parsed = codec.parse(mergeTarget[key]);
1986
- } catch (err) {
1987
- const message = err instanceof Error ? err.message : String(err);
1988
- if (isDebug()) console.warn(`[timber] Global schema codec rejected value for param "${key}"\n Error: ${message}\n Raw value: ${JSON.stringify(mergeTarget[key])}\n Hint: check the codec in app/schema.ts for key '${bracketKey}'.`);
1989
- throw new ParamCoercionError(message);
1990
- }
1991
- mergeTarget[key] = normalizeParamValue(parsed, key);
1992
- }
1993
- }
1994
- //#endregion
1995
- //#region src/server/pipeline-interception.ts
1996
- /**
1997
- * Interception route matching for the request pipeline.
1998
- *
1999
- * Matches target URLs against interception rewrites to support the
2000
- * modal route pattern (soft navigation intercepts).
2001
- *
2002
- * Extracted from pipeline.ts to keep files under 500 lines.
2003
- *
2004
- * See design/07-routing.md §"Intercepting Routes"
2005
- */
2006
- /**
2007
- * Check if an intercepting route applies for this soft navigation.
2008
- *
2009
- * Two conditions must hold:
2010
- *
2011
- * 1. The **target** pathname matches the rewrite's intercepted pattern.
2012
- * 2. The **source** route — the page the user is navigating FROM, per the
2013
- * validated X-Timber-URL header — renders the segment that owns the slot
2014
- * holding the intercepting page (`interceptingScope`). Only then does that
2015
- * slot exist in the rendered tree, so only then can the intercepting page
2016
- * render.
1643
+ * 1. The **target** pathname matches the rewrite's intercepted pattern.
1644
+ * 2. The **source** route — the page the user is navigating FROM, per the
1645
+ * validated X-Timber-URL header — renders the segment that owns the slot
1646
+ * holding the intercepting page (`interceptingScope`). Only then does that
1647
+ * slot exist in the rendered tree, so only then can the intercepting page
1648
+ * render.
2017
1649
  *
2018
1650
  * Condition 2 replaced a URL-prefix check (TIM-1279). A prefix cannot
2019
1651
  * distinguish a route-group-scoped interception from a genuinely root-level
@@ -2127,17 +1759,17 @@ function pathnameMatchesPattern(pathname, pattern) {
2127
1759
  * See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
2128
1760
  */
2129
1761
  var EMPTY_SEGMENT_UPDATES = /* @__PURE__ */ new Map();
2130
- var CTX_KEY = Symbol.for("__timber_segment_update_ctx");
2131
- function getOrCreateContext() {
2132
- const existing = globalThis[CTX_KEY];
1762
+ var CTX_KEY$1 = Symbol.for("__timber_segment_update_ctx");
1763
+ function getOrCreateContext$1() {
1764
+ const existing = globalThis[CTX_KEY$1];
2133
1765
  if (existing !== void 0) return existing;
2134
1766
  if (typeof React.createContext === "function") {
2135
1767
  const ctx = React.createContext(EMPTY_SEGMENT_UPDATES);
2136
- globalThis[CTX_KEY] = ctx;
1768
+ globalThis[CTX_KEY$1] = ctx;
2137
1769
  return ctx;
2138
1770
  }
2139
1771
  }
2140
- getOrCreateContext();
1772
+ getOrCreateContext$1();
2141
1773
  //#endregion
2142
1774
  //#region src/client/segment-outlet.tsx
2143
1775
  /**
@@ -2596,11 +2228,7 @@ async function handleRequest(config, req, method, path, pathIsCanonical) {
2596
2228
  return new Response("Metadata route must export a default function", { status: 500 });
2597
2229
  }
2598
2230
  const handlerResult = await mod.default();
2599
- if (handlerResult instanceof Response) {
2600
- const res = cloneWithMutableHeaders(handlerResult);
2601
- if (res.ok && !res.headers.has("Cache-Control")) res.headers.set("Cache-Control", "public, max-age=14400, must-revalidate");
2602
- return res;
2603
- }
2231
+ if (handlerResult instanceof Response) return cloneWithMutableHeaders(handlerResult);
2604
2232
  const contentType = metaMatch.contentType;
2605
2233
  let body;
2606
2234
  if (typeof handlerResult === "string") body = handlerResult;
@@ -2829,9 +2457,7 @@ async function hasValidRscCacheKey(req) {
2829
2457
  * #36c.
2830
2458
  */
2831
2459
  function hardenRscPayloadResponse(response) {
2832
- const contentType = response.headers.get("content-type");
2833
- if (!contentType) return;
2834
- if (contentType.split(";")[0].trim().toLowerCase() !== "text/x-component") return;
2460
+ if (!isRscContentType(response.headers.get("content-type"))) return;
2835
2461
  response.headers.set("X-Content-Type-Options", "nosniff");
2836
2462
  }
2837
2463
  /**
@@ -2933,7 +2559,8 @@ function createPipeline(config) {
2933
2559
  const path = url.pathname;
2934
2560
  const startTime = performance.now();
2935
2561
  activeRequests++;
2936
- return runWithTraceId(generateTraceId(), async () => {
2562
+ const traceIdValue = generateTraceId();
2563
+ return runWithTraceId(traceIdValue, async () => {
2937
2564
  return runWithRequestContext(req, async () => {
2938
2565
  const runRequest = async () => {
2939
2566
  logRequestReceived({
@@ -3109,547 +2736,918 @@ function collectEarlyHintHeaders(segments, manifest, options) {
3109
2736
  //#endregion
3110
2737
  //#region src/server/early-hints-sender.ts
3111
2738
  /**
3112
- * Per-request 103 Early Hints sender — ALS bridge for platform adapters.
3113
- *
3114
- * The pipeline collects Link headers for CSS, fonts, and JS chunks at
3115
- * route-match time. On platforms that support it (Node.js v18.11+, Bun),
3116
- * the adapter can send these as a 103 Early Hints interim response before
3117
- * the final response is ready.
2739
+ * Per-request 103 Early Hints sender — ALS bridge for platform adapters.
2740
+ *
2741
+ * The pipeline collects Link headers for CSS, fonts, and JS chunks at
2742
+ * route-match time. On platforms that support it (Node.js v18.11+, Bun),
2743
+ * the adapter can send these as a 103 Early Hints interim response before
2744
+ * the final response is ready.
2745
+ *
2746
+ * This module provides an ALS-based bridge: the generated entry point
2747
+ * (e.g., the Nitro entry) wraps the handler with `runWithEarlyHintsSender`,
2748
+ * binding a per-request sender function. The pipeline calls
2749
+ * `sendEarlyHints103()` to fire the 103 if a sender is available.
2750
+ *
2751
+ * On platforms where 103 is handled at the CDN level (e.g., Cloudflare
2752
+ * converts Link headers into 103 automatically), no sender is installed
2753
+ * and `sendEarlyHints103()` is a no-op.
2754
+ *
2755
+ * Design doc: 02-rendering-pipeline.md §"Early Hints (103)"
2756
+ */
2757
+ /**
2758
+ * Run a function with a per-request early hints sender installed.
2759
+ *
2760
+ * Called by generated entry points (e.g., Nitro node-server/bun) to
2761
+ * bind the platform's writeEarlyHints capability for the request duration.
2762
+ */
2763
+ function runWithEarlyHintsSender(sender, fn) {
2764
+ return earlyHintsSenderAls.run(sender, fn);
2765
+ }
2766
+ /**
2767
+ * Send collected Link headers as a 103 Early Hints response.
2768
+ *
2769
+ * No-op if no sender is installed for the current request (e.g., on
2770
+ * Cloudflare where the CDN handles 103 automatically, or in dev mode).
2771
+ *
2772
+ * Non-fatal: errors from the sender are caught and silently ignored.
2773
+ */
2774
+ function sendEarlyHints103(links) {
2775
+ if (!links.length) return;
2776
+ const sender = earlyHintsSenderAls.getStore();
2777
+ if (!sender) return;
2778
+ try {
2779
+ sender(links);
2780
+ } catch (err) {
2781
+ swallow(err, "early hints 103 send failed");
2782
+ }
2783
+ }
2784
+ //#endregion
2785
+ //#region src/server/tree-builder.ts
2786
+ /**
2787
+ * Build the unified element tree from a matched segment chain.
2788
+ *
2789
+ * Construction is bottom-up:
2790
+ * 1. Start with the page component (leaf segment)
2791
+ * 2. Wrap in status-code error boundaries (fallback chain)
2792
+ * 3. Wrap in AccessGate (if segment has access.ts)
2793
+ * 4. Pass as children to the segment's layout
2794
+ * 5. Repeat up the segment chain to root
2795
+ *
2796
+ * Parallel slots are resolved at each layout level and composed as named props.
2797
+ */
2798
+ async function buildElementTree(config) {
2799
+ const { segments, loadModule, createElement, errorBoundaryComponent } = config;
2800
+ if (segments.length === 0) throw new Error("[timber] buildElementTree: empty segment chain");
2801
+ const leaf = segments[segments.length - 1];
2802
+ if (leaf.route && !leaf.page) return {
2803
+ tree: null,
2804
+ isApiRoute: true
2805
+ };
2806
+ const PageComponent = (leaf.page ? await loadModule(leaf.page) : null)?.default;
2807
+ if (!PageComponent) throw new Error(`[timber] No page component found for route at ${leaf.urlPath}. Each route must have a page.tsx or route.ts.`);
2808
+ let element = createElement(PageComponent, {});
2809
+ for (let i = segments.length - 1; i >= 0; i--) {
2810
+ const segment = segments[i];
2811
+ element = await wrapSegmentWithErrorBoundaries(segment, element, {
2812
+ loadModule,
2813
+ createElement,
2814
+ errorBoundaryComponent
2815
+ });
2816
+ if (segment.access) {
2817
+ const accessFn = (await loadModule(segment.access)).default;
2818
+ element = createElement("timber:access-gate", {
2819
+ accessFn,
2820
+ segmentName: segment.segmentName,
2821
+ children: element
2822
+ });
2823
+ }
2824
+ if (segment.layout) {
2825
+ const LayoutComponent = (await loadModule(segment.layout)).default;
2826
+ if (LayoutComponent) {
2827
+ const slotProps = {};
2828
+ const slotNames = Object.keys(segment.slots);
2829
+ if (slotNames.length > 0) for (const slotName of slotNames) {
2830
+ const slotNode = segment.slots[slotName];
2831
+ slotProps[slotName] = await buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent);
2832
+ }
2833
+ element = createElement(LayoutComponent, {
2834
+ ...slotProps,
2835
+ children: element
2836
+ });
2837
+ }
2838
+ }
2839
+ }
2840
+ return {
2841
+ tree: element,
2842
+ isApiRoute: false
2843
+ };
2844
+ }
2845
+ /**
2846
+ * Build the element tree for a parallel slot.
2847
+ *
2848
+ * Slots have their own access.ts (SlotAccessGate) and error boundaries.
2849
+ * On access denial: denied.tsx → default.tsx → null (graceful degradation).
2850
+ */
2851
+ async function buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent) {
2852
+ const PageComponent = (slotNode.page ? await loadModule(slotNode.page) : null)?.default;
2853
+ const DefaultComponent = (slotNode.default ? await loadModule(slotNode.default) : null)?.default;
2854
+ if (!PageComponent) return DefaultComponent ? createElement(DefaultComponent, {}) : null;
2855
+ let element = createElement(PageComponent, {});
2856
+ element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
2857
+ loadModule,
2858
+ createElement,
2859
+ errorBoundaryComponent
2860
+ });
2861
+ if (slotNode.access) {
2862
+ const accessFn = (await loadModule(slotNode.access)).default;
2863
+ const DeniedComponent = (slotNode.denied ? await loadModule(slotNode.denied) : null)?.default ?? null;
2864
+ const defaultFallback = DefaultComponent ? createElement(DefaultComponent, {}) : null;
2865
+ element = createElement("timber:slot-access-gate", {
2866
+ accessFn,
2867
+ DeniedComponent,
2868
+ slotName: slotNode.segmentName.replace(/^@/, ""),
2869
+ createElement,
2870
+ defaultFallback,
2871
+ children: element
2872
+ });
2873
+ }
2874
+ return element;
2875
+ }
2876
+ //#endregion
2877
+ //#region src/server/status-code-resolver.ts
2878
+ /**
2879
+ * Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
2880
+ * status-file group on a single segment. Shared by all three fallback
2881
+ * chains — the only structural difference between component 4xx,
2882
+ * component 5xx, and JSON resolution is *which* group is searched and
2883
+ * how the per-segment loop is layered around it.
2884
+ */
2885
+ function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
2886
+ if (!group) return null;
2887
+ const exact = group[statusStr];
2888
+ if (exact) return {
2889
+ file: exact,
2890
+ status,
2891
+ kind: "exact",
2892
+ segmentIndex
2893
+ };
2894
+ const category = group[categoryKey];
2895
+ if (category) return {
2896
+ file: category,
2897
+ status,
2898
+ kind: "category",
2899
+ segmentIndex
2900
+ };
2901
+ return null;
2902
+ }
2903
+ /**
2904
+ * Resolve the status-code file to render for a given HTTP status code.
2905
+ *
2906
+ * Walks the segment chain from leaf to root following the fallback chain
2907
+ * defined in design/10-error-handling.md. Returns null if no file is found
2908
+ * (caller should render the framework default).
2909
+ *
2910
+ * @param status - The HTTP status code (4xx or 5xx).
2911
+ * @param segments - The matched segment chain from root (index 0) to leaf (last).
2912
+ * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
2913
+ * @param maxIndex - Optional upper bound on the segment index to search.
2914
+ * When set, the resolver only considers segments `0..maxIndex` (inclusive),
2915
+ * skipping any segments deeper in the tree. Used by the deny renderers to
2916
+ * prevent resolving a status file below a denied segment — matching
2917
+ * `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
2918
+ * this parameter, so the full chain is searched — a 500 from a leaf page
2919
+ * should use the leaf's own `5xx.tsx`. See TIM-1358.
2920
+ */
2921
+ function resolveStatusFile(status, segments, format = "component", maxIndex) {
2922
+ if (status < 400 || status > 599) return null;
2923
+ if (format === "json") return resolveJson(status, segments, maxIndex);
2924
+ if (status <= 499) return resolve4xx(status, segments, maxIndex);
2925
+ return resolve5xx(status, segments, maxIndex);
2926
+ }
2927
+ /**
2928
+ * 4xx component fallback chain — two separate full passes leaf→root.
2929
+ *
2930
+ * The passes must be separate (not interleaved per-segment) so that a
2931
+ * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
2932
+ * inverts this and is per-segment: a leaf's `error.tsx` beats a root's
2933
+ * `5xx.tsx`. This asymmetry is the only reason these two functions exist
2934
+ * separately.
2935
+ *
2936
+ * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
2937
+ * Pass 2 — error.tsx (error)
2938
+ */
2939
+ function resolve4xx(status, segments, maxIndex) {
2940
+ const statusStr = String(status);
2941
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
2942
+ for (let i = start; i >= 0; i--) {
2943
+ const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
2944
+ if (r) return r;
2945
+ }
2946
+ for (let i = start; i >= 0; i--) {
2947
+ const errorFile = segments[i].error;
2948
+ if (errorFile) return {
2949
+ file: errorFile,
2950
+ status,
2951
+ kind: "error",
2952
+ segmentIndex: i
2953
+ };
2954
+ }
2955
+ return null;
2956
+ }
2957
+ /**
2958
+ * 5xx component fallback chain — single pass, per-segment leaf→root.
2959
+ *
2960
+ * At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
2961
+ * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
2962
+ * intentional inverse of the 4xx chain.
2963
+ */
2964
+ function resolve5xx(status, segments, maxIndex) {
2965
+ const statusStr = String(status);
2966
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
2967
+ for (let i = start; i >= 0; i--) {
2968
+ const segment = segments[i];
2969
+ const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
2970
+ if (r) return r;
2971
+ if (segment.error) return {
2972
+ file: segment.error,
2973
+ status,
2974
+ kind: "error",
2975
+ segmentIndex: i
2976
+ };
2977
+ }
2978
+ return null;
2979
+ }
2980
+ /**
2981
+ * JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
3118
2982
  *
3119
- * This module provides an ALS-based bridge: the generated entry point
3120
- * (e.g., the Nitro entry) wraps the handler with `runWithEarlyHintsSender`,
3121
- * binding a per-request sender function. The pipeline calls
3122
- * `sendEarlyHints103()` to fire the 103 if a sender is available.
2983
+ * At each segment: {status}.json → {category}.json. No legacy compat,
2984
+ * no error.tsx — the JSON chain terminates at the category catch-all
2985
+ * and the caller falls back to a bare-JSON framework default.
2986
+ */
2987
+ function resolveJson(status, segments, maxIndex) {
2988
+ const statusStr = String(status);
2989
+ const categoryKey = status >= 500 ? "5xx" : "4xx";
2990
+ const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
2991
+ for (let i = start; i >= 0; i--) {
2992
+ const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
2993
+ if (r) return r;
2994
+ }
2995
+ return null;
2996
+ }
2997
+ /**
2998
+ * Resolve the denial file for a parallel route slot.
3123
2999
  *
3124
- * On platforms where 103 is handled at the CDN level (e.g., Cloudflare
3125
- * converts Link headers into 103 automatically), no sender is installed
3126
- * and `sendEarlyHints103()` is a no-op.
3000
+ * Slot denial is graceful degradation — no HTTP status on the wire.
3001
+ * Fallback chain: denied.tsx → default.tsx → null.
3127
3002
  *
3128
- * Design doc: 02-rendering-pipeline.md §"Early Hints (103)"
3003
+ * @param slotNode - The segment node for the slot (segmentType === 'slot').
3129
3004
  */
3005
+ function resolveSlotDenied(slotNode) {
3006
+ const slotName = slotNode.segmentName.replace(/^@/, "");
3007
+ if (slotNode.denied) return {
3008
+ file: slotNode.denied,
3009
+ slotName,
3010
+ kind: "denied"
3011
+ };
3012
+ if (slotNode.default) return {
3013
+ file: slotNode.default,
3014
+ slotName,
3015
+ kind: "default"
3016
+ };
3017
+ return null;
3018
+ }
3019
+ //#endregion
3020
+ //#region src/server/csrf.ts
3021
+ /** HTTP methods that are considered safe (no mutation). */
3022
+ var SAFE_METHODS = /* @__PURE__ */ new Set([
3023
+ "GET",
3024
+ "HEAD",
3025
+ "OPTIONS"
3026
+ ]);
3130
3027
  /**
3131
- * Run a function with a per-request early hints sender installed.
3028
+ * Derive the request's scheme from trusted sources.
3132
3029
  *
3133
- * Called by generated entry points (e.g., Nitro node-server/bun) to
3134
- * bind the platform's writeEarlyHints capability for the request duration.
3030
+ * Priority:
3031
+ * 1. X-Forwarded-Proto (set by reverse proxies / load balancers)
3032
+ * 2. Request URL protocol
3033
+ *
3034
+ * Falls back to 'https' (fail closed) if neither is available.
3135
3035
  */
3136
- function runWithEarlyHintsSender(sender, fn) {
3137
- return earlyHintsSenderAls.run(sender, fn);
3036
+ function deriveRequestScheme(req) {
3037
+ const forwarded = req.headers.get("X-Forwarded-Proto");
3038
+ if (forwarded) {
3039
+ const proto = forwarded.split(",")[0].trim().toLowerCase();
3040
+ if (proto === "http" || proto === "https") return proto;
3041
+ }
3042
+ try {
3043
+ return new URL(req.url).protocol.replace(":", "");
3044
+ } catch {
3045
+ return "https";
3046
+ }
3138
3047
  }
3139
3048
  /**
3140
- * Send collected Link headers as a 103 Early Hints response.
3049
+ * Validate the Origin header against the request's full origin
3050
+ * (scheme + host + port).
3141
3051
  *
3142
- * No-op if no sender is installed for the current request (e.g., on
3143
- * Cloudflare where the CDN handles 103 automatically, or in dev mode).
3052
+ * For mutation methods (POST, PUT, PATCH, DELETE):
3053
+ * - If `csrf: false`, skip validation.
3054
+ * - If `allowedOrigins` is set, Origin must match one exactly (no wildcards).
3055
+ * - Otherwise, Origin must match the derived request origin.
3144
3056
  *
3145
- * Non-fatal: errors from the sender are caught and silently ignored.
3057
+ * Safe methods (GET, HEAD, OPTIONS) always pass.
3146
3058
  */
3147
- function sendEarlyHints103(links) {
3148
- if (!links.length) return;
3149
- const sender = earlyHintsSenderAls.getStore();
3150
- if (!sender) return;
3059
+ function validateCsrf(req, config) {
3060
+ if (SAFE_METHODS.has(req.method)) return { ok: true };
3061
+ if (config.csrf === false) return { ok: true };
3062
+ const origin = req.headers.get("Origin");
3063
+ if (!origin) return {
3064
+ ok: false,
3065
+ status: 403
3066
+ };
3067
+ if (config.allowedOrigins) return config.allowedOrigins.includes(origin) ? { ok: true } : {
3068
+ ok: false,
3069
+ status: 403
3070
+ };
3071
+ const host = req.headers.get("Host");
3072
+ if (!host) return {
3073
+ ok: false,
3074
+ status: 403
3075
+ };
3076
+ let originOrigin;
3151
3077
  try {
3152
- sender(links);
3153
- } catch (err) {
3154
- swallow(err, "early hints 103 send failed");
3078
+ originOrigin = new URL(origin).origin;
3079
+ } catch {
3080
+ return {
3081
+ ok: false,
3082
+ status: 403
3083
+ };
3084
+ }
3085
+ const scheme = deriveRequestScheme(req);
3086
+ let expectedOrigin;
3087
+ try {
3088
+ expectedOrigin = new URL(`${scheme}://${host}`).origin;
3089
+ } catch {
3090
+ return {
3091
+ ok: false,
3092
+ status: 403
3093
+ };
3155
3094
  }
3095
+ return originOrigin === expectedOrigin ? { ok: true } : {
3096
+ ok: false,
3097
+ status: 403
3098
+ };
3156
3099
  }
3157
3100
  //#endregion
3158
- //#region src/server/tree-builder.ts
3101
+ //#region src/server/body-limits.ts
3102
+ var KB = 1024;
3103
+ var MB = 1024 * KB;
3104
+ var GB = 1024 * MB;
3105
+ var DEFAULT_LIMITS = {
3106
+ actionBodySize: 1 * MB,
3107
+ uploadBodySize: 10 * MB,
3108
+ maxFields: 100
3109
+ };
3110
+ var SIZE_PATTERN = /^(\d+(?:\.\d+)?)\s*(kb|mb|gb)?$/i;
3111
+ /** Parse a human-readable size string ("1mb", "512kb", "1024") into bytes. */
3112
+ function parseBodySize(size) {
3113
+ const match = SIZE_PATTERN.exec(size.trim());
3114
+ if (!match) throw new Error(`Invalid body size format: "${size}". Expected format like "1mb", "512kb", or "1024".`);
3115
+ const value = Number.parseFloat(match[1]);
3116
+ const unit = (match[2] ?? "").toLowerCase();
3117
+ switch (unit) {
3118
+ case "kb": return Math.floor(value * KB);
3119
+ case "mb": return Math.floor(value * MB);
3120
+ case "gb": return Math.floor(value * GB);
3121
+ case "": return Math.floor(value);
3122
+ default: throw new Error(`Unknown size unit: "${unit}"`);
3123
+ }
3124
+ }
3125
+ /** Strict integer pattern: one or more digits, nothing else. */
3126
+ var STRICT_INTEGER_RE = /^\d+$/;
3127
+ /** Check whether a request body exceeds the configured size limit (stateless, no ALS). */
3128
+ function enforceBodyLimits(req, kind, config) {
3129
+ const contentLength = req.headers.get("Content-Length");
3130
+ if (!contentLength) return {
3131
+ ok: false,
3132
+ status: 411
3133
+ };
3134
+ const trimmed = contentLength.trim();
3135
+ if (!STRICT_INTEGER_RE.test(trimmed)) return {
3136
+ ok: false,
3137
+ status: 411
3138
+ };
3139
+ return Number.parseInt(trimmed, 10) <= resolveLimit(kind, config) ? { ok: true } : {
3140
+ ok: false,
3141
+ status: 413
3142
+ };
3143
+ }
3159
3144
  /**
3160
- * Build the unified element tree from a matched segment chain.
3161
- *
3162
- * Construction is bottom-up:
3163
- * 1. Start with the page component (leaf segment)
3164
- * 2. Wrap in status-code error boundaries (fallback chain)
3165
- * 3. Wrap in AccessGate (if segment has access.ts)
3166
- * 4. Pass as children to the segment's layout
3167
- * 5. Repeat up the segment chain to root
3145
+ * Resolve the byte limit for a given body kind, using config overrides or defaults.
3146
+ */
3147
+ function resolveLimit(kind, config) {
3148
+ const userLimits = config.limits;
3149
+ if (kind === "action") return userLimits?.actionBodySize ? parseBodySize(userLimits.actionBodySize) : DEFAULT_LIMITS.actionBodySize;
3150
+ return userLimits?.uploadBodySize ? parseBodySize(userLimits.uploadBodySize) : DEFAULT_LIMITS.uploadBodySize;
3151
+ }
3152
+ //#endregion
3153
+ //#region src/server/route-handler.ts
3154
+ /** All recognized HTTP method export names. */
3155
+ var HTTP_METHODS = [
3156
+ "GET",
3157
+ "POST",
3158
+ "PUT",
3159
+ "PATCH",
3160
+ "DELETE",
3161
+ "HEAD",
3162
+ "OPTIONS"
3163
+ ];
3164
+ /**
3165
+ * Resolve the full list of allowed methods for a route module.
3168
3166
  *
3169
- * Parallel slots are resolved at each layout level and composed as named props.
3167
+ * Includes:
3168
+ * - All explicitly exported methods
3169
+ * - HEAD (implicit when GET is exported)
3170
+ * - OPTIONS (always implicit)
3170
3171
  */
3171
- async function buildElementTree(config) {
3172
- const { segments, loadModule, createElement, errorBoundaryComponent } = config;
3173
- if (segments.length === 0) throw new Error("[timber] buildElementTree: empty segment chain");
3174
- const leaf = segments[segments.length - 1];
3175
- if (leaf.route && !leaf.page) return {
3176
- tree: null,
3177
- isApiRoute: true
3178
- };
3179
- const PageComponent = (leaf.page ? await loadModule(leaf.page) : null)?.default;
3180
- if (!PageComponent) throw new Error(`[timber] No page component found for route at ${leaf.urlPath}. Each route must have a page.tsx or route.ts.`);
3181
- let element = createElement(PageComponent, {});
3182
- for (let i = segments.length - 1; i >= 0; i--) {
3183
- const segment = segments[i];
3184
- element = await wrapSegmentWithErrorBoundaries(segment, element, {
3185
- loadModule,
3186
- createElement,
3187
- errorBoundaryComponent
3188
- });
3189
- if (segment.access) {
3190
- const accessFn = (await loadModule(segment.access)).default;
3191
- element = createElement("timber:access-gate", {
3192
- accessFn,
3193
- segmentName: segment.segmentName,
3194
- children: element
3195
- });
3196
- }
3197
- if (segment.layout) {
3198
- const LayoutComponent = (await loadModule(segment.layout)).default;
3199
- if (LayoutComponent) {
3200
- const slotProps = {};
3201
- const slotNames = Object.keys(segment.slots);
3202
- if (slotNames.length > 0) for (const slotName of slotNames) {
3203
- const slotNode = segment.slots[slotName];
3204
- slotProps[slotName] = await buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent);
3205
- }
3206
- element = createElement(LayoutComponent, {
3207
- ...slotProps,
3208
- children: element
3209
- });
3210
- }
3211
- }
3172
+ function resolveAllowedMethods(mod) {
3173
+ const methods = [];
3174
+ for (const method of HTTP_METHODS) {
3175
+ if (method === "HEAD" || method === "OPTIONS") continue;
3176
+ if (mod[method]) methods.push(method);
3212
3177
  }
3213
- return {
3214
- tree: element,
3215
- isApiRoute: false
3216
- };
3178
+ if (mod.GET && !mod.HEAD) methods.push("HEAD");
3179
+ else if (mod.HEAD) methods.push("HEAD");
3180
+ if (!mod.OPTIONS) methods.push("OPTIONS");
3181
+ else methods.push("OPTIONS");
3182
+ return methods;
3217
3183
  }
3218
3184
  /**
3219
- * Build the element tree for a parallel slot.
3185
+ * Handle an incoming request against a route.ts module.
3220
3186
  *
3221
- * Slots have their own access.ts (SlotAccessGate) and error boundaries.
3222
- * On access denial: denied.tsx → default.tsx → null (graceful degradation).
3187
+ * Dispatches to the named method handler, auto-generates 405/OPTIONS,
3188
+ * and merges response headers from ctx.headers.
3223
3189
  */
3224
- async function buildSlotElement(slotNode, loadModule, createElement, errorBoundaryComponent) {
3225
- const PageComponent = (slotNode.page ? await loadModule(slotNode.page) : null)?.default;
3226
- const DefaultComponent = (slotNode.default ? await loadModule(slotNode.default) : null)?.default;
3227
- if (!PageComponent) return DefaultComponent ? createElement(DefaultComponent, {}) : null;
3228
- let element = createElement(PageComponent, {});
3229
- element = await wrapSegmentWithErrorBoundaries(slotNode, element, {
3230
- loadModule,
3231
- createElement,
3232
- errorBoundaryComponent
3190
+ async function handleRouteRequest(mod, ctx) {
3191
+ const method = ctx.req.method.toUpperCase();
3192
+ const allowHeader = resolveAllowedMethods(mod).join(", ");
3193
+ if (method === "OPTIONS") {
3194
+ if (mod.OPTIONS) return runHandler(mod.OPTIONS, ctx);
3195
+ return new Response(null, {
3196
+ status: 204,
3197
+ headers: { Allow: allowHeader }
3198
+ });
3199
+ }
3200
+ if (method === "HEAD") {
3201
+ if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
3202
+ if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
3203
+ }
3204
+ const handler = mod[method];
3205
+ if (!handler) return new Response(null, {
3206
+ status: 405,
3207
+ headers: { Allow: allowHeader }
3233
3208
  });
3234
- if (slotNode.access) {
3235
- const accessFn = (await loadModule(slotNode.access)).default;
3236
- const DeniedComponent = (slotNode.denied ? await loadModule(slotNode.denied) : null)?.default ?? null;
3237
- const defaultFallback = DefaultComponent ? createElement(DefaultComponent, {}) : null;
3238
- element = createElement("timber:slot-access-gate", {
3239
- accessFn,
3240
- DeniedComponent,
3241
- slotName: slotNode.segmentName.replace(/^@/, ""),
3242
- createElement,
3243
- defaultFallback,
3244
- children: element
3209
+ return runHandler(handler, ctx);
3210
+ }
3211
+ /**
3212
+ * Run a handler, merge ctx.headers into the response, and catch errors.
3213
+ */
3214
+ async function runHandler(handler, ctx) {
3215
+ try {
3216
+ return mergeResponseHeaders(await handler(ctx), ctx.headers);
3217
+ } catch (error) {
3218
+ if (error instanceof DenySignal || error instanceof RedirectSignal) throw error;
3219
+ logRouteError({
3220
+ method: ctx.req.method,
3221
+ path: new URL(ctx.req.url).pathname,
3222
+ error
3245
3223
  });
3224
+ return new Response(null, { status: 500 });
3246
3225
  }
3247
- return element;
3248
3226
  }
3249
- //#endregion
3250
- //#region src/server/status-code-resolver.ts
3251
3227
  /**
3252
- * Look up `{statusStr}` then `{categoryKey}` (e.g. "4xx" / "5xx") in a
3253
- * status-file group on a single segment. Shared by all three fallback
3254
- * chains — the only structural difference between component 4xx,
3255
- * component 5xx, and JSON resolution is *which* group is searched and
3256
- * how the per-segment loop is layered around it.
3228
+ * Merge response headers from ctx.headers into the handler's response.
3229
+ * ctx.headers (set by middleware or the handler) are applied to the final response.
3230
+ * Handler-set headers take precedence over ctx.headers.
3257
3231
  */
3258
- function lookupInGroup(group, statusStr, categoryKey, segmentIndex, status) {
3259
- if (!group) return null;
3260
- const exact = group[statusStr];
3261
- if (exact) return {
3262
- file: exact,
3263
- status,
3264
- kind: "exact",
3265
- segmentIndex
3266
- };
3267
- const category = group[categoryKey];
3268
- if (category) return {
3269
- file: category,
3270
- status,
3271
- kind: "category",
3272
- segmentIndex
3273
- };
3274
- return null;
3232
+ function mergeResponseHeaders(res, ctxHeaders) {
3233
+ let hasCtxHeaders = false;
3234
+ ctxHeaders.forEach(() => {
3235
+ hasCtxHeaders = true;
3236
+ });
3237
+ if (!hasCtxHeaders) return res;
3238
+ const merged = new Headers();
3239
+ ctxHeaders.forEach((value, key) => {
3240
+ if (key.toLowerCase() === "set-cookie") merged.append(key, value);
3241
+ else merged.set(key, value);
3242
+ });
3243
+ const resCookies = res.headers.getSetCookie();
3244
+ for (const cookie of resCookies) merged.append("Set-Cookie", cookie);
3245
+ res.headers.forEach((value, key) => {
3246
+ if (key.toLowerCase() !== "set-cookie") merged.set(key, value);
3247
+ });
3248
+ return new Response(res.body, {
3249
+ status: res.status,
3250
+ statusText: res.statusText,
3251
+ headers: merged
3252
+ });
3275
3253
  }
3254
+ //#endregion
3255
+ //#region src/server/render-timeout.ts
3276
3256
  /**
3277
- * Resolve the status-code file to render for a given HTTP status code.
3257
+ * Render timeout utilities for SSR streaming pipeline.
3278
3258
  *
3279
- * Walks the segment chain from leaf to root following the fallback chain
3280
- * defined in design/10-error-handling.md. Returns null if no file is found
3281
- * (caller should render the framework default).
3259
+ * Provides a RenderTimeoutError class and a helper to create
3260
+ * timeout-guarded AbortSignals. Used to defend against hung RSC
3261
+ * streams and infinite SSR renders.
3282
3262
  *
3283
- * @param status - The HTTP status code (4xx or 5xx).
3284
- * @param segments - The matched segment chain from root (index 0) to leaf (last).
3285
- * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
3286
- * @param maxIndex - Optional upper bound on the segment index to search.
3287
- * When set, the resolver only considers segments `0..maxIndex` (inclusive),
3288
- * skipping any segments deeper in the tree. Used by the deny renderers to
3289
- * prevent resolving a status file below a denied segment — matching
3290
- * `buildDenyPageChain`'s start-at-i semantics. The error renderers omit
3291
- * this parameter, so the full chain is searched — a 500 from a leaf page
3292
- * should use the leaf's own `5xx.tsx`. See TIM-1358.
3263
+ * Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
3293
3264
  */
3294
- function resolveStatusFile(status, segments, format = "component", maxIndex) {
3295
- if (status < 400 || status > 599) return null;
3296
- if (format === "json") return resolveJson(status, segments, maxIndex);
3297
- if (status <= 499) return resolve4xx(status, segments, maxIndex);
3298
- return resolve5xx(status, segments, maxIndex);
3299
- }
3300
3265
  /**
3301
- * 4xx component fallback chain — two separate full passes leaf→root.
3266
+ * Error thrown when an SSR render or RSC stream read exceeds the
3267
+ * configured timeout. Callers can check `instanceof RenderTimeoutError`
3268
+ * to distinguish timeout from other errors and return a 504 or close
3269
+ * the connection cleanly.
3270
+ */
3271
+ var RenderTimeoutError = class extends Error {
3272
+ timeoutMs;
3273
+ constructor(timeoutMs, context) {
3274
+ const message = context ? `Render timeout after ${timeoutMs}ms: ${context}` : `Render timeout after ${timeoutMs}ms`;
3275
+ super(message);
3276
+ this.name = "RenderTimeoutError";
3277
+ this.timeoutMs = timeoutMs;
3278
+ }
3279
+ };
3280
+ //#endregion
3281
+ //#region src/server/prebuilt/cache-key.ts
3282
+ /**
3283
+ * Component cache-key hashing for prebuilt (cache.component) entries.
3302
3284
  *
3303
- * The passes must be separate (not interleaved per-segment) so that a
3304
- * root-level `404.tsx` beats a leaf-level `error.tsx`. The 5xx chain
3305
- * inverts this and is per-segment: a leaf's `error.tsx` beats a root's
3306
- * `5xx.tsx`. This asymmetry is the only reason these two functions exist
3307
- * separately.
3285
+ * Key: `sha256(canonicalJson({ props, params })).slice(0, 16)` — see
3286
+ * design/44-render-at-build-time.md §Cache Key. Both the request-time
3287
+ * lookup (prebuilt-runtime.ts) and the post-build capture pass (TIM-1119)
3288
+ * MUST use this exact function, or build artifacts become unaddressable.
3308
3289
  *
3309
- * Pass 1 — {status}.tsx → 4xx.tsx (statusFiles)
3310
- * Pass 2 — error.tsx (error)
3290
+ * THE IDENTITY CONTRACT (design/44 §Cache Key): cache identity is **JSON
3291
+ * value semantics** — two inputs are the same entry iff they are the same
3292
+ * JSON value. This is an ALLOWLIST, not a denylist: a value participates
3293
+ * in caching iff it is plain JSON data — null, boolean, finite number,
3294
+ * string, dense array, or a plain `Object.prototype` object with only
3295
+ * enumerable string-keyed data properties — plus `undefined` and `-0`,
3296
+ * which hash as distinct sentinels because flight preserves them. Every
3297
+ * value outside that domain (functions, symbols, bigints, accessors,
3298
+ * hidden or symbol-keyed properties, non-`Object.prototype` prototypes
3299
+ * including null, aliased references, sparse arrays, cycles, non-finite
3300
+ * numbers) is UNKEYABLE by definition: `computeComponentCacheKey` returns
3301
+ * null and the caller renders live — slower, never wrong.
3302
+ *
3303
+ * The line between rejected and declared-out-of-domain: values are
3304
+ * REJECTED when the JSON projection itself would be ambiguous or lossy
3305
+ * (hidden/symbol/non-enumerable properties, accessors, exotic
3306
+ * prototypes). Observable state that leaves the projection intact — key
3307
+ * order, reference identity, descriptor metadata (writable/configurable,
3308
+ * frozen/sealed/extensible), proxy behavior — is DECLARED outside
3309
+ * cache.component's supported input domain rather than rejected:
3310
+ * key-order insensitivity is required for determinism, and rejecting
3311
+ * frozen objects would unkey ubiquitous legitimate inputs
3312
+ * (Object.freeze'd config props) to defend against components that
3313
+ * render differently based on property attributes.
3314
+ *
3315
+ * Two hard rules for this walk itself: it must be TOTAL (never throw
3316
+ * anything but UnkeyableValueError) and SIDE-EFFECT-FREE (descriptor
3317
+ * reads only; never invoke user code such as getters).
3318
+ *
3319
+ * Object keys sort recursively so `{a, b}` and `{b, a}` hash identically
3320
+ * (design/13-security.md checklist #14 — cache key determinism; key order
3321
+ * is deliberately NOT part of identity).
3311
3322
  */
3312
- function resolve4xx(status, segments, maxIndex) {
3313
- const statusStr = String(status);
3314
- const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3315
- for (let i = start; i >= 0; i--) {
3316
- const r = lookupInGroup(segments[i].statusFiles, statusStr, "4xx", i, status);
3317
- if (r) return r;
3323
+ var UnkeyableValueError = class extends Error {};
3324
+ function canonicalize$1(value, seen) {
3325
+ if (value === null) return "null";
3326
+ switch (typeof value) {
3327
+ case "string": return JSON.stringify(value);
3328
+ case "boolean": return value ? "true" : "false";
3329
+ case "number":
3330
+ if (!Number.isFinite(value)) throw new UnkeyableValueError("non-finite number");
3331
+ return Object.is(value, -0) ? "-0" : String(value);
3332
+ case "object": break;
3333
+ case "undefined": return "undefined";
3334
+ default: throw new UnkeyableValueError(`unsupported type: ${typeof value}`);
3318
3335
  }
3319
- for (let i = start; i >= 0; i--) {
3320
- const errorFile = segments[i].error;
3321
- if (errorFile) return {
3322
- file: errorFile,
3323
- status,
3324
- kind: "error",
3325
- segmentIndex: i
3326
- };
3336
+ const obj = value;
3337
+ if (seen.has(obj)) throw new UnkeyableValueError("repeated object reference (cycle or alias)");
3338
+ seen.add(obj);
3339
+ if (Object.getOwnPropertySymbols(obj).length > 0) throw new UnkeyableValueError("symbol-keyed property");
3340
+ {
3341
+ if (Array.isArray(obj)) {
3342
+ if (Object.getOwnPropertyNames(obj).length !== obj.length + 1) throw new UnkeyableValueError("non-index array property");
3343
+ const parts = [];
3344
+ for (let i = 0; i < obj.length; i++) {
3345
+ const desc = Object.getOwnPropertyDescriptor(obj, i);
3346
+ if (desc === void 0) throw new UnkeyableValueError("sparse array hole");
3347
+ if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
3348
+ if (!desc.enumerable) throw new UnkeyableValueError("non-enumerable array element");
3349
+ parts.push(canonicalize$1(desc.value, seen));
3350
+ }
3351
+ return "[" + parts.join(",") + "]";
3352
+ }
3353
+ if (Object.getPrototypeOf(obj) !== Object.prototype) throw new UnkeyableValueError("non-plain object");
3354
+ const keys = Object.keys(obj).sort();
3355
+ if (keys.length !== Object.getOwnPropertyNames(obj).length) throw new UnkeyableValueError("non-enumerable property");
3356
+ const parts = [];
3357
+ for (const key of keys) {
3358
+ const desc = Object.getOwnPropertyDescriptor(obj, key);
3359
+ if (desc.get !== void 0 || desc.set !== void 0) throw new UnkeyableValueError("accessor property");
3360
+ parts.push(JSON.stringify(key) + ":" + canonicalize$1(desc.value, seen));
3361
+ }
3362
+ return "{" + parts.join(",") + "}";
3327
3363
  }
3328
- return null;
3329
3364
  }
3330
3365
  /**
3331
- * 5xx component fallback chain — single pass, per-segment leaf→root.
3332
- *
3333
- * At each segment: {status}.tsx → 5xx.tsx → error.tsx. A leaf's
3334
- * `error.tsx` therefore beats a root's `5xx.tsx`, which is the
3335
- * intentional inverse of the 4xx chain.
3366
+ * Canonicalize a single value, or null if it is unkeyable. The ONE
3367
+ * definition of "faithfully comparable" — every prebuilt code path that
3368
+ * needs value equality (cache keys, slot-param divergence) goes through
3369
+ * this, so the unkeyable rules (accessors, hidden props, aliases,
3370
+ * bigints, …) live in exactly one place.
3336
3371
  */
3337
- function resolve5xx(status, segments, maxIndex) {
3338
- const statusStr = String(status);
3339
- const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3340
- for (let i = start; i >= 0; i--) {
3341
- const segment = segments[i];
3342
- const r = lookupInGroup(segment.statusFiles, statusStr, "5xx", i, status);
3343
- if (r) return r;
3344
- if (segment.error) return {
3345
- file: segment.error,
3346
- status,
3347
- kind: "error",
3348
- segmentIndex: i
3349
- };
3372
+ function tryCanonicalize(value) {
3373
+ try {
3374
+ return canonicalize$1(value, /* @__PURE__ */ new Set());
3375
+ } catch (error) {
3376
+ return toUnkeyable(error);
3350
3377
  }
3351
- return null;
3352
3378
  }
3353
3379
  /**
3354
- * JSON fallback chain (for both 4xx and 5xx) — single pass leaf→root.
3355
- *
3356
- * At each segment: {status}.json → {category}.json. No legacy compat,
3357
- * no error.tsx — the JSON chain terminates at the category catch-all
3358
- * and the caller falls back to a bare-JSON framework default.
3380
+ * Totality by construction: ANY throw from the walk means the input is
3381
+ * outside the keyable domain. The walk touches only user-controlled
3382
+ * values through reflection ops — and a Proxy trap can throw arbitrary
3383
+ * errors from any of them (codex review on PR #835) — so converting only
3384
+ * UnkeyableValueError would let hostile/exotic inputs abort a render
3385
+ * that plain live rendering would have served fine. Unexpected error
3386
+ * types are logged for visibility (they would indicate either a proxy
3387
+ * trap or a canonicalizer bug) but still degrade to unkeyable.
3359
3388
  */
3360
- function resolveJson(status, segments, maxIndex) {
3361
- const statusStr = String(status);
3362
- const categoryKey = status >= 500 ? "5xx" : "4xx";
3363
- const start = maxIndex !== void 0 ? Math.min(segments.length - 1, maxIndex) : segments.length - 1;
3364
- for (let i = start; i >= 0; i--) {
3365
- const r = lookupInGroup(segments[i].jsonStatusFiles, statusStr, categoryKey, i, status);
3366
- if (r) return r;
3367
- }
3389
+ function toUnkeyable(error) {
3390
+ if (!(error instanceof UnkeyableValueError)) console.error("[timber] cache key canonicalization threw — treating input as unkeyable:", error);
3368
3391
  return null;
3369
3392
  }
3370
3393
  /**
3371
- * Resolve the denial file for a parallel route slot.
3394
+ * Split a props record into data props (participate in the cache key) and
3395
+ * slot values (React-element holes, excluded from the key) per the
3396
+ * component's declared `slots` option (TIM-1173).
3372
3397
  *
3373
- * Slot denial is graceful degradation — no HTTP status on the wire.
3374
- * Fallback chain: denied.tsx → default.tsx → null.
3398
+ * Every DECLARED slot lands in `slotValues` — present or not — because the
3399
+ * capture render always injects a SlotOutlet placeholder for each declared
3400
+ * slot: the cached shell is identical whether the caller passed the slot
3401
+ * or omitted it, so slot presence must not vary the key. `dataProps` is
3402
+ * everything else and is what gets hashed.
3375
3403
  *
3376
- * @param slotNode - The segment node for the slot (segmentType === 'slot').
3404
+ * Returns null when the props OBJECT itself is outside the identity
3405
+ * contract at the top level — accessors, symbol-keyed or non-enumerable
3406
+ * properties, or a non-`Object.prototype` prototype. Splitting such an
3407
+ * object would normalize away observable state (and reading an accessor
3408
+ * would invoke user code, which this walk must never do) BEFORE
3409
+ * `computeComponentCacheKey` gets the chance to reject it — a cached
3410
+ * shell could then be served for inputs a live render distinguishes.
3411
+ * Null means unkeyable: the caller renders live with the original props.
3412
+ * The walk is descriptor-reads only; getters are never invoked. Nested
3413
+ * data-prop values still face the full allowlist when hashed.
3377
3414
  */
3378
- function resolveSlotDenied(slotNode) {
3379
- const slotName = slotNode.segmentName.replace(/^@/, "");
3380
- if (slotNode.denied) return {
3381
- file: slotNode.denied,
3382
- slotName,
3383
- kind: "denied"
3384
- };
3385
- if (slotNode.default) return {
3386
- file: slotNode.default,
3387
- slotName,
3388
- kind: "default"
3389
- };
3390
- return null;
3415
+ /** Create an own enumerable data property — safe for keys like `__proto__`. */
3416
+ function defineOwn(target, key, value) {
3417
+ Object.defineProperty(target, key, {
3418
+ value,
3419
+ enumerable: true,
3420
+ writable: true,
3421
+ configurable: true
3422
+ });
3391
3423
  }
3392
- //#endregion
3393
- //#region src/server/csrf.ts
3394
- /** HTTP methods that are considered safe (no mutation). */
3395
- var SAFE_METHODS = /* @__PURE__ */ new Set([
3396
- "GET",
3397
- "HEAD",
3398
- "OPTIONS"
3399
- ]);
3400
- /**
3401
- * Derive the request's scheme from trusted sources.
3402
- *
3403
- * Priority:
3404
- * 1. X-Forwarded-Proto (set by reverse proxies / load balancers)
3405
- * 2. Request URL protocol
3406
- *
3407
- * Falls back to 'https' (fail closed) if neither is available.
3408
- */
3409
- function deriveRequestScheme(req) {
3410
- const forwarded = req.headers.get("X-Forwarded-Proto");
3411
- if (forwarded) {
3412
- const proto = forwarded.split(",")[0].trim().toLowerCase();
3413
- if (proto === "http" || proto === "https") return proto;
3414
- }
3424
+ function splitSlotProps(props, slots) {
3415
3425
  try {
3416
- return new URL(req.url).protocol.replace(":", "");
3417
- } catch {
3418
- return "https";
3426
+ if (Object.getPrototypeOf(props) !== Object.prototype) return null;
3427
+ if (Object.getOwnPropertySymbols(props).length > 0) return null;
3428
+ const slotSet = new Set(slots);
3429
+ const dataProps = {};
3430
+ const slotValues = {};
3431
+ for (const key of Object.getOwnPropertyNames(props)) {
3432
+ const desc = Object.getOwnPropertyDescriptor(props, key);
3433
+ if (desc === void 0 || desc.get !== void 0 || desc.set !== void 0 || !desc.enumerable) return null;
3434
+ defineOwn(slotSet.has(key) ? slotValues : dataProps, key, desc.value);
3435
+ }
3436
+ for (const slot of slotSet) if (!Object.hasOwn(slotValues, slot)) defineOwn(slotValues, slot, void 0);
3437
+ return {
3438
+ dataProps,
3439
+ slotValues
3440
+ };
3441
+ } catch (error) {
3442
+ return toUnkeyable(error);
3419
3443
  }
3420
3444
  }
3421
3445
  /**
3422
- * Validate the Origin header against the request's full origin
3423
- * (scheme + host + port).
3424
- *
3425
- * For mutation methods (POST, PUT, PATCH, DELETE):
3426
- * - If `csrf: false`, skip validation.
3427
- * - If `allowedOrigins` is set, Origin must match one exactly (no wildcards).
3428
- * - Otherwise, Origin must match the derived request origin.
3446
+ * Compute the cache-entry hash for a component render:
3447
+ * `sha256({props, params}).slice(0, 16)` over canonical JSON.
3429
3448
  *
3430
- * Safe methods (GET, HEAD, OPTIONS) always pass.
3449
+ * Returns null when props (or params) contain unkeyable values — the
3450
+ * caller must fall back to dynamic rendering.
3431
3451
  */
3432
- function validateCsrf(req, config) {
3433
- if (SAFE_METHODS.has(req.method)) return { ok: true };
3434
- if (config.csrf === false) return { ok: true };
3435
- const origin = req.headers.get("Origin");
3436
- if (!origin) return {
3437
- ok: false,
3438
- status: 403
3439
- };
3440
- if (config.allowedOrigins) return config.allowedOrigins.includes(origin) ? { ok: true } : {
3441
- ok: false,
3442
- status: 403
3443
- };
3444
- const host = req.headers.get("Host");
3445
- if (!host) return {
3446
- ok: false,
3447
- status: 403
3448
- };
3449
- let originOrigin;
3450
- try {
3451
- originOrigin = new URL(origin).origin;
3452
- } catch {
3453
- return {
3454
- ok: false,
3455
- status: 403
3456
- };
3457
- }
3458
- const scheme = deriveRequestScheme(req);
3459
- let expectedOrigin;
3452
+ function computeComponentCacheKey(props, params) {
3453
+ let canonical;
3460
3454
  try {
3461
- expectedOrigin = new URL(`${scheme}://${host}`).origin;
3462
- } catch {
3463
- return {
3464
- ok: false,
3465
- status: 403
3466
- };
3455
+ const seen = /* @__PURE__ */ new Set();
3456
+ canonical = "{\"params\":" + canonicalize$1({ ...params }, seen) + ",\"props\":" + canonicalize$1(props, seen) + "}";
3457
+ } catch (error) {
3458
+ return toUnkeyable(error);
3467
3459
  }
3468
- return originOrigin === expectedOrigin ? { ok: true } : {
3469
- ok: false,
3470
- status: 403
3471
- };
3460
+ return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
3472
3461
  }
3473
3462
  //#endregion
3474
- //#region src/server/body-limits.ts
3475
- var KB = 1024;
3476
- var MB = 1024 * KB;
3477
- var GB = 1024 * MB;
3478
- var DEFAULT_LIMITS = {
3479
- actionBodySize: 1 * MB,
3480
- uploadBodySize: 10 * MB,
3481
- maxFields: 100
3482
- };
3483
- var SIZE_PATTERN = /^(\d+(?:\.\d+)?)\s*(kb|mb|gb)?$/i;
3484
- /** Parse a human-readable size string ("1mb", "512kb", "1024") into bytes. */
3485
- function parseBodySize(size) {
3486
- const match = SIZE_PATTERN.exec(size.trim());
3487
- if (!match) throw new Error(`Invalid body size format: "${size}". Expected format like "1mb", "512kb", or "1024".`);
3488
- const value = Number.parseFloat(match[1]);
3489
- const unit = (match[2] ?? "").toLowerCase();
3490
- switch (unit) {
3491
- case "kb": return Math.floor(value * KB);
3492
- case "mb": return Math.floor(value * MB);
3493
- case "gb": return Math.floor(value * GB);
3494
- case "": return Math.floor(value);
3495
- default: throw new Error(`Unknown size unit: "${unit}"`);
3463
+ //#region src/client/slot-context.ts
3464
+ /**
3465
+ * SlotContext — delivers live slot content to SlotOutlet holes inside
3466
+ * cached component shells (TIM-1173).
3467
+ *
3468
+ * A `cache.component(Fn, { slots: [...] })` shell is captured with a
3469
+ * stable <SlotOutlet slot={name} /> client reference in place of each
3470
+ * declared slot prop. At request time the revived shell is wrapped in a
3471
+ * SlotsProvider carrying the live slot values; each outlet reads its
3472
+ * value from this context by name.
3473
+ *
3474
+ * Providers MERGE per-key with their parent: an inner SlotsProvider
3475
+ * inherits the outer's values and overrides only the keys it declares.
3476
+ * Two sibling instances each have their own provider and resolve
3477
+ * independently. Layouts use a reserved slot name (LAYOUT_CHILDREN_SLOT)
3478
+ * that cannot collide with user-declared slot names (TIM-1191).
3479
+ *
3480
+ * Generalizes the original layout children pattern (TIM-1181) from a
3481
+ * single implicit `children` hole on layouts to explicitly declared,
3482
+ * named slot props on any cached component. Layouts use this same
3483
+ * context with the reserved LAYOUT_CHILDREN_SLOT key (TIM-1191).
3484
+ *
3485
+ * SINGLETON GUARANTEE: globalThis + Symbol.for — the RSC client bundler
3486
+ * can duplicate this module across chunks; globalThis guarantees a single
3487
+ * context instance.
3488
+ *
3489
+ * See design/45-cache-lifetimes.md §Slot Components.
3490
+ */
3491
+ var CTX_KEY = Symbol.for("__timber_slot_ctx");
3492
+ function getOrCreateContext() {
3493
+ const existing = globalThis[CTX_KEY];
3494
+ if (existing !== void 0) return existing;
3495
+ if (typeof React.createContext === "function") {
3496
+ const ctx = React.createContext(null);
3497
+ globalThis[CTX_KEY] = ctx;
3498
+ return ctx;
3496
3499
  }
3497
3500
  }
3498
- /** Strict integer pattern: one or more digits, nothing else. */
3499
- var STRICT_INTEGER_RE = /^\d+$/;
3500
- /** Check whether a request body exceeds the configured size limit (stateless, no ALS). */
3501
- function enforceBodyLimits(req, kind, config) {
3502
- const contentLength = req.headers.get("Content-Length");
3503
- if (!contentLength) return {
3504
- ok: false,
3505
- status: 411
3506
- };
3507
- const trimmed = contentLength.trim();
3508
- if (!STRICT_INTEGER_RE.test(trimmed)) return {
3509
- ok: false,
3510
- status: 411
3511
- };
3512
- return Number.parseInt(trimmed, 10) <= resolveLimit(kind, config) ? { ok: true } : {
3513
- ok: false,
3514
- status: 413
3515
- };
3516
- }
3501
+ var SlotContext = getOrCreateContext();
3502
+ //#endregion
3503
+ //#region src/client/slot-outlet.tsx
3517
3504
  /**
3518
- * Resolve the byte limit for a given body kind, using config overrides or defaults.
3505
+ * SlotOutlet — the hole in a cached component shell (TIM-1173).
3506
+ *
3507
+ * At capture time the prebuilt runtime renders the wrapped component with
3508
+ * <SlotOutlet slot={name} /> in place of each declared slot prop. Being a
3509
+ * client component, it serializes into the flight payload as a stable,
3510
+ * deterministic client reference — the same bytes regardless of what live
3511
+ * content will fill the hole. At request time the revived shell sits under
3512
+ * a SlotsProvider and each outlet reads its live value from SlotContext.
3513
+ *
3514
+ * See design/45-cache-lifetimes.md §Slot Components.
3519
3515
  */
3520
- function resolveLimit(kind, config) {
3521
- const userLimits = config.limits;
3522
- if (kind === "action") return userLimits?.actionBodySize ? parseBodySize(userLimits.actionBodySize) : DEFAULT_LIMITS.actionBodySize;
3523
- return userLimits?.uploadBodySize ? parseBodySize(userLimits.uploadBodySize) : DEFAULT_LIMITS.uploadBodySize;
3516
+ function SlotOutlet({ slot }) {
3517
+ const values = useContext(SlotContext);
3518
+ return values ? values[slot] ?? null : null;
3524
3519
  }
3525
3520
  //#endregion
3526
- //#region src/server/route-handler.ts
3527
- /** All recognized HTTP method export names. */
3528
- var HTTP_METHODS = [
3529
- "GET",
3530
- "POST",
3531
- "PUT",
3532
- "PATCH",
3533
- "DELETE",
3534
- "HEAD",
3535
- "OPTIONS"
3536
- ];
3521
+ //#region src/client/slot-provider.tsx
3537
3522
  /**
3538
- * Resolve the full list of allowed methods for a route module.
3523
+ * SlotsProvider — wraps a revived cached shell to supply live slot content
3524
+ * to the SlotOutlet holes inside it (TIM-1173).
3539
3525
  *
3540
- * Includes:
3541
- * - All explicitly exported methods
3542
- * - HEAD (implicit when GET is exported)
3543
- * - OPTIONS (always implicit)
3526
+ * Values MERGE with the nearest parent provider so that nesting a
3527
+ * cache.component SlotsProvider inside a layout SlotsProvider preserves
3528
+ * the layout's `children` slot while adding the component's own slots.
3529
+ * Inner values override outer ones with the same name (spread order).
3530
+ *
3531
+ * Tree structure per cached-component instance:
3532
+ * SlotsProvider(values={children: <Live />})
3533
+ * └── revived shell
3534
+ * └── SlotOutlet(slot="children") → reads context → <Live />
3535
+ *
3536
+ * See design/45-cache-lifetimes.md §Slot Components.
3544
3537
  */
3545
- function resolveAllowedMethods(mod) {
3546
- const methods = [];
3547
- for (const method of HTTP_METHODS) {
3548
- if (method === "HEAD" || method === "OPTIONS") continue;
3549
- if (mod[method]) methods.push(method);
3550
- }
3551
- if (mod.GET && !mod.HEAD) methods.push("HEAD");
3552
- else if (mod.HEAD) methods.push("HEAD");
3553
- if (!mod.OPTIONS) methods.push("OPTIONS");
3554
- else methods.push("OPTIONS");
3555
- return methods;
3538
+ function SlotsProvider({ values, children }) {
3539
+ const parent = useContext(SlotContext);
3540
+ const merged = parent ? {
3541
+ ...parent,
3542
+ ...values
3543
+ } : values;
3544
+ return createElement(SlotContext.Provider, { value: merged }, children);
3556
3545
  }
3557
3546
  /**
3558
- * Handle an incoming request against a route.ts module.
3547
+ * Is this value the framework-injected layout children slot outlet
3548
+ * (`<SlotOutlet slot={LAYOUT_CHILDREN_SLOT} />`)? Layout components
3549
+ * receive this as their `children` prop — a stable client reference
3550
+ * whose cache key is stripped so layout `cache.component` hits work
3551
+ * (design/45 §Layout Propagation).
3559
3552
  *
3560
- * Dispatches to the named method handler, auto-generates 405/OPTIONS,
3561
- * and merges response headers from ctx.headers.
3553
+ * Uses a framework-reserved slot name (LAYOUT_CHILDREN_SLOT) that
3554
+ * cannot collide with user-declared slot names, preventing context
3555
+ * shadowing when a cached component with its own SlotsProvider is
3556
+ * nested inside a layout. See TIM-1191.
3557
+ *
3558
+ * Total: the probes run against a user-controlled value, and a Proxy
3559
+ * trap can throw — that must classify as "not the outlet" (the
3560
+ * canonicalizer then rejects the value as unkeyable → live render), not
3561
+ * abort the request (codex P2 on PR #890).
3562
3562
  */
3563
- async function handleRouteRequest(mod, ctx) {
3564
- const method = ctx.req.method.toUpperCase();
3565
- const allowHeader = resolveAllowedMethods(mod).join(", ");
3566
- if (method === "OPTIONS") {
3567
- if (mod.OPTIONS) return runHandler(mod.OPTIONS, ctx);
3568
- return new Response(null, {
3569
- status: 204,
3570
- headers: { Allow: allowHeader }
3571
- });
3572
- }
3573
- if (method === "HEAD") {
3574
- if (mod.HEAD) return stripHeadBody(await runHandler(mod.HEAD, ctx), method);
3575
- if (mod.GET) return stripHeadBody(await runHandler(mod.GET, ctx), method);
3563
+ function isChildrenSlotOutletElement(value) {
3564
+ try {
3565
+ if (value == null || typeof value !== "object") return false;
3566
+ if (!("type" in value)) return false;
3567
+ const el = value;
3568
+ return el.type === SlotOutlet && el.props?.slot === "\0timber:children";
3569
+ } catch {
3570
+ return false;
3576
3571
  }
3577
- const handler = mod[method];
3578
- if (!handler) return new Response(null, {
3579
- status: 405,
3580
- headers: { Allow: allowHeader }
3581
- });
3582
- return runHandler(handler, ctx);
3583
3572
  }
3584
3573
  /**
3585
- * Run a handler, merge ctx.headers into the response, and catch errors.
3574
+ * Dense array of non-empty strings — no holes, no non-string entries.
3575
+ * Total: the reads run against a user-controlled value at module load,
3576
+ * and a throwing Proxy trap must classify as invalid, not crash startup.
3586
3577
  */
3587
- async function runHandler(handler, ctx) {
3578
+ function isDenseStringArray(value) {
3588
3579
  try {
3589
- return mergeResponseHeaders(await handler(ctx), ctx.headers);
3590
- } catch (error) {
3591
- if (error instanceof DenySignal || error instanceof RedirectSignal) throw error;
3592
- logRouteError({
3593
- method: ctx.req.method,
3594
- path: new URL(ctx.req.url).pathname,
3595
- error
3596
- });
3597
- return new Response(null, { status: 500 });
3580
+ if (!Array.isArray(value)) return false;
3581
+ for (let i = 0; i < value.length; i++) {
3582
+ if (!Object.hasOwn(value, i)) return false;
3583
+ const entry = value[i];
3584
+ if (typeof entry !== "string" || entry.length === 0) return false;
3585
+ }
3586
+ return true;
3587
+ } catch {
3588
+ return false;
3598
3589
  }
3599
3590
  }
3600
3591
  /**
3601
- * Merge response headers from ctx.headers into the handler's response.
3602
- * ctx.headers (set by middleware or the handler) are applied to the final response.
3603
- * Handler-set headers take precedence over ctx.headers.
3592
+ * Validate the `slots` option. Both the production and dev wrappers gate
3593
+ * on this — slot substitution is active in dev exactly when it would be
3594
+ * active in production, so the tree shape a component sees never
3595
+ * diverges between the two.
3604
3596
  */
3605
- function mergeResponseHeaders(res, ctxHeaders) {
3606
- let hasCtxHeaders = false;
3607
- ctxHeaders.forEach(() => {
3608
- hasCtxHeaders = true;
3609
- });
3610
- if (!hasCtxHeaders) return res;
3611
- const merged = new Headers();
3612
- ctxHeaders.forEach((value, key) => {
3613
- if (key.toLowerCase() === "set-cookie") merged.append(key, value);
3614
- else merged.set(key, value);
3615
- });
3616
- const resCookies = res.headers.getSetCookie();
3617
- for (const cookie of resCookies) merged.append("Set-Cookie", cookie);
3618
- res.headers.forEach((value, key) => {
3619
- if (key.toLowerCase() !== "set-cookie") merged.set(key, value);
3620
- });
3621
- return new Response(res.body, {
3622
- status: res.status,
3623
- statusText: res.statusText,
3624
- headers: merged
3625
- });
3597
+ function resolveSlotNames(id, options) {
3598
+ const slots = options.slots;
3599
+ if (slots === void 0) return { kind: "none" };
3600
+ if (!isDenseStringArray(slots)) {
3601
+ console.warn(`[timber] cache.component "${id}": invalid slots declaration — expected an array of non-empty prop-name strings. Slot caching is disabled.`);
3602
+ return { kind: "disabled" };
3603
+ }
3604
+ if (slots.length === 0) return { kind: "none" };
3605
+ if (!(options.ttl !== void 0 || options.tags != null)) {
3606
+ console.warn(`[timber] cache.component "${id}": slots require a runtime lifetime (ttl and/or tags). Slot caching is disabled.`);
3607
+ return { kind: "disabled" };
3608
+ }
3609
+ return {
3610
+ kind: "active",
3611
+ slots
3612
+ };
3626
3613
  }
3627
- //#endregion
3628
- //#region src/server/render-timeout.ts
3629
3614
  /**
3630
- * Render timeout utilities for SSR streaming pipeline.
3631
- *
3632
- * Provides a RenderTimeoutError class and a helper to create
3633
- * timeout-guarded AbortSignals. Used to defend against hung RSC
3634
- * streams and infinite SSR renders.
3615
+ * Split the live props into the cacheable shell render (data props plus a
3616
+ * deterministic SlotOutlet placeholder per declared slot — identical
3617
+ * whether or not the caller passed the slot) and the per-instance live
3618
+ * slot values.
3635
3619
  *
3636
- * Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
3620
+ * Null when the props object itself is unkeyable at the top level
3621
+ * (accessors, hidden properties, exotic prototype — see splitSlotProps):
3622
+ * the caller must render live with the original props.
3637
3623
  */
3624
+ function prepareSlotRender(props, slots) {
3625
+ const split = splitSlotProps(props ?? {}, slots);
3626
+ if (split === null) return null;
3627
+ const { dataProps, slotValues } = split;
3628
+ const shellProps = { ...dataProps };
3629
+ for (const slot of slots) Object.defineProperty(shellProps, slot, {
3630
+ value: createElement(SlotOutlet, { slot }),
3631
+ enumerable: true,
3632
+ writable: true,
3633
+ configurable: true
3634
+ });
3635
+ return {
3636
+ shellProps,
3637
+ dataProps,
3638
+ slotValues
3639
+ };
3640
+ }
3638
3641
  /**
3639
- * Error thrown when an SSR render or RSC stream read exceeds the
3640
- * configured timeout. Callers can check `instanceof RenderTimeoutError`
3641
- * to distinguish timeout from other errors and return a 504 or close
3642
- * the connection cleanly.
3642
+ * Wrap a rendered shell (revived payload or live element) in a
3643
+ * per-instance SlotsProvider feeding the live values to its outlets.
3643
3644
  */
3644
- var RenderTimeoutError = class extends Error {
3645
- timeoutMs;
3646
- constructor(timeoutMs, context) {
3647
- const message = context ? `Render timeout after ${timeoutMs}ms: ${context}` : `Render timeout after ${timeoutMs}ms`;
3648
- super(message);
3649
- this.name = "RenderTimeoutError";
3650
- this.timeoutMs = timeoutMs;
3651
- }
3652
- };
3645
+ function wrapInSlotsProvider(shell, slotValues) {
3646
+ return createElement(SlotsProvider, {
3647
+ values: slotValues,
3648
+ children: shell
3649
+ });
3650
+ }
3653
3651
  //#endregion
3654
3652
  //#region src/server/prebuilt/key-discipline.ts
3655
3653
  /** Components already warned — exported so tests can reset between cases. */