@timber-js/app 0.2.0-alpha.185 → 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 (418) 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-Byy425ng.js.map → segment-classify-C539Pa2O.js.map} +1 -1
  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-_6zKFlch.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 +865 -306
  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 +38 -25
  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 +906 -321
  136. package/dist/index.js.map +1 -1
  137. package/dist/plugin-context.d.ts +31 -0
  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 +136 -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/walkers.d.ts +6 -0
  173. package/dist/routing/walkers.d.ts.map +1 -1
  174. package/dist/search-params/define.d.ts +0 -6
  175. package/dist/search-params/define.d.ts.map +1 -1
  176. package/dist/search-params/index.js +176 -2
  177. package/dist/search-params/index.js.map +1 -1
  178. package/dist/segment-params/define.d.ts +0 -6
  179. package/dist/segment-params/define.d.ts.map +1 -1
  180. package/dist/segment-params/index.js +69 -1
  181. package/dist/segment-params/index.js.map +1 -0
  182. package/dist/server/actions.d.ts +6 -3
  183. package/dist/server/actions.d.ts.map +1 -1
  184. package/dist/server/als-registry.d.ts +2 -1
  185. package/dist/server/als-registry.d.ts.map +1 -1
  186. package/dist/server/build-manifest.d.ts +13 -1
  187. package/dist/server/build-manifest.d.ts.map +1 -1
  188. package/dist/server/chain-url-parts.d.ts +82 -0
  189. package/dist/server/chain-url-parts.d.ts.map +1 -0
  190. package/dist/server/children-interception.d.ts +49 -0
  191. package/dist/server/children-interception.d.ts.map +1 -0
  192. package/dist/server/default-status-page.d.ts +45 -0
  193. package/dist/server/default-status-page.d.ts.map +1 -0
  194. package/dist/server/deny-renderer.d.ts +14 -3
  195. package/dist/server/deny-renderer.d.ts.map +1 -1
  196. package/dist/server/head-response.d.ts +35 -0
  197. package/dist/server/head-response.d.ts.map +1 -0
  198. package/dist/server/index.js +2 -2
  199. package/dist/server/internal.js +393 -212
  200. package/dist/server/internal.js.map +1 -1
  201. package/dist/server/param-coercion.d.ts +10 -1
  202. package/dist/server/param-coercion.d.ts.map +1 -1
  203. package/dist/server/pipeline-helpers.d.ts +0 -18
  204. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  205. package/dist/server/pipeline-interception.d.ts +59 -9
  206. package/dist/server/pipeline-interception.d.ts.map +1 -1
  207. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  208. package/dist/server/pipeline-phases.d.ts.map +1 -1
  209. package/dist/server/pipeline.d.ts +11 -0
  210. package/dist/server/pipeline.d.ts.map +1 -1
  211. package/dist/server/publish-params.d.ts +33 -0
  212. package/dist/server/publish-params.d.ts.map +1 -0
  213. package/dist/server/request-context.d.ts +48 -0
  214. package/dist/server/request-context.d.ts.map +1 -1
  215. package/dist/server/route-element-builder.d.ts +32 -2
  216. package/dist/server/route-element-builder.d.ts.map +1 -1
  217. package/dist/server/route-handler.d.ts.map +1 -1
  218. package/dist/server/route-matcher.d.ts +13 -0
  219. package/dist/server/route-matcher.d.ts.map +1 -1
  220. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  221. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  222. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  223. package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
  224. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  225. package/dist/server/rsc-entry/helpers.d.ts +2 -10
  226. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  227. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  228. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  229. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
  230. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  231. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
  232. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  233. package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
  234. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  235. package/dist/server/skippable-prefix.d.ts +23 -0
  236. package/dist/server/skippable-prefix.d.ts.map +1 -0
  237. package/dist/server/slot-interception.d.ts +80 -0
  238. package/dist/server/slot-interception.d.ts.map +1 -0
  239. package/dist/server/slot-resolver.d.ts +70 -8
  240. package/dist/server/slot-resolver.d.ts.map +1 -1
  241. package/dist/server/ssr-bridge-types.d.ts +6 -0
  242. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  243. package/dist/server/ssr-entry.d.ts.map +1 -1
  244. package/dist/server/ssr-wrappers.d.ts +24 -13
  245. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  246. package/dist/server/state-tree-diff.d.ts +6 -27
  247. package/dist/server/state-tree-diff.d.ts.map +1 -1
  248. package/dist/server/static-generator.d.ts +6 -0
  249. package/dist/server/static-generator.d.ts.map +1 -1
  250. package/dist/server/static-not-found.d.ts +52 -0
  251. package/dist/server/static-not-found.d.ts.map +1 -0
  252. package/dist/shared/als-slots.d.ts +41 -0
  253. package/dist/shared/als-slots.d.ts.map +1 -0
  254. package/dist/shared/base-path.d.ts +56 -0
  255. package/dist/shared/base-path.d.ts.map +1 -0
  256. package/dist/shared/param-value.d.ts +80 -0
  257. package/dist/shared/param-value.d.ts.map +1 -0
  258. package/dist/shared/payload-root.d.ts +108 -0
  259. package/dist/shared/payload-root.d.ts.map +1 -0
  260. package/dist/shared/rsc-cache-key.d.ts +163 -0
  261. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  262. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  263. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  264. package/dist/shared/rsc-manifest.d.ts +28 -0
  265. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  266. package/dist/shared/rsc-payload-path.d.ts +42 -0
  267. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  268. package/dist/shared/segment-info.d.ts +59 -0
  269. package/dist/shared/segment-info.d.ts.map +1 -0
  270. package/dist/shared/slot-params.d.ts +51 -0
  271. package/dist/shared/slot-params.d.ts.map +1 -0
  272. package/dist/shared/static-platform-files.d.ts +71 -0
  273. package/dist/shared/static-platform-files.d.ts.map +1 -0
  274. package/docs/api/31-api-client.mdx +4 -2
  275. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  276. package/docs/learn/11-error-handling.mdx +2 -0
  277. package/docs/learn/12-client-navigation.mdx +2 -2
  278. package/docs/learn/14-deploying.mdx +23 -0
  279. package/docs/more/01-advanced-routing.mdx +172 -2
  280. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  281. package/package.json +2 -2
  282. package/src/adapters/build-output-helper.ts +116 -3
  283. package/src/adapters/cloudflare.ts +86 -11
  284. package/src/adapters/fs-identity.ts +156 -0
  285. package/src/adapters/nitro.ts +6 -6
  286. package/src/adapters/shared.ts +196 -21
  287. package/src/adapters/types.ts +13 -1
  288. package/src/cache/fast-hash.ts +5 -0
  289. package/src/client/browser-entry/action-dispatch.ts +9 -4
  290. package/src/client/browser-entry/hydrate.ts +40 -29
  291. package/src/client/browser-entry/index.ts +15 -1
  292. package/src/client/browser-entry/post-hydration.ts +10 -9
  293. package/src/client/browser-entry/router-init.ts +104 -48
  294. package/src/client/browser-entry/rsc-stream.ts +11 -2
  295. package/src/client/error-boundary.tsx +20 -15
  296. package/src/client/history.ts +13 -6
  297. package/src/client/internal.ts +4 -3
  298. package/src/client/link.tsx +55 -8
  299. package/src/client/navigation-api.ts +3 -3
  300. package/src/client/navigation-commit.ts +174 -0
  301. package/src/client/navigation-context.ts +40 -67
  302. package/src/client/navigation-root.tsx +326 -101
  303. package/src/client/params-context.ts +192 -0
  304. package/src/client/router-effects.ts +231 -0
  305. package/src/client/router-lifecycle.ts +288 -0
  306. package/src/client/router-pipeline.ts +389 -0
  307. package/src/client/router-ref.ts +1 -1
  308. package/src/client/router-skew.ts +30 -0
  309. package/src/client/router-types.ts +230 -0
  310. package/src/client/router.ts +242 -707
  311. package/src/client/rsc-fetch.ts +169 -102
  312. package/src/client/segment-cache.ts +121 -44
  313. package/src/client/ssr-data.ts +6 -0
  314. package/src/client/stale-client.ts +191 -0
  315. package/src/client/state.ts +14 -1
  316. package/src/client/top-loader.tsx +2 -1
  317. package/src/client/unload-guard.ts +34 -2
  318. package/src/client/use-cookie.ts +9 -2
  319. package/src/client/use-router.ts +1 -1
  320. package/src/client/use-segment-params.ts +41 -18
  321. package/src/index.ts +5 -1
  322. package/src/plugin-context.ts +32 -0
  323. package/src/plugins/adapter-build.ts +4 -0
  324. package/src/plugins/build-manifest.ts +34 -13
  325. package/src/plugins/entries.ts +4 -0
  326. package/src/plugins/fonts.ts +11 -8
  327. package/src/plugins/request-dep/analysis.ts +447 -0
  328. package/src/plugins/request-dep/index.ts +376 -0
  329. package/src/plugins/request-dep/origin.ts +310 -0
  330. package/src/plugins/routing.ts +59 -2
  331. package/src/plugins/static-build.ts +315 -73
  332. package/src/routing/codegen-types.ts +18 -0
  333. package/src/routing/codegen.ts +50 -10
  334. package/src/routing/collision-probe.ts +438 -0
  335. package/src/routing/collision-spaces.ts +281 -0
  336. package/src/routing/interception.ts +896 -50
  337. package/src/routing/manifest-codegen.ts +5 -3
  338. package/src/routing/scanner.ts +114 -89
  339. package/src/routing/segment-classify.ts +57 -0
  340. package/src/routing/segment-keys.ts +146 -0
  341. package/src/routing/slot-placement.ts +97 -0
  342. package/src/routing/walkers.ts +8 -0
  343. package/src/search-params/define.ts +6 -29
  344. package/src/segment-params/define.ts +4 -27
  345. package/src/server/action-handler.ts +3 -1
  346. package/src/server/actions.ts +6 -3
  347. package/src/server/als-registry.ts +2 -1
  348. package/src/server/build-manifest.ts +14 -1
  349. package/src/server/chain-url-parts.ts +163 -0
  350. package/src/server/children-interception.ts +115 -0
  351. package/src/server/default-status-page.ts +127 -0
  352. package/src/server/deny-renderer.ts +81 -20
  353. package/src/server/head-response.ts +54 -0
  354. package/src/server/param-coercion.ts +48 -18
  355. package/src/server/pipeline-helpers.ts +0 -45
  356. package/src/server/pipeline-interception.ts +81 -32
  357. package/src/server/pipeline-outcome.ts +2 -4
  358. package/src/server/pipeline-phases.ts +68 -16
  359. package/src/server/pipeline.ts +57 -10
  360. package/src/server/prebuilt-runtime.ts +4 -4
  361. package/src/server/publish-params.ts +42 -0
  362. package/src/server/request-context.ts +97 -18
  363. package/src/server/route-element-builder.ts +89 -98
  364. package/src/server/route-handler.ts +8 -8
  365. package/src/server/route-matcher.ts +21 -9
  366. package/src/server/rsc-cache-key-guard.ts +170 -0
  367. package/src/server/rsc-entry/api-handler.ts +6 -8
  368. package/src/server/rsc-entry/error-renderer.ts +64 -25
  369. package/src/server/rsc-entry/helpers.ts +31 -16
  370. package/src/server/rsc-entry/index.ts +36 -28
  371. package/src/server/rsc-entry/render-route.ts +9 -4
  372. package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
  373. package/src/server/rsc-entry/rsc-payload.ts +14 -25
  374. package/src/server/rsc-entry/rsc-stream.ts +4 -3
  375. package/src/server/rsc-entry/ssr-renderer.ts +13 -9
  376. package/src/server/skippable-prefix.ts +92 -0
  377. package/src/server/slot-interception.ts +168 -0
  378. package/src/server/slot-resolver.ts +253 -156
  379. package/src/server/ssr-bridge-types.ts +6 -0
  380. package/src/server/ssr-entry.ts +12 -4
  381. package/src/server/ssr-wrappers.tsx +33 -32
  382. package/src/server/state-tree-diff.ts +14 -55
  383. package/src/server/static-generator.ts +65 -27
  384. package/src/server/static-not-found.ts +134 -0
  385. package/src/shared/als-slots.ts +72 -0
  386. package/src/shared/base-path.ts +80 -0
  387. package/src/shared/param-value.ts +244 -0
  388. package/src/shared/payload-root.ts +151 -0
  389. package/src/shared/rsc-cache-key.ts +269 -0
  390. package/src/shared/rsc-error-envelope.ts +31 -0
  391. package/src/shared/rsc-manifest.ts +29 -0
  392. package/src/shared/rsc-payload-path.ts +45 -0
  393. package/src/shared/segment-info.ts +60 -0
  394. package/src/shared/slot-params.ts +111 -0
  395. package/src/shared/static-platform-files.ts +75 -0
  396. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  397. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  398. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  399. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
  400. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
  401. package/dist/_chunks/define-COtkxMRT.js +0 -191
  402. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  403. package/dist/_chunks/define-c4au4I9R.js +0 -82
  404. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  405. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  406. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  407. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  408. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  409. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  410. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  411. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  412. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  413. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  414. package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
  415. package/dist/client/error-boundary.js.map +0 -1
  416. package/dist/plugins/request-dep.d.ts +0 -32
  417. package/dist/plugins/request-dep.d.ts.map +0 -1
  418. package/src/plugins/request-dep.ts +0 -499
