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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (570) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-35jnMdeJ.js} +4 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-35jnMdeJ.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
  5. package/dist/_chunks/als-slots-BEEIPKYm.js +26 -0
  6. package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -0
  7. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  8. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  9. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  10. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  11. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  12. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  13. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-DjNrIWRR.js} +8 -14
  14. package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
  15. package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
  16. package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
  17. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
  18. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
  19. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-CGP6BZKO.js} +92 -14
  20. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
  21. package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
  22. package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
  23. package/dist/_chunks/error-boundary-D-ODYX41.js +353 -0
  24. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
  25. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  26. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  27. package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
  28. package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
  29. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  30. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  31. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-D8xJZXIN.js} +34 -51
  32. package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
  33. package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
  34. package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
  35. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-rCinWLiE.js} +8 -2
  36. package/dist/_chunks/plugin-context-rCinWLiE.js.map +1 -0
  37. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  38. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  39. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
  40. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
  41. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  42. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  43. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  44. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  45. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  46. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  47. package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
  48. package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
  49. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  50. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  51. package/dist/_chunks/scanner-C8b0Gcw3.js +1564 -0
  52. package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
  53. package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
  54. package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
  55. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  56. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  57. package/dist/_chunks/segment-context-CjOlyB8Y.js +97 -0
  58. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
  59. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  60. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  61. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  62. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  63. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  64. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  65. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  66. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  67. package/dist/_chunks/use-segment-params-C4r4BD9T.js +398 -0
  68. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  69. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  70. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  71. package/dist/adapters/build-output-helper.d.ts +37 -0
  72. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  73. package/dist/adapters/cloudflare-dev.js +1 -1
  74. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  75. package/dist/adapters/cloudflare.d.ts.map +1 -1
  76. package/dist/adapters/cloudflare.js +1 -1
  77. package/dist/adapters/compress-module.d.ts +12 -0
  78. package/dist/adapters/compress-module.d.ts.map +1 -1
  79. package/dist/adapters/fs-identity.d.ts +64 -0
  80. package/dist/adapters/fs-identity.d.ts.map +1 -0
  81. package/dist/adapters/nitro.d.ts.map +1 -1
  82. package/dist/adapters/nitro.js +56 -5
  83. package/dist/adapters/nitro.js.map +1 -1
  84. package/dist/adapters/shared.d.ts +57 -1
  85. package/dist/adapters/shared.d.ts.map +1 -1
  86. package/dist/adapters/types.d.ts +13 -1
  87. package/dist/adapters/types.d.ts.map +1 -1
  88. package/dist/cache/fast-hash.d.ts +5 -0
  89. package/dist/cache/fast-hash.d.ts.map +1 -1
  90. package/dist/cache/index.js +1 -1
  91. package/dist/cdn/cloudflare-purge.js +30 -0
  92. package/dist/cdn/cloudflare-purge.js.map +1 -0
  93. package/dist/cdn/fastly-purge.js +33 -0
  94. package/dist/cdn/fastly-purge.js.map +1 -0
  95. package/dist/cdn/index.js +94 -0
  96. package/dist/cdn/index.js.map +1 -0
  97. package/dist/cdn/workers-cache-purge.js +35 -0
  98. package/dist/cdn/workers-cache-purge.js.map +1 -0
  99. package/dist/cli-check.d.ts +153 -0
  100. package/dist/cli-check.d.ts.map +1 -0
  101. package/dist/cli.d.ts +34 -8
  102. package/dist/cli.d.ts.map +1 -1
  103. package/dist/cli.js +46 -21
  104. package/dist/cli.js.map +1 -1
  105. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  106. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  107. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  108. package/dist/client/browser-entry/index.d.ts +1 -1
  109. package/dist/client/browser-entry/index.d.ts.map +1 -1
  110. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  111. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  112. package/dist/client/browser-entry/router-init.d.ts +3 -0
  113. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  114. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  115. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  116. package/dist/client/error-boundary.d.ts +7 -0
  117. package/dist/client/error-boundary.d.ts.map +1 -1
  118. package/dist/client/error-boundary.js +1 -162
  119. package/dist/client/history.d.ts +13 -6
  120. package/dist/client/history.d.ts.map +1 -1
  121. package/dist/client/index.d.ts +1 -0
  122. package/dist/client/index.d.ts.map +1 -1
  123. package/dist/client/index.js +42 -41
  124. package/dist/client/index.js.map +1 -1
  125. package/dist/client/internal.d.ts +4 -3
  126. package/dist/client/internal.d.ts.map +1 -1
  127. package/dist/client/internal.js +877 -311
  128. package/dist/client/internal.js.map +1 -1
  129. package/dist/client/link.d.ts +1 -7
  130. package/dist/client/link.d.ts.map +1 -1
  131. package/dist/client/navigation-api.d.ts +2 -2
  132. package/dist/client/navigation-commit.d.ts +87 -0
  133. package/dist/client/navigation-commit.d.ts.map +1 -0
  134. package/dist/client/navigation-context.d.ts +37 -20
  135. package/dist/client/navigation-context.d.ts.map +1 -1
  136. package/dist/client/navigation-root.d.ts +63 -23
  137. package/dist/client/navigation-root.d.ts.map +1 -1
  138. package/dist/client/params-context.d.ts +81 -0
  139. package/dist/client/params-context.d.ts.map +1 -0
  140. package/dist/client/router-effects.d.ts +129 -0
  141. package/dist/client/router-effects.d.ts.map +1 -0
  142. package/dist/client/router-lifecycle.d.ts +55 -0
  143. package/dist/client/router-lifecycle.d.ts.map +1 -0
  144. package/dist/client/router-pipeline.d.ts +84 -0
  145. package/dist/client/router-pipeline.d.ts.map +1 -0
  146. package/dist/client/router-ref.d.ts +1 -1
  147. package/dist/client/router-ref.d.ts.map +1 -1
  148. package/dist/client/router-skew.d.ts +21 -0
  149. package/dist/client/router-skew.d.ts.map +1 -0
  150. package/dist/client/router-types.d.ts +207 -0
  151. package/dist/client/router-types.d.ts.map +1 -0
  152. package/dist/client/router.d.ts +1 -182
  153. package/dist/client/router.d.ts.map +1 -1
  154. package/dist/client/rsc-fetch.d.ts +47 -26
  155. package/dist/client/rsc-fetch.d.ts.map +1 -1
  156. package/dist/client/segment-cache.d.ts +87 -34
  157. package/dist/client/segment-cache.d.ts.map +1 -1
  158. package/dist/client/ssr-data.d.ts +6 -0
  159. package/dist/client/ssr-data.d.ts.map +1 -1
  160. package/dist/client/stale-client.d.ts +69 -0
  161. package/dist/client/stale-client.d.ts.map +1 -0
  162. package/dist/client/state.d.ts +10 -1
  163. package/dist/client/state.d.ts.map +1 -1
  164. package/dist/client/top-loader.d.ts +2 -1
  165. package/dist/client/top-loader.d.ts.map +1 -1
  166. package/dist/client/unload-guard.d.ts +5 -0
  167. package/dist/client/unload-guard.d.ts.map +1 -1
  168. package/dist/client/use-cookie.d.ts.map +1 -1
  169. package/dist/client/use-query-states.d.ts +9 -3
  170. package/dist/client/use-query-states.d.ts.map +1 -1
  171. package/dist/client/use-router.d.ts +1 -1
  172. package/dist/client/use-segment-params.d.ts +18 -8
  173. package/dist/client/use-segment-params.d.ts.map +1 -1
  174. package/dist/codec.js +1 -1
  175. package/dist/cookies/define-cookie.d.ts.map +1 -1
  176. package/dist/cookies/index.js +5 -5
  177. package/dist/cookies/index.js.map +1 -1
  178. package/dist/index.d.ts +4 -1
  179. package/dist/index.d.ts.map +1 -1
  180. package/dist/index.js +1009 -759
  181. package/dist/index.js.map +1 -1
  182. package/dist/params/index.js +1 -1
  183. package/dist/plugin-context.d.ts +46 -0
  184. package/dist/plugin-context.d.ts.map +1 -1
  185. package/dist/plugins/adapter-build.d.ts.map +1 -1
  186. package/dist/plugins/build-manifest.d.ts +1 -1
  187. package/dist/plugins/build-manifest.d.ts.map +1 -1
  188. package/dist/plugins/entries.d.ts.map +1 -1
  189. package/dist/plugins/fonts.d.ts.map +1 -1
  190. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  191. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  192. package/dist/plugins/request-dep/index.d.ts +75 -0
  193. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  194. package/dist/plugins/request-dep/origin.d.ts +61 -0
  195. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  196. package/dist/plugins/routing.d.ts.map +1 -1
  197. package/dist/plugins/shims.d.ts.map +1 -1
  198. package/dist/plugins/static-build.d.ts +160 -8
  199. package/dist/plugins/static-build.d.ts.map +1 -1
  200. package/dist/routing/codegen-shared.d.ts +3 -44
  201. package/dist/routing/codegen-shared.d.ts.map +1 -1
  202. package/dist/routing/codegen-types.d.ts +28 -31
  203. package/dist/routing/codegen-types.d.ts.map +1 -1
  204. package/dist/routing/codegen-write.d.ts +51 -0
  205. package/dist/routing/codegen-write.d.ts.map +1 -0
  206. package/dist/routing/codegen.d.ts.map +1 -1
  207. package/dist/routing/collision-probe.d.ts +104 -0
  208. package/dist/routing/collision-probe.d.ts.map +1 -0
  209. package/dist/routing/collision-spaces.d.ts +26 -0
  210. package/dist/routing/collision-spaces.d.ts.map +1 -0
  211. package/dist/routing/convention-lint.d.ts +18 -4
  212. package/dist/routing/convention-lint.d.ts.map +1 -1
  213. package/dist/routing/export-detect.d.ts +16 -0
  214. package/dist/routing/export-detect.d.ts.map +1 -1
  215. package/dist/routing/index.js +4 -3
  216. package/dist/routing/interception.d.ts +235 -6
  217. package/dist/routing/interception.d.ts.map +1 -1
  218. package/dist/routing/link-codegen.d.ts +19 -4
  219. package/dist/routing/link-codegen.d.ts.map +1 -1
  220. package/dist/routing/manifest-codegen.d.ts +1 -7
  221. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  222. package/dist/routing/scanner.d.ts.map +1 -1
  223. package/dist/routing/segment-classify.d.ts +33 -0
  224. package/dist/routing/segment-classify.d.ts.map +1 -1
  225. package/dist/routing/segment-keys.d.ts +95 -0
  226. package/dist/routing/segment-keys.d.ts.map +1 -0
  227. package/dist/routing/slot-placement.d.ts +48 -0
  228. package/dist/routing/slot-placement.d.ts.map +1 -0
  229. package/dist/routing/types.d.ts +0 -6
  230. package/dist/routing/types.d.ts.map +1 -1
  231. package/dist/routing/walkers.d.ts +6 -0
  232. package/dist/routing/walkers.d.ts.map +1 -1
  233. package/dist/schema-bridge.d.ts +60 -9
  234. package/dist/schema-bridge.d.ts.map +1 -1
  235. package/dist/search-params/define.d.ts +62 -14
  236. package/dist/search-params/define.d.ts.map +1 -1
  237. package/dist/search-params/index.d.ts +0 -1
  238. package/dist/search-params/index.d.ts.map +1 -1
  239. package/dist/search-params/index.js +221 -10
  240. package/dist/search-params/index.js.map +1 -1
  241. package/dist/search-params/parse-total.d.ts +70 -0
  242. package/dist/search-params/parse-total.d.ts.map +1 -0
  243. package/dist/search-params/wrappers.d.ts +26 -3
  244. package/dist/search-params/wrappers.d.ts.map +1 -1
  245. package/dist/server/access-gate.d.ts +19 -8
  246. package/dist/server/access-gate.d.ts.map +1 -1
  247. package/dist/server/action-handler.d.ts.map +1 -1
  248. package/dist/server/actions.d.ts +6 -3
  249. package/dist/server/actions.d.ts.map +1 -1
  250. package/dist/server/als-registry.d.ts +18 -1
  251. package/dist/server/als-registry.d.ts.map +1 -1
  252. package/dist/server/build-manifest.d.ts +13 -1
  253. package/dist/server/build-manifest.d.ts.map +1 -1
  254. package/dist/server/chain-url-parts.d.ts +82 -0
  255. package/dist/server/chain-url-parts.d.ts.map +1 -0
  256. package/dist/server/children-interception.d.ts +49 -0
  257. package/dist/server/children-interception.d.ts.map +1 -0
  258. package/dist/server/compress.d.ts.map +1 -1
  259. package/dist/server/default-status-page.d.ts +45 -0
  260. package/dist/server/default-status-page.d.ts.map +1 -0
  261. package/dist/server/deny-boundary.d.ts +148 -15
  262. package/dist/server/deny-boundary.d.ts.map +1 -1
  263. package/dist/server/deny-renderer.d.ts +15 -4
  264. package/dist/server/deny-renderer.d.ts.map +1 -1
  265. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  266. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  267. package/dist/server/head-response.d.ts +35 -0
  268. package/dist/server/head-response.d.ts.map +1 -0
  269. package/dist/server/index.d.ts +0 -1
  270. package/dist/server/index.d.ts.map +1 -1
  271. package/dist/server/index.js +3 -2
  272. package/dist/server/index.js.map +1 -1
  273. package/dist/server/internal.d.ts +3 -1
  274. package/dist/server/internal.d.ts.map +1 -1
  275. package/dist/server/internal.js +712 -418
  276. package/dist/server/internal.js.map +1 -1
  277. package/dist/server/metadata-collector.d.ts +52 -0
  278. package/dist/server/metadata-collector.d.ts.map +1 -0
  279. package/dist/server/param-coercion.d.ts +22 -6
  280. package/dist/server/param-coercion.d.ts.map +1 -1
  281. package/dist/server/pipeline-helpers.d.ts +0 -18
  282. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  283. package/dist/server/pipeline-interception.d.ts +59 -9
  284. package/dist/server/pipeline-interception.d.ts.map +1 -1
  285. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  286. package/dist/server/pipeline-phases.d.ts.map +1 -1
  287. package/dist/server/pipeline.d.ts +11 -0
  288. package/dist/server/pipeline.d.ts.map +1 -1
  289. package/dist/server/primitives.d.ts +23 -0
  290. package/dist/server/primitives.d.ts.map +1 -1
  291. package/dist/server/publish-params.d.ts +33 -0
  292. package/dist/server/publish-params.d.ts.map +1 -0
  293. package/dist/server/request-context.d.ts +48 -0
  294. package/dist/server/request-context.d.ts.map +1 -1
  295. package/dist/server/route-element-builder.d.ts +22 -3
  296. package/dist/server/route-element-builder.d.ts.map +1 -1
  297. package/dist/server/route-handler.d.ts.map +1 -1
  298. package/dist/server/route-matcher.d.ts +13 -0
  299. package/dist/server/route-matcher.d.ts.map +1 -1
  300. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  301. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  302. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  303. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  304. package/dist/server/rsc-entry/error-renderer.d.ts +15 -3
  305. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  306. package/dist/server/rsc-entry/helpers.d.ts +2 -17
  307. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  308. package/dist/server/rsc-entry/index.d.ts +0 -1
  309. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  310. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  311. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  312. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -2
  313. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  314. package/dist/server/rsc-entry/rsc-stream.d.ts +15 -2
  315. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  316. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  317. package/dist/server/skippable-prefix.d.ts +23 -0
  318. package/dist/server/skippable-prefix.d.ts.map +1 -0
  319. package/dist/server/slot-interception.d.ts +80 -0
  320. package/dist/server/slot-interception.d.ts.map +1 -0
  321. package/dist/server/slot-resolver.d.ts +70 -8
  322. package/dist/server/slot-resolver.d.ts.map +1 -1
  323. package/dist/server/ssr-bridge-types.d.ts +17 -0
  324. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  325. package/dist/server/ssr-entry.d.ts +0 -1
  326. package/dist/server/ssr-entry.d.ts.map +1 -1
  327. package/dist/server/ssr-wrappers.d.ts +24 -13
  328. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  329. package/dist/server/state-tree-diff.d.ts +6 -27
  330. package/dist/server/state-tree-diff.d.ts.map +1 -1
  331. package/dist/server/static-generator.d.ts +6 -0
  332. package/dist/server/static-generator.d.ts.map +1 -1
  333. package/dist/server/static-not-found.d.ts +52 -0
  334. package/dist/server/static-not-found.d.ts.map +1 -0
  335. package/dist/server/status-code-resolver.d.ts +8 -1
  336. package/dist/server/status-code-resolver.d.ts.map +1 -1
  337. package/dist/server/tree-builder.d.ts +28 -36
  338. package/dist/server/tree-builder.d.ts.map +1 -1
  339. package/dist/server/types.d.ts +12 -7
  340. package/dist/server/types.d.ts.map +1 -1
  341. package/dist/server/utils/element-type.d.ts +40 -0
  342. package/dist/server/utils/element-type.d.ts.map +1 -0
  343. package/dist/shared/als-slots.d.ts +41 -0
  344. package/dist/shared/als-slots.d.ts.map +1 -0
  345. package/dist/shared/base-path.d.ts +56 -0
  346. package/dist/shared/base-path.d.ts.map +1 -0
  347. package/dist/shared/param-value.d.ts +80 -0
  348. package/dist/shared/param-value.d.ts.map +1 -0
  349. package/dist/shared/payload-root.d.ts +108 -0
  350. package/dist/shared/payload-root.d.ts.map +1 -0
  351. package/dist/shared/rsc-cache-key.d.ts +163 -0
  352. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  353. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  354. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  355. package/dist/shared/rsc-manifest.d.ts +28 -0
  356. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  357. package/dist/shared/rsc-media-type.d.ts +40 -0
  358. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  359. package/dist/shared/rsc-payload-path.d.ts +42 -0
  360. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  361. package/dist/shared/segment-info.d.ts +59 -0
  362. package/dist/shared/segment-info.d.ts.map +1 -0
  363. package/dist/shared/slot-params.d.ts +51 -0
  364. package/dist/shared/slot-params.d.ts.map +1 -0
  365. package/dist/shared/static-platform-files.d.ts +71 -0
  366. package/dist/shared/static-platform-files.d.ts.map +1 -0
  367. package/docs/api/30-api-server.mdx +1 -1
  368. package/docs/api/31-api-client.mdx +4 -2
  369. package/docs/api/33-api-search-params.mdx +38 -16
  370. package/docs/api/35-api-typescript.mdx +3 -3
  371. package/docs/api/36-cli.mdx +34 -7
  372. package/docs/learn/00-introduction.mdx +1 -1
  373. package/docs/learn/02-pages-and-layouts.mdx +2 -2
  374. package/docs/learn/05-typed-params.mdx +8 -8
  375. package/docs/learn/07-typed-routes.mdx +12 -7
  376. package/docs/learn/11-error-handling.mdx +18 -0
  377. package/docs/learn/12-client-navigation.mdx +2 -2
  378. package/docs/learn/14-deploying.mdx +23 -0
  379. package/docs/more/01-advanced-routing.mdx +173 -3
  380. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  381. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  382. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  383. package/package.json +9 -6
  384. package/src/adapters/build-output-helper.ts +116 -3
  385. package/src/adapters/cloudflare.ts +90 -12
  386. package/src/adapters/compress-module.ts +79 -1
  387. package/src/adapters/fs-identity.ts +156 -0
  388. package/src/adapters/nitro.ts +6 -6
  389. package/src/adapters/shared.ts +196 -21
  390. package/src/adapters/types.ts +13 -1
  391. package/src/cache/fast-hash.ts +5 -0
  392. package/src/cli-check.ts +458 -0
  393. package/src/cli.ts +59 -24
  394. package/src/client/browser-entry/action-dispatch.ts +11 -5
  395. package/src/client/browser-entry/hydrate.ts +40 -29
  396. package/src/client/browser-entry/index.ts +15 -6
  397. package/src/client/browser-entry/post-hydration.ts +10 -9
  398. package/src/client/browser-entry/router-init.ts +104 -48
  399. package/src/client/browser-entry/rsc-stream.ts +11 -2
  400. package/src/client/error-boundary.tsx +85 -16
  401. package/src/client/history.ts +13 -6
  402. package/src/client/index.ts +14 -3
  403. package/src/client/internal.ts +4 -3
  404. package/src/client/link.tsx +120 -72
  405. package/src/client/navigation-api.ts +3 -3
  406. package/src/client/navigation-commit.ts +197 -0
  407. package/src/client/navigation-context.ts +40 -67
  408. package/src/client/navigation-root.tsx +326 -101
  409. package/src/client/params-context.ts +192 -0
  410. package/src/client/router-effects.ts +231 -0
  411. package/src/client/router-lifecycle.ts +288 -0
  412. package/src/client/router-pipeline.ts +393 -0
  413. package/src/client/router-ref.ts +1 -1
  414. package/src/client/router-skew.ts +30 -0
  415. package/src/client/router-types.ts +230 -0
  416. package/src/client/router.ts +243 -707
  417. package/src/client/rsc-fetch.ts +183 -106
  418. package/src/client/segment-cache.ts +129 -44
  419. package/src/client/ssr-data.ts +6 -0
  420. package/src/client/stale-client.ts +191 -0
  421. package/src/client/state.ts +14 -1
  422. package/src/client/top-loader.tsx +2 -1
  423. package/src/client/unload-guard.ts +34 -2
  424. package/src/client/use-cookie.ts +9 -2
  425. package/src/client/use-query-states.ts +102 -39
  426. package/src/client/use-router.ts +1 -1
  427. package/src/client/use-segment-params.ts +41 -18
  428. package/src/cookies/define-cookie.ts +6 -1
  429. package/src/index.ts +25 -4
  430. package/src/plugin-context.ts +58 -0
  431. package/src/plugins/adapter-build.ts +4 -0
  432. package/src/plugins/build-manifest.ts +34 -13
  433. package/src/plugins/entries.ts +4 -0
  434. package/src/plugins/fonts.ts +11 -8
  435. package/src/plugins/request-dep/analysis.ts +447 -0
  436. package/src/plugins/request-dep/index.ts +376 -0
  437. package/src/plugins/request-dep/origin.ts +310 -0
  438. package/src/plugins/routing.ts +97 -102
  439. package/src/plugins/shims.ts +0 -1
  440. package/src/plugins/static-build.ts +391 -95
  441. package/src/routing/codegen-shared.ts +3 -79
  442. package/src/routing/codegen-types.ts +28 -31
  443. package/src/routing/codegen-write.ts +139 -0
  444. package/src/routing/codegen.ts +86 -172
  445. package/src/routing/collision-probe.ts +438 -0
  446. package/src/routing/collision-spaces.ts +281 -0
  447. package/src/routing/convention-lint.ts +139 -40
  448. package/src/routing/export-detect.ts +151 -7
  449. package/src/routing/interception.ts +896 -50
  450. package/src/routing/link-codegen.ts +32 -65
  451. package/src/routing/manifest-codegen.ts +6 -62
  452. package/src/routing/scanner.ts +117 -92
  453. package/src/routing/segment-classify.ts +57 -0
  454. package/src/routing/segment-keys.ts +146 -0
  455. package/src/routing/slot-placement.ts +97 -0
  456. package/src/routing/types.ts +0 -6
  457. package/src/routing/walkers.ts +8 -0
  458. package/src/schema-bridge.ts +180 -58
  459. package/src/search-params/define.ts +108 -66
  460. package/src/search-params/index.ts +0 -1
  461. package/src/search-params/parse-total.ts +78 -0
  462. package/src/search-params/wrappers.ts +60 -11
  463. package/src/server/access-gate.tsx +60 -40
  464. package/src/server/action-handler.ts +4 -5
  465. package/src/server/actions.ts +6 -3
  466. package/src/server/als-registry.ts +18 -1
  467. package/src/server/build-manifest.ts +14 -1
  468. package/src/server/chain-url-parts.ts +163 -0
  469. package/src/server/children-interception.ts +115 -0
  470. package/src/server/compress.ts +9 -1
  471. package/src/server/default-status-page.ts +127 -0
  472. package/src/server/deny-boundary.ts +269 -41
  473. package/src/server/deny-renderer.ts +111 -39
  474. package/src/server/error-boundary-wrapper.ts +166 -79
  475. package/src/server/head-response.ts +54 -0
  476. package/src/server/index.ts +1 -3
  477. package/src/server/internal.ts +2 -2
  478. package/src/server/metadata-collector.ts +115 -0
  479. package/src/server/param-coercion.ts +51 -69
  480. package/src/server/pipeline-helpers.ts +2 -47
  481. package/src/server/pipeline-interception.ts +81 -32
  482. package/src/server/pipeline-outcome.ts +4 -5
  483. package/src/server/pipeline-phases.ts +77 -25
  484. package/src/server/pipeline.ts +57 -10
  485. package/src/server/prebuilt-runtime.ts +4 -4
  486. package/src/server/primitives.ts +25 -0
  487. package/src/server/publish-params.ts +42 -0
  488. package/src/server/request-context.ts +97 -18
  489. package/src/server/route-element-builder.ts +241 -253
  490. package/src/server/route-handler.ts +8 -8
  491. package/src/server/route-matcher.ts +21 -9
  492. package/src/server/rsc-cache-key-guard.ts +164 -0
  493. package/src/server/rsc-entry/api-handler.ts +6 -8
  494. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  495. package/src/server/rsc-entry/error-renderer.ts +94 -35
  496. package/src/server/rsc-entry/helpers.ts +33 -28
  497. package/src/server/rsc-entry/index.ts +36 -33
  498. package/src/server/rsc-entry/render-route.ts +18 -10
  499. package/src/server/rsc-entry/revalidate-renderer.ts +3 -2
  500. package/src/server/rsc-entry/rsc-payload.ts +113 -40
  501. package/src/server/rsc-entry/rsc-stream.ts +29 -5
  502. package/src/server/rsc-entry/ssr-renderer.ts +38 -22
  503. package/src/server/skippable-prefix.ts +92 -0
  504. package/src/server/slot-interception.ts +168 -0
  505. package/src/server/slot-resolver.ts +263 -158
  506. package/src/server/ssr-bridge-types.ts +18 -0
  507. package/src/server/ssr-entry.ts +20 -10
  508. package/src/server/ssr-wrappers.tsx +33 -32
  509. package/src/server/state-tree-diff.ts +14 -55
  510. package/src/server/static-generator.ts +68 -29
  511. package/src/server/static-not-found.ts +134 -0
  512. package/src/server/status-code-resolver.ts +28 -11
  513. package/src/server/tree-builder.ts +35 -218
  514. package/src/server/types.ts +12 -7
  515. package/src/server/utils/element-type.ts +72 -0
  516. package/src/shared/als-slots.ts +72 -0
  517. package/src/shared/base-path.ts +80 -0
  518. package/src/shared/param-value.ts +244 -0
  519. package/src/shared/payload-root.ts +151 -0
  520. package/src/shared/rsc-cache-key.ts +269 -0
  521. package/src/shared/rsc-error-envelope.ts +31 -0
  522. package/src/shared/rsc-manifest.ts +29 -0
  523. package/src/shared/rsc-media-type.ts +43 -0
  524. package/src/shared/rsc-payload-path.ts +45 -0
  525. package/src/shared/segment-info.ts +60 -0
  526. package/src/shared/slot-params.ts +111 -0
  527. package/src/shared/static-platform-files.ts +75 -0
  528. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  529. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  530. package/dist/_chunks/cache-api-B-lhk9p4.js.map +0 -1
  531. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  532. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
  533. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
  534. package/dist/_chunks/define-COtkxMRT.js +0 -191
  535. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  536. package/dist/_chunks/define-c4au4I9R.js +0 -82
  537. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  538. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  539. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  540. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  541. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  542. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  543. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  544. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  545. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  546. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  547. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  548. package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
  549. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  550. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  551. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  552. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  553. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  554. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  555. package/dist/_chunks/walkers-_6zKFlch.js +0 -1002
  556. package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
  557. package/dist/client/error-boundary.js.map +0 -1
  558. package/dist/plugins/request-dep.d.ts +0 -32
  559. package/dist/plugins/request-dep.d.ts.map +0 -1
  560. package/dist/search-params/registry.d.ts +0 -20
  561. package/dist/search-params/registry.d.ts.map +0 -1
  562. package/dist/segment-params/define.d.ts +0 -89
  563. package/dist/segment-params/define.d.ts.map +0 -1
  564. package/dist/segment-params/index.d.ts +0 -3
  565. package/dist/segment-params/index.d.ts.map +0 -1
  566. package/dist/segment-params/index.js +0 -2
  567. package/src/plugins/request-dep.ts +0 -499
  568. package/src/search-params/registry.ts +0 -31
  569. package/src/segment-params/define.ts +0 -249
  570. package/src/segment-params/index.ts +0 -9
