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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
  3. package/dist/_chunks/als-slots-mFweg276.js +27 -0
  4. package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
  5. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  6. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  7. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  8. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  9. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  10. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  11. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
  12. package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
  13. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
  14. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
  15. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
  16. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
  17. package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
  18. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
  19. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  20. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  21. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  22. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  23. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
  24. package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
  25. package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
  26. package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
  27. package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
  28. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  29. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
  30. package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
  31. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  32. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  33. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  34. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  35. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  36. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  37. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  38. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  39. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  40. package/dist/_chunks/{segment-classify-Byy425ng.js.map → segment-classify-C539Pa2O.js.map} +1 -1
  41. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  42. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  43. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  44. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  45. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  46. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  47. package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
  48. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
  49. package/dist/_chunks/{walkers-_6zKFlch.js → walkers-BhhwI9TD.js} +21 -87
  50. package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
  51. package/dist/adapters/build-output-helper.d.ts +37 -0
  52. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  53. package/dist/adapters/cloudflare-dev.js +1 -1
  54. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  55. package/dist/adapters/cloudflare.d.ts.map +1 -1
  56. package/dist/adapters/cloudflare.js +1 -1
  57. package/dist/adapters/fs-identity.d.ts +64 -0
  58. package/dist/adapters/fs-identity.d.ts.map +1 -0
  59. package/dist/adapters/nitro.d.ts.map +1 -1
  60. package/dist/adapters/nitro.js +2 -3
  61. package/dist/adapters/nitro.js.map +1 -1
  62. package/dist/adapters/shared.d.ts +57 -1
  63. package/dist/adapters/shared.d.ts.map +1 -1
  64. package/dist/adapters/types.d.ts +13 -1
  65. package/dist/adapters/types.d.ts.map +1 -1
  66. package/dist/cache/fast-hash.d.ts +5 -0
  67. package/dist/cache/fast-hash.d.ts.map +1 -1
  68. package/dist/cache/index.js +1 -1
  69. package/dist/cli.js +2 -2
  70. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  71. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  72. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  73. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  74. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  75. package/dist/client/browser-entry/router-init.d.ts +3 -0
  76. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  77. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  78. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  79. package/dist/client/error-boundary.d.ts +1 -0
  80. package/dist/client/error-boundary.d.ts.map +1 -1
  81. package/dist/client/error-boundary.js +1 -162
  82. package/dist/client/history.d.ts +13 -6
  83. package/dist/client/history.d.ts.map +1 -1
  84. package/dist/client/index.js +18 -11
  85. package/dist/client/index.js.map +1 -1
  86. package/dist/client/internal.d.ts +4 -3
  87. package/dist/client/internal.d.ts.map +1 -1
  88. package/dist/client/internal.js +865 -306
  89. package/dist/client/internal.js.map +1 -1
  90. package/dist/client/link.d.ts.map +1 -1
  91. package/dist/client/navigation-api.d.ts +2 -2
  92. package/dist/client/navigation-commit.d.ts +71 -0
  93. package/dist/client/navigation-commit.d.ts.map +1 -0
  94. package/dist/client/navigation-context.d.ts +37 -20
  95. package/dist/client/navigation-context.d.ts.map +1 -1
  96. package/dist/client/navigation-root.d.ts +63 -23
  97. package/dist/client/navigation-root.d.ts.map +1 -1
  98. package/dist/client/params-context.d.ts +81 -0
  99. package/dist/client/params-context.d.ts.map +1 -0
  100. package/dist/client/router-effects.d.ts +129 -0
  101. package/dist/client/router-effects.d.ts.map +1 -0
  102. package/dist/client/router-lifecycle.d.ts +55 -0
  103. package/dist/client/router-lifecycle.d.ts.map +1 -0
  104. package/dist/client/router-pipeline.d.ts +84 -0
  105. package/dist/client/router-pipeline.d.ts.map +1 -0
  106. package/dist/client/router-ref.d.ts +1 -1
  107. package/dist/client/router-ref.d.ts.map +1 -1
  108. package/dist/client/router-skew.d.ts +21 -0
  109. package/dist/client/router-skew.d.ts.map +1 -0
  110. package/dist/client/router-types.d.ts +207 -0
  111. package/dist/client/router-types.d.ts.map +1 -0
  112. package/dist/client/router.d.ts +1 -182
  113. package/dist/client/router.d.ts.map +1 -1
  114. package/dist/client/rsc-fetch.d.ts +38 -25
  115. package/dist/client/rsc-fetch.d.ts.map +1 -1
  116. package/dist/client/segment-cache.d.ts +79 -34
  117. package/dist/client/segment-cache.d.ts.map +1 -1
  118. package/dist/client/ssr-data.d.ts +6 -0
  119. package/dist/client/ssr-data.d.ts.map +1 -1
  120. package/dist/client/stale-client.d.ts +69 -0
  121. package/dist/client/stale-client.d.ts.map +1 -0
  122. package/dist/client/state.d.ts +10 -1
  123. package/dist/client/state.d.ts.map +1 -1
  124. package/dist/client/top-loader.d.ts +2 -1
  125. package/dist/client/top-loader.d.ts.map +1 -1
  126. package/dist/client/unload-guard.d.ts +5 -0
  127. package/dist/client/unload-guard.d.ts.map +1 -1
  128. package/dist/client/use-cookie.d.ts.map +1 -1
  129. package/dist/client/use-router.d.ts +1 -1
  130. package/dist/client/use-segment-params.d.ts +18 -8
  131. package/dist/client/use-segment-params.d.ts.map +1 -1
  132. package/dist/cookies/index.js +3 -3
  133. package/dist/cookies/index.js.map +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +906 -321
  136. package/dist/index.js.map +1 -1
  137. package/dist/plugin-context.d.ts +31 -0
  138. package/dist/plugin-context.d.ts.map +1 -1
  139. package/dist/plugins/adapter-build.d.ts.map +1 -1
  140. package/dist/plugins/build-manifest.d.ts +1 -1
  141. package/dist/plugins/build-manifest.d.ts.map +1 -1
  142. package/dist/plugins/entries.d.ts.map +1 -1
  143. package/dist/plugins/fonts.d.ts.map +1 -1
  144. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  145. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  146. package/dist/plugins/request-dep/index.d.ts +75 -0
  147. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  148. package/dist/plugins/request-dep/origin.d.ts +61 -0
  149. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  150. package/dist/plugins/routing.d.ts +9 -0
  151. package/dist/plugins/routing.d.ts.map +1 -1
  152. package/dist/plugins/static-build.d.ts +136 -8
  153. package/dist/plugins/static-build.d.ts.map +1 -1
  154. package/dist/routing/codegen-types.d.ts +18 -0
  155. package/dist/routing/codegen-types.d.ts.map +1 -1
  156. package/dist/routing/codegen.d.ts.map +1 -1
  157. package/dist/routing/collision-probe.d.ts +104 -0
  158. package/dist/routing/collision-probe.d.ts.map +1 -0
  159. package/dist/routing/collision-spaces.d.ts +26 -0
  160. package/dist/routing/collision-spaces.d.ts.map +1 -0
  161. package/dist/routing/index.js +3 -3
  162. package/dist/routing/interception.d.ts +235 -6
  163. package/dist/routing/interception.d.ts.map +1 -1
  164. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  165. package/dist/routing/scanner.d.ts.map +1 -1
  166. package/dist/routing/segment-classify.d.ts +33 -0
  167. package/dist/routing/segment-classify.d.ts.map +1 -1
  168. package/dist/routing/segment-keys.d.ts +95 -0
  169. package/dist/routing/segment-keys.d.ts.map +1 -0
  170. package/dist/routing/slot-placement.d.ts +48 -0
  171. package/dist/routing/slot-placement.d.ts.map +1 -0
  172. package/dist/routing/walkers.d.ts +6 -0
  173. package/dist/routing/walkers.d.ts.map +1 -1
  174. package/dist/search-params/define.d.ts +0 -6
  175. package/dist/search-params/define.d.ts.map +1 -1
  176. package/dist/search-params/index.js +176 -2
  177. package/dist/search-params/index.js.map +1 -1
  178. package/dist/segment-params/define.d.ts +0 -6
  179. package/dist/segment-params/define.d.ts.map +1 -1
  180. package/dist/segment-params/index.js +69 -1
  181. package/dist/segment-params/index.js.map +1 -0
  182. package/dist/server/actions.d.ts +6 -3
  183. package/dist/server/actions.d.ts.map +1 -1
  184. package/dist/server/als-registry.d.ts +2 -1
  185. package/dist/server/als-registry.d.ts.map +1 -1
  186. package/dist/server/build-manifest.d.ts +13 -1
  187. package/dist/server/build-manifest.d.ts.map +1 -1
  188. package/dist/server/chain-url-parts.d.ts +82 -0
  189. package/dist/server/chain-url-parts.d.ts.map +1 -0
  190. package/dist/server/children-interception.d.ts +49 -0
  191. package/dist/server/children-interception.d.ts.map +1 -0
  192. package/dist/server/default-status-page.d.ts +45 -0
  193. package/dist/server/default-status-page.d.ts.map +1 -0
  194. package/dist/server/deny-renderer.d.ts +14 -3
  195. package/dist/server/deny-renderer.d.ts.map +1 -1
  196. package/dist/server/head-response.d.ts +35 -0
  197. package/dist/server/head-response.d.ts.map +1 -0
  198. package/dist/server/index.js +2 -2
  199. package/dist/server/internal.js +393 -212
  200. package/dist/server/internal.js.map +1 -1
  201. package/dist/server/param-coercion.d.ts +10 -1
  202. package/dist/server/param-coercion.d.ts.map +1 -1
  203. package/dist/server/pipeline-helpers.d.ts +0 -18
  204. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  205. package/dist/server/pipeline-interception.d.ts +59 -9
  206. package/dist/server/pipeline-interception.d.ts.map +1 -1
  207. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  208. package/dist/server/pipeline-phases.d.ts.map +1 -1
  209. package/dist/server/pipeline.d.ts +11 -0
  210. package/dist/server/pipeline.d.ts.map +1 -1
  211. package/dist/server/publish-params.d.ts +33 -0
  212. package/dist/server/publish-params.d.ts.map +1 -0
  213. package/dist/server/request-context.d.ts +48 -0
  214. package/dist/server/request-context.d.ts.map +1 -1
  215. package/dist/server/route-element-builder.d.ts +32 -2
  216. package/dist/server/route-element-builder.d.ts.map +1 -1
  217. package/dist/server/route-handler.d.ts.map +1 -1
  218. package/dist/server/route-matcher.d.ts +13 -0
  219. package/dist/server/route-matcher.d.ts.map +1 -1
  220. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  221. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  222. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  223. package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
  224. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  225. package/dist/server/rsc-entry/helpers.d.ts +2 -10
  226. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  227. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  228. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  229. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
  230. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  231. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
  232. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  233. package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
  234. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  235. package/dist/server/skippable-prefix.d.ts +23 -0
  236. package/dist/server/skippable-prefix.d.ts.map +1 -0
  237. package/dist/server/slot-interception.d.ts +80 -0
  238. package/dist/server/slot-interception.d.ts.map +1 -0
  239. package/dist/server/slot-resolver.d.ts +70 -8
  240. package/dist/server/slot-resolver.d.ts.map +1 -1
  241. package/dist/server/ssr-bridge-types.d.ts +6 -0
  242. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  243. package/dist/server/ssr-entry.d.ts.map +1 -1
  244. package/dist/server/ssr-wrappers.d.ts +24 -13
  245. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  246. package/dist/server/state-tree-diff.d.ts +6 -27
  247. package/dist/server/state-tree-diff.d.ts.map +1 -1
  248. package/dist/server/static-generator.d.ts +6 -0
  249. package/dist/server/static-generator.d.ts.map +1 -1
  250. package/dist/server/static-not-found.d.ts +52 -0
  251. package/dist/server/static-not-found.d.ts.map +1 -0
  252. package/dist/shared/als-slots.d.ts +41 -0
  253. package/dist/shared/als-slots.d.ts.map +1 -0
  254. package/dist/shared/base-path.d.ts +56 -0
  255. package/dist/shared/base-path.d.ts.map +1 -0
  256. package/dist/shared/param-value.d.ts +80 -0
  257. package/dist/shared/param-value.d.ts.map +1 -0
  258. package/dist/shared/payload-root.d.ts +108 -0
  259. package/dist/shared/payload-root.d.ts.map +1 -0
  260. package/dist/shared/rsc-cache-key.d.ts +163 -0
  261. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  262. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  263. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  264. package/dist/shared/rsc-manifest.d.ts +28 -0
  265. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  266. package/dist/shared/rsc-payload-path.d.ts +42 -0
  267. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  268. package/dist/shared/segment-info.d.ts +59 -0
  269. package/dist/shared/segment-info.d.ts.map +1 -0
  270. package/dist/shared/slot-params.d.ts +51 -0
  271. package/dist/shared/slot-params.d.ts.map +1 -0
  272. package/dist/shared/static-platform-files.d.ts +71 -0
  273. package/dist/shared/static-platform-files.d.ts.map +1 -0
  274. package/docs/api/31-api-client.mdx +4 -2
  275. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  276. package/docs/learn/11-error-handling.mdx +2 -0
  277. package/docs/learn/12-client-navigation.mdx +2 -2
  278. package/docs/learn/14-deploying.mdx +23 -0
  279. package/docs/more/01-advanced-routing.mdx +172 -2
  280. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  281. package/package.json +2 -2
  282. package/src/adapters/build-output-helper.ts +116 -3
  283. package/src/adapters/cloudflare.ts +86 -11
  284. package/src/adapters/fs-identity.ts +156 -0
  285. package/src/adapters/nitro.ts +6 -6
  286. package/src/adapters/shared.ts +196 -21
  287. package/src/adapters/types.ts +13 -1
  288. package/src/cache/fast-hash.ts +5 -0
  289. package/src/client/browser-entry/action-dispatch.ts +9 -4
  290. package/src/client/browser-entry/hydrate.ts +40 -29
  291. package/src/client/browser-entry/index.ts +15 -1
  292. package/src/client/browser-entry/post-hydration.ts +10 -9
  293. package/src/client/browser-entry/router-init.ts +104 -48
  294. package/src/client/browser-entry/rsc-stream.ts +11 -2
  295. package/src/client/error-boundary.tsx +20 -15
  296. package/src/client/history.ts +13 -6
  297. package/src/client/internal.ts +4 -3
  298. package/src/client/link.tsx +55 -8
  299. package/src/client/navigation-api.ts +3 -3
  300. package/src/client/navigation-commit.ts +174 -0
  301. package/src/client/navigation-context.ts +40 -67
  302. package/src/client/navigation-root.tsx +326 -101
  303. package/src/client/params-context.ts +192 -0
  304. package/src/client/router-effects.ts +231 -0
  305. package/src/client/router-lifecycle.ts +288 -0
  306. package/src/client/router-pipeline.ts +389 -0
  307. package/src/client/router-ref.ts +1 -1
  308. package/src/client/router-skew.ts +30 -0
  309. package/src/client/router-types.ts +230 -0
  310. package/src/client/router.ts +242 -707
  311. package/src/client/rsc-fetch.ts +169 -102
  312. package/src/client/segment-cache.ts +121 -44
  313. package/src/client/ssr-data.ts +6 -0
  314. package/src/client/stale-client.ts +191 -0
  315. package/src/client/state.ts +14 -1
  316. package/src/client/top-loader.tsx +2 -1
  317. package/src/client/unload-guard.ts +34 -2
  318. package/src/client/use-cookie.ts +9 -2
  319. package/src/client/use-router.ts +1 -1
  320. package/src/client/use-segment-params.ts +41 -18
  321. package/src/index.ts +5 -1
  322. package/src/plugin-context.ts +32 -0
  323. package/src/plugins/adapter-build.ts +4 -0
  324. package/src/plugins/build-manifest.ts +34 -13
  325. package/src/plugins/entries.ts +4 -0
  326. package/src/plugins/fonts.ts +11 -8
  327. package/src/plugins/request-dep/analysis.ts +447 -0
  328. package/src/plugins/request-dep/index.ts +376 -0
  329. package/src/plugins/request-dep/origin.ts +310 -0
  330. package/src/plugins/routing.ts +59 -2
  331. package/src/plugins/static-build.ts +315 -73
  332. package/src/routing/codegen-types.ts +18 -0
  333. package/src/routing/codegen.ts +50 -10
  334. package/src/routing/collision-probe.ts +438 -0
  335. package/src/routing/collision-spaces.ts +281 -0
  336. package/src/routing/interception.ts +896 -50
  337. package/src/routing/manifest-codegen.ts +5 -3
  338. package/src/routing/scanner.ts +114 -89
  339. package/src/routing/segment-classify.ts +57 -0
  340. package/src/routing/segment-keys.ts +146 -0
  341. package/src/routing/slot-placement.ts +97 -0
  342. package/src/routing/walkers.ts +8 -0
  343. package/src/search-params/define.ts +6 -29
  344. package/src/segment-params/define.ts +4 -27
  345. package/src/server/action-handler.ts +3 -1
  346. package/src/server/actions.ts +6 -3
  347. package/src/server/als-registry.ts +2 -1
  348. package/src/server/build-manifest.ts +14 -1
  349. package/src/server/chain-url-parts.ts +163 -0
  350. package/src/server/children-interception.ts +115 -0
  351. package/src/server/default-status-page.ts +127 -0
  352. package/src/server/deny-renderer.ts +81 -20
  353. package/src/server/head-response.ts +54 -0
  354. package/src/server/param-coercion.ts +48 -18
  355. package/src/server/pipeline-helpers.ts +0 -45
  356. package/src/server/pipeline-interception.ts +81 -32
  357. package/src/server/pipeline-outcome.ts +2 -4
  358. package/src/server/pipeline-phases.ts +68 -16
  359. package/src/server/pipeline.ts +57 -10
  360. package/src/server/prebuilt-runtime.ts +4 -4
  361. package/src/server/publish-params.ts +42 -0
  362. package/src/server/request-context.ts +97 -18
  363. package/src/server/route-element-builder.ts +89 -98
  364. package/src/server/route-handler.ts +8 -8
  365. package/src/server/route-matcher.ts +21 -9
  366. package/src/server/rsc-cache-key-guard.ts +170 -0
  367. package/src/server/rsc-entry/api-handler.ts +6 -8
  368. package/src/server/rsc-entry/error-renderer.ts +64 -25
  369. package/src/server/rsc-entry/helpers.ts +31 -16
  370. package/src/server/rsc-entry/index.ts +36 -28
  371. package/src/server/rsc-entry/render-route.ts +9 -4
  372. package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
  373. package/src/server/rsc-entry/rsc-payload.ts +14 -25
  374. package/src/server/rsc-entry/rsc-stream.ts +4 -3
  375. package/src/server/rsc-entry/ssr-renderer.ts +13 -9
  376. package/src/server/skippable-prefix.ts +92 -0
  377. package/src/server/slot-interception.ts +168 -0
  378. package/src/server/slot-resolver.ts +253 -156
  379. package/src/server/ssr-bridge-types.ts +6 -0
  380. package/src/server/ssr-entry.ts +12 -4
  381. package/src/server/ssr-wrappers.tsx +33 -32
  382. package/src/server/state-tree-diff.ts +14 -55
  383. package/src/server/static-generator.ts +65 -27
  384. package/src/server/static-not-found.ts +134 -0
  385. package/src/shared/als-slots.ts +72 -0
  386. package/src/shared/base-path.ts +80 -0
  387. package/src/shared/param-value.ts +244 -0
  388. package/src/shared/payload-root.ts +151 -0
  389. package/src/shared/rsc-cache-key.ts +269 -0
  390. package/src/shared/rsc-error-envelope.ts +31 -0
  391. package/src/shared/rsc-manifest.ts +29 -0
  392. package/src/shared/rsc-payload-path.ts +45 -0
  393. package/src/shared/segment-info.ts +60 -0
  394. package/src/shared/slot-params.ts +111 -0
  395. package/src/shared/static-platform-files.ts +75 -0
  396. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  397. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  398. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  399. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js +0 -668
  400. package/dist/_chunks/cli-schema-sync-BTWEKJXo.js.map +0 -1
  401. package/dist/_chunks/define-COtkxMRT.js +0 -191
  402. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  403. package/dist/_chunks/define-c4au4I9R.js +0 -82
  404. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  405. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  406. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  407. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  408. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  409. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  410. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  411. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  412. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  413. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  414. package/dist/_chunks/walkers-_6zKFlch.js.map +0 -1
  415. package/dist/client/error-boundary.js.map +0 -1
  416. package/dist/plugins/request-dep.d.ts +0 -32
  417. package/dist/plugins/request-dep.d.ts.map +0 -1
  418. package/src/plugins/request-dep.ts +0 -499
