@timber-js/app 0.2.0-alpha.185 → 0.2.0-alpha.187

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 (570) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-35jnMdeJ.js} +4 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-35jnMdeJ.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
  5. package/dist/_chunks/als-slots-BEEIPKYm.js +26 -0
  6. package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -0
  7. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  8. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  9. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  10. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  11. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  12. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  13. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-DjNrIWRR.js} +8 -14
  14. package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
  15. package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
  16. package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
  17. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
  18. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
  19. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-CGP6BZKO.js} +92 -14
  20. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
  21. package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
  22. package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
  23. package/dist/_chunks/error-boundary-D-ODYX41.js +353 -0
  24. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
  25. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  26. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  27. package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
  28. package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
  29. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  30. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  31. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-D8xJZXIN.js} +34 -51
  32. package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
  33. package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
  34. package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
  35. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-rCinWLiE.js} +8 -2
  36. package/dist/_chunks/plugin-context-rCinWLiE.js.map +1 -0
  37. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  38. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  39. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
  40. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
  41. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  42. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  43. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  44. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  45. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  46. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  47. package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
  48. package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
  49. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  50. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  51. package/dist/_chunks/scanner-C8b0Gcw3.js +1564 -0
  52. package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
  53. package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
  54. package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
  55. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  56. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  57. package/dist/_chunks/segment-context-CjOlyB8Y.js +97 -0
  58. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
  59. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  60. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  61. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  62. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  63. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  64. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  65. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  66. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  67. package/dist/_chunks/use-segment-params-C4r4BD9T.js +398 -0
  68. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  69. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  70. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  71. package/dist/adapters/build-output-helper.d.ts +37 -0
  72. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  73. package/dist/adapters/cloudflare-dev.js +1 -1
  74. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  75. package/dist/adapters/cloudflare.d.ts.map +1 -1
  76. package/dist/adapters/cloudflare.js +1 -1
  77. package/dist/adapters/compress-module.d.ts +12 -0
  78. package/dist/adapters/compress-module.d.ts.map +1 -1
  79. package/dist/adapters/fs-identity.d.ts +64 -0
  80. package/dist/adapters/fs-identity.d.ts.map +1 -0
  81. package/dist/adapters/nitro.d.ts.map +1 -1
  82. package/dist/adapters/nitro.js +56 -5
  83. package/dist/adapters/nitro.js.map +1 -1
  84. package/dist/adapters/shared.d.ts +57 -1
  85. package/dist/adapters/shared.d.ts.map +1 -1
  86. package/dist/adapters/types.d.ts +13 -1
  87. package/dist/adapters/types.d.ts.map +1 -1
  88. package/dist/cache/fast-hash.d.ts +5 -0
  89. package/dist/cache/fast-hash.d.ts.map +1 -1
  90. package/dist/cache/index.js +1 -1
  91. package/dist/cdn/cloudflare-purge.js +30 -0
  92. package/dist/cdn/cloudflare-purge.js.map +1 -0
  93. package/dist/cdn/fastly-purge.js +33 -0
  94. package/dist/cdn/fastly-purge.js.map +1 -0
  95. package/dist/cdn/index.js +94 -0
  96. package/dist/cdn/index.js.map +1 -0
  97. package/dist/cdn/workers-cache-purge.js +35 -0
  98. package/dist/cdn/workers-cache-purge.js.map +1 -0
  99. package/dist/cli-check.d.ts +153 -0
  100. package/dist/cli-check.d.ts.map +1 -0
  101. package/dist/cli.d.ts +34 -8
  102. package/dist/cli.d.ts.map +1 -1
  103. package/dist/cli.js +46 -21
  104. package/dist/cli.js.map +1 -1
  105. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  106. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  107. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  108. package/dist/client/browser-entry/index.d.ts +1 -1
  109. package/dist/client/browser-entry/index.d.ts.map +1 -1
  110. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  111. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  112. package/dist/client/browser-entry/router-init.d.ts +3 -0
  113. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  114. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  115. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  116. package/dist/client/error-boundary.d.ts +7 -0
  117. package/dist/client/error-boundary.d.ts.map +1 -1
  118. package/dist/client/error-boundary.js +1 -162
  119. package/dist/client/history.d.ts +13 -6
  120. package/dist/client/history.d.ts.map +1 -1
  121. package/dist/client/index.d.ts +1 -0
  122. package/dist/client/index.d.ts.map +1 -1
  123. package/dist/client/index.js +42 -41
  124. package/dist/client/index.js.map +1 -1
  125. package/dist/client/internal.d.ts +4 -3
  126. package/dist/client/internal.d.ts.map +1 -1
  127. package/dist/client/internal.js +877 -311
  128. package/dist/client/internal.js.map +1 -1
  129. package/dist/client/link.d.ts +1 -7
  130. package/dist/client/link.d.ts.map +1 -1
  131. package/dist/client/navigation-api.d.ts +2 -2
  132. package/dist/client/navigation-commit.d.ts +87 -0
  133. package/dist/client/navigation-commit.d.ts.map +1 -0
  134. package/dist/client/navigation-context.d.ts +37 -20
  135. package/dist/client/navigation-context.d.ts.map +1 -1
  136. package/dist/client/navigation-root.d.ts +63 -23
  137. package/dist/client/navigation-root.d.ts.map +1 -1
  138. package/dist/client/params-context.d.ts +81 -0
  139. package/dist/client/params-context.d.ts.map +1 -0
  140. package/dist/client/router-effects.d.ts +129 -0
  141. package/dist/client/router-effects.d.ts.map +1 -0
  142. package/dist/client/router-lifecycle.d.ts +55 -0
  143. package/dist/client/router-lifecycle.d.ts.map +1 -0
  144. package/dist/client/router-pipeline.d.ts +84 -0
  145. package/dist/client/router-pipeline.d.ts.map +1 -0
  146. package/dist/client/router-ref.d.ts +1 -1
  147. package/dist/client/router-ref.d.ts.map +1 -1
  148. package/dist/client/router-skew.d.ts +21 -0
  149. package/dist/client/router-skew.d.ts.map +1 -0
  150. package/dist/client/router-types.d.ts +207 -0
  151. package/dist/client/router-types.d.ts.map +1 -0
  152. package/dist/client/router.d.ts +1 -182
  153. package/dist/client/router.d.ts.map +1 -1
  154. package/dist/client/rsc-fetch.d.ts +47 -26
  155. package/dist/client/rsc-fetch.d.ts.map +1 -1
  156. package/dist/client/segment-cache.d.ts +87 -34
  157. package/dist/client/segment-cache.d.ts.map +1 -1
  158. package/dist/client/ssr-data.d.ts +6 -0
  159. package/dist/client/ssr-data.d.ts.map +1 -1
  160. package/dist/client/stale-client.d.ts +69 -0
  161. package/dist/client/stale-client.d.ts.map +1 -0
  162. package/dist/client/state.d.ts +10 -1
  163. package/dist/client/state.d.ts.map +1 -1
  164. package/dist/client/top-loader.d.ts +2 -1
  165. package/dist/client/top-loader.d.ts.map +1 -1
  166. package/dist/client/unload-guard.d.ts +5 -0
  167. package/dist/client/unload-guard.d.ts.map +1 -1
  168. package/dist/client/use-cookie.d.ts.map +1 -1
  169. package/dist/client/use-query-states.d.ts +9 -3
  170. package/dist/client/use-query-states.d.ts.map +1 -1
  171. package/dist/client/use-router.d.ts +1 -1
  172. package/dist/client/use-segment-params.d.ts +18 -8
  173. package/dist/client/use-segment-params.d.ts.map +1 -1
  174. package/dist/codec.js +1 -1
  175. package/dist/cookies/define-cookie.d.ts.map +1 -1
  176. package/dist/cookies/index.js +5 -5
  177. package/dist/cookies/index.js.map +1 -1
  178. package/dist/index.d.ts +4 -1
  179. package/dist/index.d.ts.map +1 -1
  180. package/dist/index.js +1009 -759
  181. package/dist/index.js.map +1 -1
  182. package/dist/params/index.js +1 -1
  183. package/dist/plugin-context.d.ts +46 -0
  184. package/dist/plugin-context.d.ts.map +1 -1
  185. package/dist/plugins/adapter-build.d.ts.map +1 -1
  186. package/dist/plugins/build-manifest.d.ts +1 -1
  187. package/dist/plugins/build-manifest.d.ts.map +1 -1
  188. package/dist/plugins/entries.d.ts.map +1 -1
  189. package/dist/plugins/fonts.d.ts.map +1 -1
  190. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  191. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  192. package/dist/plugins/request-dep/index.d.ts +75 -0
  193. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  194. package/dist/plugins/request-dep/origin.d.ts +61 -0
  195. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  196. package/dist/plugins/routing.d.ts.map +1 -1
  197. package/dist/plugins/shims.d.ts.map +1 -1
  198. package/dist/plugins/static-build.d.ts +160 -8
  199. package/dist/plugins/static-build.d.ts.map +1 -1
  200. package/dist/routing/codegen-shared.d.ts +3 -44
  201. package/dist/routing/codegen-shared.d.ts.map +1 -1
  202. package/dist/routing/codegen-types.d.ts +28 -31
  203. package/dist/routing/codegen-types.d.ts.map +1 -1
  204. package/dist/routing/codegen-write.d.ts +51 -0
  205. package/dist/routing/codegen-write.d.ts.map +1 -0
  206. package/dist/routing/codegen.d.ts.map +1 -1
  207. package/dist/routing/collision-probe.d.ts +104 -0
  208. package/dist/routing/collision-probe.d.ts.map +1 -0
  209. package/dist/routing/collision-spaces.d.ts +26 -0
  210. package/dist/routing/collision-spaces.d.ts.map +1 -0
  211. package/dist/routing/convention-lint.d.ts +18 -4
  212. package/dist/routing/convention-lint.d.ts.map +1 -1
  213. package/dist/routing/export-detect.d.ts +16 -0
  214. package/dist/routing/export-detect.d.ts.map +1 -1
  215. package/dist/routing/index.js +4 -3
  216. package/dist/routing/interception.d.ts +235 -6
  217. package/dist/routing/interception.d.ts.map +1 -1
  218. package/dist/routing/link-codegen.d.ts +19 -4
  219. package/dist/routing/link-codegen.d.ts.map +1 -1
  220. package/dist/routing/manifest-codegen.d.ts +1 -7
  221. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  222. package/dist/routing/scanner.d.ts.map +1 -1
  223. package/dist/routing/segment-classify.d.ts +33 -0
  224. package/dist/routing/segment-classify.d.ts.map +1 -1
  225. package/dist/routing/segment-keys.d.ts +95 -0
  226. package/dist/routing/segment-keys.d.ts.map +1 -0
  227. package/dist/routing/slot-placement.d.ts +48 -0
  228. package/dist/routing/slot-placement.d.ts.map +1 -0
  229. package/dist/routing/types.d.ts +0 -6
  230. package/dist/routing/types.d.ts.map +1 -1
  231. package/dist/routing/walkers.d.ts +6 -0
  232. package/dist/routing/walkers.d.ts.map +1 -1
  233. package/dist/schema-bridge.d.ts +60 -9
  234. package/dist/schema-bridge.d.ts.map +1 -1
  235. package/dist/search-params/define.d.ts +62 -14
  236. package/dist/search-params/define.d.ts.map +1 -1
  237. package/dist/search-params/index.d.ts +0 -1
  238. package/dist/search-params/index.d.ts.map +1 -1
  239. package/dist/search-params/index.js +221 -10
  240. package/dist/search-params/index.js.map +1 -1
  241. package/dist/search-params/parse-total.d.ts +70 -0
  242. package/dist/search-params/parse-total.d.ts.map +1 -0
  243. package/dist/search-params/wrappers.d.ts +26 -3
  244. package/dist/search-params/wrappers.d.ts.map +1 -1
  245. package/dist/server/access-gate.d.ts +19 -8
  246. package/dist/server/access-gate.d.ts.map +1 -1
  247. package/dist/server/action-handler.d.ts.map +1 -1
  248. package/dist/server/actions.d.ts +6 -3
  249. package/dist/server/actions.d.ts.map +1 -1
  250. package/dist/server/als-registry.d.ts +18 -1
  251. package/dist/server/als-registry.d.ts.map +1 -1
  252. package/dist/server/build-manifest.d.ts +13 -1
  253. package/dist/server/build-manifest.d.ts.map +1 -1
  254. package/dist/server/chain-url-parts.d.ts +82 -0
  255. package/dist/server/chain-url-parts.d.ts.map +1 -0
  256. package/dist/server/children-interception.d.ts +49 -0
  257. package/dist/server/children-interception.d.ts.map +1 -0
  258. package/dist/server/compress.d.ts.map +1 -1
  259. package/dist/server/default-status-page.d.ts +45 -0
  260. package/dist/server/default-status-page.d.ts.map +1 -0
  261. package/dist/server/deny-boundary.d.ts +148 -15
  262. package/dist/server/deny-boundary.d.ts.map +1 -1
  263. package/dist/server/deny-renderer.d.ts +15 -4
  264. package/dist/server/deny-renderer.d.ts.map +1 -1
  265. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  266. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  267. package/dist/server/head-response.d.ts +35 -0
  268. package/dist/server/head-response.d.ts.map +1 -0
  269. package/dist/server/index.d.ts +0 -1
  270. package/dist/server/index.d.ts.map +1 -1
  271. package/dist/server/index.js +3 -2
  272. package/dist/server/index.js.map +1 -1
  273. package/dist/server/internal.d.ts +3 -1
  274. package/dist/server/internal.d.ts.map +1 -1
  275. package/dist/server/internal.js +712 -418
  276. package/dist/server/internal.js.map +1 -1
  277. package/dist/server/metadata-collector.d.ts +52 -0
  278. package/dist/server/metadata-collector.d.ts.map +1 -0
  279. package/dist/server/param-coercion.d.ts +22 -6
  280. package/dist/server/param-coercion.d.ts.map +1 -1
  281. package/dist/server/pipeline-helpers.d.ts +0 -18
  282. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  283. package/dist/server/pipeline-interception.d.ts +59 -9
  284. package/dist/server/pipeline-interception.d.ts.map +1 -1
  285. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  286. package/dist/server/pipeline-phases.d.ts.map +1 -1
  287. package/dist/server/pipeline.d.ts +11 -0
  288. package/dist/server/pipeline.d.ts.map +1 -1
  289. package/dist/server/primitives.d.ts +23 -0
  290. package/dist/server/primitives.d.ts.map +1 -1
  291. package/dist/server/publish-params.d.ts +33 -0
  292. package/dist/server/publish-params.d.ts.map +1 -0
  293. package/dist/server/request-context.d.ts +48 -0
  294. package/dist/server/request-context.d.ts.map +1 -1
  295. package/dist/server/route-element-builder.d.ts +22 -3
  296. package/dist/server/route-element-builder.d.ts.map +1 -1
  297. package/dist/server/route-handler.d.ts.map +1 -1
  298. package/dist/server/route-matcher.d.ts +13 -0
  299. package/dist/server/route-matcher.d.ts.map +1 -1
  300. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  301. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  302. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  303. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  304. package/dist/server/rsc-entry/error-renderer.d.ts +15 -3
  305. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  306. package/dist/server/rsc-entry/helpers.d.ts +2 -17
  307. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  308. package/dist/server/rsc-entry/index.d.ts +0 -1
  309. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  310. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  311. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  312. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -2
  313. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  314. package/dist/server/rsc-entry/rsc-stream.d.ts +15 -2
  315. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  316. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  317. package/dist/server/skippable-prefix.d.ts +23 -0
  318. package/dist/server/skippable-prefix.d.ts.map +1 -0
  319. package/dist/server/slot-interception.d.ts +80 -0
  320. package/dist/server/slot-interception.d.ts.map +1 -0
  321. package/dist/server/slot-resolver.d.ts +70 -8
  322. package/dist/server/slot-resolver.d.ts.map +1 -1
  323. package/dist/server/ssr-bridge-types.d.ts +17 -0
  324. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  325. package/dist/server/ssr-entry.d.ts +0 -1
  326. package/dist/server/ssr-entry.d.ts.map +1 -1
  327. package/dist/server/ssr-wrappers.d.ts +24 -13
  328. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  329. package/dist/server/state-tree-diff.d.ts +6 -27
  330. package/dist/server/state-tree-diff.d.ts.map +1 -1
  331. package/dist/server/static-generator.d.ts +6 -0
  332. package/dist/server/static-generator.d.ts.map +1 -1
  333. package/dist/server/static-not-found.d.ts +52 -0
  334. package/dist/server/static-not-found.d.ts.map +1 -0
  335. package/dist/server/status-code-resolver.d.ts +8 -1
  336. package/dist/server/status-code-resolver.d.ts.map +1 -1
  337. package/dist/server/tree-builder.d.ts +28 -36
  338. package/dist/server/tree-builder.d.ts.map +1 -1
  339. package/dist/server/types.d.ts +12 -7
  340. package/dist/server/types.d.ts.map +1 -1
  341. package/dist/server/utils/element-type.d.ts +40 -0
  342. package/dist/server/utils/element-type.d.ts.map +1 -0
  343. package/dist/shared/als-slots.d.ts +41 -0
  344. package/dist/shared/als-slots.d.ts.map +1 -0
  345. package/dist/shared/base-path.d.ts +56 -0
  346. package/dist/shared/base-path.d.ts.map +1 -0
  347. package/dist/shared/param-value.d.ts +80 -0
  348. package/dist/shared/param-value.d.ts.map +1 -0
  349. package/dist/shared/payload-root.d.ts +108 -0
  350. package/dist/shared/payload-root.d.ts.map +1 -0
  351. package/dist/shared/rsc-cache-key.d.ts +163 -0
  352. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  353. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  354. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  355. package/dist/shared/rsc-manifest.d.ts +28 -0
  356. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  357. package/dist/shared/rsc-media-type.d.ts +40 -0
  358. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  359. package/dist/shared/rsc-payload-path.d.ts +42 -0
  360. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  361. package/dist/shared/segment-info.d.ts +59 -0
  362. package/dist/shared/segment-info.d.ts.map +1 -0
  363. package/dist/shared/slot-params.d.ts +51 -0
  364. package/dist/shared/slot-params.d.ts.map +1 -0
  365. package/dist/shared/static-platform-files.d.ts +71 -0
  366. package/dist/shared/static-platform-files.d.ts.map +1 -0
  367. package/docs/api/30-api-server.mdx +1 -1
  368. package/docs/api/31-api-client.mdx +4 -2
  369. package/docs/api/33-api-search-params.mdx +38 -16
  370. package/docs/api/35-api-typescript.mdx +3 -3
  371. package/docs/api/36-cli.mdx +34 -7
  372. package/docs/learn/00-introduction.mdx +1 -1
  373. package/docs/learn/02-pages-and-layouts.mdx +2 -2
  374. package/docs/learn/05-typed-params.mdx +8 -8
  375. package/docs/learn/07-typed-routes.mdx +12 -7
  376. package/docs/learn/11-error-handling.mdx +18 -0
  377. package/docs/learn/12-client-navigation.mdx +2 -2
  378. package/docs/learn/14-deploying.mdx +23 -0
  379. package/docs/more/01-advanced-routing.mdx +173 -3
  380. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  381. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  382. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  383. package/package.json +9 -6
  384. package/src/adapters/build-output-helper.ts +116 -3
  385. package/src/adapters/cloudflare.ts +90 -12
  386. package/src/adapters/compress-module.ts +79 -1
  387. package/src/adapters/fs-identity.ts +156 -0
  388. package/src/adapters/nitro.ts +6 -6
  389. package/src/adapters/shared.ts +196 -21
  390. package/src/adapters/types.ts +13 -1
  391. package/src/cache/fast-hash.ts +5 -0
  392. package/src/cli-check.ts +458 -0
  393. package/src/cli.ts +59 -24
  394. package/src/client/browser-entry/action-dispatch.ts +11 -5
  395. package/src/client/browser-entry/hydrate.ts +40 -29
  396. package/src/client/browser-entry/index.ts +15 -6
  397. package/src/client/browser-entry/post-hydration.ts +10 -9
  398. package/src/client/browser-entry/router-init.ts +104 -48
  399. package/src/client/browser-entry/rsc-stream.ts +11 -2
  400. package/src/client/error-boundary.tsx +85 -16
  401. package/src/client/history.ts +13 -6
  402. package/src/client/index.ts +14 -3
  403. package/src/client/internal.ts +4 -3
  404. package/src/client/link.tsx +120 -72
  405. package/src/client/navigation-api.ts +3 -3
  406. package/src/client/navigation-commit.ts +197 -0
  407. package/src/client/navigation-context.ts +40 -67
  408. package/src/client/navigation-root.tsx +326 -101
  409. package/src/client/params-context.ts +192 -0
  410. package/src/client/router-effects.ts +231 -0
  411. package/src/client/router-lifecycle.ts +288 -0
  412. package/src/client/router-pipeline.ts +393 -0
  413. package/src/client/router-ref.ts +1 -1
  414. package/src/client/router-skew.ts +30 -0
  415. package/src/client/router-types.ts +230 -0
  416. package/src/client/router.ts +243 -707
  417. package/src/client/rsc-fetch.ts +183 -106
  418. package/src/client/segment-cache.ts +129 -44
  419. package/src/client/ssr-data.ts +6 -0
  420. package/src/client/stale-client.ts +191 -0
  421. package/src/client/state.ts +14 -1
  422. package/src/client/top-loader.tsx +2 -1
  423. package/src/client/unload-guard.ts +34 -2
  424. package/src/client/use-cookie.ts +9 -2
  425. package/src/client/use-query-states.ts +102 -39
  426. package/src/client/use-router.ts +1 -1
  427. package/src/client/use-segment-params.ts +41 -18
  428. package/src/cookies/define-cookie.ts +6 -1
  429. package/src/index.ts +25 -4
  430. package/src/plugin-context.ts +58 -0
  431. package/src/plugins/adapter-build.ts +4 -0
  432. package/src/plugins/build-manifest.ts +34 -13
  433. package/src/plugins/entries.ts +4 -0
  434. package/src/plugins/fonts.ts +11 -8
  435. package/src/plugins/request-dep/analysis.ts +447 -0
  436. package/src/plugins/request-dep/index.ts +376 -0
  437. package/src/plugins/request-dep/origin.ts +310 -0
  438. package/src/plugins/routing.ts +97 -102
  439. package/src/plugins/shims.ts +0 -1
  440. package/src/plugins/static-build.ts +391 -95
  441. package/src/routing/codegen-shared.ts +3 -79
  442. package/src/routing/codegen-types.ts +28 -31
  443. package/src/routing/codegen-write.ts +139 -0
  444. package/src/routing/codegen.ts +86 -172
  445. package/src/routing/collision-probe.ts +438 -0
  446. package/src/routing/collision-spaces.ts +281 -0
  447. package/src/routing/convention-lint.ts +139 -40
  448. package/src/routing/export-detect.ts +151 -7
  449. package/src/routing/interception.ts +896 -50
  450. package/src/routing/link-codegen.ts +32 -65
  451. package/src/routing/manifest-codegen.ts +6 -62
  452. package/src/routing/scanner.ts +117 -92
  453. package/src/routing/segment-classify.ts +57 -0
  454. package/src/routing/segment-keys.ts +146 -0
  455. package/src/routing/slot-placement.ts +97 -0
  456. package/src/routing/types.ts +0 -6
  457. package/src/routing/walkers.ts +8 -0
  458. package/src/schema-bridge.ts +180 -58
  459. package/src/search-params/define.ts +108 -66
  460. package/src/search-params/index.ts +0 -1
  461. package/src/search-params/parse-total.ts +78 -0
  462. package/src/search-params/wrappers.ts +60 -11
  463. package/src/server/access-gate.tsx +60 -40
  464. package/src/server/action-handler.ts +4 -5
  465. package/src/server/actions.ts +6 -3
  466. package/src/server/als-registry.ts +18 -1
  467. package/src/server/build-manifest.ts +14 -1
  468. package/src/server/chain-url-parts.ts +163 -0
  469. package/src/server/children-interception.ts +115 -0
  470. package/src/server/compress.ts +9 -1
  471. package/src/server/default-status-page.ts +127 -0
  472. package/src/server/deny-boundary.ts +269 -41
  473. package/src/server/deny-renderer.ts +111 -39
  474. package/src/server/error-boundary-wrapper.ts +166 -79
  475. package/src/server/head-response.ts +54 -0
  476. package/src/server/index.ts +1 -3
  477. package/src/server/internal.ts +2 -2
  478. package/src/server/metadata-collector.ts +115 -0
  479. package/src/server/param-coercion.ts +51 -69
  480. package/src/server/pipeline-helpers.ts +2 -47
  481. package/src/server/pipeline-interception.ts +81 -32
  482. package/src/server/pipeline-outcome.ts +4 -5
  483. package/src/server/pipeline-phases.ts +77 -25
  484. package/src/server/pipeline.ts +57 -10
  485. package/src/server/prebuilt-runtime.ts +4 -4
  486. package/src/server/primitives.ts +25 -0
  487. package/src/server/publish-params.ts +42 -0
  488. package/src/server/request-context.ts +97 -18
  489. package/src/server/route-element-builder.ts +241 -253
  490. package/src/server/route-handler.ts +8 -8
  491. package/src/server/route-matcher.ts +21 -9
  492. package/src/server/rsc-cache-key-guard.ts +164 -0
  493. package/src/server/rsc-entry/api-handler.ts +6 -8
  494. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  495. package/src/server/rsc-entry/error-renderer.ts +94 -35
  496. package/src/server/rsc-entry/helpers.ts +33 -28
  497. package/src/server/rsc-entry/index.ts +36 -33
  498. package/src/server/rsc-entry/render-route.ts +18 -10
  499. package/src/server/rsc-entry/revalidate-renderer.ts +3 -2
  500. package/src/server/rsc-entry/rsc-payload.ts +113 -40
  501. package/src/server/rsc-entry/rsc-stream.ts +29 -5
  502. package/src/server/rsc-entry/ssr-renderer.ts +38 -22
  503. package/src/server/skippable-prefix.ts +92 -0
  504. package/src/server/slot-interception.ts +168 -0
  505. package/src/server/slot-resolver.ts +263 -158
  506. package/src/server/ssr-bridge-types.ts +18 -0
  507. package/src/server/ssr-entry.ts +20 -10
  508. package/src/server/ssr-wrappers.tsx +33 -32
  509. package/src/server/state-tree-diff.ts +14 -55
  510. package/src/server/static-generator.ts +68 -29
  511. package/src/server/static-not-found.ts +134 -0
  512. package/src/server/status-code-resolver.ts +28 -11
  513. package/src/server/tree-builder.ts +35 -218
  514. package/src/server/types.ts +12 -7
  515. package/src/server/utils/element-type.ts +72 -0
  516. package/src/shared/als-slots.ts +72 -0
  517. package/src/shared/base-path.ts +80 -0
  518. package/src/shared/param-value.ts +244 -0
  519. package/src/shared/payload-root.ts +151 -0
  520. package/src/shared/rsc-cache-key.ts +269 -0
  521. package/src/shared/rsc-error-envelope.ts +31 -0
  522. package/src/shared/rsc-manifest.ts +29 -0
  523. package/src/shared/rsc-media-type.ts +43 -0
  524. package/src/shared/rsc-payload-path.ts +45 -0
  525. package/src/shared/segment-info.ts +60 -0
  526. package/src/shared/slot-params.ts +111 -0
  527. package/src/shared/static-platform-files.ts +75 -0
  528. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  529. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  530. package/dist/_chunks/cache-api-B-lhk9p4.js.map +0 -1
  531. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  532. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
  533. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
  534. package/dist/_chunks/define-COtkxMRT.js +0 -191
  535. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  536. package/dist/_chunks/define-c4au4I9R.js +0 -82
  537. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  538. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  539. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  540. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  541. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  542. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  543. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  544. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  545. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  546. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  547. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  548. package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
  549. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  550. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  551. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  552. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  553. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  554. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  555. package/dist/_chunks/walkers-_6zKFlch.js +0 -1002
  556. package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
  557. package/dist/client/error-boundary.js.map +0 -1
  558. package/dist/plugins/request-dep.d.ts +0 -32
  559. package/dist/plugins/request-dep.d.ts.map +0 -1
  560. package/dist/search-params/registry.d.ts +0 -20
  561. package/dist/search-params/registry.d.ts.map +0 -1
  562. package/dist/segment-params/define.d.ts +0 -89
  563. package/dist/segment-params/define.d.ts.map +0 -1
  564. package/dist/segment-params/index.d.ts +0 -3
  565. package/dist/segment-params/index.d.ts.map +0 -1
  566. package/dist/segment-params/index.js +0 -2
  567. package/src/plugins/request-dep.ts +0 -499
  568. package/src/search-params/registry.ts +0 -31
  569. package/src/segment-params/define.ts +0 -249
  570. package/src/segment-params/index.ts +0 -9
