@timber-js/app 0.2.0-alpha.184 → 0.2.0-alpha.186

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 (441) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
  3. package/dist/_chunks/als-slots-mFweg276.js +27 -0
  4. package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
  5. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  6. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  7. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  8. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  9. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  10. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  11. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
  12. package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
  13. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
  14. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
  15. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
  16. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
  17. package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
  18. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
  19. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  20. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  21. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  22. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  23. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
  24. package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
  25. package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
  26. package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
  27. package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
  28. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  29. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
  30. package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
  31. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  32. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  33. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  34. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  35. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  36. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  37. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  38. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  39. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  40. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  41. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  42. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  43. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  44. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  45. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  46. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  47. package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
  48. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
  49. package/dist/_chunks/{walkers-BtTlKviE.js → walkers-BhhwI9TD.js} +21 -87
  50. package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
  51. package/dist/adapters/build-output-helper.d.ts +37 -0
  52. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  53. package/dist/adapters/cloudflare-dev.js +1 -1
  54. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  55. package/dist/adapters/cloudflare.d.ts.map +1 -1
  56. package/dist/adapters/cloudflare.js +1 -1
  57. package/dist/adapters/fs-identity.d.ts +64 -0
  58. package/dist/adapters/fs-identity.d.ts.map +1 -0
  59. package/dist/adapters/nitro.d.ts.map +1 -1
  60. package/dist/adapters/nitro.js +2 -3
  61. package/dist/adapters/nitro.js.map +1 -1
  62. package/dist/adapters/shared.d.ts +57 -1
  63. package/dist/adapters/shared.d.ts.map +1 -1
  64. package/dist/adapters/types.d.ts +13 -1
  65. package/dist/adapters/types.d.ts.map +1 -1
  66. package/dist/cache/fast-hash.d.ts +5 -0
  67. package/dist/cache/fast-hash.d.ts.map +1 -1
  68. package/dist/cache/index.js +1 -1
  69. package/dist/cli.js +2 -2
  70. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  71. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  72. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  73. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  74. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  75. package/dist/client/browser-entry/router-init.d.ts +3 -0
  76. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  77. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  78. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  79. package/dist/client/error-boundary.d.ts +1 -0
  80. package/dist/client/error-boundary.d.ts.map +1 -1
  81. package/dist/client/error-boundary.js +1 -162
  82. package/dist/client/history.d.ts +13 -6
  83. package/dist/client/history.d.ts.map +1 -1
  84. package/dist/client/index.js +18 -11
  85. package/dist/client/index.js.map +1 -1
  86. package/dist/client/internal.d.ts +4 -3
  87. package/dist/client/internal.d.ts.map +1 -1
  88. package/dist/client/internal.js +872 -305
  89. package/dist/client/internal.js.map +1 -1
  90. package/dist/client/link.d.ts.map +1 -1
  91. package/dist/client/navigation-api.d.ts +2 -2
  92. package/dist/client/navigation-commit.d.ts +71 -0
  93. package/dist/client/navigation-commit.d.ts.map +1 -0
  94. package/dist/client/navigation-context.d.ts +37 -20
  95. package/dist/client/navigation-context.d.ts.map +1 -1
  96. package/dist/client/navigation-root.d.ts +63 -23
  97. package/dist/client/navigation-root.d.ts.map +1 -1
  98. package/dist/client/params-context.d.ts +81 -0
  99. package/dist/client/params-context.d.ts.map +1 -0
  100. package/dist/client/router-effects.d.ts +129 -0
  101. package/dist/client/router-effects.d.ts.map +1 -0
  102. package/dist/client/router-lifecycle.d.ts +55 -0
  103. package/dist/client/router-lifecycle.d.ts.map +1 -0
  104. package/dist/client/router-pipeline.d.ts +84 -0
  105. package/dist/client/router-pipeline.d.ts.map +1 -0
  106. package/dist/client/router-ref.d.ts +1 -1
  107. package/dist/client/router-ref.d.ts.map +1 -1
  108. package/dist/client/router-skew.d.ts +21 -0
  109. package/dist/client/router-skew.d.ts.map +1 -0
  110. package/dist/client/router-types.d.ts +207 -0
  111. package/dist/client/router-types.d.ts.map +1 -0
  112. package/dist/client/router.d.ts +1 -182
  113. package/dist/client/router.d.ts.map +1 -1
  114. package/dist/client/rsc-fetch.d.ts +39 -16
  115. package/dist/client/rsc-fetch.d.ts.map +1 -1
  116. package/dist/client/segment-cache.d.ts +79 -34
  117. package/dist/client/segment-cache.d.ts.map +1 -1
  118. package/dist/client/ssr-data.d.ts +6 -0
  119. package/dist/client/ssr-data.d.ts.map +1 -1
  120. package/dist/client/stale-client.d.ts +69 -0
  121. package/dist/client/stale-client.d.ts.map +1 -0
  122. package/dist/client/state.d.ts +10 -1
  123. package/dist/client/state.d.ts.map +1 -1
  124. package/dist/client/top-loader.d.ts +2 -1
  125. package/dist/client/top-loader.d.ts.map +1 -1
  126. package/dist/client/unload-guard.d.ts +5 -0
  127. package/dist/client/unload-guard.d.ts.map +1 -1
  128. package/dist/client/use-cookie.d.ts.map +1 -1
  129. package/dist/client/use-router.d.ts +1 -1
  130. package/dist/client/use-segment-params.d.ts +18 -8
  131. package/dist/client/use-segment-params.d.ts.map +1 -1
  132. package/dist/cookies/index.js +3 -3
  133. package/dist/cookies/index.js.map +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +915 -320
  136. package/dist/index.js.map +1 -1
  137. package/dist/plugin-context.d.ts +33 -1
  138. package/dist/plugin-context.d.ts.map +1 -1
  139. package/dist/plugins/adapter-build.d.ts.map +1 -1
  140. package/dist/plugins/build-manifest.d.ts +1 -1
  141. package/dist/plugins/build-manifest.d.ts.map +1 -1
  142. package/dist/plugins/entries.d.ts.map +1 -1
  143. package/dist/plugins/fonts.d.ts.map +1 -1
  144. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  145. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  146. package/dist/plugins/request-dep/index.d.ts +75 -0
  147. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  148. package/dist/plugins/request-dep/origin.d.ts +61 -0
  149. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  150. package/dist/plugins/routing.d.ts +9 -0
  151. package/dist/plugins/routing.d.ts.map +1 -1
  152. package/dist/plugins/static-build.d.ts +149 -8
  153. package/dist/plugins/static-build.d.ts.map +1 -1
  154. package/dist/routing/codegen-types.d.ts +18 -0
  155. package/dist/routing/codegen-types.d.ts.map +1 -1
  156. package/dist/routing/codegen.d.ts.map +1 -1
  157. package/dist/routing/collision-probe.d.ts +104 -0
  158. package/dist/routing/collision-probe.d.ts.map +1 -0
  159. package/dist/routing/collision-spaces.d.ts +26 -0
  160. package/dist/routing/collision-spaces.d.ts.map +1 -0
  161. package/dist/routing/index.js +3 -3
  162. package/dist/routing/interception.d.ts +235 -6
  163. package/dist/routing/interception.d.ts.map +1 -1
  164. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  165. package/dist/routing/scanner.d.ts.map +1 -1
  166. package/dist/routing/segment-classify.d.ts +33 -0
  167. package/dist/routing/segment-classify.d.ts.map +1 -1
  168. package/dist/routing/segment-keys.d.ts +95 -0
  169. package/dist/routing/segment-keys.d.ts.map +1 -0
  170. package/dist/routing/slot-placement.d.ts +48 -0
  171. package/dist/routing/slot-placement.d.ts.map +1 -0
  172. package/dist/routing/types.d.ts +1 -3
  173. package/dist/routing/types.d.ts.map +1 -1
  174. package/dist/routing/walkers.d.ts +6 -0
  175. package/dist/routing/walkers.d.ts.map +1 -1
  176. package/dist/search-params/define.d.ts +0 -6
  177. package/dist/search-params/define.d.ts.map +1 -1
  178. package/dist/search-params/index.js +176 -2
  179. package/dist/search-params/index.js.map +1 -1
  180. package/dist/segment-params/define.d.ts +0 -6
  181. package/dist/segment-params/define.d.ts.map +1 -1
  182. package/dist/segment-params/index.js +69 -1
  183. package/dist/segment-params/index.js.map +1 -0
  184. package/dist/server/actions.d.ts +6 -3
  185. package/dist/server/actions.d.ts.map +1 -1
  186. package/dist/server/als-registry.d.ts +2 -1
  187. package/dist/server/als-registry.d.ts.map +1 -1
  188. package/dist/server/build-manifest.d.ts +13 -1
  189. package/dist/server/build-manifest.d.ts.map +1 -1
  190. package/dist/server/chain-url-parts.d.ts +82 -0
  191. package/dist/server/chain-url-parts.d.ts.map +1 -0
  192. package/dist/server/children-interception.d.ts +49 -0
  193. package/dist/server/children-interception.d.ts.map +1 -0
  194. package/dist/server/default-status-page.d.ts +45 -0
  195. package/dist/server/default-status-page.d.ts.map +1 -0
  196. package/dist/server/deny-boundary.d.ts +4 -7
  197. package/dist/server/deny-boundary.d.ts.map +1 -1
  198. package/dist/server/deny-renderer.d.ts +14 -3
  199. package/dist/server/deny-renderer.d.ts.map +1 -1
  200. package/dist/server/head-response.d.ts +35 -0
  201. package/dist/server/head-response.d.ts.map +1 -0
  202. package/dist/server/index.js +2 -2
  203. package/dist/server/internal.js +528 -373
  204. package/dist/server/internal.js.map +1 -1
  205. package/dist/server/param-coercion.d.ts +10 -1
  206. package/dist/server/param-coercion.d.ts.map +1 -1
  207. package/dist/server/pipeline-helpers.d.ts +0 -18
  208. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  209. package/dist/server/pipeline-interception.d.ts +59 -9
  210. package/dist/server/pipeline-interception.d.ts.map +1 -1
  211. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  212. package/dist/server/pipeline-phases.d.ts.map +1 -1
  213. package/dist/server/pipeline.d.ts +11 -0
  214. package/dist/server/pipeline.d.ts.map +1 -1
  215. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  216. package/dist/server/primitives.d.ts +0 -12
  217. package/dist/server/primitives.d.ts.map +1 -1
  218. package/dist/server/publish-params.d.ts +33 -0
  219. package/dist/server/publish-params.d.ts.map +1 -0
  220. package/dist/server/request-context.d.ts +48 -0
  221. package/dist/server/request-context.d.ts.map +1 -1
  222. package/dist/server/route-element-builder.d.ts +32 -2
  223. package/dist/server/route-element-builder.d.ts.map +1 -1
  224. package/dist/server/route-handler.d.ts.map +1 -1
  225. package/dist/server/route-matcher.d.ts +13 -0
  226. package/dist/server/route-matcher.d.ts.map +1 -1
  227. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  228. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  229. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  230. package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
  231. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  232. package/dist/server/rsc-entry/helpers.d.ts +2 -10
  233. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  234. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  235. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  236. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
  237. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  238. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
  239. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  240. package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
  241. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  242. package/dist/server/sitemap-generator.d.ts.map +1 -1
  243. package/dist/server/skippable-prefix.d.ts +23 -0
  244. package/dist/server/skippable-prefix.d.ts.map +1 -0
  245. package/dist/server/slot-interception.d.ts +80 -0
  246. package/dist/server/slot-interception.d.ts.map +1 -0
  247. package/dist/server/slot-resolver.d.ts +70 -8
  248. package/dist/server/slot-resolver.d.ts.map +1 -1
  249. package/dist/server/ssr-bridge-types.d.ts +6 -0
  250. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  251. package/dist/server/ssr-entry.d.ts.map +1 -1
  252. package/dist/server/ssr-wrappers.d.ts +24 -13
  253. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  254. package/dist/server/state-tree-diff.d.ts +6 -27
  255. package/dist/server/state-tree-diff.d.ts.map +1 -1
  256. package/dist/server/static-generator.d.ts +6 -0
  257. package/dist/server/static-generator.d.ts.map +1 -1
  258. package/dist/server/static-not-found.d.ts +52 -0
  259. package/dist/server/static-not-found.d.ts.map +1 -0
  260. package/dist/server/status-code-resolver.d.ts +3 -10
  261. package/dist/server/status-code-resolver.d.ts.map +1 -1
  262. package/dist/shared/als-slots.d.ts +41 -0
  263. package/dist/shared/als-slots.d.ts.map +1 -0
  264. package/dist/shared/base-path.d.ts +56 -0
  265. package/dist/shared/base-path.d.ts.map +1 -0
  266. package/dist/shared/param-value.d.ts +80 -0
  267. package/dist/shared/param-value.d.ts.map +1 -0
  268. package/dist/shared/payload-root.d.ts +108 -0
  269. package/dist/shared/payload-root.d.ts.map +1 -0
  270. package/dist/shared/rsc-cache-key.d.ts +163 -0
  271. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  272. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  273. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  274. package/dist/shared/rsc-manifest.d.ts +28 -0
  275. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  276. package/dist/shared/rsc-payload-path.d.ts +42 -0
  277. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  278. package/dist/shared/segment-info.d.ts +59 -0
  279. package/dist/shared/segment-info.d.ts.map +1 -0
  280. package/dist/shared/slot-params.d.ts +51 -0
  281. package/dist/shared/slot-params.d.ts.map +1 -0
  282. package/dist/shared/static-platform-files.d.ts +71 -0
  283. package/dist/shared/static-platform-files.d.ts.map +1 -0
  284. package/dist/shims/navigation-client.d.ts +8 -19
  285. package/dist/shims/navigation-client.d.ts.map +1 -1
  286. package/dist/shims/navigation.d.ts +0 -1
  287. package/dist/shims/navigation.d.ts.map +1 -1
  288. package/docs/api/31-api-client.mdx +4 -2
  289. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  290. package/docs/learn/11-error-handling.mdx +4 -3
  291. package/docs/learn/12-client-navigation.mdx +2 -2
  292. package/docs/learn/14-deploying.mdx +23 -0
  293. package/docs/more/01-advanced-routing.mdx +172 -2
  294. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  295. package/package.json +2 -2
  296. package/src/adapters/build-output-helper.ts +116 -3
  297. package/src/adapters/cloudflare.ts +86 -11
  298. package/src/adapters/fs-identity.ts +156 -0
  299. package/src/adapters/nitro.ts +6 -6
  300. package/src/adapters/shared.ts +196 -21
  301. package/src/adapters/types.ts +13 -1
  302. package/src/cache/fast-hash.ts +5 -0
  303. package/src/client/browser-entry/action-dispatch.ts +9 -4
  304. package/src/client/browser-entry/hydrate.ts +40 -29
  305. package/src/client/browser-entry/index.ts +15 -1
  306. package/src/client/browser-entry/post-hydration.ts +10 -9
  307. package/src/client/browser-entry/router-init.ts +104 -48
  308. package/src/client/browser-entry/rsc-stream.ts +11 -2
  309. package/src/client/error-boundary.tsx +20 -15
  310. package/src/client/history.ts +13 -6
  311. package/src/client/internal.ts +4 -3
  312. package/src/client/link.tsx +55 -8
  313. package/src/client/navigation-api.ts +3 -3
  314. package/src/client/navigation-commit.ts +174 -0
  315. package/src/client/navigation-context.ts +40 -67
  316. package/src/client/navigation-root.tsx +326 -101
  317. package/src/client/params-context.ts +192 -0
  318. package/src/client/router-effects.ts +231 -0
  319. package/src/client/router-lifecycle.ts +288 -0
  320. package/src/client/router-pipeline.ts +389 -0
  321. package/src/client/router-ref.ts +1 -1
  322. package/src/client/router-skew.ts +30 -0
  323. package/src/client/router-types.ts +230 -0
  324. package/src/client/router.ts +242 -690
  325. package/src/client/rsc-fetch.ts +180 -122
  326. package/src/client/segment-cache.ts +121 -44
  327. package/src/client/ssr-data.ts +6 -0
  328. package/src/client/stale-client.ts +191 -0
  329. package/src/client/state.ts +14 -1
  330. package/src/client/top-loader.tsx +2 -1
  331. package/src/client/unload-guard.ts +34 -2
  332. package/src/client/use-cookie.ts +9 -2
  333. package/src/client/use-router.ts +1 -1
  334. package/src/client/use-segment-params.ts +41 -18
  335. package/src/index.ts +5 -1
  336. package/src/plugin-context.ts +34 -1
  337. package/src/plugins/adapter-build.ts +4 -0
  338. package/src/plugins/build-manifest.ts +34 -13
  339. package/src/plugins/entries.ts +4 -0
  340. package/src/plugins/fonts.ts +11 -8
  341. package/src/plugins/request-dep/analysis.ts +447 -0
  342. package/src/plugins/request-dep/index.ts +376 -0
  343. package/src/plugins/request-dep/origin.ts +310 -0
  344. package/src/plugins/routing.ts +60 -3
  345. package/src/plugins/static-build.ts +333 -63
  346. package/src/routing/codegen-types.ts +18 -0
  347. package/src/routing/codegen.ts +50 -10
  348. package/src/routing/collision-probe.ts +438 -0
  349. package/src/routing/collision-spaces.ts +281 -0
  350. package/src/routing/interception.ts +896 -50
  351. package/src/routing/manifest-codegen.ts +6 -9
  352. package/src/routing/scanner.ts +114 -113
  353. package/src/routing/segment-classify.ts +57 -0
  354. package/src/routing/segment-keys.ts +146 -0
  355. package/src/routing/slot-placement.ts +97 -0
  356. package/src/routing/types.ts +1 -3
  357. package/src/routing/walkers.ts +8 -0
  358. package/src/search-params/define.ts +6 -29
  359. package/src/segment-params/define.ts +4 -27
  360. package/src/server/action-handler.ts +3 -1
  361. package/src/server/actions.ts +6 -3
  362. package/src/server/als-registry.ts +2 -1
  363. package/src/server/build-manifest.ts +14 -1
  364. package/src/server/chain-url-parts.ts +163 -0
  365. package/src/server/children-interception.ts +115 -0
  366. package/src/server/default-status-page.ts +127 -0
  367. package/src/server/deny-boundary.ts +5 -31
  368. package/src/server/deny-renderer.ts +83 -22
  369. package/src/server/head-response.ts +54 -0
  370. package/src/server/param-coercion.ts +48 -18
  371. package/src/server/pipeline-helpers.ts +0 -45
  372. package/src/server/pipeline-interception.ts +81 -32
  373. package/src/server/pipeline-outcome.ts +3 -5
  374. package/src/server/pipeline-phases.ts +68 -16
  375. package/src/server/pipeline.ts +57 -10
  376. package/src/server/prebuilt-builder.ts +1 -7
  377. package/src/server/prebuilt-runtime.ts +4 -4
  378. package/src/server/primitives.ts +0 -18
  379. package/src/server/publish-params.ts +42 -0
  380. package/src/server/request-context.ts +97 -18
  381. package/src/server/route-element-builder.ts +89 -98
  382. package/src/server/route-handler.ts +8 -8
  383. package/src/server/route-matcher.ts +21 -9
  384. package/src/server/rsc-cache-key-guard.ts +170 -0
  385. package/src/server/rsc-entry/api-handler.ts +6 -8
  386. package/src/server/rsc-entry/error-renderer.ts +64 -25
  387. package/src/server/rsc-entry/helpers.ts +31 -16
  388. package/src/server/rsc-entry/index.ts +36 -28
  389. package/src/server/rsc-entry/render-route.ts +12 -7
  390. package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
  391. package/src/server/rsc-entry/rsc-payload.ts +14 -25
  392. package/src/server/rsc-entry/rsc-stream.ts +4 -3
  393. package/src/server/rsc-entry/ssr-renderer.ts +13 -9
  394. package/src/server/sitemap-generator.ts +1 -7
  395. package/src/server/skippable-prefix.ts +92 -0
  396. package/src/server/slot-interception.ts +168 -0
  397. package/src/server/slot-resolver.ts +253 -156
  398. package/src/server/ssr-bridge-types.ts +6 -0
  399. package/src/server/ssr-entry.ts +12 -4
  400. package/src/server/ssr-wrappers.tsx +33 -32
  401. package/src/server/state-tree-diff.ts +14 -55
  402. package/src/server/static-generator.ts +67 -45
  403. package/src/server/static-not-found.ts +134 -0
  404. package/src/server/status-code-resolver.ts +4 -47
  405. package/src/shared/als-slots.ts +72 -0
  406. package/src/shared/base-path.ts +80 -0
  407. package/src/shared/param-value.ts +244 -0
  408. package/src/shared/payload-root.ts +151 -0
  409. package/src/shared/rsc-cache-key.ts +269 -0
  410. package/src/shared/rsc-error-envelope.ts +31 -0
  411. package/src/shared/rsc-manifest.ts +29 -0
  412. package/src/shared/rsc-payload-path.ts +45 -0
  413. package/src/shared/segment-info.ts +60 -0
  414. package/src/shared/slot-params.ts +111 -0
  415. package/src/shared/static-platform-files.ts +75 -0
  416. package/src/shims/navigation-client.ts +8 -26
  417. package/src/shims/navigation.ts +0 -4
  418. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  419. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  420. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  421. package/dist/_chunks/cli-schema-sync-D3naS3eH.js +0 -688
  422. package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
  423. package/dist/_chunks/define-COtkxMRT.js +0 -191
  424. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  425. package/dist/_chunks/define-c4au4I9R.js +0 -82
  426. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  427. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  428. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  429. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  430. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  431. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  432. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  433. package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
  434. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  435. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  436. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  437. package/dist/_chunks/walkers-BtTlKviE.js.map +0 -1
  438. package/dist/client/error-boundary.js.map +0 -1
  439. package/dist/plugins/request-dep.d.ts +0 -32
  440. package/dist/plugins/request-dep.d.ts.map +0 -1
  441. package/src/plugins/request-dep.ts +0 -499