package/dist/index.js CHANGED
@@ -1,13 +1,19 @@
1
1
  import { a as __esmMin, o as __exportAll, s as __toCommonJS } from "./_chunks/dist-BA3u1z3W.js";
2
- import { c as isDynamicMetadataExtension, l as isMetadataRouteServePath, r as canonicalize } from "./_chunks/canonicalize-DQHyFClh.js";
3
- import { h as swallow } from "./_chunks/logger-AWfuX-KJ.js";
4
- import { i as scanRoutes } from "./_chunks/cli-schema-sync-BTWEKJXo.js";
5
- import { a as generateRouteMap, c as fileHasDirective, d as getPrerenderExport, i as validateSchemaAgainstRoutes, l as fileHasExport, n as collectInterceptionRewrites, o as fileHasAnyExport, s as fileHasDefaultExport, t as collectLeafRoutes, u as fileHasStarExport } from "./_chunks/walkers-_6zKFlch.js";
6
- import { a as resolveAppDir, c as createNoopTimer, n as loadTimberConfigFile, o as resolveBuildDir, r as mergeFileConfig, s as resolveClientJavascript, t as createPluginContext } from "./_chunks/plugin-context---kTF5v8.js";
7
- import { t as fnv1aHash } from "./_chunks/fast-hash-D6hIVt1Y.js";
2
+ import { t as RSC_CONTENT_TYPE } from "./_chunks/rsc-media-type-DRqE_lD_.js";
3
+ import { a as canonicalize, d as isMetadataRouteServePath, u as isDynamicMetadataExtension } from "./_chunks/segment-keys-BawYuNFO.js";
4
+ import { n as collectInterceptionRewrites, r as matchUrlParts, t as scanRoutes } from "./_chunks/scanner-C8b0Gcw3.js";
5
+ import { h as swallow } from "./_chunks/logger-D8xJZXIN.js";
6
+ import { c as writeRouteCodegen, i as getPrerenderExport, l as buildDirToRouteMap, n as formatConventionWarnings, r as lintConventions, s as flushCodegenWrites, t as checkAppDirExists } from "./_chunks/convention-lint-kXsgc_-7.js";
7
+ import { c as resolveClientJavascript, i as mergeFileConfig, l as createNoopTimer, o as resolveAppDir, r as loadTimberConfigFile, s as resolveBuildDir, t as createPluginContext } from "./_chunks/plugin-context-rCinWLiE.js";
8
+ import { r as validateSchemaAgainstRoutes, t as collectLeafRoutes } from "./_chunks/walkers-RzN6AFjr.js";
9
+ import { i as withBasePath, n as normalizeBasePath, t as isRelativeBase } from "./_chunks/base-path-DaQrzbez.js";
10
+ import { t as rscPayloadPath } from "./_chunks/rsc-payload-path-B_LBodc2.js";
11
+ import { a as REDIRECTS_FILE, n as detectFsIdentity, o as RESERVED_OUTPUT_PATHS, r as fsKey, t as EXACT_FS_IDENTITY } from "./_chunks/fs-identity-D7vb6GdI.js";
12
+ import { t as EMPTY_BUILD_MANIFEST } from "./_chunks/build-manifest-DWppEdLB.js";
13
+ import { t as fnv1aHash } from "./_chunks/fast-hash-C-tVYrLb.js";
8
14
  import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
9
15
  import { createLogger, parseAst } from "vite";
10
- import { basename, dirname, extname, join, normalize, relative, resolve, win32 } from "node:path";
16
+ import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, win32 } from "node:path";
11
17
  import { builtinModules, createRequire } from "node:module";
12
18
  import react, { reactCompilerPreset } from "@vitejs/plugin-react";
13
19
  import { existsSync, readFileSync, statSync } from "node:fs";
