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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) hide show
  1. package/dist/_chunks/{actions-O_LsyCE4.js → actions-C-Rw9vPc.js} +3 -3
  2. package/dist/_chunks/{actions-O_LsyCE4.js.map → actions-C-Rw9vPc.js.map} +1 -1
  3. package/dist/_chunks/als-slots-mFweg276.js +27 -0
  4. package/dist/_chunks/als-slots-mFweg276.js.map +1 -0
  5. package/dist/_chunks/base-path-DaQrzbez.js +80 -0
  6. package/dist/_chunks/base-path-DaQrzbez.js.map +1 -0
  7. package/dist/_chunks/build-manifest-DWppEdLB.js +86 -0
  8. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  9. package/dist/_chunks/build-output-helper-C3DrfzZR.js +369 -0
  10. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +1 -0
  11. package/dist/_chunks/{cache-api-B-lhk9p4.js → cache-api-Cd0VZ_Pd.js} +3 -3
  12. package/dist/_chunks/{cache-api-B-lhk9p4.js.map → cache-api-Cd0VZ_Pd.js.map} +1 -1
  13. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js +1857 -0
  14. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +1 -0
  15. package/dist/_chunks/{cloudflare-CnT5Lr7U.js → cloudflare-Cs0uZXea.js} +89 -12
  16. package/dist/_chunks/{cloudflare-CnT5Lr7U.js.map → cloudflare-Cs0uZXea.js.map} +1 -1
  17. package/dist/_chunks/error-boundary-DpYRI_I1.js +330 -0
  18. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +1 -0
  19. package/dist/_chunks/{fast-hash-D6hIVt1Y.js → fast-hash-C-tVYrLb.js} +6 -1
  20. package/dist/_chunks/fast-hash-C-tVYrLb.js.map +1 -0
  21. package/dist/_chunks/fs-identity-D7vb6GdI.js +208 -0
  22. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +1 -0
  23. package/dist/_chunks/{logger-AWfuX-KJ.js → logger-N7e5auP0.js} +9 -13
  24. package/dist/_chunks/logger-N7e5auP0.js.map +1 -0
  25. package/dist/_chunks/navigation-root-B29qg0_T.js +274 -0
  26. package/dist/_chunks/navigation-root-B29qg0_T.js.map +1 -0
  27. package/dist/_chunks/param-value-C8TNYchQ.js +66 -0
  28. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  29. package/dist/_chunks/{plugin-context---kTF5v8.js → plugin-context-DEGLSJs3.js} +2 -1
  30. package/dist/_chunks/plugin-context-DEGLSJs3.js.map +1 -0
  31. package/dist/_chunks/{router-ref-CtmF-aPv.js → router-ref-DuYuV_0Q.js} +2 -2
  32. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -0
  33. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js +243 -0
  34. package/dist/_chunks/rsc-cache-key-DD0fl_-s.js.map +1 -0
  35. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +40 -0
  36. package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +1 -0
  37. package/dist/_chunks/rsc-payload-path-B_LBodc2.js +48 -0
  38. package/dist/_chunks/rsc-payload-path-B_LBodc2.js.map +1 -0
  39. package/dist/_chunks/{segment-classify-Byy425ng.js → segment-classify-C539Pa2O.js} +23 -2
  40. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -0
  41. package/dist/_chunks/{canonicalize-DQHyFClh.js → segment-keys-BawYuNFO.js} +61 -2
  42. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -0
  43. package/dist/_chunks/slot-params-BCTmZkQB.js +76 -0
  44. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -0
  45. package/dist/_chunks/{ssr-data-BOWsq18U.js → ssr-data-14MXm7Pj.js} +17 -2
  46. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -0
  47. package/dist/_chunks/use-segment-params-ClyUNq4d.js +128 -0
  48. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +1 -0
  49. package/dist/_chunks/{walkers-BtTlKviE.js → walkers-BhhwI9TD.js} +21 -87
  50. package/dist/_chunks/walkers-BhhwI9TD.js.map +1 -0
  51. package/dist/adapters/build-output-helper.d.ts +37 -0
  52. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  53. package/dist/adapters/cloudflare-dev.js +1 -1
  54. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  55. package/dist/adapters/cloudflare.d.ts.map +1 -1
  56. package/dist/adapters/cloudflare.js +1 -1
  57. package/dist/adapters/fs-identity.d.ts +64 -0
  58. package/dist/adapters/fs-identity.d.ts.map +1 -0
  59. package/dist/adapters/nitro.d.ts.map +1 -1
  60. package/dist/adapters/nitro.js +2 -3
  61. package/dist/adapters/nitro.js.map +1 -1
  62. package/dist/adapters/shared.d.ts +57 -1
  63. package/dist/adapters/shared.d.ts.map +1 -1
  64. package/dist/adapters/types.d.ts +13 -1
  65. package/dist/adapters/types.d.ts.map +1 -1
  66. package/dist/cache/fast-hash.d.ts +5 -0
  67. package/dist/cache/fast-hash.d.ts.map +1 -1
  68. package/dist/cache/index.js +1 -1
  69. package/dist/cli.js +2 -2
  70. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  71. package/dist/client/browser-entry/hydrate.d.ts +20 -9
  72. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  73. package/dist/client/browser-entry/post-hydration.d.ts +4 -1
  74. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  75. package/dist/client/browser-entry/router-init.d.ts +3 -0
  76. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  77. package/dist/client/browser-entry/rsc-stream.d.ts +8 -0
  78. package/dist/client/browser-entry/rsc-stream.d.ts.map +1 -1
  79. package/dist/client/error-boundary.d.ts +1 -0
  80. package/dist/client/error-boundary.d.ts.map +1 -1
  81. package/dist/client/error-boundary.js +1 -162
  82. package/dist/client/history.d.ts +13 -6
  83. package/dist/client/history.d.ts.map +1 -1
  84. package/dist/client/index.js +18 -11
  85. package/dist/client/index.js.map +1 -1
  86. package/dist/client/internal.d.ts +4 -3
  87. package/dist/client/internal.d.ts.map +1 -1
  88. package/dist/client/internal.js +872 -305
  89. package/dist/client/internal.js.map +1 -1
  90. package/dist/client/link.d.ts.map +1 -1
  91. package/dist/client/navigation-api.d.ts +2 -2
  92. package/dist/client/navigation-commit.d.ts +71 -0
  93. package/dist/client/navigation-commit.d.ts.map +1 -0
  94. package/dist/client/navigation-context.d.ts +37 -20
  95. package/dist/client/navigation-context.d.ts.map +1 -1
  96. package/dist/client/navigation-root.d.ts +63 -23
  97. package/dist/client/navigation-root.d.ts.map +1 -1
  98. package/dist/client/params-context.d.ts +81 -0
  99. package/dist/client/params-context.d.ts.map +1 -0
  100. package/dist/client/router-effects.d.ts +129 -0
  101. package/dist/client/router-effects.d.ts.map +1 -0
  102. package/dist/client/router-lifecycle.d.ts +55 -0
  103. package/dist/client/router-lifecycle.d.ts.map +1 -0
  104. package/dist/client/router-pipeline.d.ts +84 -0
  105. package/dist/client/router-pipeline.d.ts.map +1 -0
  106. package/dist/client/router-ref.d.ts +1 -1
  107. package/dist/client/router-ref.d.ts.map +1 -1
  108. package/dist/client/router-skew.d.ts +21 -0
  109. package/dist/client/router-skew.d.ts.map +1 -0
  110. package/dist/client/router-types.d.ts +207 -0
  111. package/dist/client/router-types.d.ts.map +1 -0
  112. package/dist/client/router.d.ts +1 -182
  113. package/dist/client/router.d.ts.map +1 -1
  114. package/dist/client/rsc-fetch.d.ts +39 -16
  115. package/dist/client/rsc-fetch.d.ts.map +1 -1
  116. package/dist/client/segment-cache.d.ts +79 -34
  117. package/dist/client/segment-cache.d.ts.map +1 -1
  118. package/dist/client/ssr-data.d.ts +6 -0
  119. package/dist/client/ssr-data.d.ts.map +1 -1
  120. package/dist/client/stale-client.d.ts +69 -0
  121. package/dist/client/stale-client.d.ts.map +1 -0
  122. package/dist/client/state.d.ts +10 -1
  123. package/dist/client/state.d.ts.map +1 -1
  124. package/dist/client/top-loader.d.ts +2 -1
  125. package/dist/client/top-loader.d.ts.map +1 -1
  126. package/dist/client/unload-guard.d.ts +5 -0
  127. package/dist/client/unload-guard.d.ts.map +1 -1
  128. package/dist/client/use-cookie.d.ts.map +1 -1
  129. package/dist/client/use-router.d.ts +1 -1
  130. package/dist/client/use-segment-params.d.ts +18 -8
  131. package/dist/client/use-segment-params.d.ts.map +1 -1
  132. package/dist/cookies/index.js +3 -3
  133. package/dist/cookies/index.js.map +1 -1
  134. package/dist/index.d.ts.map +1 -1
  135. package/dist/index.js +915 -320
  136. package/dist/index.js.map +1 -1
  137. package/dist/plugin-context.d.ts +33 -1
  138. package/dist/plugin-context.d.ts.map +1 -1
  139. package/dist/plugins/adapter-build.d.ts.map +1 -1
  140. package/dist/plugins/build-manifest.d.ts +1 -1
  141. package/dist/plugins/build-manifest.d.ts.map +1 -1
  142. package/dist/plugins/entries.d.ts.map +1 -1
  143. package/dist/plugins/fonts.d.ts.map +1 -1
  144. package/dist/plugins/request-dep/analysis.d.ts +106 -0
  145. package/dist/plugins/request-dep/analysis.d.ts.map +1 -0
  146. package/dist/plugins/request-dep/index.d.ts +75 -0
  147. package/dist/plugins/request-dep/index.d.ts.map +1 -0
  148. package/dist/plugins/request-dep/origin.d.ts +61 -0
  149. package/dist/plugins/request-dep/origin.d.ts.map +1 -0
  150. package/dist/plugins/routing.d.ts +9 -0
  151. package/dist/plugins/routing.d.ts.map +1 -1
  152. package/dist/plugins/static-build.d.ts +149 -8
  153. package/dist/plugins/static-build.d.ts.map +1 -1
  154. package/dist/routing/codegen-types.d.ts +18 -0
  155. package/dist/routing/codegen-types.d.ts.map +1 -1
  156. package/dist/routing/codegen.d.ts.map +1 -1
  157. package/dist/routing/collision-probe.d.ts +104 -0
  158. package/dist/routing/collision-probe.d.ts.map +1 -0
  159. package/dist/routing/collision-spaces.d.ts +26 -0
  160. package/dist/routing/collision-spaces.d.ts.map +1 -0
  161. package/dist/routing/index.js +3 -3
  162. package/dist/routing/interception.d.ts +235 -6
  163. package/dist/routing/interception.d.ts.map +1 -1
  164. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  165. package/dist/routing/scanner.d.ts.map +1 -1
  166. package/dist/routing/segment-classify.d.ts +33 -0
  167. package/dist/routing/segment-classify.d.ts.map +1 -1
  168. package/dist/routing/segment-keys.d.ts +95 -0
  169. package/dist/routing/segment-keys.d.ts.map +1 -0
  170. package/dist/routing/slot-placement.d.ts +48 -0
  171. package/dist/routing/slot-placement.d.ts.map +1 -0
  172. package/dist/routing/types.d.ts +1 -3
  173. package/dist/routing/types.d.ts.map +1 -1
  174. package/dist/routing/walkers.d.ts +6 -0
  175. package/dist/routing/walkers.d.ts.map +1 -1
  176. package/dist/search-params/define.d.ts +0 -6
  177. package/dist/search-params/define.d.ts.map +1 -1
  178. package/dist/search-params/index.js +176 -2
  179. package/dist/search-params/index.js.map +1 -1
  180. package/dist/segment-params/define.d.ts +0 -6
  181. package/dist/segment-params/define.d.ts.map +1 -1
  182. package/dist/segment-params/index.js +69 -1
  183. package/dist/segment-params/index.js.map +1 -0
  184. package/dist/server/actions.d.ts +6 -3
  185. package/dist/server/actions.d.ts.map +1 -1
  186. package/dist/server/als-registry.d.ts +2 -1
  187. package/dist/server/als-registry.d.ts.map +1 -1
  188. package/dist/server/build-manifest.d.ts +13 -1
  189. package/dist/server/build-manifest.d.ts.map +1 -1
  190. package/dist/server/chain-url-parts.d.ts +82 -0
  191. package/dist/server/chain-url-parts.d.ts.map +1 -0
  192. package/dist/server/children-interception.d.ts +49 -0
  193. package/dist/server/children-interception.d.ts.map +1 -0
  194. package/dist/server/default-status-page.d.ts +45 -0
  195. package/dist/server/default-status-page.d.ts.map +1 -0
  196. package/dist/server/deny-boundary.d.ts +4 -7
  197. package/dist/server/deny-boundary.d.ts.map +1 -1
  198. package/dist/server/deny-renderer.d.ts +14 -3
  199. package/dist/server/deny-renderer.d.ts.map +1 -1
  200. package/dist/server/head-response.d.ts +35 -0
  201. package/dist/server/head-response.d.ts.map +1 -0
  202. package/dist/server/index.js +2 -2
  203. package/dist/server/internal.js +528 -373
  204. package/dist/server/internal.js.map +1 -1
  205. package/dist/server/param-coercion.d.ts +10 -1
  206. package/dist/server/param-coercion.d.ts.map +1 -1
  207. package/dist/server/pipeline-helpers.d.ts +0 -18
  208. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  209. package/dist/server/pipeline-interception.d.ts +59 -9
  210. package/dist/server/pipeline-interception.d.ts.map +1 -1
  211. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  212. package/dist/server/pipeline-phases.d.ts.map +1 -1
  213. package/dist/server/pipeline.d.ts +11 -0
  214. package/dist/server/pipeline.d.ts.map +1 -1
  215. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  216. package/dist/server/primitives.d.ts +0 -12
  217. package/dist/server/primitives.d.ts.map +1 -1
  218. package/dist/server/publish-params.d.ts +33 -0
  219. package/dist/server/publish-params.d.ts.map +1 -0
  220. package/dist/server/request-context.d.ts +48 -0
  221. package/dist/server/request-context.d.ts.map +1 -1
  222. package/dist/server/route-element-builder.d.ts +32 -2
  223. package/dist/server/route-element-builder.d.ts.map +1 -1
  224. package/dist/server/route-handler.d.ts.map +1 -1
  225. package/dist/server/route-matcher.d.ts +13 -0
  226. package/dist/server/route-matcher.d.ts.map +1 -1
  227. package/dist/server/rsc-cache-key-guard.d.ts +78 -0
  228. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -0
  229. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  230. package/dist/server/rsc-entry/error-renderer.d.ts +14 -2
  231. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  232. package/dist/server/rsc-entry/helpers.d.ts +2 -10
  233. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  234. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  235. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  236. package/dist/server/rsc-entry/rsc-payload.d.ts +3 -2
  237. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  238. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -2
  239. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  240. package/dist/server/rsc-entry/ssr-renderer.d.ts +2 -0
  241. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  242. package/dist/server/sitemap-generator.d.ts.map +1 -1
  243. package/dist/server/skippable-prefix.d.ts +23 -0
  244. package/dist/server/skippable-prefix.d.ts.map +1 -0
  245. package/dist/server/slot-interception.d.ts +80 -0
  246. package/dist/server/slot-interception.d.ts.map +1 -0
  247. package/dist/server/slot-resolver.d.ts +70 -8
  248. package/dist/server/slot-resolver.d.ts.map +1 -1
  249. package/dist/server/ssr-bridge-types.d.ts +6 -0
  250. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  251. package/dist/server/ssr-entry.d.ts.map +1 -1
  252. package/dist/server/ssr-wrappers.d.ts +24 -13
  253. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  254. package/dist/server/state-tree-diff.d.ts +6 -27
  255. package/dist/server/state-tree-diff.d.ts.map +1 -1
  256. package/dist/server/static-generator.d.ts +6 -0
  257. package/dist/server/static-generator.d.ts.map +1 -1
  258. package/dist/server/static-not-found.d.ts +52 -0
  259. package/dist/server/static-not-found.d.ts.map +1 -0
  260. package/dist/server/status-code-resolver.d.ts +3 -10
  261. package/dist/server/status-code-resolver.d.ts.map +1 -1
  262. package/dist/shared/als-slots.d.ts +41 -0
  263. package/dist/shared/als-slots.d.ts.map +1 -0
  264. package/dist/shared/base-path.d.ts +56 -0
  265. package/dist/shared/base-path.d.ts.map +1 -0
  266. package/dist/shared/param-value.d.ts +80 -0
  267. package/dist/shared/param-value.d.ts.map +1 -0
  268. package/dist/shared/payload-root.d.ts +108 -0
  269. package/dist/shared/payload-root.d.ts.map +1 -0
  270. package/dist/shared/rsc-cache-key.d.ts +163 -0
  271. package/dist/shared/rsc-cache-key.d.ts.map +1 -0
  272. package/dist/shared/rsc-error-envelope.d.ts +25 -0
  273. package/dist/shared/rsc-error-envelope.d.ts.map +1 -0
  274. package/dist/shared/rsc-manifest.d.ts +28 -0
  275. package/dist/shared/rsc-manifest.d.ts.map +1 -0
  276. package/dist/shared/rsc-payload-path.d.ts +42 -0
  277. package/dist/shared/rsc-payload-path.d.ts.map +1 -0
  278. package/dist/shared/segment-info.d.ts +59 -0
  279. package/dist/shared/segment-info.d.ts.map +1 -0
  280. package/dist/shared/slot-params.d.ts +51 -0
  281. package/dist/shared/slot-params.d.ts.map +1 -0
  282. package/dist/shared/static-platform-files.d.ts +71 -0
  283. package/dist/shared/static-platform-files.d.ts.map +1 -0
  284. package/dist/shims/navigation-client.d.ts +8 -19
  285. package/dist/shims/navigation-client.d.ts.map +1 -1
  286. package/dist/shims/navigation.d.ts +0 -1
  287. package/dist/shims/navigation.d.ts.map +1 -1
  288. package/docs/api/31-api-client.mdx +4 -2
  289. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  290. package/docs/learn/11-error-handling.mdx +4 -3
  291. package/docs/learn/12-client-navigation.mdx +2 -2
  292. package/docs/learn/14-deploying.mdx +23 -0
  293. package/docs/more/01-advanced-routing.mdx +172 -2
  294. package/docs/more/04-metadata-and-fonts.mdx +1 -1
  295. package/package.json +2 -2
  296. package/src/adapters/build-output-helper.ts +116 -3
  297. package/src/adapters/cloudflare.ts +86 -11
  298. package/src/adapters/fs-identity.ts +156 -0
  299. package/src/adapters/nitro.ts +6 -6
  300. package/src/adapters/shared.ts +196 -21
  301. package/src/adapters/types.ts +13 -1
  302. package/src/cache/fast-hash.ts +5 -0
  303. package/src/client/browser-entry/action-dispatch.ts +9 -4
  304. package/src/client/browser-entry/hydrate.ts +40 -29
  305. package/src/client/browser-entry/index.ts +15 -1
  306. package/src/client/browser-entry/post-hydration.ts +10 -9
  307. package/src/client/browser-entry/router-init.ts +104 -48
  308. package/src/client/browser-entry/rsc-stream.ts +11 -2
  309. package/src/client/error-boundary.tsx +20 -15
  310. package/src/client/history.ts +13 -6
  311. package/src/client/internal.ts +4 -3
  312. package/src/client/link.tsx +55 -8
  313. package/src/client/navigation-api.ts +3 -3
  314. package/src/client/navigation-commit.ts +174 -0
  315. package/src/client/navigation-context.ts +40 -67
  316. package/src/client/navigation-root.tsx +326 -101
  317. package/src/client/params-context.ts +192 -0
  318. package/src/client/router-effects.ts +231 -0
  319. package/src/client/router-lifecycle.ts +288 -0
  320. package/src/client/router-pipeline.ts +389 -0
  321. package/src/client/router-ref.ts +1 -1
  322. package/src/client/router-skew.ts +30 -0
  323. package/src/client/router-types.ts +230 -0
  324. package/src/client/router.ts +242 -690
  325. package/src/client/rsc-fetch.ts +180 -122
  326. package/src/client/segment-cache.ts +121 -44
  327. package/src/client/ssr-data.ts +6 -0
  328. package/src/client/stale-client.ts +191 -0
  329. package/src/client/state.ts +14 -1
  330. package/src/client/top-loader.tsx +2 -1
  331. package/src/client/unload-guard.ts +34 -2
  332. package/src/client/use-cookie.ts +9 -2
  333. package/src/client/use-router.ts +1 -1
  334. package/src/client/use-segment-params.ts +41 -18
  335. package/src/index.ts +5 -1
  336. package/src/plugin-context.ts +34 -1
  337. package/src/plugins/adapter-build.ts +4 -0
  338. package/src/plugins/build-manifest.ts +34 -13
  339. package/src/plugins/entries.ts +4 -0
  340. package/src/plugins/fonts.ts +11 -8
  341. package/src/plugins/request-dep/analysis.ts +447 -0
  342. package/src/plugins/request-dep/index.ts +376 -0
  343. package/src/plugins/request-dep/origin.ts +310 -0
  344. package/src/plugins/routing.ts +60 -3
  345. package/src/plugins/static-build.ts +333 -63
  346. package/src/routing/codegen-types.ts +18 -0
  347. package/src/routing/codegen.ts +50 -10
  348. package/src/routing/collision-probe.ts +438 -0
  349. package/src/routing/collision-spaces.ts +281 -0
  350. package/src/routing/interception.ts +896 -50
  351. package/src/routing/manifest-codegen.ts +6 -9
  352. package/src/routing/scanner.ts +114 -113
  353. package/src/routing/segment-classify.ts +57 -0
  354. package/src/routing/segment-keys.ts +146 -0
  355. package/src/routing/slot-placement.ts +97 -0
  356. package/src/routing/types.ts +1 -3
  357. package/src/routing/walkers.ts +8 -0
  358. package/src/search-params/define.ts +6 -29
  359. package/src/segment-params/define.ts +4 -27
  360. package/src/server/action-handler.ts +3 -1
  361. package/src/server/actions.ts +6 -3
  362. package/src/server/als-registry.ts +2 -1
  363. package/src/server/build-manifest.ts +14 -1
  364. package/src/server/chain-url-parts.ts +163 -0
  365. package/src/server/children-interception.ts +115 -0
  366. package/src/server/default-status-page.ts +127 -0
  367. package/src/server/deny-boundary.ts +5 -31
  368. package/src/server/deny-renderer.ts +83 -22
  369. package/src/server/head-response.ts +54 -0
  370. package/src/server/param-coercion.ts +48 -18
  371. package/src/server/pipeline-helpers.ts +0 -45
  372. package/src/server/pipeline-interception.ts +81 -32
  373. package/src/server/pipeline-outcome.ts +3 -5
  374. package/src/server/pipeline-phases.ts +68 -16
  375. package/src/server/pipeline.ts +57 -10
  376. package/src/server/prebuilt-builder.ts +1 -7
  377. package/src/server/prebuilt-runtime.ts +4 -4
  378. package/src/server/primitives.ts +0 -18
  379. package/src/server/publish-params.ts +42 -0
  380. package/src/server/request-context.ts +97 -18
  381. package/src/server/route-element-builder.ts +89 -98
  382. package/src/server/route-handler.ts +8 -8
  383. package/src/server/route-matcher.ts +21 -9
  384. package/src/server/rsc-cache-key-guard.ts +170 -0
  385. package/src/server/rsc-entry/api-handler.ts +6 -8
  386. package/src/server/rsc-entry/error-renderer.ts +64 -25
  387. package/src/server/rsc-entry/helpers.ts +31 -16
  388. package/src/server/rsc-entry/index.ts +36 -28
  389. package/src/server/rsc-entry/render-route.ts +12 -7
  390. package/src/server/rsc-entry/revalidate-renderer.ts +1 -1
  391. package/src/server/rsc-entry/rsc-payload.ts +14 -25
  392. package/src/server/rsc-entry/rsc-stream.ts +4 -3
  393. package/src/server/rsc-entry/ssr-renderer.ts +13 -9
  394. package/src/server/sitemap-generator.ts +1 -7
  395. package/src/server/skippable-prefix.ts +92 -0
  396. package/src/server/slot-interception.ts +168 -0
  397. package/src/server/slot-resolver.ts +253 -156
  398. package/src/server/ssr-bridge-types.ts +6 -0
  399. package/src/server/ssr-entry.ts +12 -4
  400. package/src/server/ssr-wrappers.tsx +33 -32
  401. package/src/server/state-tree-diff.ts +14 -55
  402. package/src/server/static-generator.ts +67 -45
  403. package/src/server/static-not-found.ts +134 -0
  404. package/src/server/status-code-resolver.ts +4 -47
  405. package/src/shared/als-slots.ts +72 -0
  406. package/src/shared/base-path.ts +80 -0
  407. package/src/shared/param-value.ts +244 -0
  408. package/src/shared/payload-root.ts +151 -0
  409. package/src/shared/rsc-cache-key.ts +269 -0
  410. package/src/shared/rsc-error-envelope.ts +31 -0
  411. package/src/shared/rsc-manifest.ts +29 -0
  412. package/src/shared/rsc-payload-path.ts +45 -0
  413. package/src/shared/segment-info.ts +60 -0
  414. package/src/shared/slot-params.ts +111 -0
  415. package/src/shared/static-platform-files.ts +75 -0
  416. package/src/shims/navigation-client.ts +8 -26
  417. package/src/shims/navigation.ts +0 -4
  418. package/dist/_chunks/build-output-helper-DOGYFb_X.js +0 -196
  419. package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +0 -1
  420. package/dist/_chunks/canonicalize-DQHyFClh.js.map +0 -1
  421. package/dist/_chunks/cli-schema-sync-D3naS3eH.js +0 -688
  422. package/dist/_chunks/cli-schema-sync-D3naS3eH.js.map +0 -1
  423. package/dist/_chunks/define-COtkxMRT.js +0 -191
  424. package/dist/_chunks/define-COtkxMRT.js.map +0 -1
  425. package/dist/_chunks/define-c4au4I9R.js +0 -82
  426. package/dist/_chunks/define-c4au4I9R.js.map +0 -1
  427. package/dist/_chunks/fast-hash-D6hIVt1Y.js.map +0 -1
  428. package/dist/_chunks/logger-AWfuX-KJ.js.map +0 -1
  429. package/dist/_chunks/navigation-root-bUDZLw74.js +0 -124
  430. package/dist/_chunks/navigation-root-bUDZLw74.js.map +0 -1
  431. package/dist/_chunks/plugin-context---kTF5v8.js.map +0 -1
  432. package/dist/_chunks/router-ref-CtmF-aPv.js.map +0 -1
  433. package/dist/_chunks/segment-classify-Byy425ng.js.map +0 -1
  434. package/dist/_chunks/ssr-data-BOWsq18U.js.map +0 -1
  435. package/dist/_chunks/use-segment-params-D1CzlgBo.js +0 -151
  436. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +0 -1
  437. package/dist/_chunks/walkers-BtTlKviE.js.map +0 -1
  438. package/dist/client/error-boundary.js.map +0 -1
  439. package/dist/plugins/request-dep.d.ts +0 -32
  440. package/dist/plugins/request-dep.d.ts.map +0 -1
  441. package/src/plugins/request-dep.ts +0 -499