@@ -31,12 +31,7 @@ const SINGLE_FILE_KEYS = [
31
31
 
32
32
  // ── Record-shaped conventions ───────────────────────────────────────────
33
33
  // Each entry maps to a node property that holds Record<string, RouteFile>.
34
- const RECORD_FILE_KEYS = [
35
- 'statusFiles',
36
- 'jsonStatusFiles',
37
- 'legacyStatusFiles',
38
- 'metadataRoutes',
39
- ] as const;
34
+ const RECORD_FILE_KEYS = ['statusFiles', 'jsonStatusFiles', 'metadataRoutes'] as const;
40
35
 
41
36
  const SCHEMA_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];
42
37
 
@@ -165,11 +160,13 @@ export function generateManifestModule(tree: RouteTree, viteRoot: string): strin
165
160
  globalErrorLine = ` globalError: { load: ${v}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
166
161
  }
167
162
 
163
+ // Serialize the rewrite objects wholesale — an explicit field list here
164
+ // silently drops any field added to InterceptionRewrite, and the guard that
165
+ // reads it would then fall back to matching too broadly. InterceptionRewrite
166
+ // is plain JSON data by construction (no file references, no functions).
168
167
  const rewrites = collectInterceptionRewrites(tree.root);
169
168
  const rewritesLine =
170
- rewrites.length > 0
171
- ? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({ interceptedPattern: r.interceptedPattern, interceptingPrefix: r.interceptingPrefix })))},`
172
- : '';
169
+ rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites)},` : '';
173
170
 
174
171
  const code = [
175
172
  '// Auto-generated route manifest — do not edit.',
@@ -18,7 +18,10 @@ import type {
18
18
  ScannerConfig,
19
19
  InterceptionMarker,
20
20
  } from './types.js';
21
- import { classifySegment } from './segment-classify.js';
21
+ import { classifySegment, effectiveUrlSegment } from './segment-classify.js';
22
+ import { validateInterceptionPlacement, computeInterceptedBase } from './interception.js';
23
+ import { validateRouteGroupCollisions } from './collision-spaces.js';
24
+ import { validateSlotPlacement } from './slot-placement.js';
22
25
  import { DEFAULT_PAGE_EXTENSIONS } from './types.js';
23
26
  import { classifyMetadataRoute, isDynamicMetadataExtension } from '../server/metadata-routes.js';
24
27
  import { swallow } from '../server/logger.js';
@@ -29,17 +32,6 @@ import { ENCODED_SEPARATOR_RE, NULL_BYTE_RE } from '../server/canonicalize.js';
29
32
  */
30
33
  const PAGE_EXT_CONVENTIONS = new Set(['page', 'layout', 'error', 'default', 'denied']);
31
34
 
32
- /**
33
- * Legacy compat status-code files.
34
- * Maps legacy file name → HTTP status code for the fallback chain.
35
- * See design/10-error-handling.md §"Fallback Chain".
36
- */
37
- const LEGACY_STATUS_FILES: Record<string, number> = {
38
- 'not-found': 404,
39
- 'forbidden': 403,
40
- 'unauthorized': 401,
41
- };
42
-
43
35
  /**
44
36
  * File convention names that are always .ts/.tsx (never .mdx etc.)
45
37
  */
@@ -87,6 +79,12 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
87
79
  // Scan children recursively
88
80
  scanChildren(appDir, tree.root, extSet);
89
81
 
82
+ // Validate: every parallel slot sits where a renderer reaches it. First,
83
+ // because a slot nested inside another slot never renders, and a collision
84
+ // reported from inside one would send the author to fix an ambiguity between
85
+ // two pages that were both already dead.
86
+ validateSlotPlacement(tree.root);
87
+
90
88
  // Validate: detect route group collisions (different groups producing pages at the same URL)
91
89
  validateRouteGroupCollisions(tree.root);
92
90
 
@@ -102,6 +100,10 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
102
100
  // e.g., a-[id] and [slug]-b both have prefix+suffix length = 2
103
101
  validateAffixedSiblingConflicts(tree.root);
104
102
 
103
+ // Validate: intercepting routes must be direct children of a parallel slot —
104
+ // the slot resolver is the only thing that can render one.
105
+ validateInterceptionPlacement(tree.root);
106
+
105
107
  return tree;
106
108
  }
107
109
 
@@ -219,19 +221,6 @@ function scanSegmentFiles(dirPath: string, node: SegmentNode, extSet: Set<string
219
221
  continue;
220
222
  }
221
223
 
222
- // Legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)
223
- if (name in LEGACY_STATUS_FILES && extSet.has(ext)) {
224
- if (!node.legacyStatusFiles) {
225
- node.legacyStatusFiles = {};
226
- }
227
- const existing = node.legacyStatusFiles[name];
228
- if (existing) {
229
- throwDuplicateConvention(name, existing.filePath, fullPath);
230
- }
231
- node.legacyStatusFiles[name] = { filePath: fullPath, extension: ext };
232
- continue;
233
- }
234
-
235
224
  // Metadata route files (sitemap.ts, robots.ts, icon.tsx, opengraph-image.tsx, etc.)
236
225
  // Both static (.xml, .txt, .png, .ico, etc.) and dynamic (.ts, .tsx) files are recognized.
237
226
  // When both exist for the same base name, dynamic takes precedence.
@@ -379,69 +368,6 @@ function scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<stri
379
368
  }
380
369
  }
381
370
 
382
- /**
383
- * Validate that route groups don't produce conflicting pages/routes at the same URL path.
384
- *
385
- * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim
386
- * /login — the scanner must detect and reject this at build time.
387
- *
388
- * Each parallel slot is validated as its OWN collision space: a slot's pages
389
- * intentionally share URL paths with the parent tree (that's the point of
390
- * parallel routes), but two route groups INSIDE one slot claiming the same
391
- * URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.
392
- */
393
- function validateRouteGroupCollisions(root: SegmentNode): void {
394
- // Map from urlPath → { filePath, source } for the first page/route seen at that path
395
- collectRoutableLeaves(root, new Map(), '');
396
- }
397
-
398
- /**
399
- * Walk the segment tree and collect all routable leaves (page or route files),
400
- * throwing on collision. Each slot recurses with a FRESH collision map —
401
- * slots are parallel routes that intentionally share URL paths with their
402
- * parent tree, but collisions within a single slot are real errors.
403
- */
404
- function collectRoutableLeaves(
405
- node: SegmentNode,
406
- seen: Map<string, { filePath: string; segmentPath: string }>,
407
- segmentPath: string
408
- ): void {
409
- // Intercepting routes are resolved by the slot resolver, not URL matching,
410
- // and their computed urlPaths intentionally mirror other routes (e.g. two
411
- // interceptors in one slot both produce /gallery/[id]). Skip them entirely.
412
- if (node.segmentType === 'intercepting') return;
413
-
414
- const currentPath = segmentPath
415
- ? `${segmentPath}/${node.segmentName}`
416
- : node.segmentName || '(root)';
417
-
418
- const routableFile = node.page ?? node.route;
419
- if (routableFile) {
420
- const existing = seen.get(node.urlPath);
421
- if (existing) {
422
- throw new Error(
423
- `Build error: route collision — multiple route groups produce a page/route at the same URL path.\n` +
424
- ` URL path: ${node.urlPath}\n` +
425
- ` File 1: ${existing.filePath} (via ${existing.segmentPath})\n` +
426
- ` File 2: ${routableFile.filePath} (via ${currentPath})\n` +
427
- `Each URL path must map to exactly one page or route handler. ` +
428
- `Rename or move one of the conflicting files.`
429
- );
430
- }
431
- seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });
432
- }
433
-
434
- // Recurse into children
435
- for (const child of node.children) {
436
- collectRoutableLeaves(child, seen, currentPath);
437
- }
438
-
439
- // Recurse into slots — each slot gets a fresh map (its own collision space)
440
- for (const slotNode of Object.values(node.slots)) {
441
- collectRoutableLeaves(slotNode, new Map(), currentPath);
442
- }
443
- }
444
-
445
371
  /**
446
372
  * Validate that no route chain contains duplicate dynamic param names.
447
373
  *
@@ -462,12 +388,20 @@ function validateDuplicateParamNames(root: SegmentNode): void {
462
388
  * Throws on the first duplicate found.
463
389
  */
464
390
  function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): void {
391
+ // Read through the effective identity so an intercepting directory is seen
392
+ // as the segment it intercepts: the scanner leaves `paramName` unset on
393
+ // `(.)[id]`, so a validator reading the node directly is blind to a param
394
+ // the resolver goes on to bind — `@modal/(.)[id]/[id]` would pass here and
395
+ // then silently overwrite the outer `id` at match time, where the ordinary
396
+ // `/[id]/[id]` is rejected. See TIM-1288.
397
+ const { paramName } = effectiveUrlSegment(node);
398
+
465
399
  // If this node introduces a param name, check for duplicates
466
- if (node.paramName) {
467
- const existing = seen.get(node.paramName);
400
+ if (paramName) {
401
+ const existing = seen.get(paramName);
468
402
  if (existing) {
469
403
  throw new Error(
470
- `[timber] Duplicate param name '${node.paramName}' in route chain.\n` +
404
+ `[timber] Duplicate param name '${paramName}' in route chain.\n` +
471
405
  ` First defined at: ${existing}\n` +
472
406
  ` Duplicate at: ${node.urlPath || '/'}\n` +
473
407
  ` Rename one of the segments to avoid ambiguity.`
@@ -475,7 +409,7 @@ function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): v
475
409
  }
476
410
  // Add to seen for descendants (use a new Map to avoid polluting siblings)
477
411
  seen = new Map(seen);
478
- seen.set(node.paramName, node.urlPath || '/');
412
+ seen.set(paramName, node.urlPath || '/');
479
413
  }
480
414
 
481
415
  // Recurse into children (they inherit the accumulated params)
@@ -483,8 +417,16 @@ function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): v
483
417
  walkForDuplicateParams(child, seen);
484
418
  }
485
419
 
486
- // Slots are independent parallel routes — start fresh param tracking
487
- // (a slot's params don't conflict with the main route's params)
420
+ // A slot INHERITS the ancestor params rather than starting fresh: a slot
421
+ // child may not reuse a name its owner already bound
422
+ // (`app/gallery/[id]/@modal/[id]` is rejected). The copy is so sibling
423
+ // slots don't see each other's names, not so the chain is reset.
424
+ //
425
+ // Whether that rule is right is a separate question — slot params are
426
+ // merged over the main route's at read time, so a collision is defined
427
+ // rather than ambiguous — but changing it would change every slot.
428
+ // Tracked as TIM-1287; do not "fix" this by making the map fresh without
429
+ // deciding that.
488
430
  for (const slotNode of Object.values(node.slots)) {
489
431
  walkForDuplicateParams(slotNode, new Map(seen));
490
432
  }
@@ -512,25 +454,29 @@ function collectBracketForms(
512
454
  node: SegmentNode,
513
455
  forms: Map<string, { segmentType: SegmentType; urlPath: string }>
514
456
  ): void {
515
- if (node.paramName && node.segmentType !== 'static' && node.segmentType !== 'group') {
516
- const existing = forms.get(node.paramName);
517
- if (existing && existing.segmentType !== node.segmentType) {
457
+ // Effective identity, so `(.)[...rest]` is compared as the catch-all it
458
+ // intercepts rather than skipped as an 'intercepting' node (TIM-1288).
459
+ const { paramName, segmentType } = effectiveUrlSegment(node);
460
+
461
+ if (paramName && segmentType !== 'static' && segmentType !== 'group') {
462
+ const existing = forms.get(paramName);
463
+ if (existing && existing.segmentType !== segmentType) {
518
464
  const bracketExamples: Record<string, string> = {
519
- 'dynamic': `[${node.paramName}]`,
520
- 'catch-all': `[...${node.paramName}]`,
521
- 'optional-catch-all': `[[...${node.paramName}]]`,
465
+ 'dynamic': `[${paramName}]`,
466
+ 'catch-all': `[...${paramName}]`,
467
+ 'optional-catch-all': `[[...${paramName}]]`,
522
468
  };
523
469
  throw new Error(
524
- `[timber] Param name '${node.paramName}' uses multiple bracket forms.\n` +
470
+ `[timber] Param name '${paramName}' uses multiple bracket forms.\n` +
525
471
  ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\n` +