@@ -67,7 +67,7 @@ This is useful for CDN-cacheable file-extension routes (e.g. image generation en
67
67
  - One param per segment — `[foo]-[bar]` is not supported
68
68
  - Affixes cannot be used with catch-all (`[...slug].png`) or optional catch-all segments
69
69
  - Matching is greedy: `[id].png` matching `a.png.png` yields `id = "a.png"`
70
- - `params.ts` codecs use the bare param name (`id`), not the full segment pattern
70
+ - `app/schema.ts` codecs use the bare bracket form (`[id]`), not the full segment pattern
71
71
 
72
72
  **Priority:** static → affixed dynamic → bare dynamic → catch-all → optional catch-all. A static `foo.png` always wins over `[id].png`.
73
73
 
@@ -112,11 +112,28 @@ export default function DashboardLayout({
112
112
 
113
113
  Slots don't add URL depth. Each slot independently matches the current URL and can have its own `access.ts`, `error.tsx`, and layouts. If a slot's access check fails, the slot degrades gracefully — the rest of the page is unaffected (see [Access Control](/docs/access-control)).
114
114
 
115
- Each slot is also its own collision space: a slot page sharing a URL with the parent page is the point of parallel routes, but two route groups inside one slot producing pages at the same URL (`@side/(a)/x/page.tsx` + `@side/(b)/x/page.tsx`) fail the build, just like the same collision outside a slot.
115
+ **A slot cannot be nested inside another slot.** Slots are resolved for segments on the matched route chain, and a slot's own subtree is not on that chain — so a nested slot could never render, and the build rejects it:
116
+
117
+ ```
118
+ app/dashboard/layout.tsx ← receives { children, sidebar }
119
+ app/dashboard/@sidebar/page.tsx ✓
120
+ app/dashboard/settings/layout.tsx ← receives { children, panel }
121
+ app/dashboard/settings/@panel/page.tsx ✓ a second slot deeper on the main chain
122
+ app/dashboard/@sidebar/@panel/page.tsx ✗ build error — nested inside @sidebar
123
+ app/dashboard/@sidebar/settings/@panel/... ✗ build error — same, further down
124
+ ```
125
+
126
+ This is a build error rather than a documented limit because nothing at request time could tell you: a slot that never rendered looks exactly like a slot that simply didn't match. Put the second slot on a segment of the main route chain instead.
127
+
128
+ Note the `layout.tsx` beside each slot. A slot is handed to its owning segment's layout as a named prop, so **the owner must have one** — `app/dashboard/@sidebar/` with no `app/dashboard/layout.tsx` has nothing to be passed to and never renders. That case is dead in the same way as a nested slot, and the build does not currently catch it.
129
+
130
+ Each slot is also its own collision space: a slot page sharing a URL with the parent page is the point of parallel routes, but two pages inside one slot that answer the same URL (`@side/(a)/x/page.tsx` + `@side/(b)/x/page.tsx`) fail the build, just like the same collision outside a slot. Interceptors that start at the same URL are their own space on the same principle — see [Intercepting routes](#intercepting-routes).
131
+
132
+ Every collision space is checked the same way: routes are compared by the URLs they match rather than by how they are spelled, so `(a)/[id]` and `(b)/[slug]` collide even though their paths read differently, and a page collides with an optional catch-all beside it because `[[...rest]]` also matches with zero segments. Segment kinds stay distinct — `[id]` beside `[...rest]`, or an affixed `img-[id]` beside a bare `[slug]`, are ordered deterministically and keep building.
116
133
 
117
134
  ## Intercepting Routes
118
135
 
119
- Intercepting routes show a different component on soft navigation (client-side link click) vs hard navigation (direct URL). This enables the modal pattern:
136
+ Intercepting routes show a different component on soft navigation (client-side link click) vs hard navigation (direct URL).
120
137
 
121
138
  | Marker | Meaning |
122
139
  | ---------- | ------------- |
@@ -125,6 +142,159 @@ Intercepting routes show a different component on soft navigation (client-side l
125
142
  | `(..)(..)` | Two levels up |
126
143
  | `(...)` | Root level |
127
144
 
145
+ Where you put the intercepting directory picks between two behaviours.
146
+
147
+ **Inside a parallel slot → modal overlay.** The slot renders the intercepting page on top; the page you came from stays mounted behind it.
148
+
149
+ ```
150
+ app/feed/@modal/(.)photo/[id]/page.tsx ← soft nav: modal over the feed
151
+ app/feed/photo/[id]/page.tsx ← hard nav: the full page
152
+ ```
153
+
154
+ **As a plain child of a segment or route group → sibling layout swap.** No overlay and no slot: the intercepting page replaces the content of *its own* directory's layout chain. This is how two route groups can serve one URL with different layouts.
155
+
156
+ ```
157
+ app/(landing)/layout.tsx
158
+ app/(landing)/[artistSlug]/[year]/page.tsx ← hard nav renders this
159
+ app/(browse)/layout.tsx
160
+ app/(browse)/(...)[artistSlug]/[year]/page.tsx ← soft nav from inside (browse) renders this
161
+ ```
162
+
163
+ Navigate directly to `/grateful-dead/1977` and you get `(landing)`'s page under `(landing)`'s layout. Click a link to it from anywhere inside `(browse)` and you stay in `(browse)`'s layout with the intercepting page beneath it. Unlike the modal shape, the intercepting page's params come from the URL you navigated to, so `getSegmentParams()` works exactly as it does on a canonical route.
164
+
165
+ An intercepting directory must be a **direct** child of one or the other. A marker one directory deeper inside a slot, or nested inside another intercepting subtree, would match the navigation and then have nothing to render — the build fails naming the directory rather than silently serving the wrong page.
166
+
167
+ ```
168
+ app/feed/@modal/(.)photo/[id]/page.tsx ✓ modal overlay
169
+ app/feed/(.)photo/[id]/page.tsx ✓ layout swap (feed/layout.tsx required)
170
+ app/(browse)/(...)photo/[id]/page.tsx ✓ layout swap, scoped to (browse)
171
+ app/feed/@modal/(x)/(.)photo/page.tsx ✗ build error — not a direct slot child
172
+ app/feed/(.)a/(.)b/page.tsx ✗ build error — nested interception
173
+ ```
174
+
175
+ Outside a slot, the directory holding the marker must have a `layout.tsx`. That is the directory the swap is scoped to, and the browser reports which directories it has mounted from its layouts — a scope it cannot report would let the swap fire once and then quietly stop. In practice you already have one, since rendering inside that layout is the point.
176
+
177
+ ```
178
+ app/(browse)/layout.tsx ✓ required
179
+ app/(browse)/(...)photo/[id]/page.tsx
180
+ ```
181
+
182
+ Also outside a slot, every directory above the marker must be a static segment or a route group. The swapped page renders its own layouts against the URL you navigated to, and that URL carries no values for params declared above the marker — so a dynamic ancestor is a build error rather than a layout that renders with a missing param. Put the same interception in a slot if you need one: the modal shape re-matches the source route, which supplies them.
183
+
184
+ ```
185
+ app/(browse)/(...)photo/[id]/page.tsx ✓
186
+ app/docs/guides/(.)[id]/page.tsx ✓
187
+ app/[team]/(..)photo/[id]/page.tsx ✗ build error — [team] is above the marker
188
+ app/[team]/@modal/(.)photo/[id]/page.tsx ✓ a slot supplies [team] from the source route
189
+ ```
190
+
191
+ Two pages that intercept the same URL are a build error when both sit under interceptors that **start at the same URL** — the set timber resolves in one pass. Route groups are transparent here just as they are elsewhere, so two of them under one marker both claim the same target; and two markers that compute the same starting point are the same case one level up. Either way, which page renders would come down to the order the filesystem happens to list them in.
192
+
193
+ Pages are compared by the URLs they match, not by how you spell them — so param names don't make two dynamic segments different, and an optional catch-all also matches with *zero* segments.
194
+
195
+ ```
196
+ app/feed/(.)photo/(a)/page.tsx ✗ build error — both intercept /feed/photo
197
+ app/feed/(.)photo/(b)/page.tsx
198
+
199
+ app/feed/(.)photo/(a)/[id]/page.tsx ✗ build error — [id] and [slug] match the same URLs
200
+ app/feed/(.)photo/(b)/[slug]/page.tsx
201
+
202
+ app/feed/(.)photo/(a)/page.tsx ✗ build error — [[...rest]] also matches zero segments,
203
+ app/feed/(.)photo/(b)/[[...rest]]/page.tsx so it claims /feed/photo too
204
+
205
+ app/feed/(.)photo/page.tsx ✓ a page plus a tail — the page always wins
206
+ app/feed/(.)photo/[[...rest]]/page.tsx /feed/photo, the catch-all takes the rest
207
+
208
+ app/feed/(.)photo/(x)/a/page.tsx ✓ different URLs
209
+ app/feed/(.)photo/(y)/b/page.tsx
210
+
211
+ app/feed/(.)photo/(x)/[id]/page.tsx ✓ different kinds — a single segment always
212
+ app/feed/(.)photo/(y)/[...rest]/page.tsx beats a catch-all, so this is unambiguous
213
+
214
+ app/feed/@modal/(.)[a]/page.tsx ✗ build error — two markers, one starting point,
215
+ app/feed/@modal/(.)[b]/page.tsx same kind: nothing separates them
216
+
217
+ app/feed/@modal/(.)photo/[id]/page.tsx ✓ same starting point, different kinds — the
218
+ app/feed/@modal/(.)[albumSlug]/page.tsx literal `photo` wins, everything else the second
219
+ ```
220
+
221
+ A route group is transparent for matching, so putting a page inside one does not give it a URL of its own:
222
+
223
+ ```
224
+ app/feed/(.)photo/page.tsx ✗ build error — both are /feed/photo, so the
225
+ app/feed/(.)photo/(a)/page.tsx one in the group could never render
226
+ ```
227
+
228
+ Interceptors that start at **different** URLs stay legal even when they target one page — `(.)x` beside `(..)x`, or interceptors in two different slots. Those are separate spaces, and the resolver orders them (deepest marker first).
229
+
230
+ ### Dynamic intercepted segments
231
+
232
+ The marker is a prefix on an ordinary segment name, so the name after it uses the same bracket syntax as anywhere else:
233
+
234
+ ```
235
+ app/gallery/@modal/(.)photo/[id]/page.tsx # intercepts /gallery/photo/{id}
236
+ app/gallery/@modal/(.)[albumSlug]/page.tsx # intercepts /gallery/{albumSlug}
237
+ app/gallery/@modal/(.)[...rest]/page.tsx # intercepts the whole tail
238
+ ```
239
+
240
+ Intercepting siblings in one slot follow the usual priority — static, then dynamic, then catch-all — so `(.)photo/[id]` and `(.)[albumSlug]` can live in the same `@modal` without ambiguity.
241
+
242
+ Two affixed interceptors are the one case priority cannot separate: `(.)a-[id]` and `(.)[slug]-b` both match `/gallery/a-b` with the same combined affix length, so which modal opened would come down to filesystem order. That fails the build, exactly as it does for ordinary affixed siblings.
243
+
244
+ One placement fails the build: an interception whose intercepted level lands below a catch-all. Markers count URL segments, and a catch-all matches a number of them that changes per request, so "same level as `/docs/[...path]`" has no fixed position in `/docs/a/b/c`. Such an interception would fire only when the catch-all consumed exactly one segment and be silently dropped otherwise, so timber names the directory in a build error instead. Climbing past the catch-all avoids it:
245
+
246
+ ```
247
+ app/docs/[...path]/@modal/(.)photo/[id]/page.tsx ✗ build error
248
+ app/docs/[...path]/@modal/(..)photo/[id]/page.tsx ✓ (..) climbs past it, basing at /docs
249
+ app/docs/[...path]/@modal/(...)photo/[id]/page.tsx ✓ (...) bases at the root
250
+ app/[artistSlug]/@modal/(.)photo/[id]/page.tsx ✓ a dynamic segment is one URL part
251
+ ```
252
+
253
+ A catch-all inside an intercepting route must also be the last segment — nothing below a catch-all can ever match, and the rewrite generated for the unreachable page would hijack the navigation with nothing to render:
254
+
255
+ ```
256
+ app/feed/@modal/(.)[...rest]/page.tsx ✓
257
+ app/feed/@modal/(.)photo/[...rest]/page.tsx ✓
258
+ app/feed/@modal/(.)[...rest]/child/page.tsx ✗ build error — child is unreachable
259
+ app/feed/@modal/(.)photo/[...rest]/child/page.tsx ✗ build error — same, one level down
260
+ ```
261
+
262
+ ### Params in intercepting directories
263
+
264
+ A dynamic intercepted segment binds a param like any other, so the ordinary param rules apply to it:
265
+
266
+ ```
267
+ app/feed/@modal/(.)[id]/[id]/page.tsx ✗ build error — duplicate param name
268
+ app/feed/@modal/(.)[albumSlug]/[id]/... ✓
269
+ ```
270
+
271
+ ### Params inside an intercepting page
272
+
273
+ This section is about the **modal** shape. A layout-swap interception has nothing special going on: its chain is the main route, so `getSegmentParams()` returns the URL's params everywhere in it, exactly as on a canonical route.
274
+
275
+ Inside a slot, `getSegmentParams()` returns **the intercepted route's** params — what the page's own directories declare:
276
+
277
+ ```tsx title="app/gallery/@modal/(.)photo/[id]/page.tsx"
278
+ export default async function PhotoModal() {
279
+ const { id } = getSegmentParams(); // the photo's id, not the gallery's params
280
+ return <Modal photo={await getPhoto(id)} />;
281
+ }
282
+ ```
283
+
284
+ That is worth stating because the request itself is matched against the *source* route (`/gallery`) — the page still rendering behind the modal. The modal's own params are scoped to it, and the source page underneath keeps reading its own.
285
+
286
+ This is the general slot rule: inside a parallel slot, a no-arg `getSegmentParams()` returns that slot's own params merged over the main route's — the same value as passing the slot's segment path explicitly. Params the slot re-derives win, so a slot that reads a URL part as `[...year]` gets `string[]` even when the main route read it as `[year]`.
287
+
288
+ ### Scope
289
+
290
+ An interception fires only when the departing page renders the directory that owns it — the slot's parent for a modal, the marker's own parent for a layout swap. For `app/feed/@modal/(.)photo/`, that directory is `/feed`: clicking a photo link from `/feed` or `/feed/trending` opens the modal; clicking one from `/settings` does not.
291
+
292
+ Route groups count here even though they are invisible in the URL. Anything owned by `app/(browse)/` is scoped to `(browse)`, so a navigation starting in a sibling `(landing)` group renders the normal route, not the interception — that is what makes the layout-swap shape work. A directory at the app root is scoped to the root, which every page renders, which is why `(...)` at the root intercepts from anywhere.
293
+
294
+ **When two interceptions both apply, the innermost one wins.** A directory and a directory beneath it may each hold a marker for the same URL, and a page inside the inner one renders both — so `app/feed/(.)photo/` and `app/feed/(a)/(.)photo/` both qualify for a click made inside `(a)`. The one scoped to `(a)` renders, on the same reasoning that a `(.)` marker beats a `(...)` one beside it: the more local directory is the more specific statement. Directories that are *not* on one chain — `(a)` beside `(b)` — never both qualify, so they stay independent.
295
+
296
+ Because scope depends on where the click starts, so does a prefetch: `<Link prefetch>` fetches the variant for the page the link is on, and the cached payload is only used by a click that starts there. Hovering an intercepting link gets you the modal; the same URL prefetched from elsewhere stays the full page.
297
+
128
298
  ## API Routes
129
299
 
130
300
  Export HTTP method handlers from `route.ts`:
@@ -64,9 +64,9 @@ app/
64
64
 
65
65
  **Next.js:** `useSearchParams()` hook returns a `URLSearchParams` object.
66
66
 
67
- **timber:** Typed search params via `defineSearchParams` in a `params.ts` file. Server reads via `.get()`, client reads/writes via `.useQueryStates()`. Segment params are defined globally in `app/schema.ts` via `defineSchema()`, accessed with `getSegmentParams(SEGMENT_PATH)` using the `$segment` module.
67
+ **timber:** Typed search params via `defineSearchParams` in any module you import. Server reads via `.get()`, client reads/writes via `.useQueryStates()`. Segment params are defined globally in `app/schema.ts` via `defineSchema()`, accessed with `getSegmentParams(SEGMENT_PATH)` using the `$segment` module.
68
68
 
69
- ```ts title="app/products/params.ts"
69
+ ```ts title="app/products/search-params.ts"
70
70
  import { defineSearchParams } from '@timber-js/app/search-params';
71
71
  import { z } from 'zod/v4';
72
72
 
@@ -91,7 +91,7 @@ If you declare `openGraph.images` in your metadata, both auto-linked tags are su
91
91
 
92
92
  `favicon.tsx` lets you generate favicons dynamically (e.g. tenant branding). The URL stays `/favicon.ico` for browser compatibility.
93
93
 
94
- For OG image generation, use `@takumi-rs/image-response` (BYO package — not bundled with timber):
94
+ For OG image generation, use `takumi-js/response` (BYO package — not bundled with timber):
95
95
 
96
96
  ```tsx title="app/opengraph-image.tsx"
97
97
  import { ImageResponse } from 'takumi-js/response';
@@ -59,7 +59,7 @@ These are the things you will get wrong if you assume Next.js behavior.
59
59
  | Middleware | Single global `middleware.ts` with matchers | Per-segment `middleware.ts` files + global `proxy.ts`. One-arg signature: `middleware(ctx)`. |
60
60
  | `notFound()` | `notFound()` from `next/navigation` | `deny(404)` from `@timber-js/app/server`. Sends a real HTTP 404. |
61
61
  | `redirect()` | `redirect()` from `next/navigation` | `redirect()` from `@timber-js/app/server`. Same concept, different import. |
62
- | Search params | Untyped `useSearchParams()` | Typed `defineSearchParams()` with Zod schemas in `params.ts`. |
62
+ | Search params | Untyped `useSearchParams()` | Typed `defineSearchParams()` with Zod schemas, in any module you import. |
63
63
  | Auth pattern | In middleware or components | `access.ts` per segment with slot degradation. |
64
64
  | Forms | `useFormState` / `useFormStatus` | `createActionClient` + `useActionState` returns a 4-tuple. Forms work without JS. |
65
65
 
@@ -71,7 +71,7 @@ These are the things you will get wrong if you assume Next.js behavior.
71
71
  | `Link`, `useRouter`, `usePathname`, `useActionState` | `@timber-js/app/client` |
72
72
  | `cache`, `CacheHandler` | `@timber-js/app/cache` |
73
73
  | `defineSearchParams` | `@timber-js/app/search-params` |
74
- | `defineSchema`, `defineSegmentParams` | `@timber-js/app/params` |
74
+ | `defineSchema`, `InferCodecType` | `@timber-js/app/params` |
75
75
  | Content collection data | `content-collections` |
76
76
  | `Metadata` type | `@timber-js/app/server` |
77
77
 
@@ -90,7 +90,7 @@ app/ # Route tree — file-system routing
90
90
  access.ts # Per-segment authorization
91
91
  denied.tsx # Rendered when access.ts denies
92
92
  error.tsx # Error boundary for this segment
93
- params.ts # Search param definitions
93
+ search-params.ts # Search param definitions (ordinary module, not a convention)
94
94
  schema.ts # Segment param schemas (from @timber-js/app/params)
95
95
  content/ # Content collections (outside route tree)
96
96
  timber.config.ts # Framework configuration
@@ -104,6 +104,8 @@ content-collections.ts # Collection definitions (optional)
104
104
 
105
105
  2. **Adding `'use client'` to everything** — components are server components by default. Only add `'use client'` when you need browser APIs, event handlers, or React hooks like `useState`/`useEffect`.
106
106
 
107
+ The exception: `error.tsx`, `global-error.tsx`, and component status files (`404.tsx`, `4xx.tsx`, `503.tsx`, …) **always** need `'use client'`. timber passes them to a client error boundary as a prop, so a server component there breaks every page under that segment. The build rejects it and names the file. MDX status files (`404.mdx`) are exempt.
108
+
107
109
  3. **Using `fetch()` and expecting caching** — timber never patches `fetch`. Wrap data-fetching functions in `cache.data()` from `@timber-js/app/cache` when you want caching.
108
110
 
109
111
  4. **Creating `loading.tsx` files** — this convention does not exist. Use `<Suspense>` with an explicit fallback where you want streaming.
@@ -114,7 +116,7 @@ content-collections.ts # Collection definitions (optional)
114
116
 
115
117
  7. **Putting all middleware in one file** — middleware is per-segment. Place `middleware.ts` at the route level that matches the concern. Use `proxy.ts` for global concerns.
116
118
 
117
- 8. **Assuming `useSearchParams()` exists** — use `defineSearchParams()` in a `params.ts` file with Zod schemas, then access via `.get()` (server) or `.useQueryStates()` (client).
119
+ 8. **Assuming `useSearchParams()` exists** — use `defineSearchParams()` in any module with Zod schemas, then access via `.get()` (server) or `.useQueryStates()` (client).
118
120
 
119
121
  ## Quick Patterns
120
122
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.185",
3
+ "version": "0.2.0-alpha.187",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -113,10 +113,6 @@
113
113
  "types": "./dist/search-params/index.d.ts",
114
114
  "import": "./dist/search-params/index.js"
115
115
  },
116
- "./segment-params": {
117
- "types": "./dist/segment-params/index.d.ts",
118
- "import": "./dist/segment-params/index.js"
119
- },
120
116
  "./params": {
121
117
  "types": "./dist/params/index.d.ts",
122
118
  "import": "./dist/params/index.js"
@@ -165,6 +161,7 @@
165
161
  "@content-collections/mdx": "^0.2.0",
166
162
  "@content-collections/vite": "^0.2.0 || ^0.3.0",
167
163
  "@mdx-js/rollup": "^3.0.0",
164
+ "@typescript/native-preview": "^7.0.0-dev.0",
168
165
  "@vitejs/plugin-react": "^6.0.0",
169
166
  "@vitejs/plugin-rsc": ">=0.5.28",
170
167
  "nuqs": "^2.0.0",
@@ -187,6 +184,9 @@
187
184
  "@mdx-js/rollup": {
188
185
  "optional": true
189
186
  },
187
+ "@typescript/native-preview": {
188
+ "optional": true
189
+ },
190
190
  "wrangler": {
191
191
  "optional": true
192
192
  },
@@ -194,12 +194,15 @@
194
194
  "optional": true
195
195
  }
196
196
  },
197
+ "devDependencies": {
198
+ "@typescript/native-preview": "7.0.0-dev.20260707.2"
199
+ },
197
200
  "engines": {
198
201
  "node": ">=22.18.0"
199
202
  },
200
203
  "scripts": {
201
204
  "copy-docs": "node scripts/copy-docs.js",
202
- "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist && pnpm run copy-docs",
205
+ "build": "vite build --config vite.lib.config.ts && node scripts/build-types.js && pnpm run copy-docs",
203
206
  "typecheck": "tsgo --noEmit"
204
207
  }
205
208
  }
@@ -13,7 +13,16 @@
13
13
 
14
14
  import { writeFile, mkdir, cp, rm, readdir, readFile } from 'node:fs/promises';
15
15
  import { join, basename, relative } from 'node:path';
16
- import { generateHeadersFile } from './shared.js';
16
+ import { adapterBase, generateHeadersFile } from './shared.js';
17
+ // Isomorphic, zero-import module — safe to load from an adapter, which Node's
18
+ // native ESM loader evaluates before Vite's resolver exists. Same precedent as
19
+ // `adapters/shared.ts` importing `../shared/base-path.js`.
20
+ import {
21
+ HEADERS_FILE,
22
+ USER_MERGED_PLATFORM_FILES,
23
+ USER_PLATFORM_DIR,
24
+ } from '../shared/static-platform-files.js';
25
+ import { detectFsIdentity, fsKey } from './fs-identity.js';
17
26
  import type { TimberConfig } from './types';
18
27
 
19
28
  /** Options for the shared build output steps. */
@@ -77,8 +86,7 @@ export async function runSharedBuildSteps(opts: BuildOutputBaseOptions): Promise
77
86
  // Scan for unhashed files so they get per-file Cache-Control rules in the
78
87
  // _headers file. On platforms like Cloudflare Workers Static Assets, the
79
88
  // worker never sees static file requests — _headers is the only mechanism.
80
- const unhashedPaths = await findUnhashedStaticFiles(publicDir);
81
- await writeFile(join(publicDir, '_headers'), generateHeadersFile(unhashedPaths));
89
+ await writeHeadersFile(publicDir, config, await findUnhashedStaticFiles(publicDir));
82
90
 
83
91
  // 4. Copy RSC + SSR server bundles into the output directory.
84
92
  await cp(join(buildDir, 'rsc'), join(outDir, 'rsc'), { recursive: true });
@@ -218,11 +226,116 @@ export async function buildStaticDirectoryOutput(
218
226
  });
219
227
 
220
228
  // Copy pre-rendered static pages (HTML + RSC flight + API routes).
229
+ //
230
+ // This is the one place the build merges two independently-produced trees
231
+ // into a single directory, and `cp` resolves an overlap by overwriting with
232
+ // no diagnostic — the deployment then contains one of the two artifacts and
233
+ // nothing recorded which. Refuse instead (TIM-1266).
221
234
  if (config.staticPagesDir) {
235
+ await assertNoTreeCollision(config.staticPagesDir, publicDir);
222
236
  await cp(config.staticPagesDir, publicDir, { recursive: true });
223
237
  }
224
238
  }
225
239
 
240
+ /**
241
+ * Write the `_headers` file, folding in the user's `public/_headers`.
242
+ *
243
+ * Every adapter used to `writeFile` a generated `_headers` over whatever
244
+ * Vite had copied out of `public/`, so an app shipping its own CSP, CORS, or
245
+ * cache rules deployed without them and without a word (TIM-1326). The
246
+ * generated rules and the user's are not competitors — they address different
247
+ * paths — so this merges rather than refusing, the way `_redirects` has since
248
+ * TIM-1249.
249
+ *
250
+ * **Read from the project, not the output.** The generated file lands under
251
+ * the same name in the output directory, and `runSharedBuildSteps` does not
252
+ * clean `outDir`; a reader aimed there would fold the previous build's
253
+ * generated rules back in and compound them on every rebuild — the defect
254
+ * this pass fixed for `_redirects`.
255
+ *
256
+ * User rules go last. On Cloudflare and Netlify a `_headers` file applies
257
+ * every matching rule, so appending means the user's rules take effect; where
258
+ * a platform resolves a conflict in favour of the earlier rule instead, the
259
+ * framework keeps the paths it owns and the user keeps the rest. Under either
260
+ * reading this is no worse than the previous behaviour, which was to drop the
261
+ * user's rules entirely.
262
+ *
263
+ * @internal Exported for testing.
264
+ */
265
+ export async function writeHeadersFile(
266
+ dir: string,
267
+ config: TimberConfig,
268
+ unhashedPaths: string[]
269
+ ): Promise<void> {
270
+ const userRules = await readUserPlatformFile(config.root, HEADERS_FILE);
271
+ await writeFile(
272
+ join(dir, HEADERS_FILE),
273
+ generateHeadersFile({
274
+ base: adapterBase(config),
275
+ unhashedPaths,
276
+ contentTypes: config.staticContentTypes,
277
+ userRules: userRules ?? undefined,
278
+ })
279
+ );
280
+ }
281
+
282
+ /**
283
+ * Read a user-authored platform file from the project's `public/` directory.
284
+ * Returns null when the project has none, or when no root is configured —
285
+ * a caller without a root cannot locate the project at all, and inventing
286
+ * one from `process.cwd()` would read a different project's file.
287
+ */
288
+ async function readUserPlatformFile(
289
+ root: string | undefined,
290
+ name: string
291
+ ): Promise<string | null> {
292
+ if (!root) return null;
293
+ try {
294
+ return await readFile(join(root, USER_PLATFORM_DIR, name), 'utf-8');
295
+ } catch {
296
+ return null;
297
+ }
298
+ }
299
+
300
+ /**
301
+ * Throw if copying `srcDir` over `destDir` would overwrite an existing file.
302
+ *
303
+ * Compared against the destination **as it stands**, not against the other
304
+ * source tree: the first copy is filtered (client JS is dropped when client
305
+ * JavaScript is disabled), so the files that are actually there are the only
306
+ * ones a second copy can clobber.
307
+ *
308
+ * @internal Exported for testing.
309
+ */
310
+ export async function assertNoTreeCollision(srcDir: string, destDir: string): Promise<void> {
311
+ const [srcFiles, destFiles, identity] = await Promise.all([
312
+ collectFiles(srcDir).catch(() => [] as string[]),
313
+ collectFiles(destDir).catch(() => [] as string[]),
314
+ // `cp` will resolve these paths the way the destination filesystem does,
315
+ // so the comparison predicting it has to as well: on macOS a hand-written
316
+ // `public/About/index.html` and a generated `about/index.html` are one
317
+ // file, and an exact-string set waves the overwrite through (TIM-1266).
318
+ detectFsIdentity(destDir),
319
+ ]);
320
+
321
+ const key = (p: string) => fsKey(p, identity);
322
+ const existing = new Set(destFiles.map((f) => key(relative(destDir, f).replace(/\\/g, '/'))));
323
+ const exempt = new Set(USER_MERGED_PLATFORM_FILES.map(key));
324
+ const collisions = srcFiles
325
+ .map((f) => relative(srcDir, f).replace(/\\/g, '/'))
326
+ .filter((rel) => existing.has(key(rel)) && !exempt.has(key(rel)))
327
+ .sort();
328
+
329
+ if (collisions.length === 0) return;
330
+
331
+ throw new Error(
332
+ `[timber] Build output: ${collisions.length} file(s) in the pre-rendered site ` +
333
+ `collide with files already in the output directory. Copying would silently ` +
334
+ `overwrite them — a generated page and a file in public/ cannot share a path:\n` +
335
+ collisions.map((c) => ` - ${c}`).join('\n')
336
+ );
337
+ }
338
+
226
339
  async function collectFiles(dir: string): Promise<string[]> {
227
340
  const result: string[] = [];
228
341
  const entries = await readdir(dir, { withFileTypes: true });
@@ -13,8 +13,16 @@ import {
13
13
  runSharedBuildSteps,
14
14
  findUnhashedStaticFiles,
15
15
  buildStaticDirectoryOutput,
16
+ writeHeadersFile,
16
17
  } from './build-output-helper.js';
17
- import { generateHeadersFile } from './shared.js';
18
+ import { adapterBase, generateHeadersFile } from './shared.js';
19
+ // Isomorphic, zero-import module — safe to load from an adapter, which Node's
20
+ // native ESM loader evaluates before Vite's resolver exists. Same precedent as
21
+ // `adapters/shared.ts` importing `../shared/base-path.js`.
22
+ import { RSC_KEY_HEADERS } from '../shared/rsc-cache-key.js';
23
+ // Same reasoning: the media type the origin's own RSC gate matches on, so the
24
+ // two predicates cannot drift apart. See `shared/rsc-media-type.ts`.
25
+ import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
18
26
 
19
27
  // ─── Bindings passthrough ─────────────────────────────────────────────────
20
28
  // ALS stores the env object per-request so server components and middleware
@@ -409,15 +417,79 @@ function initPlatformTracer() {
409
417
  return tracerInit
410
418
  }
411
419
 
420
+ // The \`Vary\` tokens this cache can serve a correct answer for.
421
+ //
422
+ // Cloudflare's Cache API keys on the request URL. It does not implement
423
+ // RFC 9111 §4.1 secondary key selection, so a stored response is handed to
424
+ // every request for that URL regardless of what \`Vary\` nominates — except
425
+ // \`Accept-Encoding\`, which the platform segregates itself. Storing a
426
+ // response whose \`Vary\` says "this answer is specific to header H" under a
427
+ // key that ignores H is cross-client reuse (TIM-1269): a developer writing
428
+ // the correct \`public, s-maxage=60\` + \`Vary: Cookie\` gets their logged-in
429
+ // page served to anonymous visitors. Refusing to store is the conservative
430
+ // answer, and the only one available without rewriting the key.
431
+ //
432
+ // The framework's own tokens are on this list because the adapter already
433
+ // segregates them, more strictly than \`Vary\` would:
434
+ //
435
+ // - \`Accept\` — \`isRsc\` above excludes \`text/x-component\` from both lookup
436
+ // and store, so the HTML/Flight split \`Vary: Accept\` exists to protect
437
+ // never reaches this cache in the first place.
438
+ // - \`X-Timber-State-Tree\`, \`X-Timber-URL\`, \`X-Timber-Deployment-Id\` — the
439
+ // origin reads all three only on RSC requests (version skew at
440
+ // \`server/pipeline-phases.ts\`, interception rewrites likewise, state-tree
441
+ // diffing at \`server/rsc-entry/render-route.ts\`). On the non-RSC requests
442
+ // this cache stores, they cannot change the response.
443
+ //
444
+ // That second bullet is a coupling, not an observation: if any of those
445
+ // headers ever starts varying a document response, this list is wrong and
446
+ // the cache poisons. Because the list is derived, a header added to
447
+ // \`RSC_KEY_HEADERS\` inherits the exemption silently — so
448
+ // \`tests/adapters/cloudflare-vary.test.ts\` fails until that header is named
449
+ // there alongside the evidence for its gate, and the gates themselves are
450
+ // covered there, in \`tests/render-route.test.ts\`, and in
451
+ // \`tests/interception-x-timber-url.test.ts\`.
452
+ //
453
+ // Interpolated from \`RSC_KEY_HEADERS\` rather than spelled again — "which
454
+ // headers vary an RSC response" has one answer, and a hand-copied second
455
+ // list here would drift silently on the fallback path nobody exercises.
456
+ //
457
+ // Known residual: a route handler that content-negotiates on \`Accept\`
458
+ // (JSON vs HTML) *and* marks itself \`public\` is still mis-keyed, because the
459
+ // \`isRsc\` split is binary and does not separate those two. That is no worse
460
+ // than the pre-TIM-1269 behaviour, which stored every \`Vary\` but \`*\`.
461
+ const VARY_HONORED = new Set(${JSON.stringify(['accept-encoding', ...RSC_KEY_HEADERS.map((h) => h.toLowerCase())])})
462
+
463
+ // Applied to BOTH sides of the cache — what may be stored, and what a hit
464
+ // may serve. Refusing to store only stops new poisoning: entries a previous
465
+ // deployment wrote under the old predicate stay servable for whatever
466
+ // s-maxage the developer chose, which can be a day. Checking on the way out
467
+ // makes the fix take effect on deploy rather than on expiry, and costs one
468
+ // header read per hit. A rejected entry falls through to the origin and is
469
+ // then declined by the same predicate on the way back in, so it is not
470
+ // rewritten — it simply ages out.
471
+ function canHonorVary(vary) {
472
+ // Absent or empty Vary nominates nothing — the URL is the whole key.
473
+ if (!vary) return true
474
+ return vary.split(',').every((token) => VARY_HONORED.has(token.trim().toLowerCase()))
475
+ }
476
+
412
477
  export default {
413
478
  ${handlersSpread} async fetch(request, env, ctx) {
414
479
  // One-time on first request; resolved-promise await thereafter.
415
480
  await initPlatformTracer()
416
481
 
417
482
  // Edge-cache GET/HEAD responses via the Cache API. RSC navigation
418
- // requests (Accept: text/x-component) are excluded — they vary by
419
- // X-Timber-State-Tree which the cache key doesn't capture.
420
- const isRsc = request.headers.get('Accept')?.includes('text/x-component')
483
+ // requests (Accept: text/x-component) are excluded. Their varying
484
+ // inputs are now in the URL via ?_rsc= (TIM-1268), but this cache keys
485
+ // on the request URL alone and cannot honour Vary, so a payload request
486
+ // that arrives without the param — stripped query, hand-rolled client —
487
+ // would share a key with the document.
488
+ //
489
+ // This exclusion is load-bearing in the other direction too: the store
490
+ // predicate below treats "Vary: Accept" as honoured *because* of it
491
+ // (see VARY_HONORED). Removing it would make that allowlist unsound.
492
+ const isRsc = request.headers.get('Accept')?.includes(${JSON.stringify(RSC_CONTENT_TYPE)})
421
493
  const reqCc = request.headers.get('Cache-Control') || ''
422
494
  const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')
423
495
  || request.headers.has('Pragma')
@@ -426,7 +498,17 @@ ${handlersSpread} async fetch(request, env, ctx) {
426
498
  const cache = cacheable && typeof caches !== 'undefined' ? caches.default : null
427
499
  if (cache) {
428
500
  const cached = await cache.match(request)
429
- if (cached) return cached
501
+ // A hit whose Vary this cache cannot key on is treated as a miss —
502
+ // see canHonorVary. Entries written before that predicate existed are
503
+ // exactly the ones that would be served to the wrong client.
504
+ if (cached) {
505
+ if (canHonorVary(cached.headers.get('Vary'))) return cached
506
+ // Declining the hit means nobody will ever read this body. Release
507
+ // it rather than leaving the stream dangling for the rest of the
508
+ // request (design/02-rendering-pipeline.md §"Cancel Readers When You
509
+ // No Longer Need the Stream").
510
+ cached.body?.cancel().catch(() => {})
511
+ }
430
512
  }
431
513
 
432
514
  // Bridge ctx.cache into the per-request ALS so WorkersCachePurge can
@@ -449,10 +531,10 @@ ${handlersSpread} async fetch(request, env, ctx) {
449
531
  // See design/25-production-deployments.md §"CDN / Edge Cache".
450
532
  if (cache && request.method === 'GET' && response.status === 200) {
451
533
  const cc = response.headers.get('Cache-Control')
452
- const vary = response.headers.get('Vary')
453
534
  if (cc && !cc.includes('no-store') && !cc.includes('private')
454
535
  && (cc.includes('public') || cc.includes('s-maxage'))
455
- && vary !== '*' && !response.headers.has('Set-Cookie')) {
536
+ && canHonorVary(response.headers.get('Vary'))
537
+ && !response.headers.has('Set-Cookie')) {
456
538
  ctx.waitUntil(cache.put(request, response.clone()))
457
539
  }
458
540
  }
@@ -804,11 +886,7 @@ export async function buildStaticOutput(
804
886
  await buildStaticDirectoryOutput(config, buildDir, outDir, '.');
805
887
 
806
888
  // Write _headers file for cache control + content-type rules.
807
- const unhashedPaths = await findUnhashedStaticFiles(outDir);
808
- await writeFile(
809
- join(outDir, '_headers'),
810
- generateHeadersFile(unhashedPaths, config.staticContentTypes)
811
- );
889
+ await writeHeadersFile(outDir, config, await findUnhashedStaticFiles(outDir));
812
890
  }
813
891
 
814
892
  // ─── Preview ─────────────────────────────────────────────────────────────────