@@ -282,8 +288,8 @@ function classifyStack(stack, projectRoot) {
282
288
  *
283
289
  * Design doc: 21-dev-server.md §"Error Overlay"
284
290
  */
285
- var noValidate$1 = { validateStream: false };
286
- var style$1 = (fmt, text) => styleText(fmt, text, noValidate$1);
291
+ var noValidate = { validateStream: false };
292
+ var style = (fmt, text) => styleText(fmt, text, noValidate);
287
293
  /**
288
294
  * Wrap text in an OSC 8 hyperlink escape sequence.
289
295
  *
@@ -305,7 +311,7 @@ function hyperlink(text, url) {
305
311
  function fileLink(filePath, line, col) {
306
312
  const display = line ? `${filePath}:${line}${col ? `:${col}` : ""}` : filePath;
307
313
  const base = pathToFileURL(filePath).href;
308
- return style$1(["cyan", "underline"], hyperlink(display, line ? `${base}:${line}${col ? `:${col}` : ""}` : base));
314
+ return style(["cyan", "underline"], hyperlink(display, line ? `${base}:${line}${col ? `:${col}` : ""}` : base));
309
315
  }
310
316
  var BOX = {
311
317
  topLeft: "╭",
@@ -324,9 +330,9 @@ var BOX = {
324
330
  function box(lines, borderFormat, width = 80) {
325
331
  const bar = BOX.horizontal.repeat(width - 2);
326
332
  const output = [];
327
- output.push(style$1(borderFormat, `${BOX.topLeft}${bar}${BOX.topRight}`));
328
- for (const line of lines) output.push(`${style$1(borderFormat, BOX.vertical)} ${line}`);
329
- output.push(style$1(borderFormat, `${BOX.bottomLeft}${bar}${BOX.bottomRight}`));
333
+ output.push(style(borderFormat, `${BOX.topLeft}${bar}${BOX.topRight}`));
334
+ for (const line of lines) output.push(`${style(borderFormat, BOX.vertical)} ${line}`);
335
+ output.push(style(borderFormat, `${BOX.bottomLeft}${bar}${BOX.bottomRight}`));
330
336
  return output.join("\n");
331
337
  }
332
338
  /**
@@ -347,32 +353,32 @@ function formatTerminalError$1(error, phase, projectRoot) {
347
353
  const appFrames = frames.filter((f) => f.type === "app");
348
354
  const internalCount = frames.filter((f) => f.type !== "app").length;
349
355
  const boxLines = [];
350
- boxLines.push(style$1(["red", "bold"], `${PHASE_LABELS$1[phase]} Error`));
356
+ boxLines.push(style(["red", "bold"], `${PHASE_LABELS$1[phase]} Error`));
351
357
  boxLines.push("");
352
- for (const msgLine of error.message.split("\n")) boxLines.push(style$1("red", msgLine));
358
+ for (const msgLine of error.message.split("\n")) boxLines.push(style("red", msgLine));
353
359
  if (loc) {
354
360
  boxLines.push("");
355
361
  const relPath = loc.file.startsWith(projectRoot) ? loc.file.slice(projectRoot.length + 1) : loc.file;
356
- boxLines.push(`${style$1("bold", "→")} ${fileLink(loc.file, loc.line, loc.column)} ${style$1("dim", `(${relPath})`)}`);
362
+ boxLines.push(`${style("bold", "→")} ${fileLink(loc.file, loc.line, loc.column)} ${style("dim", `(${relPath})`)}`);
357
363
  }
358
364
  sections.push(box(boxLines, "red"));
359
365
  if (componentStack) {
360
366
  sections.push("");
361
- sections.push(` ${style$1("bold", "Component Stack:")}`);
362
- for (const csLine of componentStack.trim().split("\n")) sections.push(` ${style$1("dim", csLine.trim())}`);
367
+ sections.push(` ${style("bold", "Component Stack:")}`);
368
+ for (const csLine of componentStack.trim().split("\n")) sections.push(` ${style("dim", csLine.trim())}`);
363
369
  }
364
370
  if (appFrames.length > 0) {
365
371
  sections.push("");
366
- sections.push(` ${style$1("bold", "Application Frames:")}`);
372
+ sections.push(` ${style("bold", "Application Frames:")}`);
367
373
  for (let i = 0; i < appFrames.length; i++) {
368
374
  const f = appFrames[i];
369
375
  if (f.file && f.line) {
370
- const prefix = i === 0 ? style$1("bold", "▸") : " ";
371
- sections.push(` ${prefix} ${fileLink(f.file, f.line, f.col)} ${style$1("dim", extractFnName(f.raw))}`);
376
+ const prefix = i === 0 ? style("bold", "▸") : " ";
377
+ sections.push(` ${prefix} ${fileLink(f.file, f.line, f.col)} ${style("dim", extractFnName(f.raw))}`);
372
378
  } else sections.push(` ${f.raw}`);
373
379
  }
374
380
  }
375
- if (internalCount > 0) sections.push(` ${style$1("dim", `… ${internalCount} internal frame${internalCount !== 1 ? "s" : ""} hidden`)}`);
381
+ if (internalCount > 0) sections.push(` ${style("dim", `… ${internalCount} internal frame${internalCount !== 1 ? "s" : ""} hidden`)}`);
376
382
  sections.push("");
377
383
  return sections.join("\n");
378
384
  }
@@ -1374,7 +1380,7 @@ var COMPRESSIBLE_TYPES = /* @__PURE__ */ new Set([
1374
1380
  "text/plain",
1375
1381
  "text/xml",
1376
1382
  "text/javascript",
1377
- "text/x-component",
1383
+ RSC_CONTENT_TYPE,
1378
1384
  "application/json",
1379
1385
  "application/javascript",
1380
1386
  "application/xml",
@@ -1479,125 +1485,6 @@ function compressWithGzip(body) {
1479
1485
  return Readable.toWeb(gzip);
1480
1486
  }
1481
1487
  //#endregion
1482
- //#region src/server/tree-match.ts
1483
- /**
1484
- * Match URL path parts against a segment tree.
1485
- *
1486
- * Walks the tree depth-first with backtracking, applying priority
1487
- * ordering globally across group boundaries. Returns the matched
1488
- * segment chain and collected params, or null if no route matches.
1489
- *
1490
- * By default, a match requires a terminal node (one with `page` or
1491
- * `route`) at the leaf. Pass `{ requireTerminal: false }` to match
1492
- * any segment (used by metadata route matching).
1493
- *
1494
- * @param root The root segment node to start matching from.
1495
- * @param parts URL path parts (e.g. ['blog', 'hello-world']).
1496
- * Empty array matches the root itself (if terminal).
1497
- */
1498
- function matchUrlParts(root, parts, options) {
1499
- const chain = [];
1500
- const params = {};
1501
- if (matchNode(root, parts, 0, chain, params, options?.requireTerminal ?? true)) return {
1502
- chain,
1503
- params
1504
- };
1505
- return null;
1506
- }
1507
- /**
1508
- * Flatten children through group segments.
1509
- *
1510
- * Groups are transparent for URL matching — their non-group descendants
1511
- * are returned with the chain of group nodes leading to them. This lets
1512
- * the caller apply priority ordering (static > dynamic > catch-all >
1513
- * optional-catch-all) across all groups uniformly instead of per-group.
1514
- */
1515
- function collectEffectiveChildren(node, groupChain = []) {
1516
- const result = [];
1517
- for (const child of node.children) if (child.segmentType === "group") {
1518
- const nested = collectEffectiveChildren(child, [...groupChain, child]);
1519
- result.push(...nested);
1520
- } else result.push({
1521
- child,
1522
- groupChain
1523
- });
1524
- return result;
1525
- }
1526
- /** A node is terminal if it can serve a response (has a page or route handler). */
1527
- function isTerminal(node) {
1528
- return !!(node.page || node.route);
1529
- }
1530
- /** Combined affix length for specificity ordering. */
1531
- function affixLength(node) {
1532
- return (node.paramPrefix?.length ?? 0) + (node.paramSuffix?.length ?? 0);
1533
- }
1534
- /**
1535
- * Recursive depth-first matcher with backtracking.
1536
- *
1537
- * Mutates `chain` and `params` in place for performance; backtracks
1538
- * (restores previous state) when a candidate doesn't lead to a match.
1539
- */
1540
- function matchNode(node, parts, index, chain, params, requireTerminal) {
1541
- chain.push(node);
1542
- if (index >= parts.length) {
1543
- if (!requireTerminal || isTerminal(node)) return true;
1544
- for (const child of node.children) if (child.segmentType === "group") {
1545
- if (matchNode(child, parts, index, chain, params, requireTerminal)) return true;
1546
- }
1547
- const effective = collectEffectiveChildren(node);
1548
- for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && (!requireTerminal || isTerminal(child))) {
1549
- chain.push(...groupChain, child);
1550
- return true;
1551
- }
1552
- chain.pop();
1553
- return false;
1554
- }
1555
- const part = parts[index];
1556
- const effective = collectEffectiveChildren(node);
1557
- for (const { child, groupChain } of effective) if (child.segmentType === "static" && child.segmentName === part) {
1558
- chain.push(...groupChain);
1559
- if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
1560
- chain.length -= groupChain.length;
1561
- }
1562
- const affixed = effective.filter(({ child }) => child.segmentType === "dynamic" && child.paramName && (child.paramPrefix || child.paramSuffix));
1563
- affixed.sort((a, b) => affixLength(b.child) - affixLength(a.child));
1564
- for (const { child, groupChain } of affixed) {
1565
- const prefix = child.paramPrefix ?? "";
1566
- const suffix = child.paramSuffix ?? "";
1567
- if (part.length > prefix.length + suffix.length && (!prefix || part.startsWith(prefix)) && (!suffix || part.endsWith(suffix))) {
1568
- const value = part.slice(prefix.length, part.length - (suffix.length || 0));
1569
- chain.push(...groupChain);
1570
- const prev = params[child.paramName];
1571
- params[child.paramName] = value;
1572
- if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
1573
- chain.length -= groupChain.length;
1574
- if (prev !== void 0) params[child.paramName] = prev;
1575
- else delete params[child.paramName];
1576
- }
1577
- }
1578
- for (const { child, groupChain } of effective) if (child.segmentType === "dynamic" && child.paramName && !child.paramPrefix && !child.paramSuffix) {
1579
- chain.push(...groupChain);
1580
- const prev = params[child.paramName];
1581
- params[child.paramName] = part;
1582
- if (matchNode(child, parts, index + 1, chain, params, requireTerminal)) return true;
1583
- chain.length -= groupChain.length;
1584
- if (prev !== void 0) params[child.paramName] = prev;
1585
- else delete params[child.paramName];
1586
- }
1587
- for (const { child, groupChain } of effective) if (child.segmentType === "catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
1588
- chain.push(...groupChain, child);
1589
- params[child.paramName] = parts.slice(index);
1590
- return true;
1591
- }
1592
- for (const { child, groupChain } of effective) if (child.segmentType === "optional-catch-all" && child.paramName && (!requireTerminal || isTerminal(child))) {
1593
- chain.push(...groupChain, child);
1594
- params[child.paramName] = parts.slice(index);
1595
- return true;
1596
- }
1597
- chain.pop();
1598
- return false;
1599
- }
1600
- //#endregion
1601
1488
  //#region src/plugins/dev-server.ts
1602
1489
  var RSC_ENTRY_ID = "virtual:timber-rsc-entry";
1603
1490
  /**
@@ -2014,6 +1901,7 @@ function generateConfigModule(ctx) {
2014
1901
  csrf: ctx.config.csrf ?? true,
2015
1902
  allowedOrigins: ctx.config.allowedOrigins,
2016
1903
  clientJavascript: ctx.clientJavascript,
1904
+ base: ctx.base,
2017
1905
  dev: ctx.dev ?? false,
2018
1906
  slowPhaseMs: ctx.config.dev?.slowPhaseMs ?? 200,
2019
1907
  slowRequestMs: ctx.config.slowRequestMs ?? 3e3,
@@ -2318,69 +2206,10 @@ function timberMdx(ctx) {
2318
2206
  };
2319
2207
  }
2320
2208
  //#endregion
2321
- //#region src/routing/segment-codegen.ts
2322
- /**
2323
- * $segment type generation and directory-to-route mapping.
2324
- *
2325
- * Generates per-route $segment.d.ts files in .timber/types/ that mirror
2326
- * the app/ directory structure. TypeScript's rootDirs feature makes these
2327
- * importable as './$segment' from route files.
2328
- *
2329
- * Design doc: design/41-global-params.md §The $segment Module
2330
- */
2331
- /**
2332
- * Build a map of absolute filesystem directory → segment info.
2333
- * Used by resolveId to map a $segment import's importer to the correct route,
2334
- * and by writeSegmentTypes to generate per-route $segment.d.ts files.
2335
- *
2336
- * Walks every segment (not just leaves) so layouts and middleware
2337
- * at any depth can import $segment.
2338
- */
2339
- function buildDirToRouteMap(tree, appDir) {
2340
- const map = /* @__PURE__ */ new Map();
2341
- function walk(node, fsPath, treePath) {
2342
- map.set(fsPath, {
2343
- route: node.urlPath || "/",
2344
- segmentPath: treePath || "/"
2345
- });
2346
- for (const child of node.children) walk(child, join(fsPath, child.segmentName), `${treePath}/${child.segmentName}`);
2347
- for (const [slotName, slotNode] of Object.entries(node.slots)) walk(slotNode, join(fsPath, `@${slotName}`), `${treePath}/@${slotName}`);
2348
- }
2349
- walk(tree.root, appDir, "");
2350
- return map;
2351
- }
2352
- /**
2353
- * Generate the full $segment.d.ts content for a segment.
2354
- */
2355
- function generateSegmentDts(info) {
2356
- return [
2357
- `export const ROUTE: '${info.route}';`,
2358
- `export const SEGMENT_PATH: '${info.segmentPath}';`,
2359
- ""
2360
- ].join("\n");
2361
- }
2362
- /**
2363
- * Generate per-route $segment.d.ts files in .timber/types/ mirroring
2364
- * the app/ directory structure. These files are importable via TypeScript's
2365
- * rootDirs feature.
2366
- */
2367
- async function writeSegmentTypes(routeTree, appDir, root) {
2368
- const typesBase = join(root, ".timber", "types", appDir.slice(root.length + 1));
2369
- const map = buildDirToRouteMap(routeTree, appDir);
2370
- const writes = [];
2371
- for (const [fsDir, info] of map) {
2372
- const segmentDir = join(typesBase, fsDir.slice(appDir.length));
2373
- const segmentFile = join(segmentDir, "$segment.d.ts");
2374
- const content = generateSegmentDts(info);
2375
- writes.push(mkdir(segmentDir, { recursive: true }).then(() => writeFile(segmentFile, content, "utf-8")));
2376
- }
2377
- await Promise.all(writes);
2378
- }
2379
- //#endregion
2380
2209
  //#region src/routing/manifest-codegen.ts
2381
2210
  /**
2382
2211
  * Route manifest codegen — generates virtual module source for the route
2383
- * manifest, search-params registry, and global schema.
2212
+ * manifest and the global schema loader.
2384
2213
  *
2385
2214
  * Extracted from plugins/routing.ts to keep it under the 500-line budget.
2386
2215
  * See design/18-build-system.md §"Virtual Modules".
@@ -2395,8 +2224,7 @@ var SINGLE_FILE_KEYS = [
2395
2224
  "route",
2396
2225
  "error",
2397
2226
  "default",
2398
- "denied",
2399
- "params"
2227
+ "denied"
2400
2228
  ];
2401
2229
  var RECORD_FILE_KEYS = [
2402
2230
  "statusFiles",
@@ -2477,10 +2305,7 @@ function generateManifestModule(tree, viteRoot) {
2477
2305
  let globalErrorLine = "";
2478
2306
  if (tree.globalError) globalErrorLine = ` globalError: { load: ${addImport(tree.globalError)}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
2479
2307
  const rewrites = collectInterceptionRewrites(tree.root);
2480
- const rewritesLine = rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({
2481
- interceptedPattern: r.interceptedPattern,
2482
- interceptingPrefix: r.interceptingPrefix
2483
- })))},` : "";
2308
+ const rewritesLine = rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites)},` : "";
2484
2309
  return [
2485
2310
  "// Auto-generated route manifest — do not edit.",
2486
2311
  "// Generated by timber-routing plugin.",
@@ -2499,50 +2324,6 @@ function generateManifestModule(tree, viteRoot) {
2499
2324
  ].filter((line) => line !== void 0 && line !== "").join("\n");
2500
2325
  }
2501
2326
  /**
2502
- * Walk the route tree and collect every page that exports a
2503
- * `searchParams` definition, then emit a module that statically imports
2504
- * each export and registers it into the shared search-params registry.
2505
- */
2506
- function generateSearchParamsRegistryModule(tree) {
2507
- const entries = [];
2508
- function walk(node) {
2509
- if (node.page) {
2510
- if (node.params && fileHasSearchParamsExport(node.params.filePath)) entries.push({
2511
- urlPath: node.urlPath,
2512
- filePath: node.params.filePath
2513
- });
2514
- else if (fileHasSearchParamsExport(node.page.filePath)) entries.push({
2515
- urlPath: node.urlPath,
2516
- filePath: node.page.filePath
2517
- });
2518
- }
2519
- for (const child of node.children) walk(child);
2520
- for (const slot of Object.values(node.slots)) walk(slot);
2521
- }
2522
- walk(tree.root);
2523
- entries.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
2524
- const lines = [];
2525
- lines.push("// Auto-generated search-params registry — do not edit.");
2526
- lines.push("// Generated by timber-routing plugin (TIM-830).");
2527
- lines.push("");
2528
- lines.push("import { registerSearchParams } from '@timber-js/app/search-params';");
2529
- lines.push("");
2530
- entries.forEach((entry, i) => {
2531
- lines.push(`import { searchParams as r${i} } from ${JSON.stringify(entry.filePath)};`);
2532
- });
2533
- lines.push("");
2534
- entries.forEach((entry, i) => {
2535
- lines.push(`registerSearchParams(${JSON.stringify(entry.urlPath)}, r${i});`);
2536
- });
2537
- lines.push("");
2538
- lines.push("export {};");
2539
- lines.push("");
2540
- return lines.join("\n");
2541
- }
2542
- function fileHasSearchParamsExport(filePath) {
2543
- return fileHasExport(filePath, "searchParams");
2544
- }
2545
- /**
2546
2327
  * Generate the virtual:timber-schema module.
2547
2328
  *
2548
2329
  * When app/schema.{ts,tsx,js,jsx} exists, generates a module that
@@ -2574,315 +2355,32 @@ function generateSchemaModule(appDir) {
2574
2355
  ].join("\n");
2575
2356
  }
2576
2357
  //#endregion
2577
- //#region src/routing/convention-lint.ts
2578
- /**
2579
- * Convention linter — validates common misconfigurations in the route tree.
2580
- *
2581
- * Runs at scan time (build and dev startup). Each check produces a warning
2582
- * with the file path, what's wrong, and what to do about it.
2583
- *
2584
- * These are warnings, not errors — they don't block the build. The goal is
2585
- * to catch issues that would otherwise produce cryptic runtime behavior
2586
- * (silent 404s, empty pages, confusing React errors).
2587
- *
2588
- * Design doc: 07-routing.md, 10-error-handling.md
2589
- */
2590
- /**
2591
- * Run all convention lint checks on a route tree.
2592
- *
2593
- * Returns an array of warnings. Empty array means everything looks good.
2594
- */
2595
- function lintConventions(tree, appDir) {
2596
- const warnings = [];
2597
- checkEmptyApp(tree.root, appDir, warnings);
2598
- checkRouteExports(tree.root, warnings);
2599
- checkRootLayout(tree.root, warnings);
2600
- checkDefaultExports(tree.root, warnings);
2601
- checkErrorDirectives(tree.root, warnings);
2602
- if (tree.globalError && isScriptExtension(tree.globalError.extension)) checkFileUseClientDirective(tree.globalError.filePath, "global-error", warnings);
2603
- return warnings;
2604
- }
2605
- /**
2606
- * Warn when the app/ directory has no routable files at all.
2607
- *
2608
- * This catches the "I created app/ but didn't add any pages" case where
2609
- * every request silently 404s with no guidance.
2610
- */
2611
- function checkEmptyApp(root, appDir, warnings) {
2612
- if (hasAnyRoutable(root)) return;
2613
- warnings.push({
2614
- id: "EMPTY_APP",
2615
- summary: "No pages or route handlers found in app/",
2616
- details: ` Directory: ${appDir}\n\n Your app/ directory has no page.tsx or route.ts files.
2617
- Every request will return 404.
2618
-
2619
- To fix: Create app/page.tsx with a default export:
2620
-
2621
- export default function Home() {
2622
- return <h1>Hello</h1>;
2623
- }
2624
- `,
2625
- level: "warn"
2626
- });
2627
- }
2628
- /**
2629
- * Check if a segment tree has any routable files (page or route) anywhere.
2630
- */
2631
- function hasAnyRoutable(node) {
2632
- if (node.page || node.route) return true;
2633
- for (const child of node.children) if (hasAnyRoutable(child)) return true;
2634
- for (const slot of Object.values(node.slots)) if (hasAnyRoutable(slot)) return true;
2635
- return false;
2636
- }
2637
- /**
2638
- * Check if a segment tree has any page files (not just route handlers).
2639
- * Used by checkRootLayout to avoid false positives for API-only apps (TIM-794).
2640
- * API-only apps (just route.ts handlers) don't need a root layout.
2641
- */
2642
- function hasAnyPage(node) {
2643
- if (node.page) return true;
2644
- for (const child of node.children) if (hasAnyPage(child)) return true;
2645
- for (const slot of Object.values(node.slots)) if (hasAnyPage(slot)) return true;
2646
- return false;
2647
- }
2648
- /** HTTP methods that route.ts can export. */
2649
- var HTTP_METHODS = [
2650
- "GET",
2651
- "POST",
2652
- "PUT",
2653
- "PATCH",
2654
- "DELETE",
2655
- "HEAD",
2656
- "OPTIONS"
2657
- ];
2658
- /**
2659
- * Warn when a route.ts file doesn't appear to export any recognized HTTP methods.
2660
- *
2661
- * Uses AST-based detection via parseAst for precise matching.
2662
- * Falls back silently when the file can't be parsed (syntax errors produce
2663
- * no spurious warning — the TS compiler will catch those).
2664
- */
2665
- function checkRouteExports(node, warnings) {
2666
- if (node.route) {
2667
- const filePath = node.route.filePath;
2668
- try {
2669
- if (fileHasAnyExport(filePath, HTTP_METHODS).size === 0 && !fileHasStarExport(filePath)) {
2670
- const hint = fileHasDefaultExport(filePath) ? " It looks like you have a default export. route.ts uses named exports\n for HTTP methods (GET, POST, etc.), not a default export.\n" : "";
2671
- warnings.push({
2672
- id: "ROUTE_NO_METHODS",
2673
- summary: `route.ts has no HTTP method exports: ${filePath}`,
2674
- details: ` File: ${filePath}\n\n route.ts files must export named HTTP method handlers.
2675
- Without them, the route matches but returns 405 Method Not Allowed.
2676
-
2677
- ` + hint + " Example:\n\n export function GET(ctx: RouteContext) {\n return Response.json({ hello: 'world' });\n }\n",
2678
- level: "warn"
2679
- });
2680
- }
2681
- } catch (err) {
2682
- swallow(err, `convention-lint: unreadable route.ts ${filePath}`);
2683
- }
2684
- }
2685
- for (const child of node.children) checkRouteExports(child, warnings);
2686
- for (const slot of Object.values(node.slots)) checkRouteExports(slot, warnings);
2687
- }
2688
- /**
2689
- * Warn when the root segment has no layout.tsx.
2690
- *
2691
- * Without a root layout, there's no HTML shell (<html>, <body>).
2692
- * The page will render but may have hydration issues or no proper document structure.
2693
- */
2694
- function checkRootLayout(root, warnings) {
2695
- if (root.layout) return;
2696
- if (!hasAnyPage(root)) return;
2697
- warnings.push({
2698
- id: "NO_ROOT_LAYOUT",
2699
- summary: "No root layout.tsx found",
2700
- details: " Your app has pages but no root layout.\n Without app/layout.tsx, pages have no <html> or <body> wrapper.\n\n To fix: Create app/layout.tsx:\n\n export default function RootLayout({ children }: { children: React.ReactNode }) {\n return (\n <html lang=\"en\">\n <body>{children}</body>\n </html>\n );\n }\n",
2701
- level: "warn"
2702
- });
2703
- }
2704
- /**
2705
- * Warn when page.tsx or layout.tsx files don't have a default export.
2706
- *
2707
- * Without a default export:
2708
- * - page.tsx: the page renders nothing (empty content)
2709
- * - layout.tsx: the layout module loads but has no component to render
2710
- *
2711
- * Uses AST-based detection via parseAst for precise matching. Falls back
2712
- * silently when the file can't be parsed (no spurious warning on syntax errors).
2713
- */
2714
- function checkDefaultExports(node, warnings) {
2715
- if (node.page && isScriptExtension(node.page.extension)) checkFileDefaultExport(node.page.filePath, "page", warnings);
2716
- if (node.layout && isScriptExtension(node.layout.extension)) checkFileDefaultExport(node.layout.filePath, "layout", warnings);
2717
- for (const child of node.children) checkDefaultExports(child, warnings);
2718
- for (const slot of Object.values(node.slots)) checkDefaultExports(slot, warnings);
2719
- }
2720
- function isScriptExtension(ext) {
2721
- return ext === "tsx" || ext === "ts" || ext === "jsx" || ext === "js";
2722
- }
2723
- function checkFileDefaultExport(filePath, fileType, warnings) {
2724
- try {
2725
- if (!fileHasDefaultExport(filePath)) warnings.push({
2726
- id: `NO_DEFAULT_EXPORT:${filePath}`,
2727
- summary: `${fileType}.tsx has no default export: ${filePath}`,
2728
- details: ` File: ${filePath}\n\n ${fileType}.tsx files must export a default React component.\n Without a default export, the ${fileType === "page" ? "page renders nothing" : "layout has no component to wrap children"}.\n\n To fix: Add a default export:\n\n export default function My${fileType === "page" ? "Page" : "Layout"}(${fileType === "layout" ? "{ children }" : ""}) {\n return ${fileType === "layout" ? "<div>{children}</div>" : "<h1>Hello</h1>"};\n }\n`,
2729
- level: "warn"
2730
- });
2731
- } catch (err) {
2732
- swallow(err, `convention-lint: unreadable ${fileType} file ${filePath}`);
2733
- }
2734
- }
2735
- /**
2736
- * Warn when error.tsx or global-error.tsx is missing 'use client'.
2737
- *
2738
- * React's error boundary API requires client components. Without the directive,
2739
- * the error boundary silently fails — the component renders as a server component
2740
- * with no error catching. Users discover this only via cryptic runtime errors.
2741
- */
2742
- function checkErrorDirectives(node, warnings) {
2743
- if (node.error && isScriptExtension(node.error.extension)) checkFileUseClientDirective(node.error.filePath, "error", warnings);
2744
- for (const child of node.children) checkErrorDirectives(child, warnings);
2745
- for (const slot of Object.values(node.slots)) checkErrorDirectives(slot, warnings);
2746
- }
2747
- function checkFileUseClientDirective(filePath, fileType, warnings) {
2748
- try {
2749
- if (!fileHasDirective(filePath, "use client")) warnings.push({
2750
- id: `ERROR_NO_USE_CLIENT:${filePath}`,
2751
- summary: `${fileType}.tsx is missing 'use client': ${filePath}`,
2752
- details: ` File: ${filePath}\n\n ${fileType}.tsx must start with 'use client' because React error\n boundaries require client components. Without it, the error boundary
2753
- silently fails and errors are uncaught.
2754
-
2755
- To fix: Add 'use client' as the first line of the file:
2756
-
2757
- 'use client';
2758
-
2759
- export default function ${fileType === "global-error" ? "GlobalError" : "ErrorBoundary"}({ error, reset }) {\n return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;
2760
- }
2761
- `,
2762
- level: "warn"
2763
- });
2764
- } catch (err) {
2765
- swallow(err, `convention-lint: unreadable ${fileType} file ${filePath}`);
2766
- }
2767
- }
2768
- /**
2769
- * Check if the app/ directory exists. Called before scanning.
2770
- * Returns a warning if missing, or null if the directory exists.
2771
- */
2772
- function checkAppDirExists(appDir) {
2773
- if (existsSync(appDir)) return null;
2774
- return {
2775
- id: "NO_APP_DIR",
2776
- summary: "No app/ directory found",
2777
- details: ` Expected: ${appDir}\n\n timber.js requires an app/ directory for file-system routing.
2778
- Every request will return 404 until you create it.
2779
-
2780
- To fix: Create the app/ directory with a page:
2781
-
2782
- mkdir -p app
2783
- # Create app/layout.tsx and app/page.tsx
2784
- `,
2785
- level: "error"
2786
- };
2787
- }
2788
- var noValidate = { validateStream: false };
2789
- var style = (fmt, text) => styleText(fmt, text, noValidate);
2790
- /**
2791
- * Format warnings for terminal output.
2792
- *
2793
- * Groups by severity, uses colors, and includes fix suggestions.
2794
- */
2795
- function formatConventionWarnings(warnings) {
2796
- if (warnings.length === 0) return "";
2797
- const errors = warnings.filter((w) => w.level === "error");
2798
- const warns = warnings.filter((w) => w.level === "warn");
2799
- const lines = [];
2800
- if (errors.length > 0) {
2801
- lines.push(`${style(["red", "bold"], "[timber]")} ${style("red", `${errors.length} configuration error${errors.length !== 1 ? "s" : ""}:`)}`);
2802
- for (const e of errors) {
2803
- lines.push("");
2804
- lines.push(` ${style("red", "✗")} ${e.summary}`);
2805
- lines.push(style("dim", e.details));
2806
- }
2807
- }
2808
- if (warns.length > 0) {
2809
- if (errors.length > 0) lines.push("");
2810
- lines.push(`${style(["yellow", "bold"], "[timber]")} ${style("yellow", `${warns.length} configuration warning${warns.length !== 1 ? "s" : ""}:`)}`);
2811
- for (const w of warns) {
2812
- lines.push("");
2813
- lines.push(` ${style("yellow", "⚠")} ${w.summary}`);
2814
- lines.push(style("dim", w.details));
2815
- }
2816
- }
2817
- return lines.join("\n");
2818
- }
2819
- //#endregion
2820
2358
  //#region src/plugins/routing.ts
2821
2359
  var VIRTUAL_MODULE_ID$1 = "virtual:timber-route-manifest";
2822
2360
  var RESOLVED_VIRTUAL_ID$1 = `\0${VIRTUAL_MODULE_ID$1}`;
2823
- var SEARCH_PARAMS_REGISTRY_ID = "virtual:timber-search-params-registry";
2824
- var RESOLVED_SEARCH_PARAMS_REGISTRY_ID = `\0${SEARCH_PARAMS_REGISTRY_ID}`;
2825
2361
  var SCHEMA_ID = "virtual:timber-schema";
2826
2362
  var RESOLVED_SCHEMA_ID = `\0${SCHEMA_ID}`;
2827
2363
  var SEGMENT_PREFIX = "\0$segment:";
2828
2364
  /**
2829
2365
  * File convention names we track for changes that require manifest regeneration.
2830
2366
  */
2831
- var ROUTE_FILE_PATTERNS = /\/(page|layout|middleware|access|route|error|global-error|default|denied|params|schema|\d{3}|[45]xx|sitemap|robots|manifest|favicon|icon|opengraph-image|apple-icon)\./;
2367
+ var ROUTE_FILE_PATTERNS = /\/(page|layout|middleware|access|route|error|global-error|default|denied|schema|\d{3}|[45]xx|sitemap|robots|manifest|favicon|icon|opengraph-image|apple-icon)\./;
2832
2368
  /**
2833
2369
  * Create the timber-routing Vite plugin.
2834
2370
  *
2835
2371
  * Hooks: resolveId, load, buildStart, configureServer
2836
2372
  */
2837
- /** Absolute path to the generated route map declaration file. */
2838
- var CODEGEN_OUTPUT = ".timber/timber-routes.d.ts";
2839
- /**
2840
- * Content of the timber-env.d.ts file written to the project root.
2841
- *
2842
- * Uses a triple-slash reference so TypeScript auto-discovers the generated
2843
- * route map without the user needing to edit tsconfig.json — same pattern
2844
- * as Next.js's next-env.d.ts.
2845
- *
2846
- * Also references `vite/client` so side-effect imports like
2847
- * `import './globals.css'` type-check without ts(2882), and so
2848
- * `import.meta.env`, `import.meta.hot`, `import.meta.glob`, asset
2849
- * imports (`*.png`, `*.svg`, …), CSS modules, and `?url`/`?raw`/
2850
- * `?inline` query variants all resolve without the user editing
2851
- * tsconfig.json themselves. Next.js's next-env.d.ts plays the same
2852
- * role via `next/types/global`.
2853
- *
2854
- * `vite` is a peer dep of `@timber-js/app`, so the reference always
2855
- * resolves in a correctly-installed project.
2856
- */
2857
- var TIMBER_ENV_DTS = [
2858
- "// This file is auto-generated by timber.js. Do not edit.",
2859
- "// It ensures TypeScript picks up the generated route types and",
2860
- "// Vite-provided ambient types (CSS, assets, import.meta.env, HMR).",
2861
- "/* eslint-disable @typescript-eslint/triple-slash-reference */",
2862
- "/// <reference path=\".timber/timber-routes.d.ts\" />",
2863
- "/// <reference types=\"vite/client\" />",
2864
- ""
2865
- ].join("\n");
2866
2373
  /**
2867
2374
  * Write the generated route map and the timber-env.d.ts reference file.
2868
2375
  *
2869
- * Fire-and-forget — never awaited to avoid blocking the hot path.
2870
- * Errors are swallowed; a missing .d.ts is a developer UX issue, not a runtime failure.
2376
+ * Not awaited, to avoid blocking the hot path — use `flushCodegenWrites()` to
2377
+ * wait for the writes to land. Failures are logged, not thrown: a missing .d.ts
2378
+ * is a developer UX issue in dev/build, not a runtime failure. (`timber check`
2379
+ * calls `writeRouteCodegen` directly and does treat a failure as fatal.)
2871
2380
  */
2872
2381
  function writeCodegen(ctx) {
2873
2382
  if (!ctx.routeTree) return;
2874
- const timberDir = join(ctx.root, ".timber");
2875
- const content = generateRouteMap(ctx.routeTree, {
2876
- appDir: ctx.appDir,
2877
- outputDir: timberDir
2878
- });
2879
- const routesPath = join(ctx.root, CODEGEN_OUTPUT);
2880
- const envPath = join(ctx.root, "timber-env.d.ts");
2881
- mkdir(timberDir, { recursive: true }).then(() => Promise.all([
2882
- writeFile(routesPath, content, "utf-8"),
2883
- writeFile(envPath, TIMBER_ENV_DTS, "utf-8"),
2884
- writeSegmentTypes(ctx.routeTree, ctx.appDir, ctx.root)
2885
- ])).catch((err) => {
2383
+ writeRouteCodegen(ctx.routeTree, ctx.appDir, ctx.root).catch((err) => {
2886
2384
  console.warn("[timber] Failed to write codegen output:", err);
2887
2385
  });
2888
2386
  }
@@ -2906,12 +2404,16 @@ function timberRouting(ctx) {
2906
2404
  return;
2907
2405
  }
2908
2406
  ctx.timer.start("route-scan");
2909
- ctx.routeTree = scanRoutes(ctx.appDir, { pageExtensions: ctx.config.pageExtensions });
2407
+ const tree = scanRoutes(ctx.appDir, { pageExtensions: ctx.config.pageExtensions });
2910
2408
  ctx.timer.end("route-scan");
2409
+ const conventionWarnings = lintConventions(tree, ctx.appDir);
2410
+ const fatal = conventionWarnings.filter((w) => w.level === "error");
2411
+ if (fatal.length > 0) throw new Error(formatConventionWarnings(fatal, { color: false }));
2412
+ ctx.routeTree = tree;
2911
2413
  dirToRoute = buildDirToRouteMap(ctx.routeTree, ctx.appDir);
2912
2414
  writeCodegen(ctx);
2913
2415
  warnSchemaValidation(ctx);
2914
- const newConventionWarnings = lintConventions(ctx.routeTree, ctx.appDir).filter((w) => !warnedFiles.has(w.id));
2416
+ const newConventionWarnings = conventionWarnings.filter((w) => !warnedFiles.has(w.id));
2915
2417
  if (newConventionWarnings.length > 0) {
2916
2418
  for (const w of newConventionWarnings) warnedFiles.add(w.id);
2917
2419
  const formatted = formatConventionWarnings(newConventionWarnings);
@@ -2932,7 +2434,6 @@ function timberRouting(ctx) {
2932
2434
  const cleanId = id.startsWith("\0") ? id.slice(1) : id;
2933
2435
  if (cleanId === VIRTUAL_MODULE_ID$1) return RESOLVED_VIRTUAL_ID$1;
2934
2436
  if (cleanId.endsWith(`/${VIRTUAL_MODULE_ID$1}`)) return RESOLVED_VIRTUAL_ID$1;
2935
- if (cleanId === SEARCH_PARAMS_REGISTRY_ID || cleanId.endsWith(`/${SEARCH_PARAMS_REGISTRY_ID}`)) return RESOLVED_SEARCH_PARAMS_REGISTRY_ID;
2936
2437
  if (cleanId === SCHEMA_ID || cleanId.endsWith(`/${SCHEMA_ID}`)) return RESOLVED_SCHEMA_ID;
2937
2438
  if (importer && (cleanId === "./$segment" || cleanId === "./$segment.js" || cleanId === "./$segment.ts")) {
2938
2439
  const cleanImporter = importer.startsWith("\0") ? importer.slice(1) : importer;
@@ -2953,10 +2454,6 @@ function timberRouting(ctx) {
2953
2454
  if (!ctx.routeTree) rescan();
2954
2455
  return generateManifestModule(ctx.routeTree, ctx.root);
2955
2456
  }
2956
- if (id === RESOLVED_SEARCH_PARAMS_REGISTRY_ID) {
2957
- if (!ctx.routeTree) rescan();
2958
- return generateSearchParamsRegistryModule(ctx.routeTree);
2959
- }
2960
2457
  if (id === RESOLVED_SCHEMA_ID) return generateSchemaModule(ctx.appDir);
2961
2458
  if (id.startsWith(SEGMENT_PREFIX)) {
2962
2459
  const info = JSON.parse(id.slice(10));
@@ -2979,6 +2476,18 @@ function timberRouting(ctx) {
2979
2476
  rescan();
2980
2477
  },
2981
2478
  /**
2479
+ * Wait for the codegen writes started by `buildStart` before the build
2480
+ * reports done.
2481
+ *
2482
+ * `writeCodegen` runs unawaited so route scanning never blocks on disk.
2483
+ * Without this the build can finish — and the CLI can exit — while the
2484
+ * generated `.d.ts` files are still being written, leaving a project root
2485
+ * with no route types after an apparently successful build.
2486
+ */
2487
+ closeBundle() {
2488
+ return flushCodegenWrites();
2489
+ },
2490
+ /**
2982
2491
  * Set up file watcher in dev mode.
2983
2492
  *
2984
2493
  * Watches app/ for file additions/deletions/renames that affect
@@ -2991,13 +2500,59 @@ function timberRouting(ctx) {
2991
2500
  /** Snapshot of the last generated manifest, used to detect structural changes. */
2992
2501
  let lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree, ctx.root) : "";
2993
2502
  /**
2503
+ * Rescan without letting a rejected route tree kill the dev server.
2504
+ *
2505
+ * `rescan()` throws on hard misconfigurations — duplicate conventions,
2506
+ * route collisions, a status file that is not a client component. Those
2507
+ * throws are correct at startup and in `vite build`, but here they run
2508
+ * inside a chokidar listener, where an exception is an unhandled
2509
+ * rejection that takes the process down mid-edit. Report and keep the
2510
+ * last good tree so the server stays up and the fix is one save away.
2511
+ *
2512
+ * Returns false when the scan failed, so callers skip the manifest
2513
+ * regeneration that would otherwise publish a stale tree as fresh.
2514
+ */
2515
+ /**
2516
+ * True while the last rescan is still failing.
2517
+ *
2518
+ * Recovery has to force a reload even when the manifest is byte-identical.
2519
+ * Adding a missing `'use client'` back to a status file changes no route
2520
+ * metadata, so the content-change path would send nothing and leave the
2521
+ * error overlay on screen over a page that is now fine.
2522
+ */
2523
+ let scanFailed = false;
2524
+ const rescanSafely = () => {
2525
+ try {
2526
+ rescan();
2527
+ if (scanFailed) {
2528
+ scanFailed = false;
2529
+ lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree, ctx.root) : "";
2530
+ invalidateManifest(devServer);
2531
+ }
2532
+ return true;
2533
+ } catch (err) {
2534
+ const message = err instanceof Error ? err.message : String(err);
2535
+ process.stderr.write(`[timber] Route scan failed — keeping previous routes.\n${message}\n`);
2536
+ scanFailed = true;
2537
+ devServer.hot.send({
2538
+ type: "error",
2539
+ err: {
2540
+ message,
2541
+ stack: "",
2542
+ plugin: "timber-routing"
2543
+ }
2544
+ });
2545
+ return false;
2546
+ }
2547
+ };
2548
+ /**
2994
2549
  * Handle a route-significant file being added or removed.
2995
2550
  * Always triggers a full-reload since the route tree structure changed.
2996
2551
  */
2997
2552
  const handleStructuralChange = (filePath) => {
2998
2553
  if (!filePath.startsWith(ctx.appDir)) return;
2999
2554
  if (!ROUTE_FILE_PATTERNS.test(filePath)) return;
3000
- rescan();
2555
+ if (!rescanSafely()) return;
3001
2556
  lastManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree, ctx.root) : "";
3002
2557
  invalidateManifest(devServer);
3003
2558
  };
@@ -3012,7 +2567,7 @@ function timberRouting(ctx) {
3012
2567
  const handleContentChange = (filePath) => {
3013
2568
  if (!filePath.startsWith(ctx.appDir)) return;
3014
2569
  if (!ROUTE_FILE_PATTERNS.test(filePath)) return;
3015
- rescan();
2570
+ if (!rescanSafely()) return;
3016
2571
  const newManifest = ctx.routeTree ? generateManifestModule(ctx.routeTree, ctx.root) : "";
3017
2572
  if (newManifest !== lastManifest) {
3018
2573
  lastManifest = newManifest;
@@ -3062,11 +2617,7 @@ function invalidateManifest(server) {
3062
2617
  for (const envName of Object.keys(server.environments)) {
3063
2618
  const env = server.environments[envName];
3064
2619
  if (!env?.moduleGraph) continue;
3065
- for (const virtualId of [
3066
- RESOLVED_VIRTUAL_ID$1,
3067
- RESOLVED_SEARCH_PARAMS_REGISTRY_ID,
3068
- RESOLVED_SCHEMA_ID
3069
- ]) {
2620
+ for (const virtualId of [RESOLVED_VIRTUAL_ID$1, RESOLVED_SCHEMA_ID]) {
3070
2621
  const mod = env.moduleGraph.getModuleById(virtualId);
3071
2622
  if (mod) env.moduleGraph.invalidateModule(mod);
3072
2623
  }
@@ -3104,7 +2655,6 @@ var SUBPATH_SRC_MAP = {
3104
2655
  "cookies": "cookies/index.ts",
3105
2656
  "params": "params/index.ts",
3106
2657
  "search-params": "search-params/index.ts",
3107
- "segment-params": "segment-params/index.ts",
3108
2658
  "cache/stores/memory": "cache/stores/memory.ts",
3109
2659
  "cache/stores/redis": "cache/stores/redis.ts",
3110
2660
  "cache/stores/cloudflare-kv": "cache/stores/cloudflare-kv.ts",
@@ -6194,16 +5744,19 @@ function writeFontManifest(pipeline, buildManifest, base = "/") {
6194
5744
  */
6195
5745
  function timberFonts(ctx) {
6196
5746
  const pipeline = new FontPipeline({ hashOnRegister: !ctx.dev });
6197
- let resolvedBase = "/";
6198
5747
  return {
6199
5748
  name: "timber-fonts",
6200
5749
  /**
6201
- * Capture Vite's resolved `base` so every font URL the pipeline emits
6202
- * (@font-face src, manifest hrefs, dev middleware matching) carries
6203
- * the prefix. Emitted file locations are unaffected — only URLs.
5750
+ * Push Vite's resolved `base` into the pipeline so every font URL it
5751
+ * emits (@font-face src, manifest hrefs, dev middleware matching)
5752
+ * carries the prefix. Emitted file locations are unaffected — only URLs.
5753
+ *
5754
+ * `config.base` is read here rather than `ctx.base`: this plugin is
5755
+ * `enforce: 'pre'`, so its configResolved runs before timber-root-sync
5756
+ * populates `ctx.base`. Every *later* hook reads `ctx.base` — the single
5757
+ * copy (TIM-1261).
6204
5758
  */
6205
5759
  configResolved(config) {
6206
- resolvedBase = config.base;
6207
5760
  pipeline.setBase(config.base);
6208
5761
  const originalWarnOnce = config.logger.warnOnce.bind(config.logger);
6209
5762
  config.logger.warnOnce = (msg, options) => {
@@ -6292,7 +5845,7 @@ function timberFonts(ctx) {
6292
5845
  } else {
6293
5846
  const familyCached = groupCachedFontsByFamily(await downloadAndCacheFonts([font], ctx.root)).get(font.family.toLowerCase()) ?? [];
6294
5847
  pipeline.attachCachedFiles(font.id, familyCached);
6295
- pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display, fontUrlPrefix(resolvedBase)));
5848
+ pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display, fontUrlPrefix(ctx.base)));
6296
5849
  }
6297
5850
  } catch (e) {
6298
5851
  if (!ctx.dev) throw e;
@@ -6310,7 +5863,7 @@ function timberFonts(ctx) {
6310
5863
  * this middleware.
6311
5864
  */
6312
5865
  configureServer(server) {
6313
- installFontDevMiddleware(server, pipeline, resolvedBase);
5866
+ installFontDevMiddleware(server, pipeline, ctx.base);
6314
5867
  },
6315
5868
  /**
6316
5869
  * No-op. Font downloading and face attachment happen lazily in `load()`
@@ -6380,7 +5933,7 @@ function timberFonts(ctx) {
6380
5933
  generateBundle() {
6381
5934
  emitFontAssets(pipeline, (asset) => this.emitFile(asset), (msg) => this.warn(msg));
6382
5935
  if (!ctx.buildManifest) return;
6383
- writeFontManifest(pipeline, ctx.buildManifest, resolvedBase);
5936
+ writeFontManifest(pipeline, ctx.buildManifest, ctx.base);
6384
5937
  }
6385
5938
  };
6386
5939
  }
@@ -6410,6 +5963,7 @@ function timberAdapterBuild(ctx) {
6410
5963
  if (ctx.clientJavascript.disabled) await stripJsFromRscAssetsManifests(buildDir);
6411
5964
  const adapterConfig = {
6412
5965
  root: ctx.root,
5966
+ base: normalizeBasePath(ctx.base),
6413
5967
  output: ctx.config.output ?? "server",
6414
5968
  clientJavascriptDisabled: ctx.clientJavascript.disabled,
6415
5969
  manifestInit,
@@ -6654,10 +6208,22 @@ function detectDynamicApis(code, fileId) {
6654
6208
  * Generate a `_redirects` file from collected redirect entries.
6655
6209
  * Uses the Cloudflare Pages / Netlify format: `/from /to statusCode`
6656
6210
  *
6211
+ * Both sides are base-prefixed (TIM-1274). `from` is matched by the host
6212
+ * against the request path and `to` is served to the browser as a `Location`,
6213
+ * so both are deployed-URL space while `StaticRedirectEntry` carries route
6214
+ * space — under `base: '/repo/'`, a rule `/old /new 301` would match nothing
6215
+ * and, if it did, would send the browser off the deployment.
6216
+ *
6217
+ * An off-origin `to` is left alone: `redirectExternal()` produces an absolute
6218
+ * URL, and a protocol-relative one addresses another host too. Neither is a
6219
+ * path on this deployment, so neither takes the prefix.
6220
+ *
6221
+ * @param base Normalized deployment base (see `shared/base-path.ts`).
6657
6222
  * @internal Exported for testing.
6658
6223
  */
6659
- function generateRedirectsFile(redirects) {
6660
- return redirects.map((r) => `${r.from} ${r.to} ${r.status}`).join("\n") + "\n";
6224
+ function generateRedirectsFile(redirects, base) {
6225
+ const target = (to) => to.startsWith("/") && !to.startsWith("//") ? withBasePath(base, to) : to;
6226
+ return redirects.map((r) => `${withBasePath(base, r.from)} ${target(r.to)} ${r.status}`).join("\n") + "\n";
6661
6227
  }
6662
6228
  /** Write a file atomically: temp sibling + rename. */
6663
6229
  async function writeFileAtomic$1(path, data) {
@@ -6669,18 +6235,203 @@ async function writeFileAtomic$1(path, data) {
6669
6235
  * Determine the output file path for a static page entry.
6670
6236
  *
6671
6237
  * Pages: /about → about/index.html, / → index.html
6672
- * RSC: /about → _rsc/about.rsc, / → _rsc/index.rsc
6238
+ * RSC: /about → _rsc/about/index.rsc, / → _rsc/index.rsc
6673
6239
  * API: /feed.xml → feed.xml, /api/data → api/data
6240
+ *
6241
+ * The RSC shape mirrors the HTML shape for the reason the HTML shape has it:
6242
+ * a route gets a directory and the artifact is the `index` inside it, so the
6243
+ * mapping is injective. See `shared/rsc-payload-path.ts` (TIM-1266).
6674
6244
  */
6675
6245
  function staticOutputPath(entry) {
6676
6246
  const urlPath = entry.urlPath === "/" ? "" : entry.urlPath;
6677
- if (entry.kind === "rsc") return `_rsc${urlPath || "/index"}.rsc`;
6247
+ if (entry.kind === "rsc") return rscPayloadPath(entry.urlPath).slice(1);
6678
6248
  if (entry.kind === "api") return urlPath.startsWith("/") ? urlPath.slice(1) : urlPath || "index";
6679
6249
  if (!urlPath) return "index.html";
6680
6250
  if (urlPath === "/_timber_not_found") return "404.html";
6681
6251
  return `${urlPath.startsWith("/") ? urlPath.slice(1) : urlPath}/index.html`;
6682
6252
  }
6683
6253
  /**
6254
+ * Records which entry claimed each output path and reports second claims.
6255
+ *
6256
+ * Two entries landing on one path is not a rendering failure — both render
6257
+ * fine — so nothing downstream notices. The file is written twice, the second
6258
+ * write wins, and the deployment serves one route's bytes at another route's
6259
+ * URL with the same content type and the same shape (TIM-1266).
6260
+ *
6261
+ * The naming rules are injective by construction (a route path is a directory
6262
+ * and the artifact is the `index` inside it), so a collision here means either
6263
+ * two entries were enumerated for one URL or a route reached a path the
6264
+ * framework reserves for itself (`404.html`). Both are build errors: there is
6265
+ * no correct file to write.
6266
+ *
6267
+ * Two entries can also be unwritable together without claiming the same
6268
+ * string: one wants a file where the other wants a directory (`about` from an
6269
+ * API route, `about/index.html` from a page). The filesystem does refuse that
6270
+ * one — `EISDIR` on the rename, `ENOTDIR` on the mkdir, depending on which
6271
+ * lands second — but an errno names a path, and in the rename case the path
6272
+ * it names is `about.tmp`. Reported here instead, so the message says which
6273
+ * two routes.
6274
+ *
6275
+ * Collisions are **collected, not thrown**. `onEntry` runs inside the
6276
+ * generator, whose RSC branch treats a throw as "this payload is unavailable,
6277
+ * SPA navigation falls back to a full load" and swallows it — which for a
6278
+ * collision is the wrong end state, since the two payloads are still
6279
+ * ambiguous. The caller raises them once generation is done.
6280
+ *
6281
+ * @internal Exported for testing.
6282
+ */
6283
+ var StaticOutputPaths = class {
6284
+ identity;
6285
+ claimed = /* @__PURE__ */ new Map();
6286
+ /** Every ancestor directory of a claimed path, mapped to its claimant. */
6287
+ directories = /* @__PURE__ */ new Map();
6288
+ collisions = [];
6289
+ /**
6290
+ * @param identity How the output filesystem decides two paths name one
6291
+ * file, from `detectFsIdentity`. The guard predicts whether two writes
6292
+ * land on one file, which is a property of the filesystem — so it is
6293
+ * asked, not assumed. Folding unconditionally would reject `/Foo` beside
6294
+ * `/foo` on Linux, where both are perfectly servable files.
6295
+ */
6296
+ constructor(identity = EXACT_FS_IDENTITY) {
6297
+ this.identity = identity;
6298
+ }
6299
+ /** The identity under which the filesystem will compare this path. */
6300
+ key(relPath) {
6301
+ return fsKey(relPath, this.identity);
6302
+ }
6303
+ /**
6304
+ * Claim `relPath` for `entry`. Returns false when the path cannot be
6305
+ * written alongside an earlier claim, in which case the caller must not
6306
+ * write the file — the losing entry is recorded and the build fails at
6307
+ * the end.
6308
+ */
6309
+ claim(relPath, entry) {
6310
+ if (RESERVED_OUTPUT_PATHS.some((name) => this.key(name) === this.key(relPath))) {
6311
+ this.collisions.push(` - ${relPath}: reserved — the framework writes this file itself, and ${entry.kind} for ${entry.urlPath} would be overwritten by it`);
6312
+ return false;
6313
+ }
6314
+ const key = this.key(relPath);
6315
+ const conflict = this.claimed.get(key) ?? this.directories.get(key) ?? ancestors(relPath).reduce((found, dir) => found ?? this.claimed.get(this.key(dir)), void 0);
6316
+ if (conflict) {
6317
+ this.collisions.push(` - ${relPath}: ${conflict.kind} for ${conflict.urlPath} and ${entry.kind} for ${entry.urlPath}`);
6318
+ return false;
6319
+ }
6320
+ this.claimed.set(key, entry);
6321
+ for (const dir of ancestors(relPath)) {
6322
+ const dirKey = this.key(dir);
6323
+ if (!this.directories.has(dirKey)) this.directories.set(dirKey, entry);
6324
+ }
6325
+ return true;
6326
+ }
6327
+ /** Throw if any two entries could not both be written. */
6328
+ assertNoCollisions() {
6329
+ if (this.collisions.length === 0) return;
6330
+ throw new Error(`[timber] Static generation: ${this.collisions.length} output path collision(s) — two entries write the same path and one would silently overwrite the other. Rename or remove one route in each pair:\n${this.collisions.join("\n")}`);
6331
+ }
6332
+ };
6333
+ /** Every directory an output path sits under, outermost first. */
6334
+ function ancestors(relPath) {
6335
+ const parts = relPath.split("/");
6336
+ parts.pop();
6337
+ return parts.map((_, i) => parts.slice(0, i + 1).join("/"));
6338
+ }
6339
+ /**
6340
+ * Writes generated entries into the static output directory, one file per
6341
+ * entry, refusing any write that would land on a path another entry holds.
6342
+ *
6343
+ * @internal Exported for testing.
6344
+ */
6345
+ var StaticSiteWriter = class {
6346
+ staticDir;
6347
+ /** Content-Type per URL path for extensionless API routes (TIM-1241). */
6348
+ contentTypes = {};
6349
+ paths;
6350
+ constructor(staticDir, identity = EXACT_FS_IDENTITY) {
6351
+ this.staticDir = staticDir;
6352
+ this.paths = new StaticOutputPaths(identity);
6353
+ }
6354
+ async write(entry) {
6355
+ const relPath = staticOutputPath(entry);
6356
+ const fullPath = resolve(this.staticDir, relPath);
6357
+ const rel = relative(this.staticDir, fullPath);
6358
+ if (rel.split(/[\\/]/)[0] === ".." || isAbsolute(rel) || resolve(this.staticDir, rel) !== fullPath) throw new Error(`[timber] Static generation: output path "${relPath}" escapes the static output directory — check generateStaticSegmentParams for path traversal characters in ${entry.urlPath}`);
6359
+ if (!this.paths.claim(rel.replace(/\\/g, "/"), entry)) return;
6360
+ await mkdir(dirname(fullPath), { recursive: true });
6361
+ await writeFileAtomic$1(fullPath, entry.body);
6362
+ if (entry.kind === "api" && !extname(relPath) && entry.contentType) this.contentTypes[entry.urlPath] = entry.contentType;
6363
+ }
6364
+ /** Throw if any two entries could not both be written. */
6365
+ assertNoCollisions() {
6366
+ this.paths.assertNoCollisions();
6367
+ }
6368
+ };
6369
+ /**
6370
+ * Remove artifacts a generation pass produces only *conditionally*, so none
6371
+ * of them can survive from an earlier build into this one.
6372
+ *
6373
+ * Pages are not in this set: every page is rewritten each pass, and a page
6374
+ * whose route was deleted is a different problem (a stale file nothing links
6375
+ * to) from the one here.
6376
+ *
6377
+ * - `_rsc/` because the files are content-hashed. Left in place, a previous
6378
+ * build's already-hashed payload gets hashed again and accumulates.
6379
+ * - `_redirects` because it is written only when this pass found redirects.
6380
+ * A build whose redirects were removed would otherwise ship the previous
6381
+ * build's rules — and would break the merge exemption in
6382
+ * `shared/static-platform-files.ts`, which lets the pre-rendered copy
6383
+ * overwrite `public/_redirects` on the grounds that it *contains* it. That
6384
+ * holds only for a file the current pass produced (TIM-1266).
6385
+ *
6386
+ * @internal Exported for testing.
6387
+ */
6388
+ async function cleanConditionalArtifacts(staticDir) {
6389
+ await rm(join(staticDir, "_rsc"), {
6390
+ recursive: true,
6391
+ force: true
6392
+ }).catch(() => {});
6393
+ await rm(join(staticDir, REDIRECTS_FILE), { force: true }).catch(() => {});
6394
+ }
6395
+ /**
6396
+ * Run `fn` with `MessageChannel` instrumented, then stop every channel opened
6397
+ * while it ran from holding the process open.
6398
+ *
6399
+ * Static generation is the only build pass that renders **HTML** in the build
6400
+ * process: it imports the built app bundle and drives the full SSR pipeline
6401
+ * through it. React's scheduler opens a `MessageChannel` at module scope in
6402
+ * that bundle (`scheduler`'s DOM build) and never closes it, and a ref'd
6403
+ * `MessagePort` keeps Node's event loop alive on its own — so `timber build`
6404
+ * finished the site, printed its summary, and then hung forever. Server-mode
6405
+ * builds are unaffected because nothing there renders through react-dom.
6406
+ *
6407
+ * `unref()` is the whole fix: it leaves the port working and only drops the
6408
+ * "keep the process alive" property. It is applied **after** `fn` resolves, so
6409
+ * a render in flight can never be cut short by the process exiting under it —
6410
+ * an unref'd port would let Node exit while the scheduler still had a
6411
+ * continuation queued, which is a silently truncated page rather than a hang.
6412
+ *
6413
+ * Nothing renders after this pass. If that ever changes, the release has to
6414
+ * move to the end of whatever renders last.
6415
+ *
6416
+ * @internal Exported for testing.
6417
+ */
6418
+ async function withSchedulerPortsReleased(fn) {
6419
+ const opened = [];
6420
+ const OriginalMessageChannel = globalThis.MessageChannel;
6421
+ globalThis.MessageChannel = class extends OriginalMessageChannel {
6422
+ constructor() {
6423
+ super();
6424
+ opened.push(this.port1, this.port2);
6425
+ }
6426
+ };
6427
+ try {
6428
+ return await fn();
6429
+ } finally {
6430
+ globalThis.MessageChannel = OriginalMessageChannel;
6431
+ for (const port of opened) port.unref?.();
6432
+ }
6433
+ }
6434
+ /**
6684
6435
  * Load the built RSC entry and run the static generation pass.
6685
6436
  *
6686
6437
  * Unlike prebuilt capture, this needs the FULL pipeline handler, so
@@ -6695,55 +6446,41 @@ async function runStaticGeneration(ctx) {
6695
6446
  g.__TIMBER_CAPTURE_MODE__ = false;
6696
6447
  if (ctx.buildManifest) await mergeRscAssetsManifest(ctx.buildDir, ctx.root, ctx.buildManifest, ctx.routeClientRefKeys);
6697
6448
  console.log("[timber] Static generation: building site…");
6698
- let mod;
6699
- try {
6700
- mod = await import(
6701
- /* @vite-ignore */
6702
- `${pathToFileURL(entryPath).href}?t=${Date.now()}`
6703
- );
6704
- } catch (error) {
6705
- throw new Error(`[timber] Static generation: failed to load the built RSC entry (${entryPath}). The full request handler must initialize for static rendering.`, { cause: error });
6706
- }
6707
- const generate = mod.__timber_generateStaticSite;
6708
- if (typeof generate !== "function") throw new Error("[timber] Static generation: built RSC entry does not export __timber_generateStaticSite — rebuild from a clean build directory");
6709
6449
  const staticDir = join(ctx.buildDir, "static-pages");
6710
- await rm(join(staticDir, "_rsc"), {
6711
- recursive: true,
6712
- force: true
6713
- }).catch(() => {});
6450
+ await cleanConditionalArtifacts(staticDir);
6714
6451
  await mkdir(staticDir, { recursive: true });
6715
6452
  const sitemapConfig = ctx.config.sitemap;
6716
6453
  const autoSitemapEnabled = Boolean(sitemapConfig?.enabled && sitemapConfig?.baseUrl);
6717
- const contentTypes = {};
6718
- const routeParams = {};
6719
- const summary = await generate({
6720
- autoSitemapEnabled,
6721
- renderTimeoutMs: ctx.config.renderTimeoutMs,
6722
- onEntry: async (entry) => {
6723
- const relPath = staticOutputPath(entry);
6724
- const fullPath = resolve(staticDir, relPath);
6725
- const rel = relative(staticDir, fullPath);
6726
- if (rel.startsWith("..") || resolve(staticDir, rel) !== fullPath) throw new Error(`[timber] Static generation: output path "${relPath}" escapes the static output directory — check generateStaticSegmentParams for path traversal characters in ${entry.urlPath}`);
6727
- await mkdir(dirname(fullPath), { recursive: true });
6728
- await writeFileAtomic$1(fullPath, entry.body);
6729
- if (entry.kind === "rsc" && entry.headers["x-timber-params"]) {
6730
- const rscUrlKey = entry.urlPath === "/" ? "/_rsc/index.rsc" : `/_rsc${entry.urlPath}.rsc`;
6731
- try {
6732
- routeParams[rscUrlKey] = JSON.parse(entry.headers["x-timber-params"]);
6733
- } catch {}
6734
- }
6735
- if (entry.kind === "api" && !extname(relPath) && entry.contentType) contentTypes[entry.urlPath] = entry.contentType;
6736
- }
6454
+ const writer = new StaticSiteWriter(staticDir, await detectFsIdentity(staticDir));
6455
+ const summary = await withSchedulerPortsReleased(async () => {
6456
+ let mod;
6457
+ try {
6458
+ mod = await import(
6459
+ /* @vite-ignore */
6460
+ `${pathToFileURL(entryPath).href}?t=${Date.now()}`
6461
+ );
6462
+ } catch (error) {
6463
+ throw new Error(`[timber] Static generation: failed to load the built RSC entry (${entryPath}). The full request handler must initialize for static rendering.`, { cause: error });
6464
+ }
6465
+ const generate = mod.__timber_generateStaticSite;
6466
+ if (typeof generate !== "function") throw new Error("[timber] Static generation: built RSC entry does not export __timber_generateStaticSite — rebuild from a clean build directory");
6467
+ return generate({
6468
+ autoSitemapEnabled,
6469
+ renderTimeoutMs: ctx.config.renderTimeoutMs,
6470
+ onEntry: (entry) => writer.write(entry)
6471
+ });
6737
6472
  });
6738
6473
  const totalUrls = summary.rendered + summary.errors.length;
6739
6474
  console.log(`[timber] Static generation: rendered ${summary.rendered}/${totalUrls} entries.` + (summary.errors.length > 0 ? ` ${summary.errors.length} failed.` : "") + (summary.redirects.length > 0 ? ` ${summary.redirects.length} redirect(s).` : ""));
6475
+ for (const warning of summary.warnings) console.warn(`[timber] Static generation warning: ${warning}`);
6740
6476
  if (summary.errors.length > 0) {
6741
6477
  const details = summary.errors.map((e) => ` - ${e.urlPath}: ${e.message}`).join("\n");
6742
6478
  throw new Error(`[timber] Static generation failed:\n${details}`);
6743
6479
  }
6744
- if (Object.keys(contentTypes).length > 0) ctx.staticContentTypes = contentTypes;
6480
+ writer.assertNoCollisions();
6481
+ if (Object.keys(writer.contentTypes).length > 0) ctx.staticContentTypes = writer.contentTypes;
6745
6482
  if (!ctx.clientJavascript.disabled) {
6746
- const rscManifest = await hashRscPayloads(staticDir, routeParams);
6483
+ const rscManifest = await hashRscPayloads(staticDir);
6747
6484
  if (Object.keys(rscManifest).length > 0) {
6748
6485
  await injectRscManifestIntoHtml(staticDir, rscManifest);
6749
6486
  ctx.rscManifest = rscManifest;
@@ -6751,8 +6488,8 @@ async function runStaticGeneration(ctx) {
6751
6488
  }
6752
6489
  }
6753
6490
  if (summary.redirects.length > 0) {
6754
- const generated = generateRedirectsFile(summary.redirects);
6755
- const redirectsPath = join(staticDir, "_redirects");
6491
+ const generated = generateRedirectsFile(summary.redirects, normalizeBasePath(ctx.base));
6492
+ const redirectsPath = join(staticDir, REDIRECTS_FILE);
6756
6493
  const userRedirects = await readUserRedirects(ctx.root);
6757
6494
  if (userRedirects !== null) {
6758
6495
  await writeFileAtomic$1(redirectsPath, generated + "\n" + userRedirects);
@@ -6765,6 +6502,27 @@ async function runStaticGeneration(ctx) {
6765
6502
  return summary;
6766
6503
  }
6767
6504
  /**
6505
+ * Warn when Vite's `base` is document-relative (`'./'`) in static mode.
6506
+ *
6507
+ * Payload URLs are addressed from the deployment root plus the base prefix.
6508
+ * A relative base gives no prefix to compute (see shared/base-path.ts), so
6509
+ * `/_rsc/about.rsc` is requested at the domain root — correct only when the
6510
+ * site is served from there. The build still succeeds: relative bases work
6511
+ * fine for assets, and for a root-served site nothing is wrong. The warning
6512
+ * exists so a sub-path deployment does not silently degrade to a hard
6513
+ * navigation on every link.
6514
+ *
6515
+ * Skipped when client JS is disabled — there is no SPA navigation and no
6516
+ * payload fetch to mis-address.
6517
+ *
6518
+ * @internal Exported for testing.
6519
+ */
6520
+ function warnUnusableBase(ctx) {
6521
+ if (ctx.clientJavascript.disabled) return;
6522
+ if (!isRelativeBase(ctx.base)) return;
6523
+ console.warn(`[timber] Static mode: base is "${ctx.base}", which is document-relative. RSC payload URLs are addressed from the domain root, so client-side navigation will fall back to a full page load if the site is served from a sub-path. Use an absolute base (e.g. base: '/repo/') to deploy under a sub-path. See design/11-platform.md.`);
6524
+ }
6525
+ /**
6768
6526
  * Compute a short content hash for immutable caching.
6769
6527
  * Uses 8 chars of base64url-encoded SHA-256, matching Vite's asset hash style.
6770
6528
  */
@@ -6799,6 +6557,12 @@ async function walkDir(dir) {
6799
6557
  * The manifest maps the original `_rsc/` URL to an entry object:
6800
6558
  * `{"/_rsc/index.rsc":{"url":"/_rsc/index-B7YxEKdN.rsc"}, ...}`
6801
6559
  *
6560
+ * Keys and URLs are **route-space**: root-relative, never carrying Vite's
6561
+ * `base`. The client applies the base once, at fetch time
6562
+ * (`client/rsc-fetch.ts` → `toStaticRscUrl`), so the generated site stays
6563
+ * relocatable and the base has exactly one expression at runtime rather
6564
+ * than two that must agree. See TIM-1261.
6565
+ *
6802
6566
  * Route params (from `routeParams`) are inlined into the manifest
6803
6567
  * entry, eliminating the need for .params.json sidecar files. TIM-1255.
6804
6568
  *
@@ -6807,7 +6571,7 @@ async function walkDir(dir) {
6807
6571
  *
6808
6572
  * @internal Exported for testing.
6809
6573
  */
6810
- async function hashRscPayloads(staticDir, routeParams) {
6574
+ async function hashRscPayloads(staticDir) {
6811
6575
  const files = await walkDir(join(staticDir, "_rsc"));
6812
6576
  const manifest = {};
6813
6577
  for (const filePath of files) {
@@ -6818,9 +6582,7 @@ async function hashRscPayloads(staticDir, routeParams) {
6818
6582
  await rename(filePath, hashedPath);
6819
6583
  const hashedRel = relative(staticDir, hashedPath).replace(/\\/g, "/");
6820
6584
  const key = `/${rel}`;
6821
- const entry = { url: `/${hashedRel}` };
6822
- if (routeParams?.[key]) entry.params = routeParams[key];
6823
- manifest[key] = entry;
6585
+ manifest[key] = { url: `/${hashedRel}` };
6824
6586
  }
6825
6587
  return manifest;
6826
6588
  }
@@ -6849,7 +6611,7 @@ async function injectRscManifestIntoHtml(staticDir, manifest) {
6849
6611
  * @internal Exported for testing.
6850
6612
  */
6851
6613
  async function readUserRedirects(root) {
6852
- const candidates = [join(root, "public", "_redirects")];
6614
+ const candidates = [join(root, "public", REDIRECTS_FILE)];
6853
6615
  for (const path of candidates) try {
6854
6616
  return await readFile(path, "utf-8");
6855
6617
  } catch {}
@@ -6897,6 +6659,7 @@ function timberStaticBuild(ctx) {
6897
6659
  async handler() {
6898
6660
  if (ctx.dev) return;
6899
6661
  if (!(ctx.config.output === "static")) return;
6662
+ warnUnusableBase(ctx);
6900
6663
  await runStaticGeneration(ctx);
6901
6664
  }
6902
6665
  }
@@ -7050,12 +6813,10 @@ function walkModuleImports(moduleId, clientRefIndex, visited, result, getModuleI
7050
6813
  * Hooks: configResolved, resolveId, load, generateBundle (client and RSC envs)
7051
6814
  */
7052
6815
  function timberBuildManifest(ctx) {
7053
- let resolvedBase = "/";
7054
6816
  let isDev = false;
7055
6817
  return {
7056
6818
  name: "timber-build-manifest",
7057
6819
  configResolved(config) {
7058
- resolvedBase = config.base;
7059
6820
  isDev = config.command === "serve";
7060
6821
  if (!isDev && !ctx.deploymentId) ctx.deploymentId = randomUUID().replace(/-/g, "").slice(0, 16);
7061
6822
  },
@@ -7067,19 +6828,22 @@ function timberBuildManifest(ctx) {
7067
6828
  },
7068
6829
  load(id) {
7069
6830
  if (id !== RESOLVED_VIRTUAL_ID) return null;
6831
+ const empty = JSON.stringify(EMPTY_BUILD_MANIFEST);
7070
6832
  if (isDev) return [
7071
6833
  "// Auto-generated build manifest — do not edit.",
7072
6834
  "// Dev mode: empty manifest (Vite HMR handles CSS/JS).",
7073
6835
  "",
7074
- "const manifest = { css: {}, js: {}, modulepreload: {}, fonts: {}, metadataRouteHashes: {} };",
6836
+ `const manifest = ${empty};`,
7075
6837
  "",
7076
6838
  "export default manifest;"
7077
6839
  ].join("\n");
7078
6840
  return [
7079
6841
  "// Auto-generated build manifest — do not edit.",
7080
6842
  "// Production: reads manifest from globalThis (set by _timber-manifest-init.js).",
6843
+ "// The object below is only the fallback for when that global is unset —",
6844
+ "// the real manifest, including the requestDep map, is in that init module.",
7081
6845
  "",
7082
- "const manifest = globalThis.__TIMBER_BUILD_MANIFEST__ ?? { css: {}, js: {}, modulepreload: {}, fonts: {}, metadataRouteHashes: {}, requestDep: {} };",
6846
+ `const manifest = globalThis.__TIMBER_BUILD_MANIFEST__ ?? ${empty};`,
7083
6847
  "",
7084
6848
  "export default manifest;"
7085
6849
  ].join("\n");
@@ -7102,10 +6866,10 @@ function timberBuildManifest(ctx) {
7102
6866
  }
7103
6867
  }
7104
6868
  if (envName === "client") {
7105
- ctx.buildManifest = buildManifestFromBundle(bundle, resolvedBase, ctx.root);
6869
+ ctx.buildManifest = buildManifestFromBundle(bundle, ctx.base, ctx.root);
7106
6870
  if (ctx.routeTree) ctx.buildManifest.metadataRouteHashes = computeMetadataRouteHashes(ctx.routeTree.root);
7107
- const requestDepMap = ctx.requestDepMap;
7108
- if (requestDepMap) ctx.buildManifest.requestDep = requestDepMap;
6871
+ if (ctx.requestDepMap === void 0) throw new Error("[timber] Build manifest: the request-dependency map was never computed. The RSC environment's buildEnd hook did not run before the client build, so no segment or slot could be skipped on client navigation. This is a plugin pipeline bug — timber-request-dep must run in the rsc environment.");
6872
+ ctx.buildManifest.requestDep = ctx.requestDepMap;
7109
6873
  if (ctx.clientJavascript.disabled) {
7110
6874
  ctx.buildManifest.js = {};
7111
6875
  ctx.buildManifest.modulepreload = {};
@@ -8200,7 +7964,319 @@ function timberCacheTransform(ctx) {
8200
7964
  };
8201
7965
  }
8202
7966
  //#endregion
8203
- //#region src/plugins/request-dep.ts
7967
+ //#region src/plugins/request-dep/analysis.ts
7968
+ /** `export default` has no local name of its own; this stands in for one. */
7969
+ var DEFAULT_EXPORT = "default";
7970
+ function createModuleFacts() {
7971
+ return {
7972
+ importedBindings: /* @__PURE__ */ new Map(),
7973
+ reExports: /* @__PURE__ */ new Map(),
7974
+ starReExports: [],
7975
+ localExports: /* @__PURE__ */ new Map(),
7976
+ factoryCalls: /* @__PURE__ */ new Map(),
7977
+ calledLocals: /* @__PURE__ */ new Set(),
7978
+ calledMembers: /* @__PURE__ */ new Map(),
7979
+ getterLocals: /* @__PURE__ */ new Set(),
7980
+ getterMembers: /* @__PURE__ */ new Map(),
7981
+ referencedLocals: /* @__PURE__ */ new Set(),
7982
+ resolvedSources: /* @__PURE__ */ new Map(),
7983
+ unresolvedSources: /* @__PURE__ */ new Set()
7984
+ };
7985
+ }
7986
+ /**
7987
+ * Specifiers the origin walk can actually reach, so the plugin resolves those
7988
+ * and not every import in the file.
7989
+ *
7990
+ * A re-export edge is always reachable — a downstream module can ask for any
7991
+ * name it forwards. An `import` edge is reachable through any local name the
7992
+ * module body mentions, or that it re-exports.
7993
+ *
7994
+ * The name set must be **the same one the taint rules consult**. Deriving it
7995
+ * from the call shapes instead (called, `.get()`d, member-called, minted from)
7996
+ * left `read(filters)` — a definition passed as an argument — with its
7997
+ * specifier unresolved, so the origin walk reported it inert and the page read
7998
+ * as request-independent. Two hand-maintained lists of "names that matter" is
7999
+ * one list too many; this is the one, and `referencedLocals` is a superset of
8000
+ * every call shape by construction, since each of them is an identifier the
8001
+ * walk visits. (Codex review round 1 on PR #991.)
8002
+ */
8003
+ function usedSources(facts) {
8004
+ const sources = new Set(facts.starReExports);
8005
+ for (const edge of facts.reExports.values()) sources.add(edge.source);
8006
+ for (const local of facts.referencedLocals) {
8007
+ const edge = facts.importedBindings.get(local);
8008
+ if (edge) sources.add(edge.source);
8009
+ }
8010
+ for (const local of facts.localExports.values()) {
8011
+ const edge = facts.importedBindings.get(local);
8012
+ if (edge) sources.add(edge.source);
8013
+ }
8014
+ return sources;
8015
+ }
8016
+ /**
8017
+ * True when the module cannot affect any answer, so its facts need not be kept.
8018
+ *
8019
+ * Every judgement bottoms out at a module edge: a call site resolves through
8020
+ * `importedBindings`, a chain link through `reExports` / `starReExports`. A
8021
+ * module with none of those can only ever resolve to `none` — its exports lead
8022
+ * to locals that lead nowhere — which is exactly what a *missing* entry
8023
+ * produces. Dropping it is behaviour-preserving and keeps the dev-time map from
8024
+ * holding an entry for every leaf module in the graph.
8025
+ */
8026
+ function contributesNothing(facts) {
8027
+ return facts.importedBindings.size === 0 && facts.reExports.size === 0 && facts.starReExports.length === 0;
8028
+ }
8029
+ function addToMapSet(map, key, value) {
8030
+ const existing = map.get(key);
8031
+ if (existing) existing.add(value);
8032
+ else map.set(key, /* @__PURE__ */ new Set([value]));
8033
+ }
8034
+ function nameOf(node) {
8035
+ if (!node) return null;
8036
+ if (node.type === "Identifier") return node.name;
8037
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
8038
+ return null;
8039
+ }
8040
+ /**
8041
+ * `import type { … }` needs no handling here. Vite's TypeScript transform is a
8042
+ * core plugin and runs before this one, so types are already erased by the time
8043
+ * the analysis parses — and the parser it runs on is JavaScript-only, so
8044
+ * unerased TypeScript would throw rather than be silently misread.
8045
+ * `tests/request-dep-plugin.test.ts` pins the resulting property through the
8046
+ * real pipeline: a type-only accessor import is not request-dependent.
8047
+ */
8048
+ function collectImportDeclaration(stmt, facts) {
8049
+ const source = stmt.source?.value;
8050
+ if (typeof source !== "string") return;
8051
+ for (const spec of stmt.specifiers ?? []) {
8052
+ const local = nameOf(spec.local);
8053
+ if (!local) continue;
8054
+ if (spec.type === "ImportNamespaceSpecifier") facts.importedBindings.set(local, {
8055
+ source,
8056
+ imported: "*"
8057
+ });
8058
+ else if (spec.type === "ImportDefaultSpecifier") facts.importedBindings.set(local, {
8059
+ source,
8060
+ imported: DEFAULT_EXPORT
8061
+ });
8062
+ else if (spec.type === "ImportSpecifier") facts.importedBindings.set(local, {
8063
+ source,
8064
+ imported: nameOf(spec.imported) ?? local
8065
+ });
8066
+ }
8067
+ }
8068
+ function collectDeclaredNames(decl, out) {
8069
+ if (decl.type === "VariableDeclaration") for (const d of decl.declarations ?? []) {
8070
+ const id = d.id;
8071
+ if (id?.type === "Identifier") out.add(id.name);
8072
+ }
8073
+ else if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
8074
+ const id = decl.id;
8075
+ if (id?.type === "Identifier") out.add(id.name);
8076
+ }
8077
+ }
8078
+ function collectExportNamed(stmt, facts) {
8079
+ const source = stmt.source?.value;
8080
+ const specifiers = stmt.specifiers ?? [];
8081
+ if (typeof source === "string") {
8082
+ for (const spec of specifiers) {
8083
+ const imported = nameOf(spec.imported) ?? nameOf(spec.local);
8084
+ const exported = nameOf(spec.exported);
8085
+ if (imported && exported) facts.reExports.set(exported, {
8086
+ source,
8087
+ imported
8088
+ });
8089
+ }
8090
+ return;
8091
+ }
8092
+ for (const spec of specifiers) {
8093
+ const local = nameOf(spec.local);
8094
+ const exported = nameOf(spec.exported);
8095
+ if (local && exported) facts.localExports.set(exported, local);
8096
+ }
8097
+ const declaration = stmt.declaration;
8098
+ if (declaration) {
8099
+ const names = /* @__PURE__ */ new Set();
8100
+ collectDeclaredNames(declaration, names);
8101
+ for (const name of names) facts.localExports.set(name, name);
8102
+ }
8103
+ }
8104
+ function collectExportAll(stmt, facts) {
8105
+ const source = stmt.source?.value;
8106
+ if (typeof source !== "string") return;
8107
+ const exported = nameOf(stmt.exported);
8108
+ if (exported) facts.reExports.set(exported, {
8109
+ source,
8110
+ imported: "*"
8111
+ });
8112
+ else facts.starReExports.push(source);
8113
+ }
8114
+ function collectExportDefault(stmt, facts) {
8115
+ const declaration = stmt.declaration;
8116
+ if (!declaration) return;
8117
+ facts.localExports.set(DEFAULT_EXPORT, DEFAULT_EXPORT);
8118
+ if (declaration.type === "Identifier") facts.localExports.set(DEFAULT_EXPORT, declaration.name);
8119
+ else if (declaration.type === "CallExpression") {
8120
+ const ref = calleeRef(declaration);
8121
+ if (ref) facts.factoryCalls.set(DEFAULT_EXPORT, ref);
8122
+ }
8123
+ }
8124
+ function calleeRef(call) {
8125
+ const callee = call.callee;
8126
+ if (callee.type === "Identifier") return {
8127
+ kind: "local",
8128
+ name: callee.name
8129
+ };
8130
+ if (callee.type === "MemberExpression") {
8131
+ const member = callee;
8132
+ if (!member.computed && member.object.type === "Identifier" && member.property.type === "Identifier") return {
8133
+ kind: "member",
8134
+ object: member.object.name,
8135
+ property: member.property.name
8136
+ };
8137
+ }
8138
+ return null;
8139
+ }
8140
+ /**
8141
+ * Walk down a `.get()` receiver to the identifier it is ultimately rooted in,
8142
+ * along with the first property accessed on that identifier.
8143
+ *
8144
+ * `x.get()` → `x`, no property. `x(a).get()` → `x`. `ns.defs.get()` → `ns`
8145
+ * plus `defs`, which is both the namespace-member shape and the
8146
+ * `cookies.theme.get()` shape — the resolver tries the base binding and the
8147
+ * member, so one walk covers both.
8148
+ */
8149
+ function chainRoot(node) {
8150
+ let current = node;
8151
+ let property = null;
8152
+ for (;;) {
8153
+ if (current.type === "Identifier") return {
8154
+ base: current.name,
8155
+ property
8156
+ };
8157
+ if (current.type === "MemberExpression") {
8158
+ const member = current;
8159
+ if (!member.computed && member.property.type === "Identifier") property = member.property.name;
8160
+ else property = null;
8161
+ current = member.object;
8162
+ continue;
8163
+ }
8164
+ if (current.type === "CallExpression") {
8165
+ property = null;
8166
+ current = current.callee;
8167
+ continue;
8168
+ }
8169
+ return null;
8170
+ }
8171
+ }
8172
+ function collectExpression(node, facts) {
8173
+ if (node.type === "VariableDeclarator") {
8174
+ const id = node.id;
8175
+ const init = node.init;
8176
+ if (id?.type === "Identifier" && init?.type === "CallExpression") {
8177
+ const ref = calleeRef(init);
8178
+ if (ref) facts.factoryCalls.set(id.name, ref);
8179
+ }
8180
+ return;
8181
+ }
8182
+ if (node.type !== "CallExpression") return;
8183
+ const callee = node.callee;
8184
+ if (callee.type === "Identifier") {
8185
+ facts.calledLocals.add(callee.name);
8186
+ return;
8187
+ }
8188
+ if (callee.type !== "MemberExpression") return;
8189
+ const member = callee;
8190
+ if (member.computed || member.property.type !== "Identifier") return;
8191
+ const propertyName = member.property.name;
8192
+ if (propertyName === "get") {
8193
+ const root = chainRoot(member.object);
8194
+ if (root) {
8195
+ facts.getterLocals.add(root.base);
8196
+ if (root.property) addToMapSet(facts.getterMembers, root.base, root.property);
8197
+ }
8198
+ return;
8199
+ }
8200
+ if (member.object.type === "Identifier") addToMapSet(facts.calledMembers, member.object.name, propertyName);
8201
+ }
8202
+ /**
8203
+ * Statements whose whole subtree is an import/export *clause* rather than code.
8204
+ *
8205
+ * The walk skips them so `referencedLocals` means "the module body mentions
8206
+ * this name". Without the skip every imported name would count as referenced,
8207
+ * and a pure barrel — which mentions its names only in the clauses — would be
8208
+ * indistinguishable from a module that uses them.
8209
+ */
8210
+ function isClauseOnly(node) {
8211
+ if (node.type === "ImportDeclaration" || node.type === "ExportAllDeclaration") return true;
8212
+ if (node.type === "ExportNamedDeclaration") return !node.declaration;
8213
+ if (node.type === "ExportDefaultDeclaration") return node.declaration?.type === "Identifier";
8214
+ return false;
8215
+ }
8216
+ /**
8217
+ * Keys holding *binding* positions rather than references: the name being
8218
+ * declared, not a mention of one.
8219
+ *
8220
+ * `referencedLocals` must exclude these or `export const filters =
8221
+ * defineSearchParams({})` would count as referencing `filters`, and possession
8222
+ * would then taint every definition module — and through it every page that
8223
+ * imports one, whether or not it reads the request. Skipping only these keys
8224
+ * keeps a shadowing parameter's *body* uses as references, which over-reports
8225
+ * in the safe direction.
8226
+ */
8227
+ var BINDING_KEYS = {
8228
+ VariableDeclarator: ["id"],
8229
+ FunctionDeclaration: ["id", "params"],
8230
+ FunctionExpression: ["id", "params"],
8231
+ ArrowFunctionExpression: ["params"],
8232
+ ClassDeclaration: ["id"],
8233
+ ClassExpression: ["id"],
8234
+ CatchClause: ["param"]
8235
+ };
8236
+ function walk(node, facts) {
8237
+ if (isClauseOnly(node)) return;
8238
+ if (node.type === "Identifier") facts.referencedLocals.add(node.name);
8239
+ collectExpression(node, facts);
8240
+ const bindingKeys = BINDING_KEYS[node.type];
8241
+ for (const key of Object.keys(node)) {
8242
+ if (key === "type" || key === "start" || key === "end") continue;
8243
+ if (bindingKeys?.includes(key)) continue;
8244
+ const value = node[key];
8245
+ if (!value || typeof value !== "object") continue;
8246
+ if (Array.isArray(value)) {
8247
+ for (const item of value) if (item && typeof item === "object" && typeof item.type === "string") walk(item, facts);
8248
+ } else if (typeof value.type === "string") walk(value, facts);
8249
+ }
8250
+ }
8251
+ /**
8252
+ * Record every binding, module edge, and call shape in one module.
8253
+ *
8254
+ * Names are recorded without scope analysis, so a parameter that shadows an
8255
+ * imported name is credited to the import. That direction is deliberate: a
8256
+ * needlessly request-dependent segment costs a re-render, a wrongly static one
8257
+ * serves stale content.
8258
+ */
8259
+ function analyzeModule(program) {
8260
+ const facts = createModuleFacts();
8261
+ for (const stmt of program.body) switch (stmt.type) {
8262
+ case "ImportDeclaration":
8263
+ collectImportDeclaration(stmt, facts);
8264
+ break;
8265
+ case "ExportNamedDeclaration":
8266
+ collectExportNamed(stmt, facts);
8267
+ break;
8268
+ case "ExportAllDeclaration":
8269
+ collectExportAll(stmt, facts);
8270
+ break;
8271
+ case "ExportDefaultDeclaration":
8272
+ collectExportDefault(stmt, facts);
8273
+ break;
8274
+ }
8275
+ walk(program, facts);
8276
+ return facts;
8277
+ }
8278
+ //#endregion
8279
+ //#region src/plugins/request-dep/origin.ts
8204
8280
  var ACCESSOR_SOURCES = /* @__PURE__ */ new Set([
8205
8281
  "@timber-js/app/server",
8206
8282
  "next/headers",
@@ -8217,165 +8293,326 @@ var ACCESSOR_NAMES = /* @__PURE__ */ new Set([
8217
8293
  ]);
8218
8294
  var DEFINITION_FACTORY_SOURCES = /* @__PURE__ */ new Set(["@timber-js/app/search-params", "@timber-js/app/cookies"]);
8219
8295
  var DEFINITION_FACTORY_NAMES = /* @__PURE__ */ new Set(["defineSearchParams", "defineCookie"]);
8220
- function isFrameworkModule(id) {
8221
- return /[\\/]node_modules[\\/]@timber-js[\\/]app[\\/]/.test(id) || /[\\/]packages[\\/]timber-app[\\/]src[\\/]/.test(id);
8296
+ /** Every specifier the classifier recognises without resolving it. */
8297
+ var FRAMEWORK_SOURCES = /* @__PURE__ */ new Set([...ACCESSOR_SOURCES, ...DEFINITION_FACTORY_SOURCES]);
8298
+ var NONE = { kind: "none" };
8299
+ var UNKNOWN = { kind: "unknown" };
8300
+ /**
8301
+ * Classify an edge that points at a framework module, without resolving it.
8302
+ *
8303
+ * Returns `null` when the specifier is not a framework source, which is the
8304
+ * signal to keep walking. Both name sets are consulted for either source so a
8305
+ * specifier that grows a second role does not silently stop matching.
8306
+ */
8307
+ function classifySpecifier(source, imported) {
8308
+ const isAccessorSource = ACCESSOR_SOURCES.has(source);
8309
+ const isFactorySource = DEFINITION_FACTORY_SOURCES.has(source);
8310
+ if (!isAccessorSource && !isFactorySource) return null;
8311
+ if (imported === "*") return {
8312
+ kind: "framework-namespace",
8313
+ source
8314
+ };
8315
+ if (isAccessorSource && ACCESSOR_NAMES.has(imported)) return { kind: "accessor" };
8316
+ if (isFactorySource && DEFINITION_FACTORY_NAMES.has(imported)) return { kind: "factory" };
8317
+ return NONE;
8222
8318
  }
8223
- var ACCESSOR_TOKENS = [
8224
- "getHeaders",
8225
- "getHeader",
8226
- "getSearchParams",
8227
- "getCookieJar",
8228
- "getCookie",
8229
- "headers",
8230
- "cookies",
8231
- "defineSearchParams",
8232
- "defineCookie",
8233
- ".get("
8234
- ];
8235
- function hasRelevantToken(code) {
8236
- for (const token of ACCESSOR_TOKENS) if (code.includes(token)) return true;
8237
- return false;
8319
+ /** An origin whose *call* reads the request. */
8320
+ function callReadsRequest(origin) {
8321
+ return origin.kind === "accessor" || origin.kind === "unknown";
8238
8322
  }
8239
- function analyzeModule(code, program) {
8240
- const facts = {
8241
- callsAccessor: false,
8242
- importsDefinitionFactory: false,
8243
- getterCallSources: []
8244
- };
8245
- for (const name of ACCESSOR_NAMES) {
8246
- const bindings = findImportBindings(program, ACCESSOR_SOURCES, name);
8247
- if (bindings.named.size > 0 || bindings.namespaces.size > 0) {
8248
- if (hasCallSite(program, bindings.named, bindings.namespaces, name)) facts.callsAccessor = true;
8323
+ /** An origin whose `.get()` reads the request. */
8324
+ function getterReadsRequest(origin) {
8325
+ return origin.kind === "definition" || origin.kind === "unknown";
8326
+ }
8327
+ function createOriginResolver(factsMap) {
8328
+ /**
8329
+ * Cycle guard. A `(module, export)` pair already being resolved contributes
8330
+ * `none`: anything it could reach is reachable on the path still unwinding.
8331
+ */
8332
+ const resolving = /* @__PURE__ */ new Map();
8333
+ function enter(moduleId, name) {
8334
+ const names = resolving.get(moduleId);
8335
+ if (!names) {
8336
+ resolving.set(moduleId, /* @__PURE__ */ new Set([name]));
8337
+ return true;
8249
8338
  }
8339
+ if (names.has(name)) return false;
8340
+ names.add(name);
8341
+ return true;
8250
8342
  }
8251
- for (const name of DEFINITION_FACTORY_NAMES) {
8252
- const bindings = findImportBindings(program, DEFINITION_FACTORY_SOURCES, name);
8253
- if (bindings.named.size > 0 || bindings.namespaces.size > 0) facts.importsDefinitionFactory = true;
8343
+ function leave(moduleId, name) {
8344
+ resolving.get(moduleId)?.delete(name);
8345
+ }
8346
+ /** Follow one `import`/`export … from` edge out of `moduleId`. */
8347
+ function resolveEdge(moduleId, edge) {
8348
+ const classified = classifySpecifier(edge.source, edge.imported);
8349
+ if (classified) return classified;
8350
+ const facts = factsMap.get(moduleId);
8351
+ const targetId = facts?.resolvedSources.get(edge.source);
8352
+ if (!targetId) return facts?.unresolvedSources.has(edge.source) ? UNKNOWN : NONE;
8353
+ if (edge.imported === "*") return {
8354
+ kind: "namespace",
8355
+ moduleId: targetId
8356
+ };
8357
+ return resolveExport(targetId, edge.imported);
8254
8358
  }
8255
- for (const stmt of program.body) {
8256
- if (stmt.type !== "ImportDeclaration") continue;
8257
- const decl = stmt;
8258
- for (const spec of decl.specifiers) if (spec.type === "ImportDefaultSpecifier" || spec.type === "ImportSpecifier") {
8259
- const localName = spec.local.name;
8260
- if (hasGetterCall(program, localName)) facts.getterCallSources.push(decl.source.value);
8261
- }
8262
- }
8263
- if (facts.importsDefinitionFactory) for (const stmt of program.body) {
8264
- if (stmt.type !== "VariableDeclaration") continue;
8265
- for (const decl of stmt.declarations ?? []) {
8266
- if (!decl.init || !decl.id) continue;
8267
- const init = decl.init;
8268
- if (init.type === "CallExpression") {
8269
- const callee = init.callee;
8270
- if (callee.type === "Identifier") {
8271
- const calleeName = callee.name;
8272
- for (const factoryName of DEFINITION_FACTORY_NAMES) if (findImportBindings(program, DEFINITION_FACTORY_SOURCES, factoryName).named.has(calleeName)) {
8273
- const localName = decl.id.type === "Identifier" ? decl.id.name : null;
8274
- if (localName && hasGetterCall(program, localName)) facts.callsAccessor = true;
8275
- }
8276
- }
8359
+ /** What does `moduleId` export under `name`? */
8360
+ function resolveExport(moduleId, name) {
8361
+ const facts = factsMap.get(moduleId);
8362
+ if (!facts) return NONE;
8363
+ if (!enter(moduleId, `export:${name}`)) return NONE;
8364
+ try {
8365
+ const reExport = facts.reExports.get(name);
8366
+ if (reExport) return resolveEdge(moduleId, reExport);
8367
+ const local = facts.localExports.get(name);
8368
+ if (local !== void 0) return resolveLocal(moduleId, local);
8369
+ for (const source of facts.starReExports) {
8370
+ const viaStar = resolveEdge(moduleId, {
8371
+ source,
8372
+ imported: name
8373
+ });
8374
+ if (viaStar.kind !== "none") return viaStar;
8277
8375
  }
8376
+ return NONE;
8377
+ } finally {
8378
+ leave(moduleId, `export:${name}`);
8278
8379
  }
8279
8380
  }
8280
- return facts;
8281
- }
8282
- /** Check if any named/namespace binding is called as a function. */
8283
- function hasCallSite(program, named, namespaces, importedName) {
8284
- return walkForCall(program, named, namespaces, importedName);
8285
- }
8286
- function walkForCall(node, named, namespaces, importedName) {
8287
- if (node.type === "CallExpression") {
8288
- const callee = node.callee;
8289
- if (callee.type === "Identifier" && named.has(callee.name)) return true;
8290
- if (callee.type === "MemberExpression") {
8291
- const member = callee;
8292
- if (!member.computed && member.object.type === "Identifier" && namespaces.has(member.object.name) && member.property.type === "Identifier" && member.property.name === importedName) return true;
8293
- }
8294
- }
8295
- for (const key of Object.keys(node)) {
8296
- if (key === "type" || key === "start" || key === "end") continue;
8297
- const val = node[key];
8298
- if (val && typeof val === "object") {
8299
- if (Array.isArray(val)) {
8300
- for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") {
8301
- if (walkForCall(item, named, namespaces, importedName)) return true;
8302
- }
8303
- } else if (typeof val.type === "string") {
8304
- if (walkForCall(val, named, namespaces, importedName)) return true;
8381
+ /** What is the local binding `name` in `moduleId`? */
8382
+ function resolveLocal(moduleId, name) {
8383
+ const facts = factsMap.get(moduleId);
8384
+ if (!facts) return NONE;
8385
+ if (!enter(moduleId, `local:${name}`)) return NONE;
8386
+ try {
8387
+ const imported = facts.importedBindings.get(name);
8388
+ if (imported) return resolveEdge(moduleId, imported);
8389
+ const factoryCall = facts.factoryCalls.get(name);
8390
+ if (factoryCall) {
8391
+ const callee = resolveCallee(moduleId, factoryCall);
8392
+ if (callee.kind === "factory") return { kind: "definition" };
8393
+ if (callee.kind === "unknown") return UNKNOWN;
8305
8394
  }
8395
+ return NONE;
8396
+ } finally {
8397
+ leave(moduleId, `local:${name}`);
8306
8398
  }
8307
8399
  }
8308
- return false;
8309
- }
8310
- /**
8311
- * Check if the object chain of a `.get()` call contains `localName`.
8312
- * Walks through MemberExpression objects and CallExpression callees
8313
- * to handle chained patterns like `getFilterCookie(key).get()` or
8314
- * `cookies.theme.get()`.
8315
- */
8316
- function objectChainContains(node, name) {
8317
- if (node.type === "Identifier" && node.name === name) return true;
8318
- if (node.type === "MemberExpression") return objectChainContains(node.object, name);
8319
- if (node.type === "CallExpression") return objectChainContains(node.callee, name);
8320
- return false;
8321
- }
8322
- /**
8323
- * Check if `.get(...)` is called on `localName` anywhere in the module.
8324
- * Handles direct (`localName.get()`), chained call (`localName(args).get()`),
8325
- * and nested member (`localName.prop.get()`) patterns.
8326
- */
8327
- function hasGetterCall(node, localName) {
8328
- if (node.type === "CallExpression") {
8329
- const call = node;
8330
- if (call.callee.type === "MemberExpression") {
8331
- const member = call.callee;
8332
- if (!member.computed && member.property.type === "Identifier" && member.property.name === "get" && objectChainContains(member.object, localName)) return true;
8400
+ function resolveCallee(moduleId, ref) {
8401
+ if (ref.kind === "local") return resolveLocal(moduleId, ref.name);
8402
+ return resolveMember(moduleId, ref.object, ref.property);
8403
+ }
8404
+ /** `ns.name` where `ns` is a local binding in `moduleId`. */
8405
+ function resolveMember(moduleId, object, name) {
8406
+ const base = resolveLocal(moduleId, object);
8407
+ if (base.kind === "framework-namespace") return classifySpecifier(base.source, name) ?? NONE;
8408
+ if (base.kind === "namespace") return resolveExport(base.moduleId, name);
8409
+ if (base.kind === "unknown") return UNKNOWN;
8410
+ return NONE;
8411
+ }
8412
+ const taintCache = /* @__PURE__ */ new Map();
8413
+ function computeTainted(moduleId, facts) {
8414
+ for (const local of facts.calledLocals) if (callReadsRequest(resolveLocal(moduleId, local))) return true;
8415
+ for (const [object, members] of facts.calledMembers) for (const member of members) if (callReadsRequest(resolveMember(moduleId, object, member))) return true;
8416
+ for (const local of facts.getterLocals) if (getterReadsRequest(resolveLocal(moduleId, local))) return true;
8417
+ for (const [object, members] of facts.getterMembers) for (const member of members) if (getterReadsRequest(resolveMember(moduleId, object, member))) return true;
8418
+ return holdsRequestBinding(moduleId, facts);
8419
+ }
8420
+ /**
8421
+ * Possession, not use: a module that binds a request accessor or a definition
8422
+ * value under a local name is request-dependent whether or not the call is
8423
+ * one this analysis can see.
8424
+ *
8425
+ * The call-shape rules above only find a read that happens through the
8426
+ * binding itself. A value can leave the binding first — `const read = flag ?
8427
+ * getSearchParams : noop`, `const { get } = filters`, a prop handed to a
8428
+ * helper — and every one of those is a fresh way to be missed, which is the
8429
+ * shape of bug this ticket exists to stop enumerating. Nothing distinguishes
8430
+ * those from an honest unused import except intent, so importing the accessor
8431
+ * at all is treated as reading it.
8432
+ *
8433
+ * The rule is scoped by *reference*, not by export shape, and that scoping is
8434
+ * what keeps it from being useless: `import { getSearchParams } from
8435
+ * '@timber-js/app/server'; export { getSearchParams }` is a barrel whose body
8436
+ * never mentions the name, and tainting it would taint every page that
8437
+ * imports anything from that barrel. The forwarded name stays resolvable
8438
+ * through `resolveExport`, so the page that actually calls it is still caught
8439
+ * one hop later, in the module that uses it.
8440
+ *
8441
+ * Scoping on "the module only forwards it" would have been the natural
8442
+ * phrasing and is wrong: a barrel can forward a name *and* use it
8443
+ * (`export { getSearchParams }` beside `const read = flag ? getSearchParams
8444
+ * : noop`), and that module is exactly the one this rule exists to catch.
8445
+ * `referencedLocals` excludes import/export clauses and nothing else, so
8446
+ * forwarding is exempt only when it is all the module does with the name.
8447
+ *
8448
+ * `unknown` is deliberately not included: an unresolvable specifier taints
8449
+ * only when something is actually called or read on it, or every module with
8450
+ * one broken import would go request-dependent.
8451
+ */
8452
+ function holdsRequestBinding(moduleId, facts) {
8453
+ for (const local of facts.referencedLocals) {
8454
+ const origin = resolveLocal(moduleId, local);
8455
+ if (origin.kind === "accessor" || origin.kind === "definition") return true;
8333
8456
  }
8457
+ return false;
8334
8458
  }
8335
- for (const key of Object.keys(node)) {
8336
- if (key === "type" || key === "start" || key === "end") continue;
8337
- const val = node[key];
8338
- if (val && typeof val === "object") {
8339
- if (Array.isArray(val)) {
8340
- for (const item of val) if (item && typeof item === "object" && typeof item.type === "string") {
8341
- if (hasGetterCall(item, localName)) return true;
8342
- }
8343
- } else if (typeof val.type === "string") {
8344
- if (hasGetterCall(val, localName)) return true;
8459
+ return {
8460
+ isTainted(moduleId) {
8461
+ const cached = taintCache.get(moduleId);
8462
+ if (cached !== void 0) return cached;
8463
+ const facts = factsMap.get(moduleId);
8464
+ const result = facts ? computeTainted(moduleId, facts) : false;
8465
+ taintCache.set(moduleId, result);
8466
+ return result;
8467
+ },
8468
+ isDefinitionModule(moduleId) {
8469
+ const facts = factsMap.get(moduleId);
8470
+ if (!facts) return false;
8471
+ for (const edge of facts.importedBindings.values()) {
8472
+ const origin = resolveEdge(moduleId, edge);
8473
+ if (origin.kind === "factory") return true;
8474
+ if (origin.kind === "framework-namespace" && DEFINITION_FACTORY_SOURCES.has(origin.source)) return true;
8345
8475
  }
8476
+ return false;
8477
+ },
8478
+ callsGetterOnImport(moduleId) {
8479
+ const facts = factsMap.get(moduleId);
8480
+ if (!facts) return false;
8481
+ for (const local of facts.getterLocals) if (facts.importedBindings.has(local)) return true;
8482
+ return false;
8346
8483
  }
8347
- }
8348
- return false;
8484
+ };
8349
8485
  }
8350
- /** Walk the module graph from an entry, returning true if any taint source is reachable. */
8351
- function isReachableTaintSource(entryId, factsMap, definitionModules, getImports) {
8486
+ //#endregion
8487
+ //#region src/plugins/request-dep/index.ts
8488
+ function isFrameworkModule(id) {
8489
+ return /[\\/]node_modules[\\/]@timber-js[\\/]app[\\/]/.test(id) || /[\\/]packages[\\/]timber-app[\\/]src[\\/]/.test(id);
8490
+ }
8491
+ /**
8492
+ * Cheap gate before parsing.
8493
+ *
8494
+ * This used to be a substring match on the accessor and factory names, which
8495
+ * was sound only for the analysis it served: when a call site had to name the
8496
+ * framework binding in its own import declaration, a module with no such name
8497
+ * could not be a call site. Resolving bindings across the graph removes that
8498
+ * guarantee — with
8499
+ *
8500
+ * // _lib/index.ts
8501
+ * export { getSearchParams as query } from '@timber-js/app/server';
8502
+ * // page.tsx
8503
+ * import { query } from './_lib';
8504
+ * export default function Page() { return query(); }
8505
+ *
8506
+ * the page contains no accessor name, no `.get(`, and no re-export syntax, and
8507
+ * skipping it would have put the rename right back where the barrel was. So the
8508
+ * gate is now structural: a module matters if it has any module-level binding
8509
+ * to follow. What it skips is source with no `import` and no `export` at all —
8510
+ * data blobs, generated constants, side-effect scripts — none of which can name
8511
+ * a framework binding or forward one.
8512
+ */
8513
+ function hasModuleBindings(code) {
8514
+ return code.includes("import") || code.includes("export");
8515
+ }
8516
+ /**
8517
+ * Walk the module graph from an entry, returning true if any module reachable
8518
+ * from it reads the request.
8519
+ *
8520
+ * Two passes, and the second is a deliberately blunt backstop:
8521
+ *
8522
+ * 1. **Binding origins.** For each reachable module, resolve every call site's
8523
+ * binding to its origin. This is precise and indifferent to indirection.
8524
+ * 2. **Definition reachability.** A module that calls `.get()` on *something*
8525
+ * it imported, with a module holding a definition factory reachable behind
8526
+ * that import, counts too. The origin walk cannot see through a value that
8527
+ * is computed rather than bound — `getFilterCookie(key).get()`, where the
8528
+ * helper returns a definition — and this pass can.
8529
+ *
8530
+ * The second pass over-reports by construction, which is the safe direction: a
8531
+ * needlessly request-dependent segment costs a re-render, a wrongly static one
8532
+ * serves stale content.
8533
+ *
8534
+ * An entry the graph does not contain is an edge we failed to walk, not the
8535
+ * absence of one — the same rule an unresolvable specifier gets. Without the
8536
+ * check it reads as an entry with no imports and no facts, and both passes fall
8537
+ * through to `false`: confidently request-INdependent, for a module nothing
8538
+ * ever looked at. In the build that answer is worse than no answer, because it
8539
+ * is written into `requestDep` as a definite `false` and the runtime's own
8540
+ * "absent key → conservative" fallback never fires.
8541
+ */
8542
+ function isReachableTaintSource(entryId, input) {
8543
+ const { factsMap, getImports, isKnownModule } = input;
8544
+ if (!isKnownModule(entryId)) return true;
8545
+ const resolver = createOriginResolver(factsMap);
8352
8546
  const visited = /* @__PURE__ */ new Set();
8353
8547
  const stack = [entryId];
8354
8548
  while (stack.length > 0) {
8355
8549
  const id = stack.pop();
8356
8550
  if (visited.has(id)) continue;
8357
8551
  visited.add(id);
8358
- const facts = factsMap.get(id);
8359
- if (facts) {
8360
- if (facts.callsAccessor) return true;
8361
- for (const source of facts.getterCallSources);
8362
- }
8363
- const imports = getImports(id);
8364
- for (const imp of imports) if (!imp.startsWith("\0") && !visited.has(imp)) stack.push(imp);
8552
+ if (resolver.isTainted(id)) return true;
8553
+ for (const imp of getImports(id)) if (!imp.startsWith("\0") && !visited.has(imp)) stack.push(imp);
8365
8554
  }
8555
+ const reachesDefinition = makeDefinitionReachability(resolver.isDefinitionModule, getImports);
8366
8556
  for (const id of visited) {
8367
- const facts = factsMap.get(id);
8368
- if (!facts) continue;
8369
- if (facts.getterCallSources.length === 0) continue;
8370
- const imports = getImports(id);
8371
- for (const imp of imports) if (definitionModules.has(imp)) return true;
8557
+ if (!resolver.callsGetterOnImport(id)) continue;
8558
+ if (reachesDefinition(id)) return true;
8372
8559
  }
8373
8560
  return false;
8374
8561
  }
8562
+ /**
8563
+ * "Can a definition module be reached from this module's imports?", memoized
8564
+ * across the queries one entry makes.
8565
+ *
8566
+ * Walks the graph rather than testing direct imports, because `export … from`
8567
+ * is an ordinary module edge and a barrel is the common way an app shares one
8568
+ * `defineSearchParams` across routes.
8569
+ *
8570
+ * Cycles terminate on the memo seeded with `false`; a module already being
8571
+ * resolved contributes nothing, which is correct — anything it could reach is
8572
+ * reachable on the path that is still unwinding.
8573
+ */
8574
+ function makeDefinitionReachability(isDefinitionModule, getImports) {
8575
+ const cache = /* @__PURE__ */ new Map();
8576
+ return function reaches(id) {
8577
+ const cached = cache.get(id);
8578
+ if (cached !== void 0) return cached;
8579
+ cache.set(id, false);
8580
+ let result = false;
8581
+ for (const imp of getImports(id)) {
8582
+ if (imp.startsWith("\0")) continue;
8583
+ if (isDefinitionModule(imp) || reaches(imp)) {
8584
+ result = true;
8585
+ break;
8586
+ }
8587
+ }
8588
+ cache.set(id, result);
8589
+ return result;
8590
+ };
8591
+ }
8375
8592
  var BRIDGE_KEY = Symbol.for("timber:request-dep");
8593
+ /**
8594
+ * Resolve every specifier the module has an edge to, so the origin walk can
8595
+ * follow them later without a module graph.
8596
+ *
8597
+ * Framework specifiers are classified by name and never resolved — that is what
8598
+ * keeps the terminal check independent of how `@timber-js/app` happens to be
8599
+ * installed. A specifier that fails to resolve is recorded as unresolved rather
8600
+ * than dropped, so the walk can take the conservative branch on it.
8601
+ */
8602
+ async function resolveEdgeSources(pluginCtx, facts, importer) {
8603
+ for (const source of usedSources(facts)) {
8604
+ if (FRAMEWORK_SOURCES.has(source)) continue;
8605
+ try {
8606
+ const resolved = await pluginCtx.resolve(source, importer);
8607
+ if (resolved?.id) facts.resolvedSources.set(source, resolved.id);
8608
+ else facts.unresolvedSources.add(source);
8609
+ } catch {
8610
+ facts.unresolvedSources.add(source);
8611
+ }
8612
+ }
8613
+ }
8376
8614
  function timberRequestDep(ctx) {
8377
8615
  const factsMap = /* @__PURE__ */ new Map();
8378
- const definitionModules = /* @__PURE__ */ new Set();
8379
8616
  const cache = /* @__PURE__ */ new Map();
8380
8617
  return {
8381
8618
  name: "timber-request-dep",
@@ -8386,12 +8623,15 @@ function timberRequestDep(ctx) {
8386
8623
  if (!/\.[mc]?[jt]sx?$/.test(id) && !/\.mdx?$/.test(id)) return;
8387
8624
  if (id.startsWith("\0") || id.startsWith("virtual:")) return;
8388
8625
  if (isFrameworkModule(id)) return;
8389
- if (!hasRelevantToken(code)) return;
8390
- const facts = analyzeModule(code, this.parse(code));
8391
- if (facts.callsAccessor || facts.importsDefinitionFactory || facts.getterCallSources.length > 0) {
8392
- factsMap.set(id, facts);
8393
- if (facts.importsDefinitionFactory) definitionModules.add(id);
8626
+ if (!hasModuleBindings(code)) return;
8627
+ const pluginCtx = this;
8628
+ const facts = analyzeModule(pluginCtx.parse(code));
8629
+ if (contributesNothing(facts)) {
8630
+ factsMap.delete(id);
8631
+ return;
8394
8632
  }
8633
+ await resolveEdgeSources(pluginCtx, facts, id);
8634
+ factsMap.set(id, facts);
8395
8635
  },
8396
8636
  configureServer(server) {
8397
8637
  const resolver = (filePath) => {
@@ -8405,8 +8645,13 @@ function timberRequestDep(ctx) {
8405
8645
  for (const imp of mod.importedModules) if (imp.id) result.push(imp.id);
8406
8646
  return result;
8407
8647
  };
8408
- const result = isReachableTaintSource(filePath, factsMap, definitionModules, getImports);
8409
- cache.set(filePath, result);
8648
+ const isKnownModule = (moduleId) => rscEnv.moduleGraph.getModuleById(moduleId) !== void 0;
8649
+ const result = isReachableTaintSource(filePath, {
8650
+ factsMap,
8651
+ getImports,
8652
+ isKnownModule
8653
+ });
8654
+ if (isKnownModule(filePath)) cache.set(filePath, result);
8410
8655
  return result;
8411
8656
  };
8412
8657
  globalThis[BRIDGE_KEY] = resolver;
@@ -8423,10 +8668,14 @@ function timberRequestDep(ctx) {
8423
8668
  if (!info) return [];
8424
8669
  return [...info.importedIds ?? [], ...info.dynamicallyImportedIds ?? []];
8425
8670
  };
8671
+ const isKnownModule = (moduleId) => getModuleInfo(moduleId) != null;
8426
8672
  const requestDepMap = {};
8427
8673
  if (ctx.routeTree) walkSegmentTree(ctx.routeTree.root, (filePath) => {
8428
- const result = isReachableTaintSource(filePath, factsMap, definitionModules, getImports);
8429
- requestDepMap[filePath] = result;
8674
+ requestDepMap[filePath] = isReachableTaintSource(filePath, {
8675
+ factsMap,
8676
+ getImports,
8677
+ isKnownModule
8678
+ });
8430
8679
  });
8431
8680
  ctx.requestDepMap = requestDepMap;
8432
8681
  }
@@ -10351,6 +10600,7 @@ function timber(config) {
10351
10600
  const earlyFileConfig = loadTimberConfigFile(process.cwd());
10352
10601
  const rootSync = {
10353
10602
  name: "timber-root-sync",
10603
+ api: { getContext: () => ctx },
10354
10604
  async config(userConfig, { command, isPreview }) {
10355
10605
  const viteRoot = resolve(userConfig.root ?? process.cwd());
10356
10606
  ctx.timer.start("config-load");
@@ -10407,7 +10657,6 @@ function timber(config) {
10407
10657
  "@timber-js/app/cookies",
10408
10658
  "@timber-js/app/cache",
10409
10659
  "@timber-js/app/search-params",
10410
- "@timber-js/app/segment-params",
10411
10660
  "@timber-js/app/params",
10412
10661
  "@timber-js/app/codec"
10413
10662
  ];
@@ -10445,6 +10694,7 @@ function timber(config) {
10445
10694
  ctx.root = resolved.root;
10446
10695
  ctx.appDir = resolveAppDir(resolved.root, ctx.config.appDir);
10447
10696
  ctx.dev = resolved.command === "serve";
10697
+ ctx.base = resolved.base;
10448
10698
  for (const name of [
10449
10699
  "timber.config.ts",
10450
10700
  "timber.config.js",