526
- ` ${bracketExamples[node.segmentType] ?? node.segmentType} at: ${node.urlPath || '/'}\n` +
472
+ ` ${bracketExamples[segmentType] ?? segmentType} at: ${node.urlPath || '/'}\n` +
527
473
  ` Each param name must use a single bracket form across all routes.\n` +
528
- ` Rename one of the segments to fix this (e.g., [${node.paramName}] → [${node.paramName}Id]).`
474
+ ` Rename one of the segments to fix this (e.g., [${paramName}] → [${paramName}Id]).`
529
475
  );
530
476
  }
531
477
  if (!existing) {
532
- forms.set(node.paramName, {
533
- segmentType: node.segmentType,
478
+ forms.set(paramName, {
479
+ segmentType,
534
480
  urlPath: node.urlPath || '/',
535
481
  });
536
482
  }
@@ -554,20 +500,39 @@ function collectBracketForms(
554
500
  * Route groups are URL-transparent, so affixed segments inside different groups
555
501
  * at the same level still conflict with each other.
556
502
  *
557
- * See design/07-routing.md §"Priority"
503
+ * Ambiguity requires the two segments to be candidates for the *same* URL
504
+ * part, which is narrower than "same parent" once interception is involved:
505
+ *
506
+ * - Plain children and intercepting children never compete. They are resolved
507
+ * by different walks — `findSlotMatch` matches the slot subtree (where
508
+ * `tree-match.ts` ignores intercepting nodes outright) and
509
+ * `findInterceptingMatch` builds a synthetic parent holding only
510
+ * interceptors.
511
+ * - Two interceptors compete only at the same anchor. `(.)a-[id]` and
512
+ * `(..)[slug]-b` in one slot are based at different URL depths, so no
513
+ * pathname reaches both, and `anchorIndex` rejects the one whose base does
514
+ * not match the target. Grouping them together would fail the build for a
515
+ * pair that can never be ambiguous.
516
+ *
517
+ * So candidates are keyed by the position they contend for, then by affix
518
+ * length. See design/07-routing.md §"Priority" and §"Intercepting Routes".
558
519
  */
559
520
  function validateAffixedSiblingConflicts(node: SegmentNode): void {
560
521
  const affixed = collectEffectiveAffixed(node);
561
522
 
562
523
  if (affixed.length >= 2) {
563
- const byLength = new Map<number, SegmentNode[]>();
524
+ const byLength = new Map<string, SegmentNode[]>();
564
525
  for (const child of affixed) {
565
- const len = (child.paramPrefix?.length ?? 0) + (child.paramSuffix?.length ?? 0);
566
- const group = byLength.get(len);
526
+ // `\0` as the separator, written as the escape rather than a literal
527
+ // byte: a raw NUL in the source makes git and grep treat this file as
528
+ // binary. A contention key is a URL path, which cannot contain one, so
529
+ // the two halves of the key can never run together.
530
+ const key = `${contentionKey(node, child)}\0${affixLengthOf(child)}`;
531
+ const group = byLength.get(key);
567
532
  if (group) {
568
533
  group.push(child);
569
534
  } else {
570
- byLength.set(len, [child]);
535
+ byLength.set(key, [child]);
571
536
  }
572
537
  }
573
538
 
@@ -577,7 +542,7 @@ function validateAffixedSiblingConflicts(node: SegmentNode): void {
577
542
  `Build error: ambiguous affixed dynamic siblings with equal specificity.\n` +
578
543
  ` Parent: ${node.urlPath || '/'}\n` +
579
544
  ` Segments: ${group.map((s) => s.segmentName).join(', ')}\n` +
580
- ` Combined prefix+suffix length: ${(group[0]!.paramPrefix?.length ?? 0) + (group[0]!.paramSuffix?.length ?? 0)}\n` +
545
+ ` Combined prefix+suffix length: ${affixLengthOf(group[0]!)}\n` +
581
546
  `The router cannot deterministically choose between these — filesystem scan order is platform-dependent.\n` +
582
547
  `Rename one segment to give it a longer or shorter combined affix.`
583
548
  );
@@ -593,16 +558,52 @@ function validateAffixedSiblingConflicts(node: SegmentNode): void {
593
558
  }
594
559
  }
595
560
 
561
+ /**
562
+ * The URL position a child contends for, as a comparable key.
563
+ *
564
+ * Plain children contend for the parent's next part. An intercepting child
565
+ * contends for a part determined by its marker's intercepted base, so two
566
+ * markers of different depth are simply different positions — and two markers
567
+ * that compute the *same* base do contend, which is why the key is the base
568
+ * rather than the marker (`(..)` and `(...)` under `/feed` both base at `/`).
569
+ *
570
+ * `groupByInterceptedAnchor` (`routing/interception.ts`) is the canonical
571
+ * statement of that grouping — it is what the resolver walks and what the
572
+ * collision validator checks. This computes the same key from the same
573
+ * `computeInterceptedBase` rather than calling it, because affix ambiguity is
574
+ * decided per *child* and `collectEffectiveAffixed` looks through route groups,
575
+ * so the two do not partition the same set of nodes.
576
+ */
577
+ function contentionKey(parent: SegmentNode, child: SegmentNode): string {
578
+ if (child.segmentType !== 'intercepting') return 'child';
579
+ return `intercept:${computeInterceptedBase(parent.urlPath, child.interceptionMarker ?? '(.)')}`;
580
+ }
581
+
582
+ /** Combined affix length of a node, read through its effective identity. */
583
+ function affixLengthOf(node: SegmentNode): number {
584
+ const { paramPrefix, paramSuffix } = effectiveUrlSegment(node);
585
+ return (paramPrefix?.length ?? 0) + (paramSuffix?.length ?? 0);
586
+ }
587
+
596
588
  /**
597
589
  * Collect affixed dynamic segments that are effective children of a node.
598
590
  * Route groups are URL-transparent, so we look through them recursively.
591
+ *
592
+ * Segments are read through `effectiveUrlSegment`, so an intercepting
593
+ * directory counts as the affixed segment it intercepts: `(.)a-[id]` is
594
+ * `a-[id]`. The proxy the slot resolver builds hands those affixes to
595
+ * `matchUrlParts`, whose equal-length sort is not stable across equal
596
+ * specificity — so two intercepting siblings with the same combined affix
597
+ * length would resolve by filesystem enumeration order, which is precisely
598
+ * what this validator exists to prevent for ordinary siblings (TIM-1288).
599
599
  */
600
600
  function collectEffectiveAffixed(node: SegmentNode): SegmentNode[] {
601
601
  const result: SegmentNode[] = [];
602
602
  for (const child of node.children) {
603
- if (child.segmentType === 'dynamic' && (child.paramPrefix || child.paramSuffix)) {
603
+ const { segmentType, paramPrefix, paramSuffix } = effectiveUrlSegment(child);
604
+ if (segmentType === 'dynamic' && (paramPrefix || paramSuffix)) {
604
605
  result.push(child);
605
- } else if (child.segmentType === 'group') {
606
+ } else if (segmentType === 'group') {
606
607
  result.push(...collectEffectiveAffixed(child));
607
608
  }
608
609
  }
@@ -192,6 +192,63 @@ export function classifySegment(dirName: string): SegmentClassification {
192
192
  return { type: 'static' };
193
193
  }
194
194
 
195
+ /**
196
+ * The URL-matching identity of a segment node.
197
+ *
198
+ * For every node except an intercepting one this is the node itself. An
199
+ * intercepting node is different: its directory name carries a marker
200
+ * (`(.)photo`, `(.)[id]`), so the node's own `segmentType` is
201
+ * `'intercepting'` and the bracket syntax of the segment it intercepts was
202
+ * never classified. Everything that has to reason about *the URL part this
203
+ * node stands for* — matching it, and keying its param for codec coercion —
204
+ * needs that classification.
205
+ *
206
+ * Deriving it here, from `interceptedSegmentName`, keeps interception out
207
+ * of `classifySegment`'s output and out of the serialized manifest: there
208
+ * is no second copy of the classification to drift from this one. The
209
+ * returned `segmentName` is the intercepted name (`photo`, `[id]`), NOT the
210
+ * node's directory name — callers that need the directory name for tree
211
+ * paths must keep reading the node. See TIM-1281.
212
+ */
213
+ export interface UrlSegmentIdentity {
214
+ segmentName: string;
215
+ segmentType: SegmentType;
216
+ paramName?: string;
217
+ paramPrefix?: string;
218
+ paramSuffix?: string;
219
+ }
220
+
221
+ export function effectiveUrlSegment(node: {
222
+ segmentName: string;
223
+ segmentType: SegmentType;
224
+ paramName?: string;
225
+ paramPrefix?: string;
226
+ paramSuffix?: string;
227
+ interceptedSegmentName?: string;
228
+ }): UrlSegmentIdentity {
229
+ if (node.segmentType !== 'intercepting' || !node.interceptedSegmentName) {
230
+ return {
231
+ segmentName: node.segmentName,
232
+ segmentType: node.segmentType,
233
+ paramName: node.paramName,
234
+ paramPrefix: node.paramPrefix,
235
+ paramSuffix: node.paramSuffix,
236
+ };
237
+ }
238
+
239
+ const seg = classifyUrlSegment(node.interceptedSegmentName);
240
+ if (seg.kind === 'static') {
241
+ return { segmentName: seg.value, segmentType: 'static' };
242
+ }
243
+ return {
244
+ segmentName: node.interceptedSegmentName,
245
+ segmentType: seg.kind,
246
+ paramName: seg.name,
247
+ paramPrefix: seg.kind === 'dynamic' ? seg.prefix : undefined,
248
+ paramSuffix: seg.kind === 'dynamic' ? seg.suffix : undefined,
249
+ };
250
+ }
251
+
195
252
  /**
196
253
  * Parse an interception marker from a directory name.
197
254
  *
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Segment key computation — the stable identity of a segment in the route tree.
3
+ *
4
+ * A segment key is the string that names a segment across the RSC/SSR/client
5
+ * boundary: it appears in the X-Timber-State-Tree header, the X-Timber-Segments
6
+ * header, SegmentOutlet props, the client segment cache, and (since TIM-1279)
7
+ * interception scoping.
8
+ *
9
+ * This module lives in `routing/` rather than `server/` because the keys are a
10
+ * property of the route tree itself — both the build-time scanner tree and the
11
+ * runtime manifest tree produce identical keys for the same directory, which is
12
+ * what lets a build-time value (an interception scope) be compared against a
13
+ * request-time matched chain.
14
+ *
15
+ * See design/19-client-navigation.md §"X-Timber-State-Tree Header"
16
+ */
17
+
18
+ /**
19
+ * Segment node shape expected by computeSegmentKeys.
20
+ *
21
+ * Structurally satisfied by both `SegmentNode` (build time) and
22
+ * `ManifestSegmentNode` (request time) — keys depend only on the
23
+ * URL path and the segment classification, never on file payloads.
24
+ */
25
+ export interface SegmentKeyInput {
26
+ urlPath: string;
27
+ segmentName?: string;
28
+ segmentType?: string;
29
+ }
30
+
31
+ /**
32
+ * Compute state-tree keys for a segment chain.
33
+ *
34
+ * Non-group segments use their urlPath as-is. Route groups accumulate
35
+ * ancestor group names to produce globally unique keys:
36
+ * app/(a)/(shared)/dashboard → keys: ['/', '/(a)', '/(a)/(shared)', '/dashboard']
37
+ *
38
+ * **An intercepting segment switches the chain to name accumulation for good.**
39
+ * A children-path interception (TIM-1280) puts an intercepting node and its
40
+ * descendants into the *main* rendered chain, and their `urlPath`s are not
41
+ * URLs: interception adds no URL depth, so `app/(browse)/(...)[artistSlug]/[year]`
42
+ * carries `/` and `/[year]`. Keyed by urlPath the intercepting node would
43
+ * collide with its own owner, and `[year]` with any top-level `[year]` route —
44
+ * so a client that had one mounted would reuse the wrong cached layout. The
45
+ * switch is sticky rather than per-node because the *descendants* are where
46
+ * the false URLs are; the intercepting node alone is not the problem.
47
+ *
48
+ * This is the single source of truth for segment keys — used by the
49
+ * element builder (skip decisions, SegmentOutlet props), segment info
50
+ * (X-Timber-Segments header), the client cache/state tree, and
51
+ * interception scoping (`routing/interception.ts`).
52
+ */
53
+ export function computeSegmentKeys(segments: SegmentKeyInput[]): string[] {
54
+ const keys: string[] = [];
55
+ let prevKey = '';
56
+ let insideIntercepting = false;
57
+
58
+ for (const segment of segments) {
59
+ if (segment.segmentType === 'intercepting') insideIntercepting = true;
60
+ if (segment.segmentType === 'group' || insideIntercepting) {
61
+ const base = prevKey === '/' ? '' : prevKey;
62
+ const key = `${base}/${segment.segmentName}`;
63
+ keys.push(key);
64
+ prevKey = key;
65
+ } else {
66
+ keys.push(segment.urlPath);
67
+ prevKey = segment.urlPath;
68
+ }
69
+ }
70
+
71
+ return keys;
72
+ }
73
+
74
+ /**
75
+ * Compute tree paths for a segment chain — the directory path of each segment
76
+ * with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.
77
+ *
78
+ * This is `computeSegmentKeys` with the group branch applied to *every*
79
+ * segment rather than only to groups. That one difference is the whole point:
80
+ * `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so
81
+ * `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories
82
+ * that render different layouts must not share an identity.
83
+ *
84
+ * Used for interception scoping (`routing/interception.ts`), where the
85
+ * question is "does this route pass through *that* directory?" — not "does it
86
+ * render this URL?". Not interchangeable with `computeSegmentKeys`: the state
87
+ * tree is keyed by URL on purpose, because the client caches by URL.
88
+ */
89
+ export function computeSegmentTreePaths(segments: SegmentKeyInput[]): string[] {
90
+ const paths: string[] = [];
91
+ let prev = '';
92
+
93
+ for (const segment of segments) {
94
+ const name = segment.segmentName ?? '';
95
+ if (!name) {
96
+ // The app root — no directory name of its own.
97
+ paths.push('/');
98
+ prev = '/';
99
+ continue;
100
+ }
101
+ const base = prev === '/' ? '' : prev;
102
+ const path = `${base}/${name}`;
103
+ paths.push(path);
104
+ prev = path;
105
+ }
106
+
107
+ return paths;
108
+ }
109
+
110
+ /**
111
+ * The directory names a tree path is built from — `/` is none, `/feed/(a)` is
112
+ * `['feed', '(a)']`.
113
+ *
114
+ * The inverse of the walk above, and it lives beside it so the format is
115
+ * stated once: `findChainByTreePath` (`server/children-interception.ts`) walks
116
+ * these names back down the tree to the directory the path addresses, and
117
+ * `treePathDepth` counts them. Both are readings of the same string, and a
118
+ * reader that disagreed with the producer about what separates two names
119
+ * resolves an interception to the wrong node or to none.
120
+ */
121
+ export function treePathNames(treePath: string): string[] {
122
+ return treePath === '/' ? [] : treePath.slice(1).split('/');
123
+ }
124
+
125
+ /**
126
+ * How many directories a tree path names — `/` is 0, `/feed` is 1,
127
+ * `/feed/(a)` is 2.
128
+ *
129
+ * Two tree paths on a single rendered chain are strictly nested, so on that
130
+ * chain depth orders them totally — which is what makes it usable as "the
131
+ * deeper of these two directories" wherever a route passes through both
132
+ * (`routing/interception.ts`).
133
+ */
134
+ export function treePathDepth(treePath: string): number {
135
+ return treePathNames(treePath).length;
136
+ }
137
+
138
+ /**
139
+ * Compute a unique key for a parallel route slot.
140
+ * Format: `{parentSegmentId}/@{slotName}`, e.g. `/@sidebar` or `/dashboard/@modal`.
141
+ */
142
+ export function computeSlotKey(parentSegmentId: string, slotName: string): string {
143
+ const name = slotName.startsWith('@') ? slotName : `@${slotName}`;
144
+ const prefix = parentSegmentId === '/' ? '' : parentSegmentId;
145
+ return `${prefix}/${name}`;
146
+ }