package/dist/index.js CHANGED
@@ -1,13 +1,17 @@
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 { a as canonicalize, d as isMetadataRouteServePath, u as isDynamicMetadataExtension } from "./_chunks/segment-keys-BawYuNFO.js";
3
+ import { a as collectInterceptionRewrites, i as scanRoutes, o as matchUrlParts } from "./_chunks/cli-schema-sync-CKgHC2MB.js";
4
+ import { h as swallow } from "./_chunks/logger-N7e5auP0.js";
5
+ import { a as fileHasAnyExport, c as fileHasExport, i as generateRouteMap, l as fileHasStarExport, o as fileHasDefaultExport, r as validateSchemaAgainstRoutes, s as fileHasDirective, t as collectLeafRoutes, u as getPrerenderExport } from "./_chunks/walkers-BhhwI9TD.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-DEGLSJs3.js";
7
+ import { i as withBasePath, n as normalizeBasePath, t as isRelativeBase } from "./_chunks/base-path-DaQrzbez.js";
8
+ import { t as rscPayloadPath } from "./_chunks/rsc-payload-path-B_LBodc2.js";
9
+ 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";
10
+ import { t as EMPTY_BUILD_MANIFEST } from "./_chunks/build-manifest-DWppEdLB.js";
11
+ import { t as fnv1aHash } from "./_chunks/fast-hash-C-tVYrLb.js";
8
12
  import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