@@ -69,7 +69,7 @@ const SEGMENT_PREFIX = '\0$segment:';
69
69
  * File convention names we track for changes that require manifest regeneration.
70
70
  */
71
71
  const ROUTE_FILE_PATTERNS =
72
- /\/(page|layout|middleware|access|route|error|global-error|default|denied|params|schema|\d{3}|[45]xx|not-found|forbidden|unauthorized|sitemap|robots|manifest|favicon|icon|opengraph-image|apple-icon)\./;
72
+ /\/(page|layout|middleware|access|route|error|global-error|default|denied|params|schema|\d{3}|[45]xx|sitemap|robots|manifest|favicon|icon|opengraph-image|apple-icon)\./;
73
73
 
74
74
  /**
75
75
  * Create the timber-routing Vite plugin.
@@ -107,10 +107,49 @@ const TIMBER_ENV_DTS = [
107
107
  '',
108
108
  ].join('\n');
109
109
 
110
+ /**
111
+ * Codegen writes that have been started but have not yet landed on disk.
112
+ *
113
+ * `writeCodegen` is deliberately unawaited on the hot path, which leaves a
114
+ * caller no way to know the files exist yet. Anything that tears down the
115
+ * project root — a build process exiting, a test removing its temp root —
116
+ * then races the writes: `rmSync` empties a directory that a pending `mkdir`
117
+ * immediately recreates, and the removal fails with ENOTEMPTY.
118
+ *
119
+ * Every started write registers here and deregisters when it settles, so
120
+ * `flushCodegenWrites()` can await them instead of guessing at a delay.
121
+ */
122
+ const pendingCodegenWrites = new Set<Promise<void>>();
123
+
124
+ /**
125
+ * Resolve once every codegen write started so far has landed on disk.
126
+ *
127
+ * Safe to call at any time and from anywhere — writes are tracked per process,
128
+ * not per plugin instance, so a caller does not need a handle on the plugin
129
+ * that started them. Never rejects: individual write failures are already
130
+ * swallowed and logged by `writeCodegen`.
131
+ */
132
+ export async function flushCodegenWrites(): Promise<void> {
133
+ // Loop rather than a single Promise.all: a write started while we were
134
+ // awaiting would otherwise be missed, and the set would still be non-empty
135
+ // on return.
136
+ //
137
+ // Each round drops what it awaited rather than trusting the per-write
138
+ // `finally` to have run, so the two removal paths are independent — a
139
+ // deregistration that stopped firing would leak memory in dev, but it can
140
+ // never turn this into a spin.
141
+ while (pendingCodegenWrites.size > 0) {
142
+ const inFlight = [...pendingCodegenWrites];
143
+ await Promise.all(inFlight);
144
+ for (const write of inFlight) pendingCodegenWrites.delete(write);
145
+ }
146
+ }
147
+
110
148
  /**
111
149
  * Write the generated route map and the timber-env.d.ts reference file.
112
150
  *
113
- * Fire-and-forget — never awaited to avoid blocking the hot path.
151
+ * Not awaited by callers, to avoid blocking the hot path — use
152
+ * `flushCodegenWrites()` to wait for the writes to land.
114
153
  * Errors are swallowed; a missing .d.ts is a developer UX issue, not a runtime failure.
115
154
  */