@@ -160,11 +160,13 @@ export function generateManifestModule(tree: RouteTree, viteRoot: string): strin
160
160
  globalErrorLine = ` globalError: { load: ${v}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
161
161
  }
162
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).
163
167
  const rewrites = collectInterceptionRewrites(tree.root);
164
168
  const rewritesLine =
165
- rewrites.length > 0
166
- ? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({ interceptedPattern: r.interceptedPattern, interceptingPrefix: r.interceptingPrefix })))},`
167
- : '';
169
+ rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites)},` : '';
168
170
 
169
171
  const code = [
170
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';
@@ -76,6 +79,12 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
76
79
  // Scan children recursively
77
80
  scanChildren(appDir, tree.root, extSet);
78
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
+
79
88
  // Validate: detect route group collisions (different groups producing pages at the same URL)
80
89
  validateRouteGroupCollisions(tree.root);
81
90
 
@@ -91,6 +100,10 @@ export function scanRoutes(appDir: string, config: ScannerConfig = {}): RouteTre
91
100
  // e.g., a-[id] and [slug]-b both have prefix+suffix length = 2
92
101
  validateAffixedSiblingConflicts(tree.root);
93
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
+
94
107
  return tree;
95
108
  }
96
109
 
@@ -355,69 +368,6 @@ function scanChildren(dirPath: string, parentNode: SegmentNode, extSet: Set<stri
355
368
  }
356
369
  }
357
370
 
358
- /**
359
- * Validate that route groups don't produce conflicting pages/routes at the same URL path.
360
- *
361
- * Two route groups like (auth)/login/page.tsx and (marketing)/login/page.tsx both claim
362
- * /login — the scanner must detect and reject this at build time.
363
- *
364
- * Each parallel slot is validated as its OWN collision space: a slot's pages
365
- * intentionally share URL paths with the parent tree (that's the point of
366
- * parallel routes), but two route groups INSIDE one slot claiming the same
367
- * URL — e.g. @side/(a)/x/page.tsx + @side/(b)/x/page.tsx — still collide.
368
- */
369
- function validateRouteGroupCollisions(root: SegmentNode): void {
370
- // Map from urlPath → { filePath, source } for the first page/route seen at that path
371
- collectRoutableLeaves(root, new Map(), '');
372
- }
373
-
374
- /**
375
- * Walk the segment tree and collect all routable leaves (page or route files),
376
- * throwing on collision. Each slot recurses with a FRESH collision map —
377
- * slots are parallel routes that intentionally share URL paths with their
378
- * parent tree, but collisions within a single slot are real errors.
379
- */
380
- function collectRoutableLeaves(
381
- node: SegmentNode,
382
- seen: Map<string, { filePath: string; segmentPath: string }>,
383
- segmentPath: string
384
- ): void {
385
- // Intercepting routes are resolved by the slot resolver, not URL matching,
386
- // and their computed urlPaths intentionally mirror other routes (e.g. two
387
- // interceptors in one slot both produce /gallery/[id]). Skip them entirely.
388
- if (node.segmentType === 'intercepting') return;
389
-
390
- const currentPath = segmentPath
391
- ? `${segmentPath}/${node.segmentName}`
392
- : node.segmentName || '(root)';
393
-
394
- const routableFile = node.page ?? node.route;
395
- if (routableFile) {
396
- const existing = seen.get(node.urlPath);
397
- if (existing) {
398
- throw new Error(
399
- `Build error: route collision — multiple route groups produce a page/route at the same URL path.\n` +
400
- ` URL path: ${node.urlPath}\n` +
401
- ` File 1: ${existing.filePath} (via ${existing.segmentPath})\n` +
402
- ` File 2: ${routableFile.filePath} (via ${currentPath})\n` +
403
- `Each URL path must map to exactly one page or route handler. ` +
404
- `Rename or move one of the conflicting files.`
405
- );
406
- }
407
- seen.set(node.urlPath, { filePath: routableFile.filePath, segmentPath: currentPath });
408
- }
409
-
410
- // Recurse into children
411
- for (const child of node.children) {
412
- collectRoutableLeaves(child, seen, currentPath);
413
- }
414
-
415
- // Recurse into slots — each slot gets a fresh map (its own collision space)
416
- for (const slotNode of Object.values(node.slots)) {
417
- collectRoutableLeaves(slotNode, new Map(), currentPath);
418
- }
419
- }
420
-
421
371
  /**
422
372
  * Validate that no route chain contains duplicate dynamic param names.
423
373
  *
@@ -438,12 +388,20 @@ function validateDuplicateParamNames(root: SegmentNode): void {
438
388
  * Throws on the first duplicate found.
439
389
  */
440
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
+
441
399
  // If this node introduces a param name, check for duplicates
442
- if (node.paramName) {
443
- const existing = seen.get(node.paramName);
400
+ if (paramName) {
401
+ const existing = seen.get(paramName);
444
402
  if (existing) {
445
403
  throw new Error(
446
- `[timber] Duplicate param name '${node.paramName}' in route chain.\n` +
404
+ `[timber] Duplicate param name '${paramName}' in route chain.\n` +
447
405
  ` First defined at: ${existing}\n` +
448
406
  ` Duplicate at: ${node.urlPath || '/'}\n` +
449
407
  ` Rename one of the segments to avoid ambiguity.`
@@ -451,7 +409,7 @@ function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): v
451
409
  }
452
410
  // Add to seen for descendants (use a new Map to avoid polluting siblings)
453
411
  seen = new Map(seen);
454
- seen.set(node.paramName, node.urlPath || '/');
412
+ seen.set(paramName, node.urlPath || '/');
455
413
  }
456
414
 
457
415
  // Recurse into children (they inherit the accumulated params)
@@ -459,8 +417,16 @@ function walkForDuplicateParams(node: SegmentNode, seen: Map<string, string>): v
459
417
  walkForDuplicateParams(child, seen);
460
418
  }
461
419
 
462
- // Slots are independent parallel routes — start fresh param tracking
463
- // (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.
464
430
  for (const slotNode of Object.values(node.slots)) {
465
431
  walkForDuplicateParams(slotNode, new Map(seen));
466
432
  }
@@ -488,25 +454,29 @@ function collectBracketForms(
488
454
  node: SegmentNode,
489
455
  forms: Map<string, { segmentType: SegmentType; urlPath: string }>
490
456
  ): void {
491
- if (node.paramName && node.segmentType !== 'static' && node.segmentType !== 'group') {
492
- const existing = forms.get(node.paramName);
493
- 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) {
494
464
  const bracketExamples: Record<string, string> = {
495
- 'dynamic': `[${node.paramName}]`,
496
- 'catch-all': `[...${node.paramName}]`,
497
- 'optional-catch-all': `[[...${node.paramName}]]`,
465
+ 'dynamic': `[${paramName}]`,
466
+ 'catch-all': `[...${paramName}]`,
467
+ 'optional-catch-all': `[[...${paramName}]]`,
498
468
  };
499
469
  throw new Error(
500
- `[timber] Param name '${node.paramName}' uses multiple bracket forms.\n` +
470
+ `[timber] Param name '${paramName}' uses multiple bracket forms.\n` +
501
471
  ` ${bracketExamples[existing.segmentType] ?? existing.segmentType} at: ${existing.urlPath}\n` +
502
- ` ${bracketExamples[node.segmentType] ?? node.segmentType} at: ${node.urlPath || '/'}\n` +
472
+ ` ${bracketExamples[segmentType] ?? segmentType} at: ${node.urlPath || '/'}\n` +
503
473
  ` Each param name must use a single bracket form across all routes.\n` +
504
- ` 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]).`
505
475
  );
506
476
  }
507
477
  if (!existing) {
508
- forms.set(node.paramName, {
509
- segmentType: node.segmentType,
478
+ forms.set(paramName, {
479
+ segmentType,
510
480
  urlPath: node.urlPath || '/',
511
481
  });
512
482
  }
@@ -530,20 +500,39 @@ function collectBracketForms(
530
500
  * Route groups are URL-transparent, so affixed segments inside different groups
531
501
  * at the same level still conflict with each other.
532
502
  *
533
- * 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".
534
519
  */
535
520
  function validateAffixedSiblingConflicts(node: SegmentNode): void {
536
521
  const affixed = collectEffectiveAffixed(node);
537
522
 
538
523
  if (affixed.length >= 2) {
539
- const byLength = new Map<number, SegmentNode[]>();
524
+ const byLength = new Map<string, SegmentNode[]>();
540
525
  for (const child of affixed) {
541
- const len = (child.paramPrefix?.length ?? 0) + (child.paramSuffix?.length ?? 0);
542
- 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);
543
532
  if (group) {
544
533
  group.push(child);
545
534
  } else {
546
- byLength.set(len, [child]);
535
+ byLength.set(key, [child]);
547
536
  }
548
537
  }
549
538
 
@@ -553,7 +542,7 @@ function validateAffixedSiblingConflicts(node: SegmentNode): void {
553
542
  `Build error: ambiguous affixed dynamic siblings with equal specificity.\n` +
554
543
  ` Parent: ${node.urlPath || '/'}\n` +
555
544
  ` Segments: ${group.map((s) => s.segmentName).join(', ')}\n` +
556
- ` Combined prefix+suffix length: ${(group[0]!.paramPrefix?.length ?? 0) + (group[0]!.paramSuffix?.length ?? 0)}\n` +
545
+ ` Combined prefix+suffix length: ${affixLengthOf(group[0]!)}\n` +
557
546
  `The router cannot deterministically choose between these — filesystem scan order is platform-dependent.\n` +
558
547
  `Rename one segment to give it a longer or shorter combined affix.`
559
548
  );
@@ -569,16 +558,52 @@ function validateAffixedSiblingConflicts(node: SegmentNode): void {
569
558
  }
570
559
  }
571
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
+
572
588
  /**
573
589
  * Collect affixed dynamic segments that are effective children of a node.
574
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).
575
599
  */
576
600
  function collectEffectiveAffixed(node: SegmentNode): SegmentNode[] {
577
601
  const result: SegmentNode[] = [];
578
602
  for (const child of node.children) {
579
- if (child.segmentType === 'dynamic' && (child.paramPrefix || child.paramSuffix)) {
603
+ const { segmentType, paramPrefix, paramSuffix } = effectiveUrlSegment(child);
604
+ if (segmentType === 'dynamic' && (paramPrefix || paramSuffix)) {
580
605
  result.push(child);
581
- } else if (child.segmentType === 'group') {
606
+ } else if (segmentType === 'group') {
582
607
  result.push(...collectEffectiveAffixed(child));
583
608
  }
584
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
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Parallel slot placement.
3
+ *
4
+ * One rule: a slot is resolved only for a segment on the **matched route
5
+ * chain**, and a slot's own subtree is never on that chain. So a slot declared
6
+ * anywhere inside another slot's subtree can never render, and is rejected at
7
+ * build time rather than left as a directory the author will keep editing.
8
+ *
9
+ * See design/07-routing.md §"Parallel Routes".
10
+ */
11
+
12
+ import type { SegmentNode } from './types.js';
13
+
14
+ /**
15
+ * Reject every parallel slot that no renderer reaches.
16
+ *
17
+ * **Where slots are resolved.** Two places read `segment.slots`, and both walk
18
+ * the main route chain: `route-element-builder.ts` (the RSC render) and
19
+ * `tree-builder.ts`. Neither is reachable from inside a slot. The slot's own
20
+ * subtree is rendered by `slot-resolver.ts`, which walks the slot's matched
21
+ * chain to apply that chain's layouts, error boundaries and access gates
22
+ * (`wrapWithIntermediateSegments`, `wrapWithLayout`) and never reads `.slots`
23
+ * on any of those nodes. So `app/feed/@modal/photo/@side/page.tsx` and
24
+ * everything below it is dead: the file is scanned, typed, code-split and
25
+ * shipped, and no request can render it.
26
+ *
27
+ * That is the TIM-1278 failure — a directory that silently never renders —
28
+ * and the reason it is a build error rather than a documented limit is that
29
+ * nothing at request time can report it. A slot that does not match renders
30
+ * `default.tsx` or `null`, which is exactly what a slot the resolver never
31
+ * looked at also produces, so the author sees the *designed* behaviour of a
32
+ * non-matching slot and has no signal to distinguish it from a slot that
33
+ * cannot match at all.
34
+ *
35
+ * **Interception is incidental.** TIM-1311 arrived as
36
+ * `feed/@modal/(.)photo/@side`, and the interception contributes nothing: the
37
+ * identical `feed/@modal/photo/@side` never renders either, for the same
38
+ * reason and with no marker anywhere. Rejecting only the intercepting spelling
39
+ * would have drawn a line the author cannot see from the directory names.
40
+ *
41
+ * The children-path interception shape is **not** affected and must not be:
42
+ * its intercepting node joins the main chain (`server/children-interception.ts`
43
+ * splices `[…owner ancestors, interceptingNode, …descendants]` into an ordinary
44
+ * `RouteMatch`), so `route-element-builder.ts` resolves slots on it and below
45
+ * it exactly as it does for any other chain segment. `(browse)/(...)photo/[id]/@side`
46
+ * renders; `feed/@modal/(.)photo/@side` does not. The discriminator is the slot
47
+ * boundary, which is what this walk tracks — not the marker.
48
+ */
49
+ export function validateSlotPlacement(root: SegmentNode): void {
50
+ walkForSlotPlacement(root, '', null);
51
+ }
52
+
53
+ /**
54
+ * `enclosingSlotPath` is the `app/`-relative path of the nearest slot at or
55
+ * above `node`, or null on the main chain. It is carried rather than recomputed
56
+ * so the error can name the slot that swallows the offender — "this is inside
57
+ * @modal" is the part of the diagnosis the author cannot see from the offending
58
+ * path alone.
59
+ */
60
+ function walkForSlotPlacement(
61
+ node: SegmentNode,
62
+ path: string,
63
+ enclosingSlotPath: string | null
64
+ ): void {
65
+ for (const [slotName, slotNode] of Object.entries(node.slots)) {
66
+ const slotPath = path ? `${path}/@${slotName}` : `@${slotName}`;
67
+ if (enclosingSlotPath !== null) {
68
+ throwNestedSlot(slotPath, enclosingSlotPath);
69
+ }
70
+ walkForSlotPlacement(slotNode, slotPath, slotPath);
71
+ }
72
+
73
+ for (const child of node.children) {
74
+ walkForSlotPlacement(
75
+ child,
76
+ path ? `${path}/${child.segmentName}` : child.segmentName,
77
+ enclosingSlotPath
78
+ );
79
+ }
80
+ }
81
+
82
+ function throwNestedSlot(slotPath: string, enclosingSlotPath: string): never {
83
+ throw new Error(
84
+ `Build error: parallel slot nested inside another parallel slot.\n` +
85
+ ` Directory: app/${slotPath}\n` +
86
+ ` Inside: app/${enclosingSlotPath}\n` +
87
+ `A parallel slot is resolved only for a segment on the matched route chain, and a ` +
88
+ `slot's own subtree is never on that chain — the slot resolver walks the slot's ` +
89
+ `matched chain to apply its layouts, error boundaries and access gates, and never ` +
90
+ `looks at slots declared inside it. So app/${slotPath} and everything below it can ` +
91
+ `never render, and no request-time signal can tell you: an unreachable slot ` +
92
+ `produces exactly what a slot that simply did not match produces.\n` +
93
+ `Declare the slot on a segment of the main route chain instead, or move its pages ` +
94
+ `into app/${enclosingSlotPath}'s own subtree.\n` +
95
+ `See design/07-routing.md §"Parallel Routes".`
96
+ );
97
+ }
@@ -49,6 +49,12 @@ export interface CollectLeafRoutesOptions {
49
49
  * entries (the build report deduplicates by URL afterward).
50
50
  * - Parallel slots are skipped unless `includeSlots: true` (slots
51
51
  * share their parent's URL and are not addressable on their own).
52
+ * - Intercepting subtrees are skipped unconditionally. They render only
53
+ * on soft navigation and add no URL depth, so their computed `urlPath`s
54
+ * are not addresses — listing them would advertise routes (`/[year]`)
55
+ * that no request can reach. This matches every other consumer of the
56
+ * tree that produces URLs: the sitemap generator, the static generator,
57
+ * prerendering and the prebuilt-payload builder all skip them too.
52
58
  * - Result is sorted by `urlPath` for deterministic output.
53
59
  */
54
60
  export function collectLeafRoutes<TFile>(
@@ -68,6 +74,8 @@ function walk<TFile>(
68
74
  result: LeafRoute<TFile>[],
69
75
  includeSlots: boolean
70
76
  ): void {
77
+ if (node.segmentType === 'intercepting') return;
78
+
71
79
  const currentChain = [...chain, node];
72
80
  const path = node.urlPath || '/';
73
81