9
13
  import { createLogger, parseAst } from "vite";
10
- import { basename, dirname, extname, join, normalize, relative, resolve, win32 } from "node:path";
14
+ import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, win32 } from "node:path";
11
15
  import { builtinModules, createRequire } from "node:module";
12
16
  import react, { reactCompilerPreset } from "@vitejs/plugin-react";
13
17
  import { existsSync, readFileSync, statSync } from "node:fs";
@@ -1479,125 +1483,6 @@ function compressWithGzip(body) {
1479
1483
  return Readable.toWeb(gzip);
1480
1484
  }
1481
1485
  //#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
1486
  //#region src/plugins/dev-server.ts
1602
1487
  var RSC_ENTRY_ID = "virtual:timber-rsc-entry";
1603
1488
  /**
@@ -2014,6 +1899,7 @@ function generateConfigModule(ctx) {
2014
1899
  csrf: ctx.config.csrf ?? true,
2015
1900
  allowedOrigins: ctx.config.allowedOrigins,
2016
1901
  clientJavascript: ctx.clientJavascript,
1902
+ base: ctx.base,
2017
1903
  dev: ctx.dev ?? false,
2018
1904
  slowPhaseMs: ctx.config.dev?.slowPhaseMs ?? 200,
2019
1905
  slowRequestMs: ctx.config.slowRequestMs ?? 3e3,
@@ -2477,10 +2363,7 @@ function generateManifestModule(tree, viteRoot) {
2477
2363
  let globalErrorLine = "";
2478
2364
  if (tree.globalError) globalErrorLine = ` globalError: { load: ${addImport(tree.globalError)}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
2479
2365
  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
- })))},` : "";
2366
+ const rewritesLine = rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites)},` : "";
2484
2367
  return [
2485
2368
  "// Auto-generated route manifest — do not edit.",
2486
2369
  "// Generated by timber-routing plugin.",
@@ -2864,9 +2747,38 @@ var TIMBER_ENV_DTS = [
2864
2747
  ""
2865
2748
  ].join("\n");
2866
2749
  /**
2750
+ * Codegen writes that have been started but have not yet landed on disk.
2751
+ *
2752
+ * `writeCodegen` is deliberately unawaited on the hot path, which leaves a
2753
+ * caller no way to know the files exist yet. Anything that tears down the
2754
+ * project root — a build process exiting, a test removing its temp root —
2755
+ * then races the writes: `rmSync` empties a directory that a pending `mkdir`
2756
+ * immediately recreates, and the removal fails with ENOTEMPTY.
2757
+ *
2758
+ * Every started write registers here and deregisters when it settles, so
2759
+ * `flushCodegenWrites()` can await them instead of guessing at a delay.
2760
+ */
2761
+ var pendingCodegenWrites = /* @__PURE__ */ new Set();
2762
+ /**
2763
+ * Resolve once every codegen write started so far has landed on disk.
2764
+ *
2765
+ * Safe to call at any time and from anywhere — writes are tracked per process,
2766
+ * not per plugin instance, so a caller does not need a handle on the plugin
2767
+ * that started them. Never rejects: individual write failures are already
2768
+ * swallowed and logged by `writeCodegen`.
2769
+ */
2770
+ async function flushCodegenWrites() {
2771
+ while (pendingCodegenWrites.size > 0) {
2772
+ const inFlight = [...pendingCodegenWrites];
2773
+ await Promise.all(inFlight);
2774
+ for (const write of inFlight) pendingCodegenWrites.delete(write);
2775
+ }
2776
+ }
2777
+ /**
2867
2778
  * Write the generated route map and the timber-env.d.ts reference file.
2868
2779
  *
2869
- * Fire-and-forget — never awaited to avoid blocking the hot path.
2780
+ * Not awaited by callers, to avoid blocking the hot path — use
2781
+ * `flushCodegenWrites()` to wait for the writes to land.
2870
2782
  * Errors are swallowed; a missing .d.ts is a developer UX issue, not a runtime failure.
2871
2783
  */
2872
2784
  function writeCodegen(ctx) {
@@ -2878,13 +2790,15 @@ function writeCodegen(ctx) {
2878
2790
  });
2879
2791
  const routesPath = join(ctx.root, CODEGEN_OUTPUT);
2880
2792
  const envPath = join(ctx.root, "timber-env.d.ts");