116
155
  function writeCodegen(ctx: PluginContext): void {
@@ -119,7 +158,7 @@ function writeCodegen(ctx: PluginContext): void {
119
158
  const content = generateRouteMap(ctx.routeTree, { appDir: ctx.appDir, outputDir: timberDir });
120
159
  const routesPath = join(ctx.root, CODEGEN_OUTPUT);
121
160
  const envPath = join(ctx.root, 'timber-env.d.ts');
122
- mkdir(timberDir, { recursive: true })
161
+ const done = mkdir(timberDir, { recursive: true })
123
162
  .then(() =>
124
163
  Promise.all([
125
164
  writeFile(routesPath, content, 'utf-8'),
@@ -127,10 +166,15 @@ function writeCodegen(ctx: PluginContext): void {
127
166
  writeSegmentTypes(ctx.routeTree!, ctx.appDir, ctx.root),
128
167
  ])
129
168
  )
169
+ .then(() => undefined)
130
170
  .catch((err) => {
131
171
  // Non-fatal — types are a dev convenience, but log so issues are visible
132
172
  console.warn('[timber] Failed to write codegen output:', err);
133
173
  });
174
+
175
+ pendingCodegenWrites.add(done);
176
+ // `done` has a catch attached, so it never rejects and this stays quiet.
177
+ void done.finally(() => pendingCodegenWrites.delete(done));
134
178
  }
135
179
 
136
180
  export function timberRouting(ctx: PluginContext): Plugin {
@@ -288,6 +332,19 @@ export function timberRouting(ctx: PluginContext): Plugin {
288
332
  rescan();
289
333
  },
290
334
 
335
+ /**
336
+ * Wait for the codegen writes started by `buildStart` before the build
337
+ * reports done.
338
+ *
339
+ * `writeCodegen` runs unawaited so route scanning never blocks on disk.
340
+ * Without this the build can finish — and the CLI can exit — while the
341
+ * generated `.d.ts` files are still being written, leaving a project root
342
+ * with no route types after an apparently successful build.
343
+ */
344
+ closeBundle() {
345
+ return flushCodegenWrites();
346
+ },
347
+
291
348
  /**
292
349
  * Set up file watcher in dev mode.
293
350
  *
@@ -10,9 +10,9 @@
10
10
 
11
11
  import type { Plugin } from 'vite';
12
12
  import { existsSync } from 'node:fs';
13
- import { mkdir, readdir, readFile, writeFile, rename, rm } from 'node:fs/promises';
14
- import { createHash } from 'node:crypto';
15
- import { dirname, extname, join, resolve, relative } from 'node:path';
13
+ import { mkdir, readdir, readFile, writeFile, rename, rm, stat } from 'node:fs/promises';
14
+ import { createHash, randomUUID } from 'node:crypto';
15
+ import { dirname, extname, isAbsolute, join, resolve, relative } from 'node:path';
16
16
  import { pathToFileURL } from 'node:url';
17
17
  import type { PluginContext } from '../plugin-context.js';
18
18
  import type {
@@ -22,12 +22,31 @@ import type {
22
22
  StaticRedirectEntry,
23
23
  } from '../server/static-generator.js';
24
24
  import { mergeRscAssetsManifest } from './adapter-build.js';
25
+ import { isRelativeBase, normalizeBasePath, withBasePath } from '../shared/base-path.js';
26
+ import type { RscManifest, RscManifestEntry } from '../shared/rsc-manifest.js';
25
27
  import { htmlEscapeJsonString } from '../server/flight-scripts.js';
28
+ import { rscPayloadPath } from '../shared/rsc-payload-path.js';
29
+ import { REDIRECTS_FILE, RESERVED_OUTPUT_PATHS } from '../shared/static-platform-files.js';
30
+ import {
31
+ detectFsIdentity,
32
+ fsKey,
33
+ EXACT_FS_IDENTITY,
34
+ type FsIdentity,
35
+ } from '../adapters/fs-identity.js';
26
36
 
27
37
  // ---------------------------------------------------------------------------
28
38
  // Types
29
39
  // ---------------------------------------------------------------------------
30
40
 
41
+ /**
42
+ * A single entry in the RSC manifest injected into static HTML.
43
+ * Maps an unhashed `/_rsc/*.rsc` path to its hashed URL, with
44
+ * optional route params inlined to avoid a sidecar fetch.
45
+ */
46
+ // Declared in shared/rsc-manifest.ts so the client that reads this manifest and
47
+ // the build that writes it cannot drift. Re-exported for existing importers.
48
+ export type { RscManifestEntry, RscManifest } from '../shared/rsc-manifest.js';
49
+
31
50
  export interface StaticValidationError {
32
51
  type: 'dynamic-api';
33
52
  file: string;
@@ -95,10 +114,23 @@ export function detectDynamicApis(code: string, fileId: string): StaticValidatio
95
114
  * Generate a `_redirects` file from collected redirect entries.
96
115
  * Uses the Cloudflare Pages / Netlify format: `/from /to statusCode`
97
116
  *
117
+ * Both sides are base-prefixed (TIM-1274). `from` is matched by the host
118
+ * against the request path and `to` is served to the browser as a `Location`,
119
+ * so both are deployed-URL space while `StaticRedirectEntry` carries route
120
+ * space — under `base: '/repo/'`, a rule `/old /new 301` would match nothing
121
+ * and, if it did, would send the browser off the deployment.
122
+ *
123
+ * An off-origin `to` is left alone: `redirectExternal()` produces an absolute
124
+ * URL, and a protocol-relative one addresses another host too. Neither is a
125
+ * path on this deployment, so neither takes the prefix.
126
+ *
127
+ * @param base Normalized deployment base (see `shared/base-path.ts`).
98
128
  * @internal Exported for testing.
99
129
  */
100
- export function generateRedirectsFile(redirects: StaticRedirectEntry[]): string {
101
- const lines = redirects.map((r) => `${r.from} ${r.to} ${r.status}`);
130
+ export function generateRedirectsFile(redirects: StaticRedirectEntry[], base: string): string {
131
+ const target = (to: string) =>
132
+ to.startsWith('/') && !to.startsWith('//') ? withBasePath(base, to) : to;
133
+ const lines = redirects.map((r) => `${withBasePath(base, r.from)} ${target(r.to)} ${r.status}`);
102
134
  return lines.join('\n') + '\n';
103
135
  }
104
136
 
@@ -117,15 +149,20 @@ async function writeFileAtomic(path: string, data: Uint8Array | string): Promise
117
149
  * Determine the output file path for a static page entry.
118
150
  *
119
151
  * Pages: /about → about/index.html, / → index.html
120
- * RSC: /about → _rsc/about.rsc, / → _rsc/index.rsc
152
+ * RSC: /about → _rsc/about/index.rsc, / → _rsc/index.rsc
121
153
  * API: /feed.xml → feed.xml, /api/data → api/data
154
+ *
155
+ * The RSC shape mirrors the HTML shape for the reason the HTML shape has it:
156
+ * a route gets a directory and the artifact is the `index` inside it, so the
157
+ * mapping is injective. See `shared/rsc-payload-path.ts` (TIM-1266).
122
158
  */
123
159
  export function staticOutputPath(entry: StaticPageEntry): string {
124
160
  const urlPath = entry.urlPath === '/' ? '' : entry.urlPath;
125
161
 
126
162
  if (entry.kind === 'rsc') {
127
- const rscPath = urlPath || '/index';
128
- return `_rsc${rscPath}.rsc`;
163
+ // Route space is `/…`; the static output path is relative to the output
164
+ // directory, so drop the leading slash.
165
+ return rscPayloadPath(entry.urlPath).slice(1);
129
166
  }
130
167
 
131
168
  if (entry.kind === 'api') {
@@ -144,6 +181,216 @@ export function staticOutputPath(entry: StaticPageEntry): string {
144
181
  return `${urlPath.startsWith('/') ? urlPath.slice(1) : urlPath}/index.html`;
145
182
  }
146
183
 
184
+ // ---------------------------------------------------------------------------
185
+ // Output path collisions (TIM-1266)
186
+ // ---------------------------------------------------------------------------
187
+
188
+ /**
189
+ * Records which entry claimed each output path and reports second claims.
190
+ *
191
+ * Two entries landing on one path is not a rendering failure — both render
192
+ * fine — so nothing downstream notices. The file is written twice, the second
193
+ * write wins, and the deployment serves one route's bytes at another route's
194
+ * URL with the same content type and the same shape (TIM-1266).
195
+ *
196
+ * The naming rules are injective by construction (a route path is a directory
197
+ * and the artifact is the `index` inside it), so a collision here means either
198
+ * two entries were enumerated for one URL or a route reached a path the
199
+ * framework reserves for itself (`404.html`). Both are build errors: there is
200
+ * no correct file to write.
201
+ *
202
+ * Two entries can also be unwritable together without claiming the same
203
+ * string: one wants a file where the other wants a directory (`about` from an
204
+ * API route, `about/index.html` from a page). The filesystem does refuse that
205
+ * one — `EISDIR` on the rename, `ENOTDIR` on the mkdir, depending on which
206
+ * lands second — but an errno names a path, and in the rename case the path
207
+ * it names is `about.tmp`. Reported here instead, so the message says which
208
+ * two routes.
209
+ *
210
+ * Collisions are **collected, not thrown**. `onEntry` runs inside the
211
+ * generator, whose RSC branch treats a throw as "this payload is unavailable,
212
+ * SPA navigation falls back to a full load" and swallows it — which for a
213
+ * collision is the wrong end state, since the two payloads are still
214
+ * ambiguous. The caller raises them once generation is done.
215
+ *
216
+ * @internal Exported for testing.
217
+ */
218
+ export class StaticOutputPaths {
219
+ private readonly claimed = new Map<string, StaticPageEntry>();
220
+ /** Every ancestor directory of a claimed path, mapped to its claimant. */
221
+ private readonly directories = new Map<string, StaticPageEntry>();
222
+ private readonly collisions: string[] = [];
223
+
224
+ /**
225
+ * @param identity How the output filesystem decides two paths name one
226
+ * file, from `detectFsIdentity`. The guard predicts whether two writes
227
+ * land on one file, which is a property of the filesystem — so it is
228
+ * asked, not assumed. Folding unconditionally would reject `/Foo` beside
229
+ * `/foo` on Linux, where both are perfectly servable files.
230
+ */
231
+ constructor(private readonly identity: FsIdentity = EXACT_FS_IDENTITY) {}
232
+
233
+ /** The identity under which the filesystem will compare this path. */
234
+ private key(relPath: string): string {
235
+ return fsKey(relPath, this.identity);
236
+ }
237
+
238
+ /**
239
+ * Claim `relPath` for `entry`. Returns false when the path cannot be
240
+ * written alongside an earlier claim, in which case the caller must not
241
+ * write the file — the losing entry is recorded and the build fails at
242
+ * the end.
243
+ */
244
+ claim(relPath: string, entry: StaticPageEntry): boolean {
245
+ // Folded like every other comparison here: on a case-insensitive
246
+ // filesystem a route rendering to `_HEADERS` is the same file the
247
+ // adapter is about to write, so a case-sensitive match would wave it
248
+ // through into the overwrite this reservation exists to stop.
249
+ if (RESERVED_OUTPUT_PATHS.some((name) => this.key(name) === this.key(relPath))) {
250
+ this.collisions.push(
251
+ ` - ${relPath}: reserved — the framework writes this file itself, ` +
252
+ `and ${entry.kind} for ${entry.urlPath} would be overwritten by it`
253
+ );
254
+ return false;
255
+ }
256
+
257
+ const key = this.key(relPath);
258
+ const conflict =
259
+ this.claimed.get(key) ??
260
+ // This entry wants a file where an earlier one wants a directory…
261
+ this.directories.get(key) ??
262
+ // …or a directory where an earlier one wants a file.
263
+ ancestors(relPath).reduce<StaticPageEntry | undefined>(
264
+ (found, dir) => found ?? this.claimed.get(this.key(dir)),
265
+ undefined
266
+ );
267
+
268
+ if (conflict) {
269
+ this.collisions.push(
270
+ ` - ${relPath}: ${conflict.kind} for ${conflict.urlPath} and ${entry.kind} for ${entry.urlPath}`
271
+ );
272
+ return false;
273
+ }
274
+
275
+ this.claimed.set(key, entry);
276
+ for (const dir of ancestors(relPath)) {
277
+ const dirKey = this.key(dir);
278
+ if (!this.directories.has(dirKey)) this.directories.set(dirKey, entry);
279
+ }
280
+ return true;
281
+ }
282
+
283
+ /** Throw if any two entries could not both be written. */
284
+ assertNoCollisions(): void {
285
+ if (this.collisions.length === 0) return;
286
+ throw new Error(
287
+ `[timber] Static generation: ${this.collisions.length} output path collision(s) — ` +
288
+ `two entries write the same path and one would silently overwrite the other. ` +
289
+ `Rename or remove one route in each pair:\n${this.collisions.join('\n')}`
290
+ );
291
+ }
292
+ }
293
+
294
+ /** Every directory an output path sits under, outermost first. */
295
+ function ancestors(relPath: string): string[] {
296
+ const parts = relPath.split('/');
297
+ parts.pop();
298
+ return parts.map((_, i) => parts.slice(0, i + 1).join('/'));
299
+ }
300
+
301
+ /**
302
+ * Writes generated entries into the static output directory, one file per
303
+ * entry, refusing any write that would land on a path another entry holds.
304
+ *
305
+ * @internal Exported for testing.
306
+ */
307
+ export class StaticSiteWriter {
308
+ /** Content-Type per URL path for extensionless API routes (TIM-1241). */
309
+ readonly contentTypes: Record<string, string> = {};
310
+ private readonly paths: StaticOutputPaths;
311
+
312
+ constructor(
313
+ private readonly staticDir: string,
314
+ identity: FsIdentity = EXACT_FS_IDENTITY
315
+ ) {
316
+ this.paths = new StaticOutputPaths(identity);
317
+ }
318
+
319
+ async write(entry: StaticPageEntry): Promise<void> {
320
+ const relPath = staticOutputPath(entry);
321
+ const fullPath = resolve(this.staticDir, relPath);
322
+
323
+ // Reject path traversal: generated params with "../" could escape
324
+ // the output directory and overwrite build artifacts.
325
+ //
326
+ // Tested on the first *segment*, not on the prefix. A slug is arbitrary
327
+ // application data, and `..draft` is an ordinary directory name whose
328
+ // relative path merely begins with two dots — a prefix test failed the
329
+ // build on it. `isAbsolute` covers the case `relative()` gives up on and
330
+ // returns an absolute path for: a different Windows drive, where the
331
+ // round-trip check below compares equal and would let it through.
332
+ const rel = relative(this.staticDir, fullPath);
333
+ const firstSegment = rel.split(/[\\/]/)[0];
334
+ if (firstSegment === '..' || isAbsolute(rel) || resolve(this.staticDir, rel) !== fullPath) {
335
+ throw new Error(
336
+ `[timber] Static generation: output path "${relPath}" escapes the ` +
337
+ `static output directory — check generateStaticSegmentParams for ` +
338
+ `path traversal characters in ${entry.urlPath}`
339
+ );
340
+ }
341
+
342
+ // Claim the *normalized* path, not the one `staticOutputPath` built. Two
343
+ // distinct strings can name one file — a generated param carrying `..`
344
+ // makes `_rsc/a/../b/index.rsc` and `_rsc/b/index.rsc` different keys for
345
+ // the same write, which is the collision this check exists to stop. The
346
+ // traversal guard above only rejects escapes; it lets an in-bounds detour
347
+ // through.
348
+ //
349
+ // A losing claim is not written: the build is about to fail, and
350
+ // clobbering the winner would make the failure state worse than the one
351
+ // being reported.
352
+ if (!this.paths.claim(rel.replace(/\\/g, '/'), entry)) return;
353
+
354
+ await mkdir(dirname(fullPath), { recursive: true });
355
+ await writeFileAtomic(fullPath, entry.body);
356
+
357
+ // Track Content-Type for extensionless API routes so adapters
358
+ // can write per-path Content-Type rules in _headers. TIM-1241.
359
+ if (entry.kind === 'api' && !extname(relPath) && entry.contentType) {
360
+ this.contentTypes[entry.urlPath] = entry.contentType;
361
+ }
362
+ }
363
+
364
+ /** Throw if any two entries could not both be written. */
365
+ assertNoCollisions(): void {
366
+ this.paths.assertNoCollisions();
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Remove artifacts a generation pass produces only *conditionally*, so none
372
+ * of them can survive from an earlier build into this one.
373
+ *
374
+ * Pages are not in this set: every page is rewritten each pass, and a page
375
+ * whose route was deleted is a different problem (a stale file nothing links
376
+ * to) from the one here.
377
+ *
378
+ * - `_rsc/` because the files are content-hashed. Left in place, a previous
379
+ * build's already-hashed payload gets hashed again and accumulates.
380
+ * - `_redirects` because it is written only when this pass found redirects.
381
+ * A build whose redirects were removed would otherwise ship the previous
382
+ * build's rules — and would break the merge exemption in
383
+ * `shared/static-platform-files.ts`, which lets the pre-rendered copy
384
+ * overwrite `public/_redirects` on the grounds that it *contains* it. That
385
+ * holds only for a file the current pass produced (TIM-1266).
386
+ *
387
+ * @internal Exported for testing.
388
+ */
389
+ export async function cleanConditionalArtifacts(staticDir: string): Promise<void> {
390
+ await rm(join(staticDir, '_rsc'), { recursive: true, force: true }).catch(() => {});
391
+ await rm(join(staticDir, REDIRECTS_FILE), { force: true }).catch(() => {});
392
+ }
393
+
147
394
  // ---------------------------------------------------------------------------
148
395
  // Static generation orchestration (runs after Vite build)
149
396
  // ---------------------------------------------------------------------------
@@ -209,52 +456,24 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
209
456
  }
210
457
 
211
458
  const staticDir = join(ctx.buildDir, 'static-pages');
212
- // Clean stale RSC artifacts from previous builds before generating
213
- // new ones. Without this, already-hashed files from a prior build
214
- // would get re-hashed and accumulate in the output.
215
- await rm(join(staticDir, '_rsc'), { recursive: true, force: true }).catch(() => {});
459
+ await cleanConditionalArtifacts(staticDir);
216
460
  await mkdir(staticDir, { recursive: true });
217
461
 
218
462
  const sitemapConfig = ctx.config.sitemap;
219
463
  const autoSitemapEnabled = Boolean(sitemapConfig?.enabled && sitemapConfig?.baseUrl);
220
464
 
221
- const contentTypes: Record<string, string> = {};
465
+ const writer = new StaticSiteWriter(staticDir, await detectFsIdentity(staticDir));
466
+ // Route params are not collected here any more. They used to be lifted off
467
+ // the generated entry's response headers and inlined into the manifest,
468
+ // because a `.rsc` file carries no headers and the manifest was the only
469
+ // channel a static SPA navigation had (TIM-1255, TIM-1285). The params are
470
+ // now inside the payload itself — a field on its root row — so the `.rsc`
471
+ // file carries them and there is nothing to lift (TIM-1294).
222
472
 
223
473
  const summary = await generate({
224
474
  autoSitemapEnabled,
225
475
  renderTimeoutMs: ctx.config.renderTimeoutMs,
226
- onEntry: async (entry: StaticPageEntry) => {
227
- const relPath = staticOutputPath(entry);
228
- const fullPath = resolve(staticDir, relPath);
229
-
230
- // Reject path traversal: generated params with "../" could escape
231
- // the output directory and overwrite build artifacts.
232
- const rel = relative(staticDir, fullPath);
233
- if (rel.startsWith('..') || resolve(staticDir, rel) !== fullPath) {
234
- throw new Error(
235
- `[timber] Static generation: output path "${relPath}" escapes the ` +
236
- `static output directory — check generateStaticSegmentParams for ` +
237
- `path traversal characters in ${entry.urlPath}`
238
- );
239
- }
240
-
241
- await mkdir(dirname(fullPath), { recursive: true });
242
- await writeFileAtomic(fullPath, entry.body);
243
-
244
- // Write params sidecar for RSC entries with route params.
245
- // Static .rsc files don't carry HTTP headers, so the client
246
- // fetches this sidecar to populate useSegmentParams(). TIM-1246.
247
- if (entry.kind === 'rsc' && entry.headers['x-timber-params']) {
248
- const paramsPath = fullPath.replace(/\.rsc$/, '.params.json');
249
- await writeFileAtomic(paramsPath, entry.headers['x-timber-params']);
250
- }
251
-
252
- // Track Content-Type for extensionless API routes so adapters
253
- // can write per-path Content-Type rules in _headers. TIM-1241.
254
- if (entry.kind === 'api' && !extname(relPath) && entry.contentType) {
255
- contentTypes[entry.urlPath] = entry.contentType;
256
- }
257
- },
476
+ onEntry: (entry: StaticPageEntry) => writer.write(entry),
258
477
  });
259
478
 
260
479
  const totalUrls = summary.rendered + summary.errors.length;
@@ -264,15 +483,25 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
264
483
  (summary.redirects.length > 0 ? ` ${summary.redirects.length} redirect(s).` : '')
265
484
  );
266
485
 
486
+ // Warnings are non-fatal but must not be buried in the per-entry log stream —
487
+ // they name a degraded artifact the developer is about to deploy (TIM-1258).
488
+ for (const warning of summary.warnings) {
489
+ console.warn(`[timber] Static generation warning: ${warning}`);
490
+ }
491
+
267
492
  if (summary.errors.length > 0) {
268
493
  const details = summary.errors.map((e) => ` - ${e.urlPath}: ${e.message}`).join('\n');
269
494
  throw new Error(`[timber] Static generation failed:\n${details}`);
270
495
  }
271
496
 
497
+ // Raised after the render errors above: a collision means the site is
498
+ // ambiguous, not that a page failed, and the message names both routes.
499
+ writer.assertNoCollisions();
500
+
272
501
  // Store content-type mappings on the plugin context so adapters
273
502
  // can include them in the _headers file. TIM-1241.
274
- if (Object.keys(contentTypes).length > 0) {
275
- ctx.staticContentTypes = contentTypes;
503
+ if (Object.keys(writer.contentTypes).length > 0) {
504
+ ctx.staticContentTypes = writer.contentTypes;
276
505
  }
277
506
 
278
507
  // Hash RSC flight payloads for immutable caching (TIM-1254).
@@ -294,8 +523,8 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
294
523
  // Write _redirects file for platform redirect rules (Cloudflare Pages,
295
524
  // Netlify, etc.). Format: /from /to status. See TIM-1245.
296
525
  if (summary.redirects.length > 0) {
297
- const generated = generateRedirectsFile(summary.redirects);
298
- const redirectsPath = join(staticDir, '_redirects');
526
+ const generated = generateRedirectsFile(summary.redirects, normalizeBasePath(ctx.base));
527
+ const redirectsPath = join(staticDir, REDIRECTS_FILE);
299
528
 
300
529
  // Merge with user-authored _redirects if present. TIM-1249.
301
530
  const userRedirects = await readUserRedirects(ctx.root);
@@ -316,6 +545,38 @@ async function runStaticGeneration(ctx: PluginContext): Promise<StaticGeneration
316
545
  return summary;
317
546
  }
318
547
 
548
+ // ---------------------------------------------------------------------------
549
+ // Base path validation (TIM-1261)
550
+ // ---------------------------------------------------------------------------
551
+
552
+ /**
553
+ * Warn when Vite's `base` is document-relative (`'./'`) in static mode.
554
+ *
555
+ * Payload URLs are addressed from the deployment root plus the base prefix.
556
+ * A relative base gives no prefix to compute (see shared/base-path.ts), so
557
+ * `/_rsc/about.rsc` is requested at the domain root — correct only when the
558
+ * site is served from there. The build still succeeds: relative bases work
559
+ * fine for assets, and for a root-served site nothing is wrong. The warning
560
+ * exists so a sub-path deployment does not silently degrade to a hard
561
+ * navigation on every link.
562
+ *
563
+ * Skipped when client JS is disabled — there is no SPA navigation and no
564
+ * payload fetch to mis-address.
565
+ *
566
+ * @internal Exported for testing.
567
+ */
568
+ export function warnUnusableBase(ctx: PluginContext): void {
569
+ if (ctx.clientJavascript.disabled) return;
570
+ if (!isRelativeBase(ctx.base)) return;
571
+ console.warn(
572
+ `[timber] Static mode: base is "${ctx.base}", which is document-relative. ` +
573
+ `RSC payload URLs are addressed from the domain root, so client-side ` +
574
+ `navigation will fall back to a full page load if the site is served ` +
575
+ `from a sub-path. Use an absolute base (e.g. base: '/repo/') to deploy ` +
576
+ `under a sub-path. See design/11-platform.md.`
577
+ );
578
+ }
579
+
319
580
  // ---------------------------------------------------------------------------
320
581
  // RSC flight payload hashing (TIM-1254)
321
582
  // ---------------------------------------------------------------------------
@@ -351,41 +612,49 @@ async function walkDir(dir: string): Promise<string[]> {
351
612
  }
352
613
 
353
614
  /**
354
- * Hash all RSC flight payloads and params sidecars in `_rsc/`, rename
355
- * them with content hashes, and inject a manifest into each HTML file.
615
+ * Hash all RSC flight payloads in `_rsc/`, rename them with content
616
+ * hashes, and build a manifest with optional inlined route params.
356
617
  *
357
618
  * Before: `_rsc/index.rsc`, `_rsc/about.rsc`
358
619
  * After: `_rsc/index-B7YxEKdN.rsc`, `_rsc/about-C8ZzFLfO.rsc`
359
620
  *
360
- * The manifest maps the original `_rsc/` URL to the hashed filename:
361
- * `{"/_rsc/index.rsc":"/_rsc/index-B7YxEKdN.rsc", ...}`
621
+ * The manifest maps the original `_rsc/` URL to an entry object:
622
+ * `{"/_rsc/index.rsc":{"url":"/_rsc/index-B7YxEKdN.rsc"}, ...}`
623
+ *
624
+ * Keys and URLs are **route-space**: root-relative, never carrying Vite's
625
+ * `base`. The client applies the base once, at fetch time
626
+ * (`client/rsc-fetch.ts` → `toStaticRscUrl`), so the generated site stays
627
+ * relocatable and the base has exactly one expression at runtime rather
628
+ * than two that must agree. See TIM-1261.
629
+ *
630
+ * Route params (from `routeParams`) are inlined into the manifest
631
+ * entry, eliminating the need for .params.json sidecar files. TIM-1255.
362
632
  *
363
633
  * Injected into HTML as:
364
634
  * `<script>window.__TIMBER_RSC_MANIFEST__={...}</script>`
365
635
  *
366
636
  * @internal Exported for testing.
367
637
  */
368
- export async function hashRscPayloads(staticDir: string): Promise<Record<string, string>> {
638
+ export async function hashRscPayloads(staticDir: string): Promise<RscManifest> {
369
639
  const rscDir = join(staticDir, '_rsc');
370
640
  const files = await walkDir(rscDir);
371
- const manifest: Record<string, string> = {};
641
+ const manifest: RscManifest = {};
372
642
 
373
643
  for (const filePath of files) {
374
- if (!filePath.endsWith('.rsc') && !filePath.endsWith('.params.json')) continue;
644
+ if (!filePath.endsWith('.rsc')) continue;
375
645
 
376
646
  const content = await readFile(filePath);
377
647
  const hash = contentHash(content);
378
648
  const rel = relative(staticDir, filePath).replace(/\\/g, '/');
379
649
 
380
- // Compute hashed filename: index.rsc → index-B7YxEKdN.rsc
381
- const ext = filePath.endsWith('.params.json') ? '.params.json' : '.rsc';
382
- const base = filePath.slice(0, -ext.length);
383
- const hashedPath = `${base}-${hash}${ext}`;
650
+ const base = filePath.slice(0, -'.rsc'.length);
651
+ const hashedPath = `${base}-${hash}.rsc`;
384
652
 
385
653
  await rename(filePath, hashedPath);
386
654
 
387
655
  const hashedRel = relative(staticDir, hashedPath).replace(/\\/g, '/');
388
- manifest[`/${rel}`] = `/${hashedRel}`;
656
+ const key = `/${rel}`;
657
+ manifest[key] = { url: `/${hashedRel}` };
389
658
  }
390
659
 
391
660
  return manifest;
@@ -400,7 +669,7 @@ export async function hashRscPayloads(staticDir: string): Promise<Record<string,
400
669
  */
401
670
  export async function injectRscManifestIntoHtml(
402
671
  staticDir: string,
403
- manifest: Record<string, string>
672
+ manifest: RscManifest
404
673
  ): Promise<void> {
405
674
  const allFiles = await walkDir(staticDir);
406
675
  const htmlFiles = allFiles.filter((f) => f.endsWith('.html'));
@@ -432,7 +701,7 @@ export async function injectRscManifestIntoHtml(
432
701
  * @internal Exported for testing.
433
702
  */
434
703
  export async function readUserRedirects(root: string): Promise<string | null> {
435
- const candidates = [join(root, 'public', '_redirects')];
704
+ const candidates = [join(root, 'public', REDIRECTS_FILE)];
436
705
  for (const path of candidates) {
437
706
  try {
438
707
  return await readFile(path, 'utf-8');
@@ -514,6 +783,7 @@ export function timberStaticBuild(ctx: PluginContext): Plugin {
514
783
  const isStatic = ctx.config.output === 'static';
515
784
  if (!isStatic) return;
516
785
 
786
+ warnUnusableBase(ctx);
517
787
  await runStaticGeneration(ctx);
518
788
  },
519
789
  },
@@ -25,6 +25,24 @@ export interface RouteEntry {
25
25
  * share their parent's urlPath — urlPath-keyed emissions dedupe them,
26
26
  * preferring the non-slot entry. segmentPath-keyed emissions keep all. */
27
27
  isSlotRoute?: boolean;
28
+ /**
29
+ * Whether this entry lives inside an intercepting subtree.
30
+ *
31
+ * Such an entry has **no URL of its own**. Interception adds no URL depth,
32
+ * so the scanner's `urlPath` for the subtree is the owner's path with the
33
+ * descendants' names appended — `(browse)/(...)[artistSlug]/[year]` computes
34
+ * `/[year]`, which is not the route it renders at (that is
35
+ * `/[artistSlug]/[year]`) and is not reachable by any request. It is also
36
+ * soft-navigation-only.
37
+ *
38
+ * So these entries are dropped from every urlPath-keyed emission (`Routes`,
39
+ * `useQueryStates`, typed `<Link>`) rather than deduped against the
40
+ * canonical route: dedupe only helps where the paths happen to collide, and
41
+ * a non-colliding computed path would be published as a real route.
42
+ * segmentPath-keyed emissions keep them, so the intercepting page's own
43
+ * params stay typed.
44
+ */
45
+ isInterceptingRoute?: boolean;
28
46
  /** Whether a global schema (app/schema.ts) exists for this app */
29
47
  hasSchema?: boolean;
30
48
  }