2881
- mkdir(timberDir, { recursive: true }).then(() => Promise.all([
2793
+ const done = mkdir(timberDir, { recursive: true }).then(() => Promise.all([
2882
2794
  writeFile(routesPath, content, "utf-8"),
2883
2795
  writeFile(envPath, TIMBER_ENV_DTS, "utf-8"),
2884
2796
  writeSegmentTypes(ctx.routeTree, ctx.appDir, ctx.root)
2885
- ])).catch((err) => {
2797
+ ])).then(() => void 0).catch((err) => {
2886
2798
  console.warn("[timber] Failed to write codegen output:", err);
2887
2799
  });
2800
+ pendingCodegenWrites.add(done);
2801
+ done.finally(() => pendingCodegenWrites.delete(done));
2888
2802
  }
2889
2803
  function timberRouting(ctx) {
2890
2804
  /** Track warned files to avoid repeating warnings on rescan. */
@@ -2979,6 +2893,18 @@ function timberRouting(ctx) {
2979
2893
  rescan();
2980
2894
  },
2981
2895
  /**
2896
+ * Wait for the codegen writes started by `buildStart` before the build
2897
+ * reports done.
2898
+ *
2899
+ * `writeCodegen` runs unawaited so route scanning never blocks on disk.
2900
+ * Without this the build can finish — and the CLI can exit — while the
2901
+ * generated `.d.ts` files are still being written, leaving a project root
2902
+ * with no route types after an apparently successful build.
2903
+ */
2904
+ closeBundle() {
2905
+ return flushCodegenWrites();
2906
+ },
2907
+ /**
2982
2908
  * Set up file watcher in dev mode.
2983
2909
  *
2984
2910
  * Watches app/ for file additions/deletions/renames that affect
@@ -6194,16 +6120,19 @@ function writeFontManifest(pipeline, buildManifest, base = "/") {
6194
6120
  */
6195
6121
  function timberFonts(ctx) {
6196
6122
  const pipeline = new FontPipeline({ hashOnRegister: !ctx.dev });
6197
- let resolvedBase = "/";
6198
6123
  return {
6199
6124
  name: "timber-fonts",
6200
6125
  /**
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.
6126
+ * Push Vite's resolved `base` into the pipeline so every font URL it
6127
+ * emits (@font-face src, manifest hrefs, dev middleware matching)
6128
+ * carries the prefix. Emitted file locations are unaffected — only URLs.
6129
+ *
6130
+ * `config.base` is read here rather than `ctx.base`: this plugin is
6131
+ * `enforce: 'pre'`, so its configResolved runs before timber-root-sync
6132
+ * populates `ctx.base`. Every *later* hook reads `ctx.base` — the single
6133
+ * copy (TIM-1261).
6204
6134
  */
6205
6135
  configResolved(config) {
6206
- resolvedBase = config.base;
6207
6136
  pipeline.setBase(config.base);
6208
6137
  const originalWarnOnce = config.logger.warnOnce.bind(config.logger);
6209
6138
  config.logger.warnOnce = (msg, options) => {
@@ -6292,7 +6221,7 @@ function timberFonts(ctx) {
6292
6221
  } else {
6293
6222
  const familyCached = groupCachedFontsByFamily(await downloadAndCacheFonts([font], ctx.root)).get(font.family.toLowerCase()) ?? [];
6294
6223
  pipeline.attachCachedFiles(font.id, familyCached);
6295
- pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display, fontUrlPrefix(resolvedBase)));
6224
+ pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display, fontUrlPrefix(ctx.base)));
6296
6225
  }
6297
6226
  } catch (e) {
6298
6227
  if (!ctx.dev) throw e;
@@ -6310,7 +6239,7 @@ function timberFonts(ctx) {
6310
6239
  * this middleware.
6311
6240
  */
6312
6241
  configureServer(server) {
6313
- installFontDevMiddleware(server, pipeline, resolvedBase);
6242
+ installFontDevMiddleware(server, pipeline, ctx.base);
6314
6243
  },
6315
6244
  /**
6316
6245
  * No-op. Font downloading and face attachment happen lazily in `load()`
@@ -6380,7 +6309,7 @@ function timberFonts(ctx) {
6380
6309
  generateBundle() {
6381
6310
  emitFontAssets(pipeline, (asset) => this.emitFile(asset), (msg) => this.warn(msg));
6382
6311
  if (!ctx.buildManifest) return;
6383
- writeFontManifest(pipeline, ctx.buildManifest, resolvedBase);
6312
+ writeFontManifest(pipeline, ctx.buildManifest, ctx.base);
6384
6313
  }
6385
6314
  };
6386
6315
  }
@@ -6410,6 +6339,7 @@ function timberAdapterBuild(ctx) {
6410
6339
  if (ctx.clientJavascript.disabled) await stripJsFromRscAssetsManifests(buildDir);
6411
6340
  const adapterConfig = {
6412
6341
  root: ctx.root,
6342
+ base: normalizeBasePath(ctx.base),
6413
6343
  output: ctx.config.output ?? "server",
6414
6344
  clientJavascriptDisabled: ctx.clientJavascript.disabled,
6415
6345
  manifestInit,
@@ -6654,10 +6584,22 @@ function detectDynamicApis(code, fileId) {
6654
6584
  * Generate a `_redirects` file from collected redirect entries.
6655
6585
  * Uses the Cloudflare Pages / Netlify format: `/from /to statusCode`
6656
6586
  *
6587
+ * Both sides are base-prefixed (TIM-1274). `from` is matched by the host
6588
+ * against the request path and `to` is served to the browser as a `Location`,
6589
+ * so both are deployed-URL space while `StaticRedirectEntry` carries route
6590
+ * space — under `base: '/repo/'`, a rule `/old /new 301` would match nothing
6591
+ * and, if it did, would send the browser off the deployment.
6592
+ *
6593
+ * An off-origin `to` is left alone: `redirectExternal()` produces an absolute
6594
+ * URL, and a protocol-relative one addresses another host too. Neither is a
6595
+ * path on this deployment, so neither takes the prefix.
6596
+ *
6597
+ * @param base Normalized deployment base (see `shared/base-path.ts`).
6657
6598
  * @internal Exported for testing.
6658
6599
  */
6659
- function generateRedirectsFile(redirects) {
6660
- return redirects.map((r) => `${r.from} ${r.to} ${r.status}`).join("\n") + "\n";
6600
+ function generateRedirectsFile(redirects, base) {
6601
+ const target = (to) => to.startsWith("/") && !to.startsWith("//") ? withBasePath(base, to) : to;
6602
+ return redirects.map((r) => `${withBasePath(base, r.from)} ${target(r.to)} ${r.status}`).join("\n") + "\n";
6661
6603
  }
6662
6604
  /** Write a file atomically: temp sibling + rename. */
6663
6605
  async function writeFileAtomic$1(path, data) {
@@ -6669,18 +6611,164 @@ async function writeFileAtomic$1(path, data) {
6669
6611
  * Determine the output file path for a static page entry.
6670
6612
  *
6671
6613
  * Pages: /about → about/index.html, / → index.html
6672
- * RSC: /about → _rsc/about.rsc, / → _rsc/index.rsc
6614
+ * RSC: /about → _rsc/about/index.rsc, / → _rsc/index.rsc
6673
6615
  * API: /feed.xml → feed.xml, /api/data → api/data
6616
+ *
6617
+ * The RSC shape mirrors the HTML shape for the reason the HTML shape has it:
6618
+ * a route gets a directory and the artifact is the `index` inside it, so the
6619
+ * mapping is injective. See `shared/rsc-payload-path.ts` (TIM-1266).
6674
6620
  */
6675
6621
  function staticOutputPath(entry) {
6676
6622
  const urlPath = entry.urlPath === "/" ? "" : entry.urlPath;
6677
- if (entry.kind === "rsc") return `_rsc${urlPath || "/index"}.rsc`;
6623
+ if (entry.kind === "rsc") return rscPayloadPath(entry.urlPath).slice(1);
6678
6624
  if (entry.kind === "api") return urlPath.startsWith("/") ? urlPath.slice(1) : urlPath || "index";
6679
6625
  if (!urlPath) return "index.html";
6680
6626
  if (urlPath === "/_timber_not_found") return "404.html";
6681
6627
  return `${urlPath.startsWith("/") ? urlPath.slice(1) : urlPath}/index.html`;
6682
6628
  }
6683
6629
  /**
6630
+ * Records which entry claimed each output path and reports second claims.
6631
+ *
6632
+ * Two entries landing on one path is not a rendering failure — both render
6633
+ * fine — so nothing downstream notices. The file is written twice, the second
6634
+ * write wins, and the deployment serves one route's bytes at another route's
6635
+ * URL with the same content type and the same shape (TIM-1266).
6636
+ *
6637
+ * The naming rules are injective by construction (a route path is a directory
6638
+ * and the artifact is the `index` inside it), so a collision here means either
6639
+ * two entries were enumerated for one URL or a route reached a path the
6640
+ * framework reserves for itself (`404.html`). Both are build errors: there is
6641
+ * no correct file to write.
6642
+ *
6643
+ * Two entries can also be unwritable together without claiming the same
6644
+ * string: one wants a file where the other wants a directory (`about` from an
6645
+ * API route, `about/index.html` from a page). The filesystem does refuse that
6646
+ * one — `EISDIR` on the rename, `ENOTDIR` on the mkdir, depending on which
6647
+ * lands second — but an errno names a path, and in the rename case the path
6648
+ * it names is `about.tmp`. Reported here instead, so the message says which
6649
+ * two routes.
6650
+ *
6651
+ * Collisions are **collected, not thrown**. `onEntry` runs inside the
6652
+ * generator, whose RSC branch treats a throw as "this payload is unavailable,
6653
+ * SPA navigation falls back to a full load" and swallows it — which for a
6654
+ * collision is the wrong end state, since the two payloads are still
6655
+ * ambiguous. The caller raises them once generation is done.
6656
+ *
6657
+ * @internal Exported for testing.
6658
+ */
6659
+ var StaticOutputPaths = class {
6660
+ identity;
6661
+ claimed = /* @__PURE__ */ new Map();
6662
+ /** Every ancestor directory of a claimed path, mapped to its claimant. */
6663
+ directories = /* @__PURE__ */ new Map();
6664
+ collisions = [];
6665
+ /**
6666
+ * @param identity How the output filesystem decides two paths name one
6667
+ * file, from `detectFsIdentity`. The guard predicts whether two writes
6668
+ * land on one file, which is a property of the filesystem — so it is
6669
+ * asked, not assumed. Folding unconditionally would reject `/Foo` beside
6670
+ * `/foo` on Linux, where both are perfectly servable files.
6671
+ */
6672
+ constructor(identity = EXACT_FS_IDENTITY) {
6673
+ this.identity = identity;
6674
+ }
6675
+ /** The identity under which the filesystem will compare this path. */
6676
+ key(relPath) {
6677
+ return fsKey(relPath, this.identity);
6678
+ }
6679
+ /**
6680
+ * Claim `relPath` for `entry`. Returns false when the path cannot be
6681
+ * written alongside an earlier claim, in which case the caller must not
6682
+ * write the file — the losing entry is recorded and the build fails at
6683
+ * the end.
6684
+ */
6685
+ claim(relPath, entry) {
6686
+ if (RESERVED_OUTPUT_PATHS.some((name) => this.key(name) === this.key(relPath))) {
6687
+ this.collisions.push(` - ${relPath}: reserved — the framework writes this file itself, and ${entry.kind} for ${entry.urlPath} would be overwritten by it`);
6688
+ return false;
6689
+ }
6690
+ const key = this.key(relPath);
6691
+ const conflict = this.claimed.get(key) ?? this.directories.get(key) ?? ancestors(relPath).reduce((found, dir) => found ?? this.claimed.get(this.key(dir)), void 0);
6692
+ if (conflict) {
6693
+ this.collisions.push(` - ${relPath}: ${conflict.kind} for ${conflict.urlPath} and ${entry.kind} for ${entry.urlPath}`);
6694
+ return false;
6695
+ }
6696
+ this.claimed.set(key, entry);
6697
+ for (const dir of ancestors(relPath)) {
6698
+ const dirKey = this.key(dir);
6699
+ if (!this.directories.has(dirKey)) this.directories.set(dirKey, entry);
6700
+ }
6701
+ return true;
6702
+ }
6703
+ /** Throw if any two entries could not both be written. */
6704
+ assertNoCollisions() {
6705
+ if (this.collisions.length === 0) return;
6706
+ 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")}`);
6707
+ }
6708
+ };
6709
+ /** Every directory an output path sits under, outermost first. */
6710
+ function ancestors(relPath) {
6711
+ const parts = relPath.split("/");
6712
+ parts.pop();
6713
+ return parts.map((_, i) => parts.slice(0, i + 1).join("/"));
6714
+ }
6715
+ /**
6716
+ * Writes generated entries into the static output directory, one file per
6717
+ * entry, refusing any write that would land on a path another entry holds.
6718
+ *
6719
+ * @internal Exported for testing.
6720
+ */
6721
+ var StaticSiteWriter = class {
6722
+ staticDir;
6723
+ /** Content-Type per URL path for extensionless API routes (TIM-1241). */
6724
+ contentTypes = {};
6725
+ paths;
6726
+ constructor(staticDir, identity = EXACT_FS_IDENTITY) {
6727
+ this.staticDir = staticDir;
6728
+ this.paths = new StaticOutputPaths(identity);
6729
+ }
6730
+ async write(entry) {
6731
+ const relPath = staticOutputPath(entry);
6732
+ const fullPath = resolve(this.staticDir, relPath);
6733
+ const rel = relative(this.staticDir, fullPath);
6734
+ 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}`);
6735
+ if (!this.paths.claim(rel.replace(/\\/g, "/"), entry)) return;
6736
+ await mkdir(dirname(fullPath), { recursive: true });
6737
+ await writeFileAtomic$1(fullPath, entry.body);
6738
+ if (entry.kind === "api" && !extname(relPath) && entry.contentType) this.contentTypes[entry.urlPath] = entry.contentType;
6739
+ }
6740
+ /** Throw if any two entries could not both be written. */
6741
+ assertNoCollisions() {
6742
+ this.paths.assertNoCollisions();
6743
+ }
6744
+ };
6745
+ /**
6746
+ * Remove artifacts a generation pass produces only *conditionally*, so none
6747
+ * of them can survive from an earlier build into this one.
6748
+ *
6749
+ * Pages are not in this set: every page is rewritten each pass, and a page
6750
+ * whose route was deleted is a different problem (a stale file nothing links
6751
+ * to) from the one here.
6752
+ *
6753
+ * - `_rsc/` because the files are content-hashed. Left in place, a previous
6754
+ * build's already-hashed payload gets hashed again and accumulates.
6755
+ * - `_redirects` because it is written only when this pass found redirects.
6756
+ * A build whose redirects were removed would otherwise ship the previous
6757
+ * build's rules — and would break the merge exemption in
6758
+ * `shared/static-platform-files.ts`, which lets the pre-rendered copy
6759
+ * overwrite `public/_redirects` on the grounds that it *contains* it. That
6760
+ * holds only for a file the current pass produced (TIM-1266).
6761
+ *
6762
+ * @internal Exported for testing.
6763
+ */
6764
+ async function cleanConditionalArtifacts(staticDir) {
6765
+ await rm(join(staticDir, "_rsc"), {
6766
+ recursive: true,
6767
+ force: true
6768
+ }).catch(() => {});
6769
+ await rm(join(staticDir, REDIRECTS_FILE), { force: true }).catch(() => {});
6770
+ }
6771
+ /**
6684
6772
  * Load the built RSC entry and run the static generation pass.
6685
6773
  *
6686
6774
  * Unlike prebuilt capture, this needs the FULL pipeline handler, so
@@ -6707,43 +6795,27 @@ async function runStaticGeneration(ctx) {
6707
6795
  const generate = mod.__timber_generateStaticSite;
6708
6796
  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
6797
  const staticDir = join(ctx.buildDir, "static-pages");
6710
- await rm(join(staticDir, "_rsc"), {
6711
- recursive: true,
6712
- force: true
6713
- }).catch(() => {});
6798
+ await cleanConditionalArtifacts(staticDir);
6714
6799
  await mkdir(staticDir, { recursive: true });
6715
6800
  const sitemapConfig = ctx.config.sitemap;
6716
6801
  const autoSitemapEnabled = Boolean(sitemapConfig?.enabled && sitemapConfig?.baseUrl);
6717
- const contentTypes = {};
6718
- const routeParams = {};
6802
+ const writer = new StaticSiteWriter(staticDir, await detectFsIdentity(staticDir));
6719
6803
  const summary = await generate({
6720
6804
  autoSitemapEnabled,
6721
6805
  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
- }
6806
+ onEntry: (entry) => writer.write(entry)
6737
6807
  });
6738
6808
  const totalUrls = summary.rendered + summary.errors.length;
6739
6809
  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).` : ""));
6810
+ for (const warning of summary.warnings) console.warn(`[timber] Static generation warning: ${warning}`);
6740
6811
  if (summary.errors.length > 0) {
6741
6812
  const details = summary.errors.map((e) => ` - ${e.urlPath}: ${e.message}`).join("\n");
6742
6813
  throw new Error(`[timber] Static generation failed:\n${details}`);
6743
6814
  }
6744
- if (Object.keys(contentTypes).length > 0) ctx.staticContentTypes = contentTypes;
6815
+ writer.assertNoCollisions();
6816
+ if (Object.keys(writer.contentTypes).length > 0) ctx.staticContentTypes = writer.contentTypes;
6745
6817
  if (!ctx.clientJavascript.disabled) {
6746
- const rscManifest = await hashRscPayloads(staticDir, routeParams);
6818
+ const rscManifest = await hashRscPayloads(staticDir);
6747
6819
  if (Object.keys(rscManifest).length > 0) {
6748
6820
  await injectRscManifestIntoHtml(staticDir, rscManifest);
6749
6821
  ctx.rscManifest = rscManifest;
@@ -6751,8 +6823,8 @@ async function runStaticGeneration(ctx) {
6751
6823
  }
6752
6824
  }
6753
6825
  if (summary.redirects.length > 0) {
6754
- const generated = generateRedirectsFile(summary.redirects);
6755
- const redirectsPath = join(staticDir, "_redirects");
6826
+ const generated = generateRedirectsFile(summary.redirects, normalizeBasePath(ctx.base));
6827
+ const redirectsPath = join(staticDir, REDIRECTS_FILE);
6756
6828
  const userRedirects = await readUserRedirects(ctx.root);
6757
6829
  if (userRedirects !== null) {
6758
6830
  await writeFileAtomic$1(redirectsPath, generated + "\n" + userRedirects);
@@ -6765,6 +6837,27 @@ async function runStaticGeneration(ctx) {
6765
6837
  return summary;
6766
6838
  }
6767
6839
  /**
6840
+ * Warn when Vite's `base` is document-relative (`'./'`) in static mode.
6841
+ *
6842
+ * Payload URLs are addressed from the deployment root plus the base prefix.
6843
+ * A relative base gives no prefix to compute (see shared/base-path.ts), so
6844
+ * `/_rsc/about.rsc` is requested at the domain root — correct only when the
6845
+ * site is served from there. The build still succeeds: relative bases work
6846
+ * fine for assets, and for a root-served site nothing is wrong. The warning
6847
+ * exists so a sub-path deployment does not silently degrade to a hard
6848
+ * navigation on every link.
6849
+ *
6850
+ * Skipped when client JS is disabled — there is no SPA navigation and no
6851
+ * payload fetch to mis-address.
6852
+ *
6853
+ * @internal Exported for testing.
6854
+ */
6855
+ function warnUnusableBase(ctx) {
6856
+ if (ctx.clientJavascript.disabled) return;
6857
+ if (!isRelativeBase(ctx.base)) return;
6858
+ 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.`);
6859
+ }
6860
+ /**
6768
6861
  * Compute a short content hash for immutable caching.
6769
6862
  * Uses 8 chars of base64url-encoded SHA-256, matching Vite's asset hash style.
6770
6863
  */
@@ -6799,6 +6892,12 @@ async function walkDir(dir) {
6799
6892
  * The manifest maps the original `_rsc/` URL to an entry object:
6800
6893
  * `{"/_rsc/index.rsc":{"url":"/_rsc/index-B7YxEKdN.rsc"}, ...}`
6801
6894
  *
6895
+ * Keys and URLs are **route-space**: root-relative, never carrying Vite's
6896
+ * `base`. The client applies the base once, at fetch time
6897
+ * (`client/rsc-fetch.ts` → `toStaticRscUrl`), so the generated site stays
6898
+ * relocatable and the base has exactly one expression at runtime rather
6899
+ * than two that must agree. See TIM-1261.
6900
+ *
6802
6901
  * Route params (from `routeParams`) are inlined into the manifest
6803
6902
  * entry, eliminating the need for .params.json sidecar files. TIM-1255.
6804
6903
  *
@@ -6807,7 +6906,7 @@ async function walkDir(dir) {
6807
6906
  *
6808
6907
  * @internal Exported for testing.
6809
6908
  */
6810
- async function hashRscPayloads(staticDir, routeParams) {
6909
+ async function hashRscPayloads(staticDir) {
6811
6910
  const files = await walkDir(join(staticDir, "_rsc"));
6812
6911
  const manifest = {};
6813
6912
  for (const filePath of files) {
@@ -6818,9 +6917,7 @@ async function hashRscPayloads(staticDir, routeParams) {
6818
6917
  await rename(filePath, hashedPath);
6819
6918
  const hashedRel = relative(staticDir, hashedPath).replace(/\\/g, "/");
6820
6919
  const key = `/${rel}`;
6821
- const entry = { url: `/${hashedRel}` };
6822
- if (routeParams?.[key]) entry.params = routeParams[key];
6823
- manifest[key] = entry;
6920
+ manifest[key] = { url: `/${hashedRel}` };
6824
6921
  }
6825
6922
  return manifest;
6826
6923
  }
@@ -6849,7 +6946,7 @@ async function injectRscManifestIntoHtml(staticDir, manifest) {
6849
6946
  * @internal Exported for testing.
6850
6947
  */
6851
6948
  async function readUserRedirects(root) {
6852
- const candidates = [join(root, "public", "_redirects")];
6949
+ const candidates = [join(root, "public", REDIRECTS_FILE)];
6853
6950
  for (const path of candidates) try {
6854
6951
  return await readFile(path, "utf-8");
6855
6952
  } catch {}
@@ -6897,6 +6994,7 @@ function timberStaticBuild(ctx) {
6897
6994
  async handler() {
6898
6995
  if (ctx.dev) return;
6899
6996
  if (!(ctx.config.output === "static")) return;
6997
+ warnUnusableBase(ctx);
6900
6998
  await runStaticGeneration(ctx);
6901
6999
  }
6902
7000
  }
@@ -7050,12 +7148,10 @@ function walkModuleImports(moduleId, clientRefIndex, visited, result, getModuleI
7050
7148
  * Hooks: configResolved, resolveId, load, generateBundle (client and RSC envs)
7051
7149
  */
7052
7150
  function timberBuildManifest(ctx) {
7053
- let resolvedBase = "/";
7054
7151
  let isDev = false;
7055
7152
  return {
7056
7153
  name: "timber-build-manifest",
7057
7154
  configResolved(config) {
7058
- resolvedBase = config.base;
7059
7155
  isDev = config.command === "serve";
7060
7156
  if (!isDev && !ctx.deploymentId) ctx.deploymentId = randomUUID().replace(/-/g, "").slice(0, 16);
7061
7157
  },
@@ -7067,19 +7163,22 @@ function timberBuildManifest(ctx) {
7067
7163
  },
7068
7164
  load(id) {
7069
7165
  if (id !== RESOLVED_VIRTUAL_ID) return null;
7166
+ const empty = JSON.stringify(EMPTY_BUILD_MANIFEST);
7070
7167
  if (isDev) return [
7071
7168
  "// Auto-generated build manifest — do not edit.",
7072
7169
  "// Dev mode: empty manifest (Vite HMR handles CSS/JS).",
7073
7170
  "",
7074
- "const manifest = { css: {}, js: {}, modulepreload: {}, fonts: {}, metadataRouteHashes: {} };",
7171
+ `const manifest = ${empty};`,
7075
7172
  "",
7076
7173
  "export default manifest;"
7077
7174
  ].join("\n");
7078
7175
  return [
7079
7176
  "// Auto-generated build manifest — do not edit.",
7080
7177
  "// Production: reads manifest from globalThis (set by _timber-manifest-init.js).",
7178
+ "// The object below is only the fallback for when that global is unset —",
7179
+ "// the real manifest, including the requestDep map, is in that init module.",
7081
7180
  "",
7082
- "const manifest = globalThis.__TIMBER_BUILD_MANIFEST__ ?? { css: {}, js: {}, modulepreload: {}, fonts: {}, metadataRouteHashes: {}, requestDep: {} };",
7181
+ `const manifest = globalThis.__TIMBER_BUILD_MANIFEST__ ?? ${empty};`,
7083
7182
  "",
7084
7183
  "export default manifest;"
7085
7184
  ].join("\n");
@@ -7102,10 +7201,10 @@ function timberBuildManifest(ctx) {
7102
7201
  }
7103
7202
  }
7104
7203
  if (envName === "client") {
7105
- ctx.buildManifest = buildManifestFromBundle(bundle, resolvedBase, ctx.root);
7204
+ ctx.buildManifest = buildManifestFromBundle(bundle, ctx.base, ctx.root);
7106
7205
  if (ctx.routeTree) ctx.buildManifest.metadataRouteHashes = computeMetadataRouteHashes(ctx.routeTree.root);
7107
- const requestDepMap = ctx.requestDepMap;
7108
- if (requestDepMap) ctx.buildManifest.requestDep = requestDepMap;
7206
+ 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.");
7207
+ ctx.buildManifest.requestDep = ctx.requestDepMap;
7109
7208
  if (ctx.clientJavascript.disabled) {
7110
7209
  ctx.buildManifest.js = {};
7111
7210
  ctx.buildManifest.modulepreload = {};
@@ -8200,7 +8299,319 @@ function timberCacheTransform(ctx) {
8200
8299
  };
8201
8300
  }
8202
8301
  //#endregion
8203
- //#region src/plugins/request-dep.ts
8302
+ //#region src/plugins/request-dep/analysis.ts
8303
+ /** `export default` has no local name of its own; this stands in for one. */
8304
+ var DEFAULT_EXPORT = "default";
8305
+ function createModuleFacts() {
8306
+ return {
8307
+ importedBindings: /* @__PURE__ */ new Map(),
8308
+ reExports: /* @__PURE__ */ new Map(),
8309
+ starReExports: [],
8310
+ localExports: /* @__PURE__ */ new Map(),
8311
+ factoryCalls: /* @__PURE__ */ new Map(),
8312
+ calledLocals: /* @__PURE__ */ new Set(),
8313
+ calledMembers: /* @__PURE__ */ new Map(),
8314
+ getterLocals: /* @__PURE__ */ new Set(),
8315
+ getterMembers: /* @__PURE__ */ new Map(),
8316
+ referencedLocals: /* @__PURE__ */ new Set(),
8317
+ resolvedSources: /* @__PURE__ */ new Map(),
8318
+ unresolvedSources: /* @__PURE__ */ new Set()
8319
+ };
8320
+ }
8321
+ /**
8322
+ * Specifiers the origin walk can actually reach, so the plugin resolves those
8323
+ * and not every import in the file.
8324
+ *
8325
+ * A re-export edge is always reachable — a downstream module can ask for any
8326
+ * name it forwards. An `import` edge is reachable through any local name the
8327
+ * module body mentions, or that it re-exports.
8328
+ *
8329
+ * The name set must be **the same one the taint rules consult**. Deriving it
8330
+ * from the call shapes instead (called, `.get()`d, member-called, minted from)
8331
+ * left `read(filters)` — a definition passed as an argument — with its
8332
+ * specifier unresolved, so the origin walk reported it inert and the page read
8333
+ * as request-independent. Two hand-maintained lists of "names that matter" is
8334
+ * one list too many; this is the one, and `referencedLocals` is a superset of
8335
+ * every call shape by construction, since each of them is an identifier the
8336
+ * walk visits. (Codex review round 1 on PR #991.)
8337
+ */
8338
+ function usedSources(facts) {
8339
+ const sources = new Set(facts.starReExports);
8340
+ for (const edge of facts.reExports.values()) sources.add(edge.source);
8341
+ for (const local of facts.referencedLocals) {
8342
+ const edge = facts.importedBindings.get(local);
8343
+ if (edge) sources.add(edge.source);
8344
+ }
8345
+ for (const local of facts.localExports.values()) {
8346
+ const edge = facts.importedBindings.get(local);
8347
+ if (edge) sources.add(edge.source);
8348
+ }
8349
+ return sources;
8350
+ }
8351
+ /**
8352
+ * True when the module cannot affect any answer, so its facts need not be kept.
8353
+ *
8354
+ * Every judgement bottoms out at a module edge: a call site resolves through
8355
+ * `importedBindings`, a chain link through `reExports` / `starReExports`. A
8356
+ * module with none of those can only ever resolve to `none` — its exports lead
8357
+ * to locals that lead nowhere — which is exactly what a *missing* entry
8358
+ * produces. Dropping it is behaviour-preserving and keeps the dev-time map from
8359
+ * holding an entry for every leaf module in the graph.
8360
+ */
8361
+ function contributesNothing(facts) {
8362
+ return facts.importedBindings.size === 0 && facts.reExports.size === 0 && facts.starReExports.length === 0;
8363
+ }
8364
+ function addToMapSet(map, key, value) {
8365
+ const existing = map.get(key);
8366
+ if (existing) existing.add(value);
8367
+ else map.set(key, /* @__PURE__ */ new Set([value]));
8368
+ }
8369
+ function nameOf(node) {
8370
+ if (!node) return null;
8371
+ if (node.type === "Identifier") return node.name;
8372
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
8373
+ return null;
8374
+ }
8375
+ /**
8376
+ * `import type { … }` needs no handling here. Vite's TypeScript transform is a
8377
+ * core plugin and runs before this one, so types are already erased by the time
8378
+ * the analysis parses — and the parser it runs on is JavaScript-only, so
8379
+ * unerased TypeScript would throw rather than be silently misread.
8380
+ * `tests/request-dep-plugin.test.ts` pins the resulting property through the
8381
+ * real pipeline: a type-only accessor import is not request-dependent.
8382
+ */
8383
+ function collectImportDeclaration(stmt, facts) {
8384
+ const source = stmt.source?.value;
8385
+ if (typeof source !== "string") return;
8386
+ for (const spec of stmt.specifiers ?? []) {
8387
+ const local = nameOf(spec.local);
8388
+ if (!local) continue;
8389
+ if (spec.type === "ImportNamespaceSpecifier") facts.importedBindings.set(local, {
8390
+ source,
8391
+ imported: "*"
8392
+ });
8393
+ else if (spec.type === "ImportDefaultSpecifier") facts.importedBindings.set(local, {
8394
+ source,
8395
+ imported: DEFAULT_EXPORT
8396
+ });
8397
+ else if (spec.type === "ImportSpecifier") facts.importedBindings.set(local, {
8398
+ source,
8399
+ imported: nameOf(spec.imported) ?? local
8400
+ });
8401
+ }
8402
+ }
8403
+ function collectDeclaredNames(decl, out) {
8404
+ if (decl.type === "VariableDeclaration") for (const d of decl.declarations ?? []) {
8405
+ const id = d.id;
8406
+ if (id?.type === "Identifier") out.add(id.name);
8407
+ }
8408
+ else if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
8409
+ const id = decl.id;
8410
+ if (id?.type === "Identifier") out.add(id.name);
8411
+ }
8412
+ }
8413
+ function collectExportNamed(stmt, facts) {
8414
+ const source = stmt.source?.value;
8415
+ const specifiers = stmt.specifiers ?? [];
8416
+ if (typeof source === "string") {
8417
+ for (const spec of specifiers) {
8418
+ const imported = nameOf(spec.imported) ?? nameOf(spec.local);
8419
+ const exported = nameOf(spec.exported);
8420
+ if (imported && exported) facts.reExports.set(exported, {
8421
+ source,
8422
+ imported
8423
+ });
8424
+ }
8425
+ return;
8426
+ }
8427
+ for (const spec of specifiers) {
8428
+ const local = nameOf(spec.local);
8429
+ const exported = nameOf(spec.exported);
8430
+ if (local && exported) facts.localExports.set(exported, local);
8431
+ }
8432
+ const declaration = stmt.declaration;
8433
+ if (declaration) {
8434
+ const names = /* @__PURE__ */ new Set();
8435
+ collectDeclaredNames(declaration, names);
8436
+ for (const name of names) facts.localExports.set(name, name);
8437
+ }
8438
+ }
8439
+ function collectExportAll(stmt, facts) {
8440
+ const source = stmt.source?.value;
8441
+ if (typeof source !== "string") return;
8442
+ const exported = nameOf(stmt.exported);
8443
+ if (exported) facts.reExports.set(exported, {
8444
+ source,
8445
+ imported: "*"
8446
+ });
8447
+ else facts.starReExports.push(source);
8448
+ }
8449
+ function collectExportDefault(stmt, facts) {
8450
+ const declaration = stmt.declaration;
8451
+ if (!declaration) return;
8452
+ facts.localExports.set(DEFAULT_EXPORT, DEFAULT_EXPORT);
8453
+ if (declaration.type === "Identifier") facts.localExports.set(DEFAULT_EXPORT, declaration.name);
8454
+ else if (declaration.type === "CallExpression") {
8455
+ const ref = calleeRef(declaration);
8456
+ if (ref) facts.factoryCalls.set(DEFAULT_EXPORT, ref);
8457
+ }
8458
+ }
8459
+ function calleeRef(call) {
8460
+ const callee = call.callee;
8461
+ if (callee.type === "Identifier") return {
8462
+ kind: "local",
8463
+ name: callee.name
8464
+ };
8465
+ if (callee.type === "MemberExpression") {
8466
+ const member = callee;
8467
+ if (!member.computed && member.object.type === "Identifier" && member.property.type === "Identifier") return {
8468
+ kind: "member",
8469
+ object: member.object.name,
8470
+ property: member.property.name
8471
+ };
8472
+ }
8473
+ return null;
8474
+ }
8475
+ /**
8476
+ * Walk down a `.get()` receiver to the identifier it is ultimately rooted in,
8477
+ * along with the first property accessed on that identifier.
8478
+ *
8479
+ * `x.get()` → `x`, no property. `x(a).get()` → `x`. `ns.defs.get()` → `ns`
8480
+ * plus `defs`, which is both the namespace-member shape and the
8481
+ * `cookies.theme.get()` shape — the resolver tries the base binding and the
8482
+ * member, so one walk covers both.
8483
+ */
8484
+ function chainRoot(node) {
8485
+ let current = node;
8486
+ let property = null;
8487
+ for (;;) {
8488
+ if (current.type === "Identifier") return {
8489
+ base: current.name,
8490
+ property
8491
+ };
8492
+ if (current.type === "MemberExpression") {
8493
+ const member = current;
8494
+ if (!member.computed && member.property.type === "Identifier") property = member.property.name;
8495
+ else property = null;
8496
+ current = member.object;
8497
+ continue;
8498
+ }
8499
+ if (current.type === "CallExpression") {
8500
+ property = null;
8501
+ current = current.callee;
8502
+ continue;
8503
+ }
8504
+ return null;
8505
+ }
8506
+ }
8507
+ function collectExpression(node, facts) {
8508
+ if (node.type === "VariableDeclarator") {
8509
+ const id = node.id;
8510
+ const init = node.init;
8511
+ if (id?.type === "Identifier" && init?.type === "CallExpression") {
8512
+ const ref = calleeRef(init);
8513
+ if (ref) facts.factoryCalls.set(id.name, ref);
8514
+ }
8515
+ return;
8516
+ }
8517
+ if (node.type !== "CallExpression") return;
8518
+ const callee = node.callee;
8519
+ if (callee.type === "Identifier") {
8520
+ facts.calledLocals.add(callee.name);
8521
+ return;
8522
+ }
8523
+ if (callee.type !== "MemberExpression") return;
8524
+ const member = callee;
8525
+ if (member.computed || member.property.type !== "Identifier") return;
8526
+ const propertyName = member.property.name;
8527
+ if (propertyName === "get") {
8528
+ const root = chainRoot(member.object);
8529
+ if (root) {
8530
+ facts.getterLocals.add(root.base);
8531
+ if (root.property) addToMapSet(facts.getterMembers, root.base, root.property);
8532
+ }
8533
+ return;
8534
+ }
8535
+ if (member.object.type === "Identifier") addToMapSet(facts.calledMembers, member.object.name, propertyName);
8536
+ }
8537
+ /**
8538
+ * Statements whose whole subtree is an import/export *clause* rather than code.
8539
+ *
8540
+ * The walk skips them so `referencedLocals` means "the module body mentions
8541
+ * this name". Without the skip every imported name would count as referenced,
8542
+ * and a pure barrel — which mentions its names only in the clauses — would be
8543
+ * indistinguishable from a module that uses them.
8544
+ */
8545
+ function isClauseOnly(node) {
8546
+ if (node.type === "ImportDeclaration" || node.type === "ExportAllDeclaration") return true;
8547
+ if (node.type === "ExportNamedDeclaration") return !node.declaration;
8548
+ if (node.type === "ExportDefaultDeclaration") return node.declaration?.type === "Identifier";
8549
+ return false;
8550
+ }
8551
+ /**
8552
+ * Keys holding *binding* positions rather than references: the name being
8553
+ * declared, not a mention of one.
8554
+ *
8555
+ * `referencedLocals` must exclude these or `export const filters =
8556
+ * defineSearchParams({})` would count as referencing `filters`, and possession
8557
+ * would then taint every definition module — and through it every page that
8558
+ * imports one, whether or not it reads the request. Skipping only these keys
8559
+ * keeps a shadowing parameter's *body* uses as references, which over-reports
8560
+ * in the safe direction.
8561
+ */
8562
+ var BINDING_KEYS = {
8563
+ VariableDeclarator: ["id"],
8564
+ FunctionDeclaration: ["id", "params"],
8565
+ FunctionExpression: ["id", "params"],
8566
+ ArrowFunctionExpression: ["params"],
8567
+ ClassDeclaration: ["id"],
8568
+ ClassExpression: ["id"],
8569
+ CatchClause: ["param"]
8570
+ };
8571
+ function walk(node, facts) {
8572
+ if (isClauseOnly(node)) return;
8573
+ if (node.type === "Identifier") facts.referencedLocals.add(node.name);
8574
+ collectExpression(node, facts);
8575
+ const bindingKeys = BINDING_KEYS[node.type];
8576
+ for (const key of Object.keys(node)) {
8577
+ if (key === "type" || key === "start" || key === "end") continue;
8578
+ if (bindingKeys?.includes(key)) continue;
8579
+ const value = node[key];
8580
+ if (!value || typeof value !== "object") continue;
8581
+ if (Array.isArray(value)) {
8582
+ for (const item of value) if (item && typeof item === "object" && typeof item.type === "string") walk(item, facts);
8583
+ } else if (typeof value.type === "string") walk(value, facts);
8584
+ }
8585
+ }
8586
+ /**
8587
+ * Record every binding, module edge, and call shape in one module.
8588
+ *
8589
+ * Names are recorded without scope analysis, so a parameter that shadows an
8590
+ * imported name is credited to the import. That direction is deliberate: a
8591
+ * needlessly request-dependent segment costs a re-render, a wrongly static one
8592
+ * serves stale content.
8593
+ */
8594
+ function analyzeModule(program) {
8595
+ const facts = createModuleFacts();
8596
+ for (const stmt of program.body) switch (stmt.type) {
8597
+ case "ImportDeclaration":
8598
+ collectImportDeclaration(stmt, facts);
8599
+ break;
8600
+ case "ExportNamedDeclaration":
8601
+ collectExportNamed(stmt, facts);
8602
+ break;
8603
+ case "ExportAllDeclaration":
8604
+ collectExportAll(stmt, facts);
8605
+ break;
8606
+ case "ExportDefaultDeclaration":
8607
+ collectExportDefault(stmt, facts);
8608
+ break;
8609
+ }
8610
+ walk(program, facts);
8611
+ return facts;
8612
+ }
8613
+ //#endregion
8614
+ //#region src/plugins/request-dep/origin.ts
8204
8615
  var ACCESSOR_SOURCES = /* @__PURE__ */ new Set([
8205
8616
  "@timber-js/app/server",
8206
8617
  "next/headers",
@@ -8217,165 +8628,326 @@ var ACCESSOR_NAMES = /* @__PURE__ */ new Set([
8217
8628
  ]);
8218
8629
  var DEFINITION_FACTORY_SOURCES = /* @__PURE__ */ new Set(["@timber-js/app/search-params", "@timber-js/app/cookies"]);
8219
8630
  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);
8631
+ /** Every specifier the classifier recognises without resolving it. */
8632
+ var FRAMEWORK_SOURCES = /* @__PURE__ */ new Set([...ACCESSOR_SOURCES, ...DEFINITION_FACTORY_SOURCES]);
8633
+ var NONE = { kind: "none" };
8634
+ var UNKNOWN = { kind: "unknown" };
8635
+ /**
8636
+ * Classify an edge that points at a framework module, without resolving it.
8637
+ *
8638
+ * Returns `null` when the specifier is not a framework source, which is the
8639
+ * signal to keep walking. Both name sets are consulted for either source so a
8640
+ * specifier that grows a second role does not silently stop matching.
8641
+ */
8642
+ function classifySpecifier(source, imported) {
8643
+ const isAccessorSource = ACCESSOR_SOURCES.has(source);
8644
+ const isFactorySource = DEFINITION_FACTORY_SOURCES.has(source);
8645
+ if (!isAccessorSource && !isFactorySource) return null;
8646
+ if (imported === "*") return {
8647
+ kind: "framework-namespace",
8648
+ source
8649
+ };
8650
+ if (isAccessorSource && ACCESSOR_NAMES.has(imported)) return { kind: "accessor" };
8651
+ if (isFactorySource && DEFINITION_FACTORY_NAMES.has(imported)) return { kind: "factory" };
8652
+ return NONE;
8222
8653
  }
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;
8654
+ /** An origin whose *call* reads the request. */
8655
+ function callReadsRequest(origin) {
8656
+ return origin.kind === "accessor" || origin.kind === "unknown";
8238
8657
  }
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;
8658
+ /** An origin whose `.get()` reads the request. */
8659
+ function getterReadsRequest(origin) {
8660
+ return origin.kind === "definition" || origin.kind === "unknown";
8661
+ }
8662
+ function createOriginResolver(factsMap) {
8663
+ /**
8664
+ * Cycle guard. A `(module, export)` pair already being resolved contributes
8665
+ * `none`: anything it could reach is reachable on the path still unwinding.
8666
+ */
8667
+ const resolving = /* @__PURE__ */ new Map();
8668
+ function enter(moduleId, name) {
8669
+ const names = resolving.get(moduleId);
8670
+ if (!names) {
8671
+ resolving.set(moduleId, /* @__PURE__ */ new Set([name]));
8672
+ return true;
8249
8673
  }
8674
+ if (names.has(name)) return false;
8675
+ names.add(name);
8676
+ return true;
8250
8677
  }
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;
8678
+ function leave(moduleId, name) {
8679
+ resolving.get(moduleId)?.delete(name);
8680
+ }
8681
+ /** Follow one `import`/`export … from` edge out of `moduleId`. */
8682
+ function resolveEdge(moduleId, edge) {
8683
+ const classified = classifySpecifier(edge.source, edge.imported);
8684
+ if (classified) return classified;
8685
+ const facts = factsMap.get(moduleId);
8686
+ const targetId = facts?.resolvedSources.get(edge.source);
8687
+ if (!targetId) return facts?.unresolvedSources.has(edge.source) ? UNKNOWN : NONE;
8688
+ if (edge.imported === "*") return {
8689
+ kind: "namespace",
8690
+ moduleId: targetId
8691
+ };
8692
+ return resolveExport(targetId, edge.imported);
8254
8693
  }
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
- }
8694
+ /** What does `moduleId` export under `name`? */
8695
+ function resolveExport(moduleId, name) {
8696
+ const facts = factsMap.get(moduleId);
8697
+ if (!facts) return NONE;
8698
+ if (!enter(moduleId, `export:${name}`)) return NONE;
8699
+ try {
8700
+ const reExport = facts.reExports.get(name);
8701
+ if (reExport) return resolveEdge(moduleId, reExport);
8702
+ const local = facts.localExports.get(name);
8703
+ if (local !== void 0) return resolveLocal(moduleId, local);
8704
+ for (const source of facts.starReExports) {
8705
+ const viaStar = resolveEdge(moduleId, {
8706
+ source,
8707
+ imported: name
8708
+ });
8709
+ if (viaStar.kind !== "none") return viaStar;
8277
8710
  }
8711
+ return NONE;
8712
+ } finally {
8713
+ leave(moduleId, `export:${name}`);
8278
8714
  }
8279
8715
  }
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;
8716
+ /** What is the local binding `name` in `moduleId`? */
8717
+ function resolveLocal(moduleId, name) {
8718
+ const facts = factsMap.get(moduleId);
8719
+ if (!facts) return NONE;
8720
+ if (!enter(moduleId, `local:${name}`)) return NONE;
8721
+ try {
8722
+ const imported = facts.importedBindings.get(name);
8723
+ if (imported) return resolveEdge(moduleId, imported);
8724
+ const factoryCall = facts.factoryCalls.get(name);
8725
+ if (factoryCall) {
8726
+ const callee = resolveCallee(moduleId, factoryCall);
8727
+ if (callee.kind === "factory") return { kind: "definition" };
8728
+ if (callee.kind === "unknown") return UNKNOWN;
8305
8729
  }
8730
+ return NONE;
8731
+ } finally {
8732
+ leave(moduleId, `local:${name}`);
8306
8733
  }
8307
8734
  }
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;
8735
+ function resolveCallee(moduleId, ref) {
8736
+ if (ref.kind === "local") return resolveLocal(moduleId, ref.name);
8737
+ return resolveMember(moduleId, ref.object, ref.property);
8738
+ }
8739
+ /** `ns.name` where `ns` is a local binding in `moduleId`. */
8740
+ function resolveMember(moduleId, object, name) {
8741
+ const base = resolveLocal(moduleId, object);
8742
+ if (base.kind === "framework-namespace") return classifySpecifier(base.source, name) ?? NONE;
8743
+ if (base.kind === "namespace") return resolveExport(base.moduleId, name);
8744
+ if (base.kind === "unknown") return UNKNOWN;
8745
+ return NONE;
8746
+ }
8747
+ const taintCache = /* @__PURE__ */ new Map();
8748
+ function computeTainted(moduleId, facts) {
8749
+ for (const local of facts.calledLocals) if (callReadsRequest(resolveLocal(moduleId, local))) return true;
8750
+ for (const [object, members] of facts.calledMembers) for (const member of members) if (callReadsRequest(resolveMember(moduleId, object, member))) return true;
8751
+ for (const local of facts.getterLocals) if (getterReadsRequest(resolveLocal(moduleId, local))) return true;
8752
+ for (const [object, members] of facts.getterMembers) for (const member of members) if (getterReadsRequest(resolveMember(moduleId, object, member))) return true;
8753
+ return holdsRequestBinding(moduleId, facts);
8754
+ }
8755
+ /**
8756
+ * Possession, not use: a module that binds a request accessor or a definition
8757
+ * value under a local name is request-dependent whether or not the call is
8758
+ * one this analysis can see.
8759
+ *
8760
+ * The call-shape rules above only find a read that happens through the
8761
+ * binding itself. A value can leave the binding first — `const read = flag ?
8762
+ * getSearchParams : noop`, `const { get } = filters`, a prop handed to a
8763
+ * helper — and every one of those is a fresh way to be missed, which is the
8764
+ * shape of bug this ticket exists to stop enumerating. Nothing distinguishes
8765
+ * those from an honest unused import except intent, so importing the accessor
8766
+ * at all is treated as reading it.
8767
+ *
8768
+ * The rule is scoped by *reference*, not by export shape, and that scoping is
8769
+ * what keeps it from being useless: `import { getSearchParams } from
8770
+ * '@timber-js/app/server'; export { getSearchParams }` is a barrel whose body
8771
+ * never mentions the name, and tainting it would taint every page that
8772
+ * imports anything from that barrel. The forwarded name stays resolvable
8773
+ * through `resolveExport`, so the page that actually calls it is still caught
8774
+ * one hop later, in the module that uses it.
8775
+ *
8776
+ * Scoping on "the module only forwards it" would have been the natural
8777
+ * phrasing and is wrong: a barrel can forward a name *and* use it
8778
+ * (`export { getSearchParams }` beside `const read = flag ? getSearchParams
8779
+ * : noop`), and that module is exactly the one this rule exists to catch.
8780
+ * `referencedLocals` excludes import/export clauses and nothing else, so
8781
+ * forwarding is exempt only when it is all the module does with the name.
8782
+ *
8783
+ * `unknown` is deliberately not included: an unresolvable specifier taints
8784
+ * only when something is actually called or read on it, or every module with
8785
+ * one broken import would go request-dependent.
8786
+ */
8787
+ function holdsRequestBinding(moduleId, facts) {
8788
+ for (const local of facts.referencedLocals) {
8789
+ const origin = resolveLocal(moduleId, local);
8790
+ if (origin.kind === "accessor" || origin.kind === "definition") return true;
8333
8791
  }
8792
+ return false;
8334
8793
  }
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;
8794
+ return {
8795
+ isTainted(moduleId) {
8796
+ const cached = taintCache.get(moduleId);
8797
+ if (cached !== void 0) return cached;
8798
+ const facts = factsMap.get(moduleId);
8799
+ const result = facts ? computeTainted(moduleId, facts) : false;
8800
+ taintCache.set(moduleId, result);
8801
+ return result;
8802
+ },
8803
+ isDefinitionModule(moduleId) {
8804
+ const facts = factsMap.get(moduleId);
8805
+ if (!facts) return false;
8806
+ for (const edge of facts.importedBindings.values()) {
8807
+ const origin = resolveEdge(moduleId, edge);
8808
+ if (origin.kind === "factory") return true;
8809
+ if (origin.kind === "framework-namespace" && DEFINITION_FACTORY_SOURCES.has(origin.source)) return true;
8345
8810
  }
8811
+ return false;
8812
+ },
8813
+ callsGetterOnImport(moduleId) {
8814
+ const facts = factsMap.get(moduleId);
8815
+ if (!facts) return false;
8816
+ for (const local of facts.getterLocals) if (facts.importedBindings.has(local)) return true;
8817
+ return false;
8346
8818
  }
8347
- }
8348
- return false;
8819
+ };
8820
+ }
8821
+ //#endregion
8822
+ //#region src/plugins/request-dep/index.ts
8823
+ function isFrameworkModule(id) {
8824
+ return /[\\/]node_modules[\\/]@timber-js[\\/]app[\\/]/.test(id) || /[\\/]packages[\\/]timber-app[\\/]src[\\/]/.test(id);
8349
8825
  }
8350
- /** Walk the module graph from an entry, returning true if any taint source is reachable. */
8351
- function isReachableTaintSource(entryId, factsMap, definitionModules, getImports) {
8826
+ /**
8827
+ * Cheap gate before parsing.
8828
+ *
8829
+ * This used to be a substring match on the accessor and factory names, which
8830
+ * was sound only for the analysis it served: when a call site had to name the
8831
+ * framework binding in its own import declaration, a module with no such name
8832
+ * could not be a call site. Resolving bindings across the graph removes that
8833
+ * guarantee — with
8834
+ *
8835
+ * // _lib/index.ts
8836
+ * export { getSearchParams as query } from '@timber-js/app/server';
8837
+ * // page.tsx
8838
+ * import { query } from './_lib';
8839
+ * export default function Page() { return query(); }
8840
+ *
8841
+ * the page contains no accessor name, no `.get(`, and no re-export syntax, and
8842
+ * skipping it would have put the rename right back where the barrel was. So the
8843
+ * gate is now structural: a module matters if it has any module-level binding
8844
+ * to follow. What it skips is source with no `import` and no `export` at all —
8845
+ * data blobs, generated constants, side-effect scripts — none of which can name
8846
+ * a framework binding or forward one.
8847
+ */
8848
+ function hasModuleBindings(code) {
8849
+ return code.includes("import") || code.includes("export");
8850
+ }
8851
+ /**
8852
+ * Walk the module graph from an entry, returning true if any module reachable
8853
+ * from it reads the request.
8854
+ *
8855
+ * Two passes, and the second is a deliberately blunt backstop:
8856
+ *
8857
+ * 1. **Binding origins.** For each reachable module, resolve every call site's
8858
+ * binding to its origin. This is precise and indifferent to indirection.
8859
+ * 2. **Definition reachability.** A module that calls `.get()` on *something*
8860
+ * it imported, with a module holding a definition factory reachable behind
8861
+ * that import, counts too. The origin walk cannot see through a value that
8862
+ * is computed rather than bound — `getFilterCookie(key).get()`, where the
8863
+ * helper returns a definition — and this pass can.
8864
+ *
8865
+ * The second pass over-reports by construction, which is the safe direction: a
8866
+ * needlessly request-dependent segment costs a re-render, a wrongly static one
8867
+ * serves stale content.
8868
+ *
8869
+ * An entry the graph does not contain is an edge we failed to walk, not the
8870
+ * absence of one — the same rule an unresolvable specifier gets. Without the
8871
+ * check it reads as an entry with no imports and no facts, and both passes fall
8872
+ * through to `false`: confidently request-INdependent, for a module nothing
8873
+ * ever looked at. In the build that answer is worse than no answer, because it
8874
+ * is written into `requestDep` as a definite `false` and the runtime's own
8875
+ * "absent key → conservative" fallback never fires.
8876
+ */
8877
+ function isReachableTaintSource(entryId, input) {
8878
+ const { factsMap, getImports, isKnownModule } = input;
8879
+ if (!isKnownModule(entryId)) return true;
8880
+ const resolver = createOriginResolver(factsMap);
8352
8881
  const visited = /* @__PURE__ */ new Set();
8353
8882
  const stack = [entryId];
8354
8883
  while (stack.length > 0) {
8355
8884
  const id = stack.pop();
8356
8885
  if (visited.has(id)) continue;
8357
8886
  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);
8887
+ if (resolver.isTainted(id)) return true;
8888
+ for (const imp of getImports(id)) if (!imp.startsWith("\0") && !visited.has(imp)) stack.push(imp);
8365
8889
  }
8890
+ const reachesDefinition = makeDefinitionReachability(resolver.isDefinitionModule, getImports);
8366
8891
  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;
8892
+ if (!resolver.callsGetterOnImport(id)) continue;
8893
+ if (reachesDefinition(id)) return true;
8372
8894
  }
8373
8895
  return false;
8374
8896
  }
8897
+ /**
8898
+ * "Can a definition module be reached from this module's imports?", memoized
8899
+ * across the queries one entry makes.
8900
+ *
8901
+ * Walks the graph rather than testing direct imports, because `export … from`
8902
+ * is an ordinary module edge and a barrel is the common way an app shares one
8903
+ * `defineSearchParams` across routes.
8904
+ *
8905
+ * Cycles terminate on the memo seeded with `false`; a module already being
8906
+ * resolved contributes nothing, which is correct — anything it could reach is
8907
+ * reachable on the path that is still unwinding.
8908
+ */
8909
+ function makeDefinitionReachability(isDefinitionModule, getImports) {
8910
+ const cache = /* @__PURE__ */ new Map();
8911
+ return function reaches(id) {
8912
+ const cached = cache.get(id);
8913
+ if (cached !== void 0) return cached;
8914
+ cache.set(id, false);
8915
+ let result = false;
8916
+ for (const imp of getImports(id)) {
8917
+ if (imp.startsWith("\0")) continue;
8918
+ if (isDefinitionModule(imp) || reaches(imp)) {
8919
+ result = true;
8920
+ break;
8921
+ }
8922
+ }
8923
+ cache.set(id, result);
8924
+ return result;
8925
+ };
8926
+ }
8375
8927
  var BRIDGE_KEY = Symbol.for("timber:request-dep");
8928
+ /**
8929
+ * Resolve every specifier the module has an edge to, so the origin walk can
8930
+ * follow them later without a module graph.
8931
+ *
8932
+ * Framework specifiers are classified by name and never resolved — that is what
8933
+ * keeps the terminal check independent of how `@timber-js/app` happens to be
8934
+ * installed. A specifier that fails to resolve is recorded as unresolved rather
8935
+ * than dropped, so the walk can take the conservative branch on it.
8936
+ */
8937
+ async function resolveEdgeSources(pluginCtx, facts, importer) {
8938
+ for (const source of usedSources(facts)) {
8939
+ if (FRAMEWORK_SOURCES.has(source)) continue;
8940
+ try {
8941
+ const resolved = await pluginCtx.resolve(source, importer);
8942
+ if (resolved?.id) facts.resolvedSources.set(source, resolved.id);
8943
+ else facts.unresolvedSources.add(source);
8944
+ } catch {
8945
+ facts.unresolvedSources.add(source);
8946
+ }
8947
+ }
8948
+ }
8376
8949
  function timberRequestDep(ctx) {
8377
8950
  const factsMap = /* @__PURE__ */ new Map();
8378
- const definitionModules = /* @__PURE__ */ new Set();
8379
8951
  const cache = /* @__PURE__ */ new Map();
8380
8952
  return {
8381
8953
  name: "timber-request-dep",
@@ -8386,12 +8958,15 @@ function timberRequestDep(ctx) {
8386
8958
  if (!/\.[mc]?[jt]sx?$/.test(id) && !/\.mdx?$/.test(id)) return;
8387
8959
  if (id.startsWith("\0") || id.startsWith("virtual:")) return;
8388
8960
  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);
8961
+ if (!hasModuleBindings(code)) return;
8962
+ const pluginCtx = this;
8963
+ const facts = analyzeModule(pluginCtx.parse(code));
8964
+ if (contributesNothing(facts)) {
8965
+ factsMap.delete(id);
8966
+ return;
8394
8967
  }
8968
+ await resolveEdgeSources(pluginCtx, facts, id);
8969
+ factsMap.set(id, facts);
8395
8970
  },
8396
8971
  configureServer(server) {
8397
8972
  const resolver = (filePath) => {
@@ -8405,8 +8980,13 @@ function timberRequestDep(ctx) {
8405
8980
  for (const imp of mod.importedModules) if (imp.id) result.push(imp.id);
8406
8981
  return result;
8407
8982
  };
8408
- const result = isReachableTaintSource(filePath, factsMap, definitionModules, getImports);
8409
- cache.set(filePath, result);
8983
+ const isKnownModule = (moduleId) => rscEnv.moduleGraph.getModuleById(moduleId) !== void 0;
8984
+ const result = isReachableTaintSource(filePath, {
8985
+ factsMap,
8986
+ getImports,
8987
+ isKnownModule
8988
+ });
8989
+ if (isKnownModule(filePath)) cache.set(filePath, result);
8410
8990
  return result;
8411
8991
  };
8412
8992
  globalThis[BRIDGE_KEY] = resolver;
@@ -8423,10 +9003,14 @@ function timberRequestDep(ctx) {
8423
9003
  if (!info) return [];
8424
9004
  return [...info.importedIds ?? [], ...info.dynamicallyImportedIds ?? []];
8425
9005
  };
9006
+ const isKnownModule = (moduleId) => getModuleInfo(moduleId) != null;
8426
9007
  const requestDepMap = {};
8427
9008
  if (ctx.routeTree) walkSegmentTree(ctx.routeTree.root, (filePath) => {
8428
- const result = isReachableTaintSource(filePath, factsMap, definitionModules, getImports);
8429
- requestDepMap[filePath] = result;
9009
+ requestDepMap[filePath] = isReachableTaintSource(filePath, {
9010
+ factsMap,
9011
+ getImports,
9012
+ isKnownModule
9013
+ });
8430
9014
  });
8431
9015
  ctx.requestDepMap = requestDepMap;
8432
9016
  }
@@ -10445,6 +11029,7 @@ function timber(config) {
10445
11029
  ctx.root = resolved.root;
10446
11030
  ctx.appDir = resolveAppDir(resolved.root, ctx.config.appDir);
10447
11031
  ctx.dev = resolved.command === "serve";
11032
+ ctx.base = resolved.base;
10448
11033
  for (const name of [
10449
11034
  "timber.config.ts",
10450
11035
  "timber.config.js",