@timber-js/app 0.2.0-alpha.208 → 0.2.0-alpha.209

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 (520) hide show
  1. package/dist/_chunks/{primitives-DLAnvsrk.js → actions-BerlqoXA.js} +640 -410
  2. package/dist/_chunks/actions-BerlqoXA.js.map +1 -0
  3. package/dist/_chunks/{als-registry-C6kcfprT.js → als-registry-DaxkVjt5.js} +6 -2
  4. package/dist/_chunks/als-registry-DaxkVjt5.js.map +1 -0
  5. package/dist/_chunks/{build-manifest-DTmSGLRz.js → build-manifest-DWppEdLB.js} +2 -51
  6. package/dist/_chunks/build-manifest-DWppEdLB.js.map +1 -0
  7. package/dist/_chunks/{build-output-helper-9dqCv1pZ.js → build-output-helper-RV_BRcF1.js} +50 -53
  8. package/dist/_chunks/build-output-helper-RV_BRcF1.js.map +1 -0
  9. package/dist/_chunks/{cache-api-CUD9Ezaq.js → cache-api-CR23J_NC.js} +64 -32
  10. package/dist/_chunks/cache-api-CR23J_NC.js.map +1 -0
  11. package/dist/_chunks/{chains-B4bgOl2g.js → chains-CpFg56UB.js} +2 -2
  12. package/dist/_chunks/{chains-B4bgOl2g.js.map → chains-CpFg56UB.js.map} +1 -1
  13. package/dist/_chunks/{cli-check-G8o30ldg.js → cli-check-C6Ev6wBO.js} +11 -52
  14. package/dist/_chunks/cli-check-C6Ev6wBO.js.map +1 -0
  15. package/dist/_chunks/cli-context-By7k2xVo.js +70 -0
  16. package/dist/_chunks/cli-context-By7k2xVo.js.map +1 -0
  17. package/dist/_chunks/{cli-schema-sync--LZKs8Zn.js → cli-schema-sync-CbT2AUUI.js} +3 -3
  18. package/dist/_chunks/{cli-schema-sync--LZKs8Zn.js.map → cli-schema-sync-CbT2AUUI.js.map} +1 -1
  19. package/dist/_chunks/{cloudflare-BFb__LYG.js → cloudflare-Byxl8FWP.js} +96 -53
  20. package/dist/_chunks/{cloudflare-BFb__LYG.js.map → cloudflare-Byxl8FWP.js.map} +1 -1
  21. package/dist/_chunks/{convention-lint-5KzoMg_A.js → convention-lint-jKTwKwPe.js} +21 -6
  22. package/dist/_chunks/convention-lint-jKTwKwPe.js.map +1 -0
  23. package/dist/_chunks/{dev-server-BzBxgYAV.js → dev-server-C4WZdB7L.js} +15 -167
  24. package/dist/_chunks/dev-server-C4WZdB7L.js.map +1 -0
  25. package/dist/_chunks/{error-boundary-BrHmG-2X.js → error-boundary-tA7kVfs4.js} +84 -104
  26. package/dist/_chunks/error-boundary-tA7kVfs4.js.map +1 -0
  27. package/dist/_chunks/{fs-identity-D7vb6GdI.js → fs-identity-BRKuL215.js} +3 -4
  28. package/dist/_chunks/fs-identity-BRKuL215.js.map +1 -0
  29. package/dist/_chunks/{graph-cache-CP4GEmf9.js → graph-cache-CYiUS2aS.js} +4 -5
  30. package/dist/_chunks/graph-cache-CYiUS2aS.js.map +1 -0
  31. package/dist/_chunks/{json-lossy-check-ClNvBM_3.js → json-lossy-check-ip0Qi0MT.js} +6 -3
  32. package/dist/_chunks/json-lossy-check-ip0Qi0MT.js.map +1 -0
  33. package/dist/_chunks/{live-graph-Bm5WTvcr.js → live-graph-Dv-JJCZw.js} +3 -3
  34. package/dist/_chunks/{live-graph-Bm5WTvcr.js.map → live-graph-Dv-JJCZw.js.map} +1 -1
  35. package/dist/_chunks/{logger-k2DQ4EUf.js → logger-DiDt5ppH.js} +192 -51
  36. package/dist/_chunks/logger-DiDt5ppH.js.map +1 -0
  37. package/dist/_chunks/{navigation-root-mHSK9psY.js → navigation-root-BQfo1-kG.js} +2 -2
  38. package/dist/_chunks/{navigation-root-mHSK9psY.js.map → navigation-root-BQfo1-kG.js.map} +1 -1
  39. package/dist/_chunks/{plugin-context-8idfR3UZ.js → plugin-context-BPBPc3lL.js} +28 -4
  40. package/dist/_chunks/plugin-context-BPBPc3lL.js.map +1 -0
  41. package/dist/_chunks/{poison-scan-DG5eI2_z.js → poison-scan-CpeT6_OJ.js} +2 -2
  42. package/dist/_chunks/{poison-scan-DG5eI2_z.js.map → poison-scan-CpeT6_OJ.js.map} +1 -1
  43. package/dist/_chunks/render-timeout-DOf_BBcO.js +46 -0
  44. package/dist/_chunks/render-timeout-DOf_BBcO.js.map +1 -0
  45. package/dist/_chunks/{rsc-payload-path-B_LBodc2.js → rsc-payload-path-dffcs4jp.js} +9 -3
  46. package/dist/_chunks/{rsc-payload-path-B_LBodc2.js.map → rsc-payload-path-dffcs4jp.js.map} +1 -1
  47. package/dist/_chunks/{scanner-BQ5sPIJ7.js → scanner-Bw0oq1HB.js} +2 -2
  48. package/dist/_chunks/{scanner-BQ5sPIJ7.js.map → scanner-Bw0oq1HB.js.map} +1 -1
  49. package/dist/_chunks/{navigation-context-D0TU0Jog.js → segment-context-xtPUGfdq.js} +36 -34
  50. package/dist/_chunks/segment-context-xtPUGfdq.js.map +1 -0
  51. package/dist/_chunks/{singleflight-2lUWfcAk.js → singleflight-BeVMMKi0.js} +4 -3
  52. package/dist/_chunks/{singleflight-2lUWfcAk.js.map → singleflight-BeVMMKi0.js.map} +1 -1
  53. package/dist/_chunks/{sizeof-lXffQ1Vr.js → sizeof-UwzwB1uM.js} +3 -2
  54. package/dist/_chunks/sizeof-UwzwB1uM.js.map +1 -0
  55. package/dist/_chunks/ssr-data-nA3I70_n.js +49 -0
  56. package/dist/_chunks/ssr-data-nA3I70_n.js.map +1 -0
  57. package/dist/_chunks/{state-FippDgxN.js → state-BhNGPsdi.js} +6 -8
  58. package/dist/_chunks/state-BhNGPsdi.js.map +1 -0
  59. package/dist/_chunks/{status-page-marker-CNbpUOPB.js → status-page-marker-gaihi0KZ.js} +53 -20
  60. package/dist/_chunks/status-page-marker-gaihi0KZ.js.map +1 -0
  61. package/dist/_chunks/{walkers-CN2N_bA-.js → walkers-BXExhzzk.js} +2 -2
  62. package/dist/_chunks/{walkers-CN2N_bA-.js.map → walkers-BXExhzzk.js.map} +1 -1
  63. package/dist/_chunks/write-file-atomic-D1qWqHb4.js +32 -0
  64. package/dist/_chunks/write-file-atomic-D1qWqHb4.js.map +1 -0
  65. package/dist/adapters/build-output-helper.d.ts +7 -7
  66. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  67. package/dist/adapters/canonicalize-module.d.ts +8 -0
  68. package/dist/adapters/canonicalize-module.d.ts.map +1 -0
  69. package/dist/adapters/cloudflare-dev.js +1 -1
  70. package/dist/adapters/cloudflare-kv-cache.js +2 -2
  71. package/dist/adapters/cloudflare.d.ts +30 -8
  72. package/dist/adapters/cloudflare.d.ts.map +1 -1
  73. package/dist/adapters/cloudflare.js +2 -2
  74. package/dist/adapters/compress-module.d.ts +8 -0
  75. package/dist/adapters/compress-module.d.ts.map +1 -1
  76. package/dist/adapters/fs-identity.d.ts +2 -3
  77. package/dist/adapters/fs-identity.d.ts.map +1 -1
  78. package/dist/adapters/nitro-presets.d.ts +0 -6
  79. package/dist/adapters/nitro-presets.d.ts.map +1 -1
  80. package/dist/adapters/nitro-preview.d.ts +43 -11
  81. package/dist/adapters/nitro-preview.d.ts.map +1 -1
  82. package/dist/adapters/nitro.d.ts +1 -13
  83. package/dist/adapters/nitro.d.ts.map +1 -1
  84. package/dist/adapters/nitro.js +291 -176
  85. package/dist/adapters/nitro.js.map +1 -1
  86. package/dist/adapters/shared.d.ts +7 -2
  87. package/dist/adapters/shared.d.ts.map +1 -1
  88. package/dist/adapters/types.d.ts +105 -21
  89. package/dist/adapters/types.d.ts.map +1 -1
  90. package/dist/adapters/types.js +0 -0
  91. package/dist/analyze/crawl-entry.js +3 -3
  92. package/dist/analyze/graph-cache.d.ts.map +1 -1
  93. package/dist/analyze/graph-command.js +4 -4
  94. package/dist/cache/index.js +2 -2
  95. package/dist/cache/json-lossy-check.d.ts.map +1 -1
  96. package/dist/cache/singleflight.d.ts.map +1 -1
  97. package/dist/cache/sizeof.d.ts.map +1 -1
  98. package/dist/cache/stores/memory.js +1 -1
  99. package/dist/cache/tag-aware-handler.d.ts.map +1 -1
  100. package/dist/cdn/workers-cache-purge.js +1 -1
  101. package/dist/cli-check.d.ts +7 -37
  102. package/dist/cli-check.d.ts.map +1 -1
  103. package/dist/cli-context.d.ts +54 -0
  104. package/dist/cli-context.d.ts.map +1 -0
  105. package/dist/cli.d.ts +9 -9
  106. package/dist/cli.d.ts.map +1 -1
  107. package/dist/cli.js +20 -24
  108. package/dist/cli.js.map +1 -1
  109. package/dist/client/browser-dev.d.ts +1 -1
  110. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  111. package/dist/client/browser-entry/hydrate.d.ts +19 -29
  112. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  113. package/dist/client/browser-entry/index.d.ts +7 -7
  114. package/dist/client/browser-entry/post-hydration.d.ts +6 -9
  115. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  116. package/dist/client/browser-entry/router-init.d.ts +6 -11
  117. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  118. package/dist/client/error-boundary.d.ts +4 -3
  119. package/dist/client/error-boundary.d.ts.map +1 -1
  120. package/dist/client/error-boundary.js +1 -1
  121. package/dist/client/error-reconstituter.d.ts +3 -9
  122. package/dist/client/error-reconstituter.d.ts.map +1 -1
  123. package/dist/client/history.d.ts +12 -11
  124. package/dist/client/history.d.ts.map +1 -1
  125. package/dist/client/index.d.ts +0 -1
  126. package/dist/client/index.d.ts.map +1 -1
  127. package/dist/client/index.js +5 -6
  128. package/dist/client/index.js.map +1 -1
  129. package/dist/client/internal.d.ts +3 -3
  130. package/dist/client/internal.d.ts.map +1 -1
  131. package/dist/client/internal.js +68 -104
  132. package/dist/client/internal.js.map +1 -1
  133. package/dist/client/navigation-commit.d.ts +16 -7
  134. package/dist/client/navigation-commit.d.ts.map +1 -1
  135. package/dist/client/navigation-context.d.ts +3 -5
  136. package/dist/client/navigation-context.d.ts.map +1 -1
  137. package/dist/client/react-root.d.ts +3 -2
  138. package/dist/client/react-root.d.ts.map +1 -1
  139. package/dist/client/router-pipeline.d.ts +1 -6
  140. package/dist/client/router-pipeline.d.ts.map +1 -1
  141. package/dist/client/router-types.d.ts +45 -39
  142. package/dist/client/router-types.d.ts.map +1 -1
  143. package/dist/client/router-url.d.ts +24 -0
  144. package/dist/client/router-url.d.ts.map +1 -0
  145. package/dist/client/router.d.ts.map +1 -1
  146. package/dist/client/rsc-fetch.d.ts +8 -4
  147. package/dist/client/rsc-fetch.d.ts.map +1 -1
  148. package/dist/client/ssr-data.d.ts +18 -64
  149. package/dist/client/ssr-data.d.ts.map +1 -1
  150. package/dist/client/state.d.ts +2 -5
  151. package/dist/client/state.d.ts.map +1 -1
  152. package/dist/client/use-pathname.d.ts +1 -1
  153. package/dist/config-types.d.ts +2 -1
  154. package/dist/config-types.d.ts.map +1 -1
  155. package/dist/config-validation.d.ts.map +1 -1
  156. package/dist/cookies/index.js +1 -1
  157. package/dist/dev-tools/dev-page-shell.d.ts +17 -0
  158. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  159. package/dist/dev-tools/index.d.ts +1 -1
  160. package/dist/dev-tools/index.d.ts.map +1 -1
  161. package/dist/dev-tools/instrumentation.d.ts +17 -2
  162. package/dist/dev-tools/instrumentation.d.ts.map +1 -1
  163. package/dist/dev-tools/overlay.d.ts +6 -27
  164. package/dist/dev-tools/overlay.d.ts.map +1 -1
  165. package/dist/index.d.ts.map +1 -1
  166. package/dist/index.js +531 -373
  167. package/dist/index.js.map +1 -1
  168. package/dist/plugin-context.d.ts +26 -40
  169. package/dist/plugin-context.d.ts.map +1 -1
  170. package/dist/plugins/build-manifest.d.ts.map +1 -1
  171. package/dist/plugins/build-report.d.ts +15 -2
  172. package/dist/plugins/build-report.d.ts.map +1 -1
  173. package/dist/plugins/dev-server.d.ts.map +1 -1
  174. package/dist/plugins/entries.d.ts.map +1 -1
  175. package/dist/plugins/fonts.d.ts +19 -1
  176. package/dist/plugins/fonts.d.ts.map +1 -1
  177. package/dist/plugins/post-build.d.ts +110 -0
  178. package/dist/plugins/post-build.d.ts.map +1 -0
  179. package/dist/plugins/prebuilt-capture.d.ts +11 -4
  180. package/dist/plugins/prebuilt-capture.d.ts.map +1 -1
  181. package/dist/plugins/prebuilt.d.ts +17 -1
  182. package/dist/plugins/prebuilt.d.ts.map +1 -1
  183. package/dist/plugins/prerender-sugar.d.ts.map +1 -1
  184. package/dist/plugins/rsc-assets-manifest.d.ts +33 -0
  185. package/dist/plugins/rsc-assets-manifest.d.ts.map +1 -0
  186. package/dist/plugins/shims.d.ts.map +1 -1
  187. package/dist/plugins/static-build.d.ts +32 -9
  188. package/dist/plugins/static-build.d.ts.map +1 -1
  189. package/dist/routing/codegen-write.d.ts +0 -9
  190. package/dist/routing/codegen-write.d.ts.map +1 -1
  191. package/dist/routing/index.js +2 -2
  192. package/dist/routing/segment-codegen.d.ts.map +1 -1
  193. package/dist/server/access-gate.d.ts.map +1 -1
  194. package/dist/server/action-handler.d.ts +5 -4
  195. package/dist/server/action-handler.d.ts.map +1 -1
  196. package/dist/server/actions.d.ts +46 -7
  197. package/dist/server/actions.d.ts.map +1 -1
  198. package/dist/server/als-registry.d.ts +58 -5
  199. package/dist/server/als-registry.d.ts.map +1 -1
  200. package/dist/server/children-interception.d.ts.map +1 -1
  201. package/dist/server/cookie-context.d.ts +2 -23
  202. package/dist/server/cookie-context.d.ts.map +1 -1
  203. package/dist/server/csp-nonce.d.ts +0 -9
  204. package/dist/server/csp-nonce.d.ts.map +1 -1
  205. package/dist/server/csrf.d.ts +27 -4
  206. package/dist/server/csrf.d.ts.map +1 -1
  207. package/dist/server/debug.d.ts +0 -1
  208. package/dist/server/debug.d.ts.map +1 -1
  209. package/dist/server/default-logger.d.ts +2 -1
  210. package/dist/server/default-logger.d.ts.map +1 -1
  211. package/dist/server/deny-boundary.d.ts +12 -6
  212. package/dist/server/deny-boundary.d.ts.map +1 -1
  213. package/dist/server/deny-renderer.d.ts +2 -2
  214. package/dist/server/deny-renderer.d.ts.map +1 -1
  215. package/dist/server/error-boundary-wrapper.d.ts +7 -3
  216. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  217. package/dist/server/fallback-error.d.ts +9 -2
  218. package/dist/server/fallback-error.d.ts.map +1 -1
  219. package/dist/server/head-response.d.ts +4 -4
  220. package/dist/server/html-injectors.d.ts.map +1 -1
  221. package/dist/server/index.d.ts +0 -1
  222. package/dist/server/index.d.ts.map +1 -1
  223. package/dist/server/index.js +3 -4
  224. package/dist/server/index.js.map +1 -1
  225. package/dist/server/instrumentation.d.ts +10 -3
  226. package/dist/server/instrumentation.d.ts.map +1 -1
  227. package/dist/server/internal.d.ts +1 -4
  228. package/dist/server/internal.d.ts.map +1 -1
  229. package/dist/server/internal.js +961 -581
  230. package/dist/server/internal.js.map +1 -1
  231. package/dist/server/json-status-response.d.ts +34 -0
  232. package/dist/server/json-status-response.d.ts.map +1 -0
  233. package/dist/server/logger.d.ts +26 -0
  234. package/dist/server/logger.d.ts.map +1 -1
  235. package/dist/server/param-coercion.d.ts +11 -6
  236. package/dist/server/param-coercion.d.ts.map +1 -1
  237. package/dist/server/pipeline-helpers.d.ts +95 -7
  238. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  239. package/dist/server/pipeline-interception.d.ts +11 -0
  240. package/dist/server/pipeline-interception.d.ts.map +1 -1
  241. package/dist/server/pipeline-outcome.d.ts +11 -1
  242. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  243. package/dist/server/pipeline-phases.d.ts +2 -2
  244. package/dist/server/pipeline-phases.d.ts.map +1 -1
  245. package/dist/server/pipeline.d.ts +23 -35
  246. package/dist/server/pipeline.d.ts.map +1 -1
  247. package/dist/server/prebuilt/capture-render.d.ts +25 -0
  248. package/dist/server/prebuilt/capture-render.d.ts.map +1 -0
  249. package/dist/server/prebuilt/capture-routes.d.ts +37 -0
  250. package/dist/server/prebuilt/capture-routes.d.ts.map +1 -0
  251. package/dist/server/prebuilt/capture-state.d.ts +79 -35
  252. package/dist/server/prebuilt/capture-state.d.ts.map +1 -1
  253. package/dist/server/prebuilt/lifetime.d.ts +38 -0
  254. package/dist/server/prebuilt/lifetime.d.ts.map +1 -0
  255. package/dist/server/prebuilt/slots.d.ts +7 -0
  256. package/dist/server/prebuilt/slots.d.ts.map +1 -1
  257. package/dist/server/prebuilt/synthetic-store.d.ts.map +1 -1
  258. package/dist/server/prebuilt-builder.d.ts.map +1 -1
  259. package/dist/server/prebuilt-runtime.d.ts.map +1 -1
  260. package/dist/server/primitives.d.ts +6 -81
  261. package/dist/server/primitives.d.ts.map +1 -1
  262. package/dist/server/react-cache-bridge.d.ts +59 -0
  263. package/dist/server/react-cache-bridge.d.ts.map +1 -0
  264. package/dist/server/react-cache-scope.d.ts +49 -0
  265. package/dist/server/react-cache-scope.d.ts.map +1 -0
  266. package/dist/server/render-timeout.d.ts +15 -0
  267. package/dist/server/render-timeout.d.ts.map +1 -1
  268. package/dist/server/render-utils.d.ts +9 -0
  269. package/dist/server/render-utils.d.ts.map +1 -1
  270. package/dist/server/request-context.d.ts +26 -5
  271. package/dist/server/request-context.d.ts.map +1 -1
  272. package/dist/server/route-element-builder.d.ts.map +1 -1
  273. package/dist/server/route-handler.d.ts.map +1 -1
  274. package/dist/server/route-matcher.d.ts +7 -0
  275. package/dist/server/route-matcher.d.ts.map +1 -1
  276. package/dist/server/rsc-entry/action-middleware-runner.d.ts +22 -22
  277. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
  278. package/dist/server/rsc-entry/api-handler.d.ts +7 -0
  279. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  280. package/dist/server/rsc-entry/deny-fallback.d.ts +32 -6
  281. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  282. package/dist/server/rsc-entry/error-renderer.d.ts +11 -18
  283. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  284. package/dist/server/rsc-entry/helpers.d.ts +0 -1
  285. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  286. package/dist/server/rsc-entry/index.d.ts +5 -15
  287. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  288. package/dist/server/rsc-entry/render-route.d.ts +5 -6
  289. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  290. package/dist/server/rsc-entry/render-signals.d.ts +65 -0
  291. package/dist/server/rsc-entry/render-signals.d.ts.map +1 -0
  292. package/dist/server/rsc-entry/revalidate-renderer.d.ts +6 -16
  293. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  294. package/dist/server/rsc-entry/rsc-payload.d.ts +7 -3
  295. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  296. package/dist/server/rsc-entry/rsc-stream.d.ts +8 -53
  297. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  298. package/dist/server/rsc-entry/ssr-bridge.d.ts +2 -2
  299. package/dist/server/rsc-entry/ssr-bridge.d.ts.map +1 -1
  300. package/dist/server/rsc-entry/ssr-renderer.d.ts +9 -4
  301. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  302. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +1 -1
  303. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -1
  304. package/dist/server/signal-identity.d.ts +43 -0
  305. package/dist/server/signal-identity.d.ts.map +1 -0
  306. package/dist/server/slot-resolver.d.ts +4 -3
  307. package/dist/server/slot-resolver.d.ts.map +1 -1
  308. package/dist/server/slot-subtree-contain.d.ts.map +1 -1
  309. package/dist/server/ssr-bridge-types.d.ts +91 -53
  310. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  311. package/dist/server/ssr-entry.d.ts +8 -5
  312. package/dist/server/ssr-entry.d.ts.map +1 -1
  313. package/dist/server/ssr-render.d.ts +26 -40
  314. package/dist/server/ssr-render.d.ts.map +1 -1
  315. package/dist/server/ssr-wrappers.d.ts +1 -1
  316. package/dist/server/static-generator.d.ts.map +1 -1
  317. package/dist/server/stream-utils.d.ts +12 -0
  318. package/dist/server/stream-utils.d.ts.map +1 -1
  319. package/dist/server/tracing.d.ts +12 -16
  320. package/dist/server/tracing.d.ts.map +1 -1
  321. package/dist/server/types.d.ts +1 -1
  322. package/dist/server/version-skew.d.ts +3 -3
  323. package/dist/shared/csrf-reject.d.ts +16 -0
  324. package/dist/shared/csrf-reject.d.ts.map +1 -0
  325. package/dist/shared/error-digest.d.ts +28 -0
  326. package/dist/shared/error-digest.d.ts.map +1 -0
  327. package/dist/shared/payload-root.d.ts +11 -10
  328. package/dist/shared/payload-root.d.ts.map +1 -1
  329. package/dist/shared/rsc-payload-path.d.ts +6 -0
  330. package/dist/shared/rsc-payload-path.d.ts.map +1 -1
  331. package/dist/utils/write-file-atomic.d.ts +17 -0
  332. package/dist/utils/write-file-atomic.d.ts.map +1 -0
  333. package/docs/api/30-api-server.mdx +17 -14
  334. package/docs/api/32-api-cache.mdx +51 -23
  335. package/docs/api/34-api-config.mdx +34 -6
  336. package/docs/api/36-cli.mdx +11 -1
  337. package/docs/learn/03-fetching-data.mdx +17 -1
  338. package/docs/learn/04-access-control.mdx +1 -1
  339. package/docs/learn/10-caching.mdx +29 -11
  340. package/docs/learn/11-middleware.mdx +8 -2
  341. package/docs/learn/12-error-handling.mdx +6 -1
  342. package/package.json +5 -1
  343. package/src/adapters/build-output-helper.ts +55 -53
  344. package/src/adapters/canonicalize-module.ts +29 -0
  345. package/src/adapters/cloudflare.ts +105 -63
  346. package/src/adapters/compress-module.ts +16 -5
  347. package/src/adapters/fs-identity.ts +2 -3
  348. package/src/adapters/nitro-presets.ts +0 -15
  349. package/src/adapters/nitro-preview.ts +272 -136
  350. package/src/adapters/nitro.ts +119 -82
  351. package/src/adapters/shared.ts +23 -14
  352. package/src/adapters/types.ts +110 -21
  353. package/src/analyze/graph-cache.ts +3 -4
  354. package/src/cache/json-lossy-check.ts +3 -9
  355. package/src/cache/singleflight.ts +5 -2
  356. package/src/cache/sizeof.ts +3 -1
  357. package/src/cache/tag-aware-handler.ts +43 -26
  358. package/src/cli-check.ts +8 -98
  359. package/src/cli-context.ts +119 -0
  360. package/src/cli.ts +19 -28
  361. package/src/client/browser-dev.ts +1 -1
  362. package/src/client/browser-entry/action-dispatch.ts +26 -4
  363. package/src/client/browser-entry/hydrate.ts +53 -58
  364. package/src/client/browser-entry/index.ts +16 -27
  365. package/src/client/browser-entry/post-hydration.ts +6 -40
  366. package/src/client/browser-entry/router-init.ts +66 -84
  367. package/src/client/error-boundary.tsx +9 -41
  368. package/src/client/error-reconstituter.tsx +10 -5
  369. package/src/client/history.ts +18 -21
  370. package/src/client/index.ts +0 -1
  371. package/src/client/internal.ts +2 -7
  372. package/src/client/navigation-commit.ts +35 -18
  373. package/src/client/navigation-context.ts +3 -39
  374. package/src/client/react-root.ts +3 -2
  375. package/src/client/router-pipeline.ts +65 -128
  376. package/src/client/router-types.ts +46 -44
  377. package/src/client/router-url.ts +32 -0
  378. package/src/client/router.ts +43 -22
  379. package/src/client/rsc-fetch.ts +117 -155
  380. package/src/client/ssr-data.ts +19 -80
  381. package/src/client/state.ts +2 -9
  382. package/src/client/use-pathname.ts +1 -1
  383. package/src/config-types.ts +2 -1
  384. package/src/config-validation.ts +2 -1
  385. package/src/dev-tools/dev-page-shell.ts +18 -0
  386. package/src/dev-tools/index.ts +0 -2
  387. package/src/dev-tools/instrumentation.ts +50 -2
  388. package/src/dev-tools/overlay.ts +8 -238
  389. package/src/index.ts +24 -14
  390. package/src/plugin-context.ts +39 -40
  391. package/src/plugins/build-manifest.ts +4 -11
  392. package/src/plugins/build-report.ts +70 -70
  393. package/src/plugins/dev-server.ts +24 -44
  394. package/src/plugins/entries.ts +2 -1
  395. package/src/plugins/fonts.ts +35 -7
  396. package/src/plugins/post-build.ts +327 -0
  397. package/src/plugins/prebuilt-capture.ts +28 -32
  398. package/src/plugins/prebuilt.ts +54 -39
  399. package/src/plugins/prerender-sugar.ts +0 -4
  400. package/src/plugins/{adapter-build.ts → rsc-assets-manifest.ts} +20 -109
  401. package/src/plugins/shims.ts +1 -0
  402. package/src/plugins/static-build.ts +42 -74
  403. package/src/routing/codegen-write.ts +25 -3
  404. package/src/routing/segment-codegen.ts +3 -2
  405. package/src/rsc-runtime/vendor-types.d.ts +8 -0
  406. package/src/server/access-gate.tsx +2 -1
  407. package/src/server/action-client.ts +1 -1
  408. package/src/server/action-handler.ts +253 -110
  409. package/src/server/actions.ts +73 -25
  410. package/src/server/als-registry.ts +73 -5
  411. package/src/server/chain-url-parts.ts +1 -1
  412. package/src/server/children-interception.ts +2 -1
  413. package/src/server/cookie-context.ts +18 -48
  414. package/src/server/csp-nonce.ts +0 -12
  415. package/src/server/csrf.ts +140 -8
  416. package/src/server/debug.ts +0 -1
  417. package/src/server/default-logger.ts +2 -1
  418. package/src/server/deny-boundary.ts +25 -13
  419. package/src/server/deny-renderer.ts +30 -97
  420. package/src/server/error-boundary-wrapper.ts +7 -3
  421. package/src/server/fallback-error.ts +26 -11
  422. package/src/server/flight-scripts.ts +1 -1
  423. package/src/server/head-response.ts +4 -4
  424. package/src/server/html-injector-core.ts +2 -2
  425. package/src/server/html-injectors.ts +2 -4
  426. package/src/server/index.ts +0 -4
  427. package/src/server/instrumentation.ts +10 -3
  428. package/src/server/internal.ts +1 -6
  429. package/src/server/json-status-response.ts +105 -0
  430. package/src/server/logger.ts +39 -3
  431. package/src/server/param-coercion.ts +15 -12
  432. package/src/server/pipeline-helpers.ts +257 -12
  433. package/src/server/pipeline-interception.ts +133 -0
  434. package/src/server/pipeline-outcome.ts +103 -31
  435. package/src/server/pipeline-phases.ts +127 -237
  436. package/src/server/pipeline.ts +204 -207
  437. package/src/server/prebuilt/capture-render.ts +118 -0
  438. package/src/server/prebuilt/capture-routes.ts +104 -0
  439. package/src/server/prebuilt/capture-state.ts +155 -48
  440. package/src/server/prebuilt/lifetime.ts +151 -0
  441. package/src/server/prebuilt/slots.ts +14 -2
  442. package/src/server/prebuilt/synthetic-store.ts +3 -0
  443. package/src/server/prebuilt-builder.ts +30 -320
  444. package/src/server/prebuilt-runtime.ts +125 -129
  445. package/src/server/primitives.ts +8 -168
  446. package/src/server/react-cache-bridge.ts +124 -0
  447. package/src/server/react-cache-scope.ts +104 -0
  448. package/src/server/render-timeout.ts +20 -0
  449. package/src/server/render-utils.ts +11 -1
  450. package/src/server/request-context.ts +36 -9
  451. package/src/server/route-element-builder.ts +11 -6
  452. package/src/server/route-handler.ts +36 -23
  453. package/src/server/route-matcher.ts +15 -1
  454. package/src/server/rsc-entry/action-middleware-runner.ts +79 -123
  455. package/src/server/rsc-entry/api-handler.ts +27 -103
  456. package/src/server/rsc-entry/deny-fallback.ts +93 -26
  457. package/src/server/rsc-entry/error-renderer.ts +39 -91
  458. package/src/server/rsc-entry/helpers.ts +2 -4
  459. package/src/server/rsc-entry/index.ts +64 -58
  460. package/src/server/rsc-entry/render-route.ts +15 -80
  461. package/src/server/rsc-entry/render-signals.ts +108 -0
  462. package/src/server/rsc-entry/revalidate-renderer.ts +56 -57
  463. package/src/server/rsc-entry/rsc-payload.ts +28 -61
  464. package/src/server/rsc-entry/rsc-stream.ts +113 -213
  465. package/src/server/rsc-entry/ssr-bridge.ts +2 -2
  466. package/src/server/rsc-entry/ssr-renderer.ts +182 -263
  467. package/src/server/rsc-entry/wrap-action-dispatch.ts +12 -14
  468. package/src/server/signal-identity.ts +112 -0
  469. package/src/server/slot-resolver.ts +18 -18
  470. package/src/server/slot-subtree-contain.ts +2 -1
  471. package/src/server/ssr-bridge-types.ts +92 -56
  472. package/src/server/ssr-entry.ts +87 -111
  473. package/src/server/ssr-render.ts +55 -64
  474. package/src/server/ssr-wrappers.tsx +2 -2
  475. package/src/server/static-generator.ts +7 -42
  476. package/src/server/static-not-found.ts +3 -3
  477. package/src/server/stream-utils.ts +51 -0
  478. package/src/server/tracing.ts +58 -75
  479. package/src/server/types.ts +1 -1
  480. package/src/server/version-skew.ts +5 -5
  481. package/src/shared/csrf-reject.ts +15 -0
  482. package/src/shared/error-digest.ts +46 -0
  483. package/src/shared/payload-root.ts +11 -10
  484. package/src/shared/rsc-payload-path.ts +8 -1
  485. package/src/utils/write-file-atomic.ts +32 -0
  486. package/dist/_chunks/actions-jNrXdzgs.js +0 -346
  487. package/dist/_chunks/actions-jNrXdzgs.js.map +0 -1
  488. package/dist/_chunks/als-registry-C6kcfprT.js.map +0 -1
  489. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +0 -1
  490. package/dist/_chunks/build-output-helper-9dqCv1pZ.js.map +0 -1
  491. package/dist/_chunks/cache-api-CUD9Ezaq.js.map +0 -1
  492. package/dist/_chunks/cli-check-G8o30ldg.js.map +0 -1
  493. package/dist/_chunks/convention-lint-5KzoMg_A.js.map +0 -1
  494. package/dist/_chunks/dev-server-BzBxgYAV.js.map +0 -1
  495. package/dist/_chunks/error-boundary-BrHmG-2X.js.map +0 -1
  496. package/dist/_chunks/fs-identity-D7vb6GdI.js.map +0 -1
  497. package/dist/_chunks/graph-cache-CP4GEmf9.js.map +0 -1
  498. package/dist/_chunks/json-lossy-check-ClNvBM_3.js.map +0 -1
  499. package/dist/_chunks/logger-k2DQ4EUf.js.map +0 -1
  500. package/dist/_chunks/navigation-context-D0TU0Jog.js.map +0 -1
  501. package/dist/_chunks/plugin-context-8idfR3UZ.js.map +0 -1
  502. package/dist/_chunks/primitives-DLAnvsrk.js.map +0 -1
  503. package/dist/_chunks/segment-context-D9_89u34.js +0 -34
  504. package/dist/_chunks/segment-context-D9_89u34.js.map +0 -1
  505. package/dist/_chunks/sizeof-lXffQ1Vr.js.map +0 -1
  506. package/dist/_chunks/ssr-data-D6T6Y3ef.js +0 -60
  507. package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +0 -1
  508. package/dist/_chunks/state-FippDgxN.js.map +0 -1
  509. package/dist/_chunks/status-page-marker-CNbpUOPB.js.map +0 -1
  510. package/dist/client/types.d.ts +0 -14
  511. package/dist/client/types.d.ts.map +0 -1
  512. package/dist/plugins/adapter-build.d.ts +0 -38
  513. package/dist/plugins/adapter-build.d.ts.map +0 -1
  514. package/dist/server/asset-headers.d.ts +0 -32
  515. package/dist/server/asset-headers.d.ts.map +0 -1
  516. package/dist/server/dev-source-map.d.ts +0 -22
  517. package/dist/server/dev-source-map.d.ts.map +0 -1
  518. package/src/client/types.ts +0 -21
  519. package/src/server/asset-headers.ts +0 -59
  520. package/src/server/dev-source-map.ts +0 -31
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger-DiDt5ppH.js","names":[],"sources":["../../src/server/tracing.ts","../../src/server/debug.ts","../../src/server/error-formatter.ts","../../src/server/default-logger.ts","../../src/server/signal-identity.ts","../../src/server/logger.ts"],"sourcesContent":["/**\n * Tracing — per-request trace ID via AsyncLocalStorage, OTEL span helpers.\n *\n * getTraceId() is always available in server code (middleware, access, components, actions).\n * Returns a 32-char lowercase hex string — the OTEL trace ID when an SDK is active,\n * or a crypto.randomUUID()-derived fallback otherwise.\n *\n * See design/17-logging.md §\"trace_id is Always Set\"\n */\n\nimport { randomUUID } from 'node:crypto';\nimport { traceAls, type TraceStore } from './als-registry.ts';\n\n// Re-export the TraceStore type for public API consumers.\nexport type { TraceStore } from './als-registry.ts';\n\n// ─── Public API ───────────────────────────────────────────────────────────\n\n/**\n * Returns the current request's trace ID — always a 32-char lowercase hex string.\n *\n * With OTEL: the real OTEL trace ID (matches Jaeger/Honeycomb/Datadog).\n * Without OTEL: crypto.randomUUID() with hyphens stripped.\n *\n * Throws if called outside a request context (no ALS store).\n */\nexport function getTraceId(): string {\n const store = traceAls.getStore();\n if (!store) {\n throw new Error(\n '[timber] getTraceId() called outside of a request context. ' +\n 'It can only be used in middleware, access checks, server components, and server actions.'\n );\n }\n return store.request.traceId;\n}\n\n/**\n * Returns the current OTEL span ID if available, undefined otherwise.\n */\nexport function getSpanId(): string | undefined {\n return traceAls.getStore()?.spanId;\n}\n\n// ─── Framework-Internal Helpers ───────────────────────────────────────────\n\n/**\n * Generate a 32-char lowercase hex ID from crypto.randomUUID().\n * Same format as OTEL trace IDs — zero-friction upgrade path.\n */\nexport function generateTraceId(): string {\n return randomUUID().replace(/-/g, '');\n}\n\n/**\n * Run a callback within a trace context. Used by the pipeline to establish\n * per-request ALS scope.\n */\nexport function runWithTraceId<T>(id: string, fn: () => T): T {\n return traceAls.run({ request: { traceId: id } }, fn);\n}\n\n/**\n * Replace the request's trace ID. Used when OTEL creates the root span and\n * we want to switch from the UUID fallback to the real OTEL trace ID. Writes\n * the request's shared trace, so every scope in the request sees it —\n * including the ones outside the span this runs in.\n */\nexport function replaceTraceId(newTraceId: string): void {\n const store = traceAls.getStore();\n if (store) {\n store.request.traceId = newTraceId;\n }\n}\n\n/**\n * Run `fn` in a new trace scope for one span: the enclosing scope's request\n * trace, with this span's pointers. A scope per span, not a save/restore on\n * a shared store — spans overlap (MetadataCollector does not await\n * `timber.metadata`), and a restore that runs after a sibling's would\n * leave the sibling's pointer behind (TIM-1538).\n */\nfunction runInSpanScope<T>(\n pointers: Pick<TraceStore, 'spanId'> | Pick<TraceStore, 'platformSpan'>,\n fn: () => T\n): T {\n const store = traceAls.getStore();\n return store ? traceAls.run({ ...store, ...pointers }, fn) : fn();\n}\n\n/**\n * Get the current trace store, or undefined if outside a request context.\n * Framework-internal — use getTraceId()/getSpanId() in user code.\n */\nexport function getTraceStore(): TraceStore | undefined {\n return traceAls.getStore();\n}\n\n// ─── Dev-Mode OTEL Auto-Init ─────────────────────────────────────────────\n\n/**\n * Well-known key marking dev tracing as initialized.\n *\n * The RSC entry module re-evaluates on every HMR invalidation and calls\n * initDevTracing() again — without this guard, each call would register a\n * fresh provider/processor and duplicate every span's output. Symbol.for()\n * survives module re-evaluation. See TIM-1067, B49.\n */\nconst DEV_TRACING_INIT_KEY = Symbol.for('timber.dev.tracing-initialized');\n\n/**\n * Initialize a minimal OTEL SDK in dev mode so spans are recorded and\n * fed to the DevSpanProcessor for dev log output.\n *\n * If the user already configured an OTEL SDK in register(), we add\n * our DevSpanProcessor alongside theirs. If no SDK is configured,\n * we create a BasicTracerProvider with our processor.\n *\n * Idempotent across module re-evaluations (HMR) — only the first call\n * registers. Called in dev, and in production only when TIMBER_DEBUG is\n * set — otherwise production never loads the OTEL SDK.\n */\nexport async function initDevTracing(\n config: import('../dev-tools/logger.ts').DevLoggerConfig\n): Promise<void> {\n const globals = globalThis as Record<symbol, unknown>;\n if (globals[DEV_TRACING_INIT_KEY]) return;\n\n const api = await getOtelApi();\n if (!api) return;\n\n // Tracing is a diagnostic: any failure to load or construct the SDK\n // disables it with a warning and never fails the server's boot (TIM-1554).\n try {\n const { registerDevTracing } = await import('../dev-tools/instrumentation.ts');\n registerDevTracing(config);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(`[timber] Dev tracing disabled — failed to set up OTEL:\\n ${msg}`);\n return;\n }\n\n // Reset cached tracer so next getTracer() picks up the new provider\n _tracer = undefined;\n\n globals[DEV_TRACING_INIT_KEY] = true;\n}\n\n// ─── Platform Tracer ─────────────────────────────────────────────────────\n\n/**\n * A native platform span. Mirrors the subset of the Cloudflare Workers\n * `Span` API that timber uses. See design/17-logging.md §\"Cloudflare Native\n * Spans\" and TIM-1135.\n */\nexport interface PlatformSpan {\n setAttribute(key: string, value: string | number | boolean): void;\n}\n\n/**\n * Adapter-provided native tracer. Callback-scoped like Cloudflare's\n * `tracing.enterSpan()` — the span starts when the callback is invoked and\n * ends when it returns or its promise settles. Nesting follows the\n * platform's async context.\n *\n * When registered, withSpan() wraps every framework span in a native span\n * *in addition to* the OTEL emission — dual emission, so external OTEL\n * collectors keep working alongside the platform's native trace view.\n *\n * Register via setPlatformTracer(), or by writing the\n * Symbol.for('timber:platform-tracer') key on globalThis from\n * adapter-generated entry code (what the Cloudflare _worker.js does).\n */\nexport interface PlatformTracer {\n enterSpan<T>(name: string, fn: (span: PlatformSpan) => T): T;\n}\n\n// globalThis + Symbol.for so a tracer registered by adapter-generated entry\n// code (a separate module instance) is visible in both the RSC and SSR\n// environments — same pattern as the cf-bindings ALS.\nconst PLATFORM_TRACER_KEY = Symbol.for('timber:platform-tracer');\n\n/** Register (or clear) the native platform tracer for this runtime. */\nexport function setPlatformTracer(tracer: PlatformTracer | undefined): void {\n (globalThis as Record<symbol, unknown>)[PLATFORM_TRACER_KEY] = tracer;\n}\n\n/** The registered native platform tracer, if any. */\nexport function getPlatformTracer(): PlatformTracer | undefined {\n return (globalThis as Record<symbol, unknown>)[PLATFORM_TRACER_KEY] as PlatformTracer | undefined;\n}\n\n// ─── OTEL Span Helpers ───────────────────────────────────────────────────\n\n/**\n * Attempt to get the @opentelemetry/api tracer. Returns undefined if the\n * package is not installed or no SDK is registered.\n *\n * timber.js depends on @opentelemetry/api as the vendor-neutral interface.\n * The API is a no-op by default — spans are only emitted when the developer\n * initializes an SDK in register().\n */\nlet _otelApi: typeof import('@opentelemetry/api') | null | undefined;\n\nasync function getOtelApi(): Promise<typeof import('@opentelemetry/api') | null> {\n if (_otelApi === undefined) {\n try {\n _otelApi = await import('@opentelemetry/api');\n } catch {\n _otelApi = null;\n }\n }\n return _otelApi;\n}\n\n/** OTEL tracer instance, lazily created. */\nlet _tracer: import('@opentelemetry/api').Tracer | null | undefined;\n\n/**\n * Get the timber.js OTEL tracer. Returns null if @opentelemetry/api is not available.\n */\nexport async function getTracer(): Promise<import('@opentelemetry/api').Tracer | null> {\n if (_tracer === undefined) {\n const api = await getOtelApi();\n if (api) {\n _tracer = api.trace.getTracer('timber.js');\n } else {\n _tracer = null;\n }\n }\n return _tracer;\n}\n\n/**\n * Run a function within a framework span. Composes two emission channels:\n *\n * - **Native platform span** — when an adapter registered a PlatformTracer\n * (Cloudflare Workers), the fn is wrapped in platformTracer.enterSpan()\n * so it appears in the platform's native trace view.\n * - **OTEL span** — when an OTEL SDK is active, the fn also runs inside an\n * OTEL span (dual emission). No SDK and no platform tracer = zero overhead.\n *\n * Automatically:\n * - Creates the span as a child of the current context\n * - Runs fn in its own trace scope carrying the span's IDs, for log–trace\n * correlation and setSpanAttribute()\n * - Ends the span when the function completes\n * - Records exceptions on error (OTEL channel)\n */\nexport async function withSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: () => T | Promise<T>\n): Promise<T> {\n const platformTracer = getPlatformTracer();\n if (!platformTracer) {\n return runOtelSpan(name, attributes, fn);\n }\n\n // Native platform span wraps the OTEL emission (dual emission). The\n // span is tracked on its own trace scope so setSpanAttribute() can reach\n // it after creation — the platform API has no getActiveSpan() equivalent.\n return platformTracer.enterSpan(name, (span) => {\n for (const key of Object.keys(attributes)) {\n span.setAttribute(key, attributes[key]);\n }\n return runInSpanScope({ platformSpan: span }, () => runOtelSpan(name, attributes, fn));\n });\n}\n\n/** The OTEL half of withSpan() — no-op passthrough when no SDK is active. */\nasync function runOtelSpan<T>(\n name: string,\n attributes: Record<string, string | number | boolean>,\n fn: () => T | Promise<T>\n): Promise<T> {\n const tracer = await getTracer();\n if (!tracer) {\n return fn();\n }\n\n const api = (await getOtelApi())!;\n return tracer.startActiveSpan(name, { attributes }, (span) =>\n runInSpanScope({ spanId: span.spanContext().spanId }, () => settleOtelSpan(api, span, fn))\n );\n}\n\n/** Run `fn` and end `span` with its outcome. */\nasync function settleOtelSpan<T>(\n api: typeof import('@opentelemetry/api'),\n span: import('@opentelemetry/api').Span,\n fn: () => T | Promise<T>\n): Promise<T> {\n try {\n const result = await fn();\n span.setStatus({ code: api.SpanStatusCode.OK });\n return result;\n } catch (error) {\n span.setStatus({ code: api.SpanStatusCode.ERROR });\n if (error instanceof Error) {\n span.recordException(error);\n }\n throw error;\n } finally {\n span.end();\n }\n}\n\n/**\n * Set an attribute on the current active span (if any).\n * Used for setting span attributes after span creation (e.g. timber.result on access spans).\n */\nexport async function setSpanAttribute(\n key: string,\n value: string | number | boolean\n): Promise<void> {\n // Forward to the innermost active native platform span, if any.\n const platformSpan = traceAls.getStore()?.platformSpan;\n if (platformSpan) {\n platformSpan.setAttribute(key, value);\n }\n\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.setAttribute(key, value);\n }\n}\n\n/**\n * Add a span event to the current active span (if any).\n * Used for timber.cache HIT/MISS events — recorded as span events, not child spans.\n */\nexport async function addSpanEvent(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): Promise<void> {\n const api = await getOtelApi();\n if (!api) return;\n\n const activeSpan = api.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Fire-and-forget span event — no await, no microtask overhead.\n *\n * Used on the cache hot path where awaiting addSpanEvent creates an\n * unnecessary microtask per cache operation. If OTEL is not loaded yet,\n * the event is silently dropped (acceptable for diagnostics).\n *\n * See TIM-370 for perf motivation.\n */\nexport function addSpanEventSync(\n name: string,\n attributes?: Record<string, string | number | boolean>\n): void {\n // Fast path: if OTEL API hasn't been loaded yet, skip entirely.\n // _otelApi is undefined (not yet loaded), null (failed to load), or the module.\n if (!_otelApi) return;\n\n const activeSpan = _otelApi.trace.getActiveSpan();\n if (activeSpan) {\n activeSpan.addEvent(name, attributes);\n }\n}\n\n/**\n * Try to extract the OTEL trace ID from the current active span context.\n * Returns undefined if OTEL is not active or no span exists. (The span ID\n * reaches logs through the span's own trace scope, not from here.)\n */\nexport async function getOtelTraceId(): Promise<string | undefined> {\n const api = await getOtelApi();\n if (!api) return undefined;\n\n const activeSpan = api.trace.getActiveSpan();\n if (!activeSpan) return undefined;\n\n const ctx = activeSpan.spanContext();\n // OTEL uses \"0000000000000000\" as invalid trace IDs\n if (!ctx.traceId || ctx.traceId === '00000000000000000000000000000000') {\n return undefined;\n }\n\n return ctx.traceId;\n}\n","/**\n * Runtime debug flag for timber.js.\n *\n * Two distinct functions for two distinct security levels:\n *\n * ## `isDebug()` — server-side logging only\n *\n * Returns true when timber's debug/warning messages should be written to\n * stderr / the server console. This NEVER affects what is sent to the\n * client (no error details, no timing headers, no stack traces).\n *\n * Active when any of:\n * - `NODE_ENV !== 'production'` (standard dev mode)\n * - `TIMBER_DEBUG` env var is set to a truthy value at runtime\n * - `timber.config.ts` has `debug: true`\n *\n * ## `isDevMode()` — client-visible dev behavior\n *\n * Returns true ONLY when `NODE_ENV !== 'production'`. This gates anything\n * that changes what clients can observe:\n * - Dev error pages with stack traces (fallback-error.ts)\n * - Detailed Server-Timing headers (pipeline.ts)\n * - Error messages in action INTERNAL_ERROR payloads (action-client.ts)\n * - Pipeline error handler wiring (Vite overlay)\n *\n * `isDevMode()` is statically replaced in production builds → the guarded\n * code is tree-shaken to zero bytes. TIMBER_DEBUG cannot enable it.\n *\n * Usage:\n * In Cloudflare Workers wrangler.toml:\n * [vars]\n * TIMBER_DEBUG = \"1\"\n *\n * In Node.js:\n * TIMBER_DEBUG=1 node server.js\n *\n * In timber.config.ts:\n * export default { debug: true }\n *\n * See design/13-security.md for the security taxonomy.\n * See design/18-build-system.md for build pipeline details.\n */\n\n// ─── Dev Mode (client-visible) ──────────────────────────────────────────────\n\n/**\n * Check if the application is running in development mode.\n *\n * This is the ONLY function that should gate client-visible dev behavior:\n * - Dev error pages with stack traces\n * - Error messages in action `INTERNAL_ERROR` payloads\n * - Pipeline error handler wiring (Vite overlay)\n *\n * Returns `process.env.NODE_ENV !== 'production'`, which is statically\n * replaced by the bundler in production builds. Code guarded by this\n * function is tree-shaken to zero bytes in production.\n *\n * TIMBER_DEBUG does NOT enable this — that would leak server internals\n * to clients. Use `isDebug()` for server-side-only logging.\n */\nexport function isDevMode(): boolean {\n return process.env.NODE_ENV !== 'production';\n}\n\n// ─── Debug Flag (server-side logging only) ──────────────────────────────────\n\n/**\n * Config-level debug override. Set via `setDebugFromConfig()` during\n * initialization when timber.config.ts has `debug: true`.\n */\nlet _configDebug = false;\n\n/**\n * Set the debug flag from timber.config.ts.\n * Called during handler initialization.\n */\nexport function setDebugFromConfig(debug: boolean): void {\n _configDebug = debug;\n}\n\n/**\n * Check if timber debug logging is active (server-side only).\n *\n * Returns true if ANY of these conditions hold:\n * - NODE_ENV is not 'production' (standard dev mode)\n * - TIMBER_DEBUG environment variable is set to a truthy value at runtime\n * - timber.config.ts has `debug: true`\n *\n * This function controls ONLY server-side logging — messages written to\n * stderr or the server console. It NEVER affects client-visible behavior\n * (error pages, response headers, action payloads). For client-visible\n * behavior, use `isDevMode()`.\n *\n * The TIMBER_DEBUG check is deliberately written as a dynamic property\n * access so bundlers cannot statically replace it.\n */\nexport function isDebug(): boolean {\n // Fast path: dev mode (statically replaced to `true` in dev, `false` in prod)\n if (process.env.NODE_ENV !== 'production') return true;\n\n // Config override\n if (_configDebug) return true;\n\n // Runtime env var check — uses dynamic access to prevent static replacement.\n // In production builds, process.env.NODE_ENV is statically replaced, but\n // TIMBER_DEBUG must survive as a runtime check. The dynamic key access\n // pattern ensures the bundler treats this as opaque.\n return _readTimberDebugEnv();\n}\n\n/**\n * Read TIMBER_DEBUG from the environment at runtime.\n *\n * Extracted to a separate function to:\n * 1. Prevent bundler inlining (cross-module function calls are not inlined)\n * 2. Handle platforms where `process` may not exist (Cloudflare Workers)\n * 3. Support globalThis.__TIMBER_DEBUG for programmatic control\n */\nfunction _readTimberDebugEnv(): boolean {\n // globalThis override — useful for programmatic control and testing\n if ((globalThis as Record<string, unknown>).__TIMBER_DEBUG) return true;\n\n // process.env — works in Node.js and platforms that polyfill process\n try {\n const key = 'TIMBER_DEBUG';\n const val =\n typeof process !== 'undefined' && process.env\n ? (process.env as Record<string, string | undefined>)[key]\n : undefined;\n if (val && val !== '0' && val !== 'false') return true;\n } catch {\n // process may not exist or env may throw — safe to ignore\n }\n\n return false;\n}\n","/**\n * Error Formatter — rewrites SSR/RSC error messages to surface user code.\n *\n * When React or Vite throw errors during SSR, stack traces reference\n * vendored dependency paths (e.g. `.vite/deps_ssr/@vitejs_plugin-rsc_vendor_...`)\n * and mangled export names (`__vite_ssr_export_default__`). This module\n * rewrites error messages and stack traces to point at user code instead.\n *\n * Dev-only — in production, errors go through the structured logger\n * without formatting.\n */\n\n// ─── Stack Trace Rewriting ──────────────────────────────────────────────────\n\n/**\n * Patterns that identify internal Vite/RSC vendor paths in stack traces.\n * These are replaced with human-readable labels.\n */\nconst VENDOR_PATH_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/@vitejs_plugin-rsc_vendor_react-server-dom[^\\s)]+/g,\n replacement: '<react-server-dom>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/@vitejs_plugin-rsc_vendor[^\\s)]+/g,\n replacement: '<rsc-vendor>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps_ssr\\/[^\\s)]+/g,\n replacement: '<vite-dep>',\n },\n {\n pattern: /node_modules\\/\\.vite\\/deps\\/[^\\s)]+/g,\n replacement: '<vite-dep>',\n },\n];\n\n/**\n * Patterns that identify Vite-mangled export names in error messages.\n */\nconst MANGLED_NAME_PATTERNS: Array<{ pattern: RegExp; replacement: string }> = [\n {\n pattern: /__vite_ssr_export_default__/g,\n replacement: '<default export>',\n },\n {\n pattern: /__vite_ssr_export_(\\w+)__/g,\n replacement: '<export $1>',\n },\n];\n\n/**\n * Rewrite an error's message and stack to replace internal Vite paths\n * and mangled names with human-readable labels.\n */\nexport function formatSsrError(error: unknown): string {\n if (!(error instanceof Error)) {\n return String(error);\n }\n\n let message = error.message;\n let stack = error.stack ?? '';\n\n // Rewrite mangled names in the message\n for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) {\n message = message.replace(pattern, replacement);\n }\n\n // Rewrite vendor paths in the stack\n for (const { pattern, replacement } of VENDOR_PATH_PATTERNS) {\n stack = stack.replace(pattern, replacement);\n }\n\n // Rewrite mangled names in the stack too\n for (const { pattern, replacement } of MANGLED_NAME_PATTERNS) {\n stack = stack.replace(pattern, replacement);\n }\n\n // Extract hints from React-specific error patterns\n const hint = extractErrorHint(error.message);\n\n // Build formatted output: cleaned message, hint (if any), then cleaned stack\n const parts: string[] = [];\n parts.push(message);\n if (hint) {\n parts.push(` → ${hint}`);\n }\n\n // Include only the user-code frames from the stack (skip the first line\n // which is the message itself, and filter out vendor-only frames)\n const userFrames = extractUserFrames(stack);\n if (userFrames.length > 0) {\n parts.push('');\n parts.push(' User code in stack:');\n for (const frame of userFrames) {\n parts.push(` ${frame}`);\n }\n }\n\n return parts.join('\\n');\n}\n\n// ─── Error Hint Extraction ──────────────────────────────────────────────────\n\n/**\n * Extract a human-readable hint from common React/RSC error messages.\n *\n * React error messages contain useful information but the surrounding\n * context (vendor paths, mangled names) obscures it. This extracts the\n * actionable part as a one-line hint.\n */\nfunction extractErrorHint(message: string): string | null {\n // \"Functions cannot be passed directly to Client Components\"\n // Extract the component and prop name from the JSX-like syntax in the message\n const fnPassedMatch = message.match(/Functions cannot be passed directly to Client Components/);\n if (fnPassedMatch) {\n // Try to extract the prop name from the message\n // React formats: <... propName={function ...} ...>\n const propMatch = message.match(/<[^>]*?\\s(\\w+)=\\{function/);\n if (propMatch) {\n return `Prop \"${propMatch[1]}\" is a function — mark it \"use server\" or call it before passing`;\n }\n return 'A function prop was passed to a Client Component — mark it \"use server\" or call it before passing';\n }\n\n // \"Objects are not valid as a React child\"\n if (message.includes('Objects are not valid as a React child')) {\n return 'An object was rendered as JSX children — convert to string or extract the value';\n }\n\n // \"Cannot read properties of undefined/null\"\n const nullRefMatch = message.match(\n /Cannot read propert(?:y|ies) of (undefined|null) \\(reading '(\\w+)'\\)/\n );\n if (nullRefMatch) {\n return `Accessed .${nullRefMatch[2]} on ${nullRefMatch[1]} — check that the value exists`;\n }\n\n // \"X is not a function\"\n const notFnMatch = message.match(/(\\w+) is not a function/);\n if (notFnMatch) {\n return `\"${notFnMatch[1]}\" is not a function — check imports and exports`;\n }\n\n // \"Element type is invalid\"\n if (message.includes('Element type is invalid')) {\n return 'A component resolved to undefined/null — check default exports and import paths';\n }\n\n // \"Invalid hook call\" — hooks called outside React's render context.\n // In RSC, this typically means a 'use client' component was executed as a\n // server component instead of being serialized as a client reference.\n if (message.includes('Invalid hook call')) {\n return (\n 'A hook was called outside of a React component render. ' +\n \"If this is a 'use client' component, ensure the directive is at the very top of the file \" +\n '(before any imports) and that @vitejs/plugin-rsc is loaded correctly. ' +\n \"Barrel re-exports from non-'use client' files do not propagate the directive.\"\n );\n }\n\n return null;\n}\n\n// ─── Stack Frame Filtering ──────────────────────────────────────────────────\n\n/**\n * Extract stack frames that reference user code (not node_modules,\n * not framework internals).\n *\n * Returns at most 5 frames to keep output concise.\n */\nfunction extractUserFrames(stack: string): string[] {\n const lines = stack.split('\\n');\n const userFrames: string[] = [];\n\n for (const line of lines) {\n const trimmed = line.trim();\n // Skip non-frame lines\n if (!trimmed.startsWith('at ')) continue;\n // Skip node_modules, vendor, and internal frames\n if (\n trimmed.includes('node_modules') ||\n trimmed.includes('<react-server-dom>') ||\n trimmed.includes('<rsc-vendor>') ||\n trimmed.includes('<vite-dep>') ||\n trimmed.includes('node:internal')\n ) {\n continue;\n }\n userFrames.push(trimmed);\n if (userFrames.length >= 5) break;\n }\n\n return userFrames;\n}\n","/**\n * DefaultLogger — human-readable stderr logging when no custom logger is configured.\n *\n * Ships as the fallback so production deployments always have error visibility,\n * even without an `instrumentation.ts` logger export. Output is one line per\n * event, designed for `fly logs`, `kubectl logs`, Cloudflare dashboard tails, etc.\n *\n * Format:\n * [timber] ERROR message key=value key=value trace_id=4bf92f35\n * [timber] WARN message key=value key=value trace_id=4bf92f35\n * [timber] INFO message method=GET path=/dashboard status=200 durationMs=43 trace_id=4bf92f35\n *\n * Behavior:\n * - `info` and `debug` suppressed in dev mode (dev logging handles them);\n * `warn` and `error` are always written (TIM-555)\n * - `debug` suppressed unless TIMBER_DEBUG is set\n * - Replaced entirely when a custom logger is set via `setLogger()`\n *\n * See design/17-logging.md §\"DefaultLogger\"\n */\n\nimport { isDevMode, isDebug } from './debug.ts';\nimport { formatSsrError } from './error-formatter.ts';\nimport type { TimberLogger } from './logger.ts';\n\n/**\n * Format data fields as `key=value` pairs for human-readable output.\n * - `error` key is serialized via formatSsrError for stack trace cleanup\n * - `trace_id` is truncated to 8 chars for readability (full ID in OTEL)\n * - Other values are stringified inline\n */\nfunction formatDataFields(data?: Record<string, unknown>): string {\n if (!data) return '';\n\n const parts: string[] = [];\n let traceId: string | undefined;\n\n for (const [key, value] of Object.entries(data)) {\n if (key === 'trace_id') {\n // Defer trace_id to the end\n traceId = typeof value === 'string' ? value : String(value);\n continue;\n }\n if (key === 'error') {\n // Serialize errors with formatSsrError for clean output\n parts.push(`error=${formatSsrError(value)}`);\n continue;\n }\n if (value === undefined || value === null) continue;\n parts.push(`${key}=${value}`);\n }\n\n // trace_id always last, truncated to 8 chars for readability\n if (traceId) {\n parts.push(`trace_id=${traceId.slice(0, 8)}`);\n }\n\n return parts.length > 0 ? ' ' + parts.join(' ') : '';\n}\n\n/** Pad level string to fixed width for alignment. */\nfunction padLevel(level: string): string {\n return level.padEnd(5);\n}\n\nexport function createDefaultLogger(): TimberLogger {\n return {\n error(msg: string, data?: Record<string, unknown>): void {\n // Errors are ALWAYS logged, including dev mode. Suppressing errors\n // in dev causes silent 500s with no stack trace, making route.ts\n // and render errors impossible to debug. See TIM-555.\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('ERROR')} ${msg}${fields}\\n`);\n },\n\n warn(msg: string, data?: Record<string, unknown>): void {\n // Warnings are always logged — same rationale as errors.\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('WARN')} ${msg}${fields}\\n`);\n },\n\n info(msg: string, data?: Record<string, unknown>): void {\n // info is suppressed by default — per-request lines are too noisy\n // without a custom logger. Enable with TIMBER_DEBUG.\n if (isDevMode()) return;\n if (!isDebug()) return;\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('INFO')} ${msg}${fields}\\n`);\n },\n\n debug(msg: string, data?: Record<string, unknown>): void {\n // debug is suppressed in dev (dev logger handles it) and in\n // production unless TIMBER_DEBUG is explicitly set.\n if (isDevMode()) return;\n if (!isDebug()) return;\n const fields = formatDataFields(data);\n process.stderr.write(`[timber] ${padLevel('DEBUG')} ${msg}${fields}\\n`);\n },\n };\n}\n","// Signal identity — the brands framework signals carry, the predicates that\n// recognise them, and SsrStreamError.\n//\n// No runtime imports (the one import is type-only), so any environment can\n// load it. The SSR entry's import graph\n// must not reach RSC request state (`request-context.ts` and friends, held\n// to that by `tests/ssr-import-boundary.test.ts`), and the signal classes\n// in primitives.ts sit beside `deny()` and `redirect()`, which read it. SSR\n// code that has to recognise a signal (the logger skips them) imports the\n// predicates from here; the classes stamp the brands exported here.\n\nimport type { DenySignal, RedirectSignal } from './primitives.ts';\n\n// ─── Signal branding ────────────────────────────────────────────────────────\n//\n// Catch sites must NOT use `instanceof` on these classes. In dev, an HMR\n// program reload clears the RSC module cache; a request that is mid-render\n// still holds the catch sites from the old evaluation of primitives.ts while\n// user code loaded after the reload throws from the new one. The two\n// evaluations are distinct classes, so `instanceof` fails and a correct\n// deny() falls through every catch site as an unhandled 500 (TIM-1499).\n//\n// Each class stamps a `Symbol.for` brand on its instances in the constructor.\n// `Symbol.for` keys live in the realm-wide symbol registry, so every\n// evaluation of these modules — and every module graph in the same process —\n// agrees on the key. The `is*` predicates below check the brand and are the\n// only supported way to detect a signal. The classes themselves are NOT\n// anchored on globalThis: a re-evaluated module must hand out its own\n// (possibly updated) class, not a stale one.\n//\n// See design/18-build-system.md §\"Dev Mode: Signal Identity Across Program Reloads\".\n\nexport const DENY_BRAND: unique symbol = Symbol.for('timber:signal.deny');\nexport const REDIRECT_BRAND: unique symbol = Symbol.for('timber:signal.redirect');\nconst SSR_STREAM_ERROR_BRAND: unique symbol = Symbol.for('timber:signal.ssr-stream-error');\n\nfunction hasBrand(value: unknown, brand: symbol): boolean {\n return typeof value === 'object' && value !== null && brand in value;\n}\n\n/** True for any `DenySignal`, regardless of which module evaluation created it. */\nexport function isDenySignal(error: unknown): error is DenySignal {\n return hasBrand(error, DENY_BRAND);\n}\n\n/** True for any `RedirectSignal`, regardless of which module evaluation created it. */\nexport function isRedirectSignal(error: unknown): error is RedirectSignal {\n return hasBrand(error, REDIRECT_BRAND);\n}\n\n/**\n * True for any `SsrStreamError`. This one also crosses Vite environments:\n * it is thrown in SSR and caught in RSC, which are separate module graphs in\n * the same process — the brand is the only identity the two share.\n */\nexport function isSsrStreamError(error: unknown): error is SsrStreamError {\n return hasBrand(error, SSR_STREAM_ERROR_BRAND);\n}\n\n/**\n * Returns true if the error is a framework control-flow signal (RedirectSignal\n * or DenySignal) rather than a genuine application error. Works both for direct\n * instances (RSC-side) and deserialized digest errors (SSR cross-boundary).\n *\n * See also: isFrameworkSignalError() in client/browser-dev.ts (client-only).\n */\nexport function isControlFlowSignal(error: unknown): boolean {\n if (isDenySignal(error) || isRedirectSignal(error)) {\n return true;\n }\n if (error && typeof error === 'object') {\n const digest = (error as { digest?: unknown }).digest;\n if (typeof digest === 'string') {\n try {\n const parsed = JSON.parse(digest) as { type?: unknown } | null;\n if (parsed && typeof parsed === 'object') {\n return parsed.type === 'redirect' || parsed.type === 'deny';\n }\n } catch {\n // Not valid digest JSON\n }\n }\n }\n return false;\n}\n\n// ─── SsrStreamError ─────────────────────────────────────────────────────────\n\n/**\n * Error thrown when SSR's renderToReadableStream fails due to an error\n * in the decoded RSC stream (e.g., uncontained slot errors).\n *\n * The RSC entry checks for this error type in its catch block. A captured\n * deny or redirect is rethrown to the pipeline first; with no captured\n * signal, it renders the error page for the uncontained error.\n *\n * Not defined in ssr-entry.ts because ssr-entry.ts imports react-dom/server,\n * which cannot be loaded in the RSC environment.\n *\n * Detect with `isSsrStreamError()` — it is thrown in the SSR environment and\n * caught in RSC, so `instanceof` never matches. See \"Signal branding\" above.\n */\nexport class SsrStreamError extends Error {\n readonly [SSR_STREAM_ERROR_BRAND] = true;\n override readonly cause: unknown;\n\n constructor(message: string, cause: unknown) {\n super(message);\n this.name = 'SsrStreamError';\n this.cause = cause;\n }\n}\n","/**\n * Logger — structured logging with environment-aware formatting.\n *\n * timber.js ships a DefaultLogger that writes human-readable lines to stderr\n * in production. Users can export a custom logger from instrumentation.ts to\n * replace it with pino, winston, or any TimberLogger-compatible object.\n *\n * See design/17-logging.md §\"Production Logging\"\n */\n\nimport { generateTraceId, getTraceStore } from './tracing.ts';\nimport { createDefaultLogger } from './default-logger.ts';\nimport { isDevMode } from './debug.ts';\nimport { isControlFlowSignal } from './signal-identity.ts';\nimport type { CsrfRejectReason } from './csrf.ts';\n\n// ─── Logger Interface ─────────────────────────────────────────────────────\n\n/** Any object with standard log methods satisfies this — pino, winston, consola, console. */\nexport interface TimberLogger {\n info(msg: string, data?: Record<string, unknown>): void;\n warn(msg: string, data?: Record<string, unknown>): void;\n error(msg: string, data?: Record<string, unknown>): void;\n debug(msg: string, data?: Record<string, unknown>): void;\n}\n\n// ─── Logger Registry ──────────────────────────────────────────────────────\n\n// Initialize with DefaultLogger so production errors are never silent.\n// Replaced when setLogger() is called from instrumentation.ts.\nlet _logger: TimberLogger = createDefaultLogger();\n\n/**\n * Set the user-provided logger. Called by the instrumentation loader\n * when it finds a `logger` export in instrumentation.ts. Replaces\n * the DefaultLogger entirely.\n */\nexport function setLogger(logger: TimberLogger): void {\n _logger = logger;\n}\n\n/**\n * Get the current logger. Always non-null — returns DefaultLogger when\n * no custom logger is configured.\n */\nexport function getLogger(): TimberLogger {\n return _logger;\n}\n\n// ─── Framework Log Helpers ────────────────────────────────────────────────\n\n/**\n * Inject trace_id and span_id into log data for log–trace correlation.\n * Always injects trace_id (never undefined). Injects span_id only when OTEL is active.\n */\nfunction withTraceContext(data?: Record<string, unknown>): Record<string, unknown> {\n const store = getTraceStore();\n const enriched: Record<string, unknown> = { ...data };\n if (store) {\n enriched.trace_id = store.request.traceId;\n if (store.spanId) {\n enriched.span_id = store.spanId;\n }\n }\n return enriched;\n}\n\n// ─── Framework Event Emitters ─────────────────────────────────────────────\n\n/**\n * Log a CSRF rejection. Level: warn — the 403 body is deliberately terse,\n * so this line is the only place the rejected origin is visible (TIM-1459).\n * `detail` names the observed origin and, where one exists, the expected one.\n *\n * The CSRF gate runs at the pipeline boundary, before the pipeline opens its\n * trace scope, so a rejection mints its own `trace_id` — the field is always\n * present (design/17-logging.md §\"trace_id is Always Set\").\n */\nexport function logCsrfRejected(\n detail: string,\n data: { method: string; path: string; reason: CsrfRejectReason }\n): void {\n const fields = withTraceContext(data);\n fields.trace_id ??= generateTraceId();\n _logger.warn(`CSRF origin check failed: ${detail}`, fields);\n}\n\n/** Log a completed request. Level: info. */\nexport function logRequestCompleted(data: {\n method: string;\n path: string;\n status: number;\n durationMs: number;\n /** Number of concurrent in-flight requests (including this one) at completion time. */\n concurrency?: number;\n}): void {\n _logger.info('request completed', withTraceContext(data));\n}\n\n/** Log request received. Level: debug. */\nexport function logRequestReceived(data: { method: string; path: string }): void {\n _logger.debug('request received', withTraceContext(data));\n}\n\n/** Log a slow request warning. Level: warn. */\nexport function logSlowRequest(data: {\n method: string;\n path: string;\n durationMs: number;\n threshold: number;\n /** Number of concurrent in-flight requests at the time the slow request completed. */\n concurrency?: number;\n}): void {\n _logger.warn('slow request exceeded threshold', withTraceContext(data));\n}\n\n/** Log middleware short-circuit. Level: debug. */\nexport function logMiddlewareShortCircuit(data: {\n method: string;\n path: string;\n status: number;\n}): void {\n _logger.debug('middleware short-circuited', withTraceContext(data));\n}\n\n/** Log unhandled error in middleware phase. Level: error. */\nexport function logMiddlewareError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled error in middleware phase', withTraceContext(data));\n}\n\n/** Log unhandled error in route setup (matching, param coercion). Level: error. */\nexport function logRouteSetupError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled error in route setup', withTraceContext(data));\n}\n\n/** Log unhandled render-phase error. Level: error. */\nexport function logRenderError(data: {\n method: string;\n path: string;\n error: unknown;\n errorId?: string;\n}): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled render-phase error', withTraceContext(data));\n}\n\n/** Log proxy.ts uncaught error. Level: error. */\nexport function logProxyError(data: { error: unknown }): void {\n _logger.error('proxy.ts threw uncaught error', withTraceContext(data));\n}\n\n/** Log unhandled error in server action. Level: error. */\nexport function logActionError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled server action error', withTraceContext(data));\n}\n\n/** Log unhandled error in route handler. Level: error. */\nexport function logRouteError(data: { method: string; path: string; error: unknown }): void {\n if (isControlFlowSignal(data.error)) return;\n _logger.error('unhandled route handler error', withTraceContext(data));\n}\n\n/** Log SSR streaming error (post-shell). Level: error. */\nexport function logStreamingError(data: { error: unknown }): void {\n _logger.error('SSR streaming error (post-shell)', withTraceContext(data));\n}\n\n/** Log waitUntil() adapter missing (once at startup). Level: warn. */\nexport function logWaitUntilUnsupported(): void {\n _logger.warn('adapter does not support waitUntil()');\n}\n\n/** Log waitUntil() promise rejection. Level: warn. */\nexport function logWaitUntilRejected(data: { error: unknown }): void {\n _logger.warn('waitUntil() promise rejected', withTraceContext(data));\n}\n\n/** Log staleWhileRevalidate refetch failure. Level: warn. */\nexport function logSwrRefetchFailed(data: { cacheKey: string; error: unknown }): void {\n _logger.warn('staleWhileRevalidate refetch failed', withTraceContext(data));\n}\n\n/** Log a cache value that loses fidelity through JSON serialization. Level: warn. */\nexport function logLossyCacheValue(data: { cacheKey: string; types: string }): void {\n _logger.warn(\n 'cache value contains types that lose fidelity through JSON serialization in ' +\n 'Redis/KV cache handlers: Date becomes an ISO string, Map/Set become {}, BigInt throws. ' +\n 'The MemoryCacheHandler preserves them. Transform values to JSON-safe types before ' +\n 'caching, or use MemoryCacheHandler for values that require type preservation.',\n withTraceContext(data)\n );\n}\n\n/** Log cache miss. Level: debug. */\nexport function logCacheMiss(data: { cacheKey: string }): void {\n _logger.debug('timber.cache MISS', withTraceContext(data));\n}\n\n// ─── Swallow Helper ───────────────────────────────────────────────────────\n\n/**\n * Log an intentionally swallowed error. Provides observability into catch\n * blocks that are deliberately empty — the error is consumed, never rethrown.\n *\n * Default level: `warn` in dev (so the overlay surfaces patterns), `debug`\n * in production (low noise unless TIMBER_DEBUG is set). Pass `opts.level`\n * to override.\n *\n * **Infallible** — swallow() itself never throws, even if the logger is\n * broken. A thrown swallow would turn a benign catch into a crash.\n */\nexport function swallow(err: unknown, reason: string, opts?: { level?: 'debug' | 'warn' }): void {\n try {\n const level = opts?.level ?? (isDevMode() ? 'warn' : 'debug');\n _logger[level](`swallowed: ${reason}`, withTraceContext({ error: err }));\n } catch {\n // swallow() must never throw.\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { r as _setGlobalRouter, u as globalRouter } from "./state-FippDgxN.js";
1
+ import { l as globalRouter, n as _setGlobalRouter } from "./state-BhNGPsdi.js";
2
2
  import { useSyncExternalStore } from "react";
3
3
  //#region src/client/router-ref.ts
4
4
  /**
@@ -123,4 +123,4 @@ function setHardNavigating(value) {
123
123
  //#endregion
124
124
  export { setGlobalRouter as a, getRouterOrNull as i, usePendingNavigation as n, getRouter as r, setHardNavigating as t };
125
125
 
126
- //# sourceMappingURL=navigation-root-mHSK9psY.js.map
126
+ //# sourceMappingURL=navigation-root-BQfo1-kG.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"navigation-root-mHSK9psY.js","names":[],"sources":["../../src/client/router-ref.ts","../../src/client/use-pending-navigation.ts","../../src/client/top-loader.tsx","../../src/client/navigation-root.tsx"],"sourcesContent":["// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router-types.ts';\nimport { globalRouter, _setGlobalRouter } from './state.ts';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (usePendingNavigation, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n","import { useSyncExternalStore } from 'react';\nimport { getRouterOrNull } from './router-ref.ts';\n\nfunction subscribe(onStoreChange: () => void): () => void {\n const router = getRouterOrNull();\n if (!router) return () => {};\n return router.onPendingChange(onStoreChange);\n}\n\nfunction getSnapshot(): boolean {\n const router = getRouterOrNull();\n return router ? router.isPending() : false;\n}\n\nconst getServerSnapshot = getSnapshot;\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * Reads from the router's external pending store via useSyncExternalStore.\n * Only components that call this hook re-render when pending state\n * changes — no full-tree re-render.\n *\n * ```tsx\n * 'use client'\n * import { usePendingNavigation } from '@timber-js/app/client'\n *\n * export function NavBar() {\n * const isPending = usePendingNavigation()\n * return (\n * <nav className={isPending ? 'opacity-50' : ''}>\n * <Link href=\"/dashboard\">Dashboard</Link>\n * </nav>\n * )\n * }\n * ```\n */\nexport function usePendingNavigation(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","/**\n * TopLoader — Built-in progress bar for client navigations.\n *\n * Shows an animated progress bar at the top of the viewport while an RSC\n * navigation is in flight. Injected automatically by the framework into\n * NavigationRoot — users never render this component directly.\n *\n * Configuration is via timber.config.ts `topLoader` key. Enabled by default.\n * Users who want a fully custom progress indicator disable the built-in one\n * (`topLoader: { enabled: false }`) and use `usePendingNavigation()` directly.\n *\n * Animation approach: pure CSS @keyframes. The bar crawls from 0% to ~90%\n * width over ~30s using ease-out timing. When navigation completes, the bar\n * snaps to 100% and fades out over 200ms. No JS animation loops (RAF, setInterval).\n *\n * Phase transitions are derived synchronously during render (React's\n * getDerivedStateFromProps pattern) — no useEffect needed for state tracking.\n * The finishing → hidden cleanup uses onTransitionEnd from the CSS transition.\n *\n * When delay > 0, CSS animation-delay + a visibility keyframe ensure the bar\n * stays invisible during the delay period. If navigation finishes before the\n * delay, the bar was never visible so the finish transition is also invisible.\n *\n * See design/19-client-navigation.md §\"usePendingNavigation()\"\n * See LOCAL-336 for design decisions.\n */\n\n'use client';\n\nimport { useState, createElement } from 'react';\nimport { usePendingNavigation } from './use-pending-navigation.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface TopLoaderConfig {\n /** Whether the top-loader is enabled. Default: true. */\n enabled?: boolean;\n /** Bar color. Default: '#2299DD'. */\n color?: string;\n /** Bar height in pixels. Default: 3. */\n height?: number;\n /** Show subtle glow/shadow effect. Default: false. */\n shadow?: boolean;\n /** Delay in ms before showing the bar. Default: 0. */\n delay?: number;\n /** CSS z-index. Default: 1600. */\n zIndex?: number;\n}\n\n// ─── Defaults ────────────────────────────────────────────────────\n\nconst DEFAULT_COLOR = '#2299DD';\nconst DEFAULT_HEIGHT = 3;\nconst DEFAULT_SHADOW = false;\nconst DEFAULT_DELAY = 0;\nconst DEFAULT_Z_INDEX = 1600;\n\n// ─── Keyframes ───────────────────────────────────────────────────\n\n// Unique keyframes name to avoid collisions with user styles.\nconst CRAWL_KEYFRAMES = '__timber_top_loader_crawl';\nconst APPEAR_KEYFRAMES = '__timber_top_loader_appear';\nconst FINISH_KEYFRAMES = '__timber_top_loader_finish';\n\n// Track whether the @keyframes rules have been injected into the document.\nlet keyframesInjected = false;\n\n/**\n * Inject the @keyframes rules into the document head once.\n * Called during render (idempotent). Uses a <style> tag so the\n * animations are available for inline-styled elements.\n */\nfunction ensureKeyframes(): void {\n if (keyframesInjected) return;\n if (typeof document === 'undefined') return;\n\n const style = document.createElement('style');\n style.textContent = `\n@keyframes ${CRAWL_KEYFRAMES} {\n 0% { width: 0%; }\n 100% { width: 90%; }\n}\n@keyframes ${APPEAR_KEYFRAMES} {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes ${FINISH_KEYFRAMES} {\n 0% { width: 90%; opacity: 1; }\n 50% { width: 100%; opacity: 1; }\n 100% { width: 100%; opacity: 0; }\n}\n`;\n document.head.appendChild(style);\n keyframesInjected = true;\n}\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Internal top-loader component. Injected by NavigationRoot.\n *\n * Reads pending navigation state from the router's external store, via\n * usePendingNavigation() — the one pending representation (TIM-1307).\n * Phase transitions are derived synchronously during render:\n *\n * hidden → crawling: when isPending becomes true\n * crawling → finishing: when isPending becomes false\n * finishing → hidden: when CSS transition ends (onTransitionEnd)\n * finishing → crawling: when isPending becomes true again\n *\n * No useEffect — all state changes are either derived during render\n * (getDerivedStateFromProps pattern) or triggered by DOM events.\n */\nexport function TopLoader({ config }: { config?: TopLoaderConfig }): React.ReactElement | null {\n // Read pending state from the router's external store via\n // useSyncExternalStore (inside usePendingNavigation). Only this\n // component re-renders when pending changes — no full-tree re-render.\n const isPending = usePendingNavigation();\n\n const color = config?.color ?? DEFAULT_COLOR;\n const height = config?.height ?? DEFAULT_HEIGHT;\n const shadow = config?.shadow ?? DEFAULT_SHADOW;\n const delay = config?.delay ?? DEFAULT_DELAY;\n const zIndex = config?.zIndex ?? DEFAULT_Z_INDEX;\n\n const [phase, setPhase] = useState<'hidden' | 'crawling' | 'finishing'>('hidden');\n\n // ─── Synchronous phase derivation (getDerivedStateFromProps) ──\n // React allows setState during render if the value changes — it\n // immediately re-renders with the updated state before committing.\n\n if (isPending && (phase === 'hidden' || phase === 'finishing')) {\n setPhase('crawling');\n }\n if (!isPending && phase === 'crawling') {\n setPhase('finishing');\n }\n\n // Inject keyframes on first visible render (idempotent)\n if (phase !== 'hidden') {\n ensureKeyframes();\n }\n\n if (phase === 'hidden') return null;\n\n // ─── Styles ──────────────────────────────────────────────────\n\n const containerStyle: React.CSSProperties = {\n position: 'fixed',\n top: 0,\n left: 0,\n width: '100%',\n height: `${height}px`,\n zIndex,\n pointerEvents: 'none',\n };\n\n const barStyle: React.CSSProperties = {\n height: '100%',\n backgroundColor: color,\n ...(phase === 'crawling'\n ? {\n // Crawl from 0% to 90% over 30s. When delay > 0, both the crawl\n // and a visibility animation are delayed — the bar stays at width 0%\n // and opacity 0 during the delay, then appears and starts crawling.\n // With delay 0, the appear animation is instant (0s duration, no delay).\n animation: [\n `${CRAWL_KEYFRAMES} 30s ease-out ${delay}ms forwards`,\n `${APPEAR_KEYFRAMES} 0s ${delay}ms both`,\n ].join(', '),\n }\n : {\n // Finishing: fill to 100% then fade out via a keyframe animation.\n // We use a keyframe instead of a CSS transition because the\n // animation-to-transition handoff is unreliable — the browser\n // may not capture the animated width as the transition's \"from\"\n // value when both the animation removal and transition are\n // applied in the same render frame.\n animation: `${FINISH_KEYFRAMES} 400ms ease forwards`,\n }),\n ...(shadow\n ? {\n boxShadow: `0 0 10px ${color}, 0 0 5px ${color}`,\n }\n : {}),\n };\n\n // Clean up the finishing phase when the finish animation completes.\n const handleAnimationEnd =\n phase === 'finishing'\n ? (e: React.AnimationEvent) => {\n if (e.animationName === FINISH_KEYFRAMES) {\n setPhase('hidden');\n }\n }\n : undefined;\n\n return createElement(\n 'div',\n {\n 'style': containerStyle,\n 'aria-hidden': 'true',\n 'data-timber-top-loader': '',\n },\n createElement('div', { style: barStyle, onAnimationEnd: handleAnimationEnd })\n );\n}\n","/**\n * NavigationRoot — the component the router renders the page through.\n *\n * It is stateless. The router owns the displayed tree and drives React with\n * `root.render(<NavigationRoot rendered={{ element, publish }} />)` inside a\n * synchronous `startTransition` (see `client/react-root.ts`). A transition\n * update keeps the committed tree on screen while the incoming one resolves,\n * instead of replacing it with a Suspense fallback — the reason this\n * component exists (TIM-1306).\n *\n * It used to hold the tree in `useState` and register `setState` closures\n * into module globals during render so the router could reach into it, with a\n * second set of stand-in closures for the window before a root existed\n * (TIM-600). Inverting that — router calls React, not the reverse — deleted\n * all of it (TIM-1431). Next.js's `use-action-queue.ts` says it wants this\n * shape and cannot have it because it must decode Flight during render;\n * timber decodes in `router-pipeline.ts`, so nothing stands in the way.\n *\n * What this component does own is **the commit-time publish**: a navigation's\n * state (segment cache, address bar, history stack, pathname) is published\n * from a layout effect keyed on the tree object, so the event that puts a\n * route on screen is the event that makes it current (TIM-1301). Props work\n * for that exactly as state did.\n *\n * This component holds no pending state. The `TopLoader` it renders and the\n * public `usePendingNavigation()` both subscribe to the router's external\n * pending store; NavigationRoot's own `pendingUrl` was a second, unread\n * representation of the same fact and is gone (TIM-1307).\n *\n * Hard navigation guard: When a hard navigation is triggered (500 error,\n * version skew), the component throws an unresolved thenable AFTER all\n * hooks to suspend forever — preventing React from rendering children\n * during page teardown. The throw must come after hooks to satisfy\n * React's rules (same hook count every render) while still preventing\n * child renders that could hit hook count mismatches in components\n * whose positions shift during teardown. This pattern is borrowed from\n * Next.js (app-router.tsx pushRef.mpaNavigation — also after hooks).\n *\n * See design/05-streaming.md §\"deferSuspenseFor\"\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport { createElement, Fragment, useLayoutEffect, useRef, type ReactNode } from 'react';\nimport { TopLoader, type TopLoaderConfig } from './top-loader.tsx';\n\n// ─── Rendered Tree ──────────────────────────────────────────────\n\n/**\n * The tree the router has handed to React, and the state publish that belongs\n * to it. They travel as one object so React's commit of the tree is the event\n * that publishes — see the effect in NavigationRoot. Every `render` builds a\n * fresh one, which is what keys that effect: a tree is published once, however\n * many times React re-runs the effect for it.\n */\nexport interface RenderedTree {\n element: ReactNode;\n /**\n * Publishes the navigation's state — segment cache, pathname, address bar,\n * history stack, and the client's record of the mounted tree — and\n * announces it to listeners outside React. Runs when React commits\n * `element`, never before (TIM-1301). Null for a render that has nothing to\n * publish on commit (hydration, the shallow search re-wrap).\n */\n publish: (() => void) | null;\n}\n\n/**\n * Hand a tree to React in a transition. `publish` runs when React commits it.\n *\n * Every path that puts a page on screen goes through one of these — a\n * navigation, a revalidation, a popstate replay, a shallow search re-wrap —\n * and it is always a synchronous `startTransition` around `root.render`.\n * The production one is `createReactRoot().render`.\n */\nexport type NavigationRender = (element: ReactNode, publish: (() => void) | null) => void;\n\n// ─── Hard Navigation Guard ──────────────────────────────────────\n\n/**\n * Module-level flag indicating a hard (MPA) navigation is in progress.\n *\n * When true:\n * - NavigationRoot throws an unresolved thenable to suspend forever,\n * preventing React from rendering children during page teardown\n * (avoids \"Rendered more hooks\" crashes).\n * - The Navigation API handler skips interception, letting the browser\n * perform a full page load (prevents infinite loops where\n * window.location.href → navigate event → router.navigate → 500 →\n * window.location.href → ...).\n *\n * Uses globalThis for singleton guarantee across chunks (same pattern\n * as NavigationContext). See design/19-client-navigation.md §\"Singleton\n * Guarantee via globalThis\".\n */\nconst HARD_NAV_KEY = Symbol.for('__timber_hard_navigating');\n\nfunction getHardNavStore(): { value: boolean } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[HARD_NAV_KEY]) {\n g[HARD_NAV_KEY] = { value: false };\n }\n return g[HARD_NAV_KEY] as { value: boolean };\n}\n\n/**\n * Set the hard-navigating flag. Call this BEFORE setting\n * window.location.href or window.location.reload() to prevent:\n * 1. React from rendering children during page teardown\n * 2. Navigation API from intercepting the hard navigation\n */\nexport function setHardNavigating(value: boolean): void {\n getHardNavStore().value = value;\n}\n\n/**\n * Check if a hard navigation is in progress.\n * Used by NavigationRoot (throw unresolvedThenable) and by the\n * Navigation API handler (skip interception).\n */\nexport function isHardNavigating(): boolean {\n return getHardNavStore().value;\n}\n\n/**\n * A thenable that never resolves. When thrown during React render,\n * it causes the component to suspend forever — React keeps the\n * old committed tree visible and never attempts to render children.\n *\n * This is the same pattern Next.js uses in app-router.tsx for MPA\n * navigations (pushRef.mpaNavigation → throw unresolvedThenable).\n */\n// for React's Suspense mechanism. Same pattern as Next.js's unresolvedThenable.\n// eslint-disable-next-line unicorn/no-thenable -- Intentionally a never-resolving thenable\nconst unresolvedThenable = { then() {} } as PromiseLike<never>;\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Root component the router renders the page through.\n *\n * Renders the TopLoader alongside the tree it is given. Neither adds a DOM\n * element on the hydration path, so the tree matches the server HTML.\n *\n * Rendered only by `createReactRoot` (client/react-root.ts): hydration passes\n * `{ element, publish: null }`, and every later render is a synchronous\n * `startTransition(() => root.render(...))` with a fresh `rendered` object.\n */\nexport function NavigationRoot({\n rendered,\n topLoaderConfig,\n}: {\n rendered: RenderedTree;\n topLoaderConfig?: TopLoaderConfig;\n}): ReactNode {\n // Publish the navigation's state when React commits its tree — not when the\n // tree is handed over. A tree React is still waiting on, or one a later\n // navigation replaces before React renders it, has been *given* to React\n // without being on screen; publishing then describes a route the user never\n // saw, which is the defect this ordering exists to prevent (TIM-1301).\n //\n // A **layout** effect, so the publish lands before the browser paints and,\n // more importantly, before every descendant's passive effect. A destination\n // component that navigates from a mount effect — a redirect guard, a\n // `refresh()` on mount — would otherwise start its navigation while the\n // segment cache, address bar and pathname still described the departing\n // route, and send an X-Timber-State-Tree for a tree that is no longer\n // mounted (codex on #998).\n //\n // Descendant *layout* effects still run first: React runs layout effects\n // child-before-parent, and the only way to precede them would be a fiber\n // rendered as an earlier sibling of the payload. That is not free here —\n // `server/ssr-wrappers.tsx` mirrors this component's fiber shape so `useId`\n // agrees across hydration, and an extra sibling shifts every id in the\n // payload subtree. A layout effect that navigates on mount is the price.\n //\n // Keyed on the tree object rather than on the effect run. A publish moves\n // the address bar, which is not idempotent, so any second invocation for the\n // same tree — an effect re-run React is entitled to perform, StrictMode's\n // double-invoke — must be a no-op rather than a second history entry.\n const publishedRef = useRef<RenderedTree | null>(null);\n useLayoutEffect(() => {\n if (publishedRef.current === rendered) return;\n publishedRef.current = rendered;\n rendered.publish?.();\n }, [rendered]);\n\n // ─── Hard navigation guard ─────────────────────────────────\n // When a hard navigation is in progress (500 error, version skew),\n // suspend forever to prevent React from rendering children during\n // page teardown. This avoids \"Rendered more hooks\" crashes in\n // CHILD components whose hook counts may shift during teardown.\n //\n // CRITICAL: This throw MUST come AFTER all hooks (the useRef and\n // useLayoutEffect above). React requires the same hooks to run on every\n // render. If we threw before hooks, React would see 0 hooks on the\n // re-render vs 2 on the initial render — triggering the exact \"Rendered\n // more hooks\" error we're trying to prevent.\n //\n // By placing it after hooks but before the return, all hooks\n // satisfy React's rules, but the thrown thenable prevents any\n // children from rendering. Same pattern as Next.js app-router.tsx\n // (pushRef.mpaNavigation — also placed after all hooks).\n if (isHardNavigating()) {\n throw unresolvedThenable;\n }\n\n // Inject TopLoader alongside the element tree. It subscribes to the router's\n // pending store itself, so it takes no props from here beyond its config,\n // and only it re-renders when a navigation starts or ends. Rendered only\n // when not explicitly disabled via config.\n //\n // This Fragment is a FORK — two children — so it advances React's tree\n // context and shifts every `useId` below it. `server/ssr-wrappers.tsx`\n // mirrors it for exactly that reason; a single-child wrapper would not need\n // a mirror. See tests/ssr-wrapper-parity.test.ts.\n const showTopLoader = topLoaderConfig?.enabled !== false;\n if (!showTopLoader) return rendered.element;\n return createElement(\n Fragment,\n null,\n createElement(TopLoader, { config: topLoaderConfig }),\n rendered.element\n );\n}\n"],"mappings":";;;;;;AAcA,SAAgB,gBAAgB,QAA8B;CAC5D,iBAAiB,MAAM;AACzB;;;;;AAMA,SAAgB,YAA4B;CAC1C,IAAI,CAAC,cACH,MAAM,IAAI,MAAM,6EAA6E;CAE/F,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAyC;CACvD,OAAO;AACT;;;ACjCA,SAAS,UAAU,eAAuC;CACxD,MAAM,SAAS,gBAAgB;CAC/B,IAAI,CAAC,QAAQ,aAAa,CAAC;CAC3B,OAAO,OAAO,gBAAgB,aAAa;AAC7C;AAEA,SAAS,cAAuB;CAC9B,MAAM,SAAS,gBAAgB;CAC/B,OAAO,SAAS,OAAO,UAAU,IAAI;AACvC;AAEA,IAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;AAuB1B,SAAgB,uBAAgC;CAC9C,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AEuDA,IAAM,eAAe,OAAO,IAAI,0BAA0B;AAE1D,SAAS,kBAAsC;CAC7C,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,eACL,EAAE,gBAAgB,EAAE,OAAO,MAAM;CAEnC,OAAO,EAAE;AACX;;;;;;;AAQA,SAAgB,kBAAkB,OAAsB;CACtD,gBAAgB,CAAC,CAAC,QAAQ;AAC5B"}
1
+ {"version":3,"file":"navigation-root-BQfo1-kG.js","names":[],"sources":["../../src/client/router-ref.ts","../../src/client/use-pending-navigation.ts","../../src/client/top-loader.tsx","../../src/client/navigation-root.tsx"],"sourcesContent":["// Global router reference — shared between browser-entry and client hooks.\n//\n// Delegates to client/state.ts for the actual module-level variable.\n// This ensures singleton semantics regardless of import path — all\n// callers converge on the same state.ts instance via the barrel.\n//\n// See design/18-build-system.md §\"Module Singleton Strategy\"\n\nimport type { RouterInstance } from './router-types.ts';\nimport { globalRouter, _setGlobalRouter } from './state.ts';\n\n/**\n * Set the global router instance. Called once during bootstrap.\n */\nexport function setGlobalRouter(router: RouterInstance): void {\n _setGlobalRouter(router);\n}\n\n/**\n * Get the global router instance. Throws if called before bootstrap.\n * Used by client-side hooks (usePendingNavigation, etc.)\n */\nexport function getRouter(): RouterInstance {\n if (!globalRouter) {\n throw new Error('[timber] Router not initialized. getRouter() was called before bootstrap().');\n }\n return globalRouter;\n}\n\n/**\n * Get the global router instance or null if not yet initialized.\n * Used by useRouter() methods to avoid silent failures — callers\n * can log a meaningful warning instead of silently no-oping.\n */\nexport function getRouterOrNull(): RouterInstance | null {\n return globalRouter;\n}\n\n/**\n * Reset the global router to null. Used only in tests to isolate\n * module-level state between test cases.\n * @internal\n */\nexport function resetGlobalRouter(): void {\n _setGlobalRouter(null);\n}\n","import { useSyncExternalStore } from 'react';\nimport { getRouterOrNull } from './router-ref.ts';\n\nfunction subscribe(onStoreChange: () => void): () => void {\n const router = getRouterOrNull();\n if (!router) return () => {};\n return router.onPendingChange(onStoreChange);\n}\n\nfunction getSnapshot(): boolean {\n const router = getRouterOrNull();\n return router ? router.isPending() : false;\n}\n\nconst getServerSnapshot = getSnapshot;\n\n/**\n * Returns true while an RSC navigation is in flight.\n *\n * Reads from the router's external pending store via useSyncExternalStore.\n * Only components that call this hook re-render when pending state\n * changes — no full-tree re-render.\n *\n * ```tsx\n * 'use client'\n * import { usePendingNavigation } from '@timber-js/app/client'\n *\n * export function NavBar() {\n * const isPending = usePendingNavigation()\n * return (\n * <nav className={isPending ? 'opacity-50' : ''}>\n * <Link href=\"/dashboard\">Dashboard</Link>\n * </nav>\n * )\n * }\n * ```\n */\nexport function usePendingNavigation(): boolean {\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n","/**\n * TopLoader — Built-in progress bar for client navigations.\n *\n * Shows an animated progress bar at the top of the viewport while an RSC\n * navigation is in flight. Injected automatically by the framework into\n * NavigationRoot — users never render this component directly.\n *\n * Configuration is via timber.config.ts `topLoader` key. Enabled by default.\n * Users who want a fully custom progress indicator disable the built-in one\n * (`topLoader: { enabled: false }`) and use `usePendingNavigation()` directly.\n *\n * Animation approach: pure CSS @keyframes. The bar crawls from 0% to ~90%\n * width over ~30s using ease-out timing. When navigation completes, the bar\n * snaps to 100% and fades out over 200ms. No JS animation loops (RAF, setInterval).\n *\n * Phase transitions are derived synchronously during render (React's\n * getDerivedStateFromProps pattern) — no useEffect needed for state tracking.\n * The finishing → hidden cleanup uses onTransitionEnd from the CSS transition.\n *\n * When delay > 0, CSS animation-delay + a visibility keyframe ensure the bar\n * stays invisible during the delay period. If navigation finishes before the\n * delay, the bar was never visible so the finish transition is also invisible.\n *\n * See design/19-client-navigation.md §\"usePendingNavigation()\"\n * See LOCAL-336 for design decisions.\n */\n\n'use client';\n\nimport { useState, createElement } from 'react';\nimport { usePendingNavigation } from './use-pending-navigation.ts';\n\n// ─── Types ───────────────────────────────────────────────────────\n\nexport interface TopLoaderConfig {\n /** Whether the top-loader is enabled. Default: true. */\n enabled?: boolean;\n /** Bar color. Default: '#2299DD'. */\n color?: string;\n /** Bar height in pixels. Default: 3. */\n height?: number;\n /** Show subtle glow/shadow effect. Default: false. */\n shadow?: boolean;\n /** Delay in ms before showing the bar. Default: 0. */\n delay?: number;\n /** CSS z-index. Default: 1600. */\n zIndex?: number;\n}\n\n// ─── Defaults ────────────────────────────────────────────────────\n\nconst DEFAULT_COLOR = '#2299DD';\nconst DEFAULT_HEIGHT = 3;\nconst DEFAULT_SHADOW = false;\nconst DEFAULT_DELAY = 0;\nconst DEFAULT_Z_INDEX = 1600;\n\n// ─── Keyframes ───────────────────────────────────────────────────\n\n// Unique keyframes name to avoid collisions with user styles.\nconst CRAWL_KEYFRAMES = '__timber_top_loader_crawl';\nconst APPEAR_KEYFRAMES = '__timber_top_loader_appear';\nconst FINISH_KEYFRAMES = '__timber_top_loader_finish';\n\n// Track whether the @keyframes rules have been injected into the document.\nlet keyframesInjected = false;\n\n/**\n * Inject the @keyframes rules into the document head once.\n * Called during render (idempotent). Uses a <style> tag so the\n * animations are available for inline-styled elements.\n */\nfunction ensureKeyframes(): void {\n if (keyframesInjected) return;\n if (typeof document === 'undefined') return;\n\n const style = document.createElement('style');\n style.textContent = `\n@keyframes ${CRAWL_KEYFRAMES} {\n 0% { width: 0%; }\n 100% { width: 90%; }\n}\n@keyframes ${APPEAR_KEYFRAMES} {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n@keyframes ${FINISH_KEYFRAMES} {\n 0% { width: 90%; opacity: 1; }\n 50% { width: 100%; opacity: 1; }\n 100% { width: 100%; opacity: 0; }\n}\n`;\n document.head.appendChild(style);\n keyframesInjected = true;\n}\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Internal top-loader component. Injected by NavigationRoot.\n *\n * Reads pending navigation state from the router's external store, via\n * usePendingNavigation() — the one pending representation (TIM-1307).\n * Phase transitions are derived synchronously during render:\n *\n * hidden → crawling: when isPending becomes true\n * crawling → finishing: when isPending becomes false\n * finishing → hidden: when CSS transition ends (onTransitionEnd)\n * finishing → crawling: when isPending becomes true again\n *\n * No useEffect — all state changes are either derived during render\n * (getDerivedStateFromProps pattern) or triggered by DOM events.\n */\nexport function TopLoader({ config }: { config?: TopLoaderConfig }): React.ReactElement | null {\n // Read pending state from the router's external store via\n // useSyncExternalStore (inside usePendingNavigation). Only this\n // component re-renders when pending changes — no full-tree re-render.\n const isPending = usePendingNavigation();\n\n const color = config?.color ?? DEFAULT_COLOR;\n const height = config?.height ?? DEFAULT_HEIGHT;\n const shadow = config?.shadow ?? DEFAULT_SHADOW;\n const delay = config?.delay ?? DEFAULT_DELAY;\n const zIndex = config?.zIndex ?? DEFAULT_Z_INDEX;\n\n const [phase, setPhase] = useState<'hidden' | 'crawling' | 'finishing'>('hidden');\n\n // ─── Synchronous phase derivation (getDerivedStateFromProps) ──\n // React allows setState during render if the value changes — it\n // immediately re-renders with the updated state before committing.\n\n if (isPending && (phase === 'hidden' || phase === 'finishing')) {\n setPhase('crawling');\n }\n if (!isPending && phase === 'crawling') {\n setPhase('finishing');\n }\n\n // Inject keyframes on first visible render (idempotent)\n if (phase !== 'hidden') {\n ensureKeyframes();\n }\n\n if (phase === 'hidden') return null;\n\n // ─── Styles ──────────────────────────────────────────────────\n\n const containerStyle: React.CSSProperties = {\n position: 'fixed',\n top: 0,\n left: 0,\n width: '100%',\n height: `${height}px`,\n zIndex,\n pointerEvents: 'none',\n };\n\n const barStyle: React.CSSProperties = {\n height: '100%',\n backgroundColor: color,\n ...(phase === 'crawling'\n ? {\n // Crawl from 0% to 90% over 30s. When delay > 0, both the crawl\n // and a visibility animation are delayed — the bar stays at width 0%\n // and opacity 0 during the delay, then appears and starts crawling.\n // With delay 0, the appear animation is instant (0s duration, no delay).\n animation: [\n `${CRAWL_KEYFRAMES} 30s ease-out ${delay}ms forwards`,\n `${APPEAR_KEYFRAMES} 0s ${delay}ms both`,\n ].join(', '),\n }\n : {\n // Finishing: fill to 100% then fade out via a keyframe animation.\n // We use a keyframe instead of a CSS transition because the\n // animation-to-transition handoff is unreliable — the browser\n // may not capture the animated width as the transition's \"from\"\n // value when both the animation removal and transition are\n // applied in the same render frame.\n animation: `${FINISH_KEYFRAMES} 400ms ease forwards`,\n }),\n ...(shadow\n ? {\n boxShadow: `0 0 10px ${color}, 0 0 5px ${color}`,\n }\n : {}),\n };\n\n // Clean up the finishing phase when the finish animation completes.\n const handleAnimationEnd =\n phase === 'finishing'\n ? (e: React.AnimationEvent) => {\n if (e.animationName === FINISH_KEYFRAMES) {\n setPhase('hidden');\n }\n }\n : undefined;\n\n return createElement(\n 'div',\n {\n 'style': containerStyle,\n 'aria-hidden': 'true',\n 'data-timber-top-loader': '',\n },\n createElement('div', { style: barStyle, onAnimationEnd: handleAnimationEnd })\n );\n}\n","/**\n * NavigationRoot — the component the router renders the page through.\n *\n * It is stateless. The router owns the displayed tree and drives React with\n * `root.render(<NavigationRoot rendered={{ element, publish }} />)` inside a\n * synchronous `startTransition` (see `client/react-root.ts`). A transition\n * update keeps the committed tree on screen while the incoming one resolves,\n * instead of replacing it with a Suspense fallback — the reason this\n * component exists (TIM-1306).\n *\n * It used to hold the tree in `useState` and register `setState` closures\n * into module globals during render so the router could reach into it, with a\n * second set of stand-in closures for the window before a root existed\n * (TIM-600). Inverting that — router calls React, not the reverse — deleted\n * all of it (TIM-1431). Next.js's `use-action-queue.ts` says it wants this\n * shape and cannot have it because it must decode Flight during render;\n * timber decodes in `router-pipeline.ts`, so nothing stands in the way.\n *\n * What this component does own is **the commit-time publish**: a navigation's\n * state (segment cache, address bar, history stack, pathname) is published\n * from a layout effect keyed on the tree object, so the event that puts a\n * route on screen is the event that makes it current (TIM-1301). Props work\n * for that exactly as state did.\n *\n * This component holds no pending state. The `TopLoader` it renders and the\n * public `usePendingNavigation()` both subscribe to the router's external\n * pending store; NavigationRoot's own `pendingUrl` was a second, unread\n * representation of the same fact and is gone (TIM-1307).\n *\n * Hard navigation guard: When a hard navigation is triggered (500 error,\n * version skew), the component throws an unresolved thenable AFTER all\n * hooks to suspend forever — preventing React from rendering children\n * during page teardown. The throw must come after hooks to satisfy\n * React's rules (same hook count every render) while still preventing\n * child renders that could hit hook count mismatches in components\n * whose positions shift during teardown. This pattern is borrowed from\n * Next.js (app-router.tsx pushRef.mpaNavigation — also after hooks).\n *\n * See design/05-streaming.md §\"deferSuspenseFor\"\n * See design/19-client-navigation.md §\"NavigationContext\"\n */\n\nimport { createElement, Fragment, useLayoutEffect, useRef, type ReactNode } from 'react';\nimport { TopLoader, type TopLoaderConfig } from './top-loader.tsx';\n\n// ─── Rendered Tree ──────────────────────────────────────────────\n\n/**\n * The tree the router has handed to React, and the state publish that belongs\n * to it. They travel as one object so React's commit of the tree is the event\n * that publishes — see the effect in NavigationRoot. Every `render` builds a\n * fresh one, which is what keys that effect: a tree is published once, however\n * many times React re-runs the effect for it.\n */\nexport interface RenderedTree {\n element: ReactNode;\n /**\n * Publishes the navigation's state — segment cache, pathname, address bar,\n * history stack, and the client's record of the mounted tree — and\n * announces it to listeners outside React. Runs when React commits\n * `element`, never before (TIM-1301). Null for a render that has nothing to\n * publish on commit (hydration, the shallow search re-wrap).\n */\n publish: (() => void) | null;\n}\n\n/**\n * Hand a tree to React in a transition. `publish` runs when React commits it.\n *\n * Every path that puts a page on screen goes through one of these — a\n * navigation, a revalidation, a popstate replay, a shallow search re-wrap —\n * and it is always a synchronous `startTransition` around `root.render`.\n * The production one is `createReactRoot().render`.\n */\nexport type NavigationRender = (element: ReactNode, publish: (() => void) | null) => void;\n\n// ─── Hard Navigation Guard ──────────────────────────────────────\n\n/**\n * Module-level flag indicating a hard (MPA) navigation is in progress.\n *\n * When true:\n * - NavigationRoot throws an unresolved thenable to suspend forever,\n * preventing React from rendering children during page teardown\n * (avoids \"Rendered more hooks\" crashes).\n * - The Navigation API handler skips interception, letting the browser\n * perform a full page load (prevents infinite loops where\n * window.location.href → navigate event → router.navigate → 500 →\n * window.location.href → ...).\n *\n * Uses globalThis for singleton guarantee across chunks (same pattern\n * as NavigationContext). See design/19-client-navigation.md §\"Singleton\n * Guarantee via globalThis\".\n */\nconst HARD_NAV_KEY = Symbol.for('__timber_hard_navigating');\n\nfunction getHardNavStore(): { value: boolean } {\n const g = globalThis as Record<symbol, unknown>;\n if (!g[HARD_NAV_KEY]) {\n g[HARD_NAV_KEY] = { value: false };\n }\n return g[HARD_NAV_KEY] as { value: boolean };\n}\n\n/**\n * Set the hard-navigating flag. Call this BEFORE setting\n * window.location.href or window.location.reload() to prevent:\n * 1. React from rendering children during page teardown\n * 2. Navigation API from intercepting the hard navigation\n */\nexport function setHardNavigating(value: boolean): void {\n getHardNavStore().value = value;\n}\n\n/**\n * Check if a hard navigation is in progress.\n * Used by NavigationRoot (throw unresolvedThenable) and by the\n * Navigation API handler (skip interception).\n */\nexport function isHardNavigating(): boolean {\n return getHardNavStore().value;\n}\n\n/**\n * A thenable that never resolves. When thrown during React render,\n * it causes the component to suspend forever — React keeps the\n * old committed tree visible and never attempts to render children.\n *\n * This is the same pattern Next.js uses in app-router.tsx for MPA\n * navigations (pushRef.mpaNavigation → throw unresolvedThenable).\n */\n// for React's Suspense mechanism. Same pattern as Next.js's unresolvedThenable.\n// eslint-disable-next-line unicorn/no-thenable -- Intentionally a never-resolving thenable\nconst unresolvedThenable = { then() {} } as PromiseLike<never>;\n\n// ─── Component ───────────────────────────────────────────────────\n\n/**\n * Root component the router renders the page through.\n *\n * Renders the TopLoader alongside the tree it is given. Neither adds a DOM\n * element on the hydration path, so the tree matches the server HTML.\n *\n * Rendered only by `createReactRoot` (client/react-root.ts): hydration passes\n * `{ element, publish: null }`, and every later render is a synchronous\n * `startTransition(() => root.render(...))` with a fresh `rendered` object.\n */\nexport function NavigationRoot({\n rendered,\n topLoaderConfig,\n}: {\n rendered: RenderedTree;\n topLoaderConfig?: TopLoaderConfig;\n}): ReactNode {\n // Publish the navigation's state when React commits its tree — not when the\n // tree is handed over. A tree React is still waiting on, or one a later\n // navigation replaces before React renders it, has been *given* to React\n // without being on screen; publishing then describes a route the user never\n // saw, which is the defect this ordering exists to prevent (TIM-1301).\n //\n // A **layout** effect, so the publish lands before the browser paints and,\n // more importantly, before every descendant's passive effect. A destination\n // component that navigates from a mount effect — a redirect guard, a\n // `refresh()` on mount — would otherwise start its navigation while the\n // segment cache, address bar and pathname still described the departing\n // route, and send an X-Timber-State-Tree for a tree that is no longer\n // mounted (codex on #998).\n //\n // Descendant *layout* effects still run first: React runs layout effects\n // child-before-parent, and the only way to precede them would be a fiber\n // rendered as an earlier sibling of the payload. That is not free here —\n // `server/ssr-wrappers.tsx` mirrors this component's fiber shape so `useId`\n // agrees across hydration, and an extra sibling shifts every id in the\n // payload subtree. A layout effect that navigates on mount is the price.\n //\n // Keyed on the tree object rather than on the effect run. A publish moves\n // the address bar, which is not idempotent, so any second invocation for the\n // same tree — an effect re-run React is entitled to perform, StrictMode's\n // double-invoke — must be a no-op rather than a second history entry.\n const publishedRef = useRef<RenderedTree | null>(null);\n useLayoutEffect(() => {\n if (publishedRef.current === rendered) return;\n publishedRef.current = rendered;\n rendered.publish?.();\n }, [rendered]);\n\n // ─── Hard navigation guard ─────────────────────────────────\n // When a hard navigation is in progress (500 error, version skew),\n // suspend forever to prevent React from rendering children during\n // page teardown. This avoids \"Rendered more hooks\" crashes in\n // CHILD components whose hook counts may shift during teardown.\n //\n // CRITICAL: This throw MUST come AFTER all hooks (the useRef and\n // useLayoutEffect above). React requires the same hooks to run on every\n // render. If we threw before hooks, React would see 0 hooks on the\n // re-render vs 2 on the initial render — triggering the exact \"Rendered\n // more hooks\" error we're trying to prevent.\n //\n // By placing it after hooks but before the return, all hooks\n // satisfy React's rules, but the thrown thenable prevents any\n // children from rendering. Same pattern as Next.js app-router.tsx\n // (pushRef.mpaNavigation — also placed after all hooks).\n if (isHardNavigating()) {\n throw unresolvedThenable;\n }\n\n // Inject TopLoader alongside the element tree. It subscribes to the router's\n // pending store itself, so it takes no props from here beyond its config,\n // and only it re-renders when a navigation starts or ends. Rendered only\n // when not explicitly disabled via config.\n //\n // This Fragment is a FORK — two children — so it advances React's tree\n // context and shifts every `useId` below it. `server/ssr-wrappers.tsx`\n // mirrors it for exactly that reason; a single-child wrapper would not need\n // a mirror. See tests/ssr-wrapper-parity.test.ts.\n const showTopLoader = topLoaderConfig?.enabled !== false;\n if (!showTopLoader) return rendered.element;\n return createElement(\n Fragment,\n null,\n createElement(TopLoader, { config: topLoaderConfig }),\n rendered.element\n );\n}\n"],"mappings":";;;;;;AAcA,SAAgB,gBAAgB,QAA8B;CAC5D,iBAAiB,MAAM;AACzB;;;;;AAMA,SAAgB,YAA4B;CAC1C,IAAI,CAAC,cACH,MAAM,IAAI,MAAM,6EAA6E;CAE/F,OAAO;AACT;;;;;;AAOA,SAAgB,kBAAyC;CACvD,OAAO;AACT;;;ACjCA,SAAS,UAAU,eAAuC;CACxD,MAAM,SAAS,gBAAgB;CAC/B,IAAI,CAAC,QAAQ,aAAa,CAAC;CAC3B,OAAO,OAAO,gBAAgB,aAAa;AAC7C;AAEA,SAAS,cAAuB;CAC9B,MAAM,SAAS,gBAAgB;CAC/B,OAAO,SAAS,OAAO,UAAU,IAAI;AACvC;AAEA,IAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;AAuB1B,SAAgB,uBAAgC;CAC9C,OAAO,qBAAqB,WAAW,aAAa,iBAAiB;AACvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AEuDA,IAAM,eAAe,OAAO,IAAI,0BAA0B;AAE1D,SAAS,kBAAsC;CAC7C,MAAM,IAAI;CACV,IAAI,CAAC,EAAE,eACL,EAAE,gBAAgB,EAAE,OAAO,MAAM;CAEnC,OAAO,EAAE;AACX;;;;;;;AAQA,SAAgB,kBAAkB,OAAsB;CACtD,gBAAgB,CAAC,CAAC,QAAQ;AAC5B"}
@@ -1,4 +1,5 @@
1
1
  import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.js";
2
+ import { n as normalizeBasePath } from "./base-path-DaQrzbez.js";
2
3
  import { parseAst } from "vite";
3
4
  import { join, resolve } from "node:path";
4
5
  import { createRequire } from "node:module";
@@ -92,7 +93,9 @@ function createNoopTimer() {
92
93
  var plugin_context_exports = /* @__PURE__ */ __exportAll({
93
94
  DEFAULT_BUILD_DIR: () => DEFAULT_BUILD_DIR,
94
95
  ParseMemo: () => ParseMemo,
96
+ adapterConfig: () => adapterConfig,
95
97
  configLoadHint: () => configLoadHint,
98
+ contextAdapter: () => contextAdapter,
96
99
  createPluginContext: () => createPluginContext,
97
100
  isTimberPluginApi: () => isTimberPluginApi,
98
101
  loadTimberConfigFile: () => loadTimberConfigFile,
@@ -158,6 +161,26 @@ function isTimberPluginApi(api) {
158
161
  return typeof api === "object" && api !== null && "getContext" in api && typeof api.getContext === "function";
159
162
  }
160
163
  /**
164
+ * The configured platform adapter. `adapter` is typed `unknown` on the user
165
+ * config so that `timber.config.ts` does not depend on the adapter types; the
166
+ * adapter factories are what produce values of this shape.
167
+ */
168
+ function contextAdapter(ctx) {
169
+ return ctx.config.adapter;
170
+ }
171
+ /**
172
+ * The configuration an adapter is handed, derived once from the resolved
173
+ * context — by the post-build phase for `buildOutput` and by `timber preview`
174
+ * for `preview`.
175
+ */
176
+ function adapterConfig(ctx) {
177
+ return {
178
+ root: ctx.root,
179
+ base: normalizeBasePath(ctx.base),
180
+ clientJavascriptDisabled: ctx.clientJavascript.disabled
181
+ };
182
+ }
183
+ /**
161
184
  * Resolve the app directory. Checks (in order):
162
185
  * 1. Explicit `configAppDir` from timber.config.ts
163
186
  * 2. `<root>/app`
@@ -270,8 +293,9 @@ function loadTimberConfigFile(root) {
270
293
  }
271
294
  /**
272
295
  * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)
273
- * and warn the user. The `output` key is excluded because it defaults to 'server'
274
- * in createPluginContext and would always appear as an inline key.
296
+ * and warn the user. Only keys the inline config actually sets count — the
297
+ * `output` default is applied after the merge (timber-root-sync), so it never
298
+ * appears here as an inline key.
275
299
  *
276
300
  * Returns the list of conflicting key names (for testing).
277
301
  */
@@ -316,6 +340,6 @@ function mergeFileConfig(inline, fileConfig) {
316
340
  };
317
341
  }
318
342
  //#endregion
319
- export { plugin_context_exports as a, resolveClientJavascript as c, mergeFileConfig as i, createNoopTimer as l, isTimberPluginApi as n, resolveAppDir as o, loadTimberConfigFile as r, resolveBuildDir as s, createPluginContext as t };
343
+ export { loadTimberConfigFile as a, resolveAppDir as c, createNoopTimer as d, isTimberPluginApi as i, resolveBuildDir as l, contextAdapter as n, mergeFileConfig as o, createPluginContext as r, plugin_context_exports as s, adapterConfig as t, resolveClientJavascript as u };
320
344
 
321
- //# sourceMappingURL=plugin-context-8idfR3UZ.js.map
345
+ //# sourceMappingURL=plugin-context-BPBPc3lL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-context-BPBPc3lL.js","names":[],"sources":["../../src/utils/startup-timer.ts","../../src/plugin-context.ts"],"sourcesContent":["/**\n * Startup timer — records named phases with their durations.\n *\n * Used by the plugin system to instrument cold start and report a\n * timing breakdown in dev mode. Zero overhead in production (disabled).\n *\n * See design/18-build-system.md, TIM-155.\n */\n\nimport { performance } from 'node:perf_hooks';\n\nexport interface PhaseRecord {\n name: string;\n startMs: number;\n durationMs: number;\n}\n\nexport interface StartupTimer {\n /** Mark the beginning of a named phase. */\n start(phase: string): void;\n /** Mark the end of a named phase. Returns duration in ms. */\n end(phase: string): number;\n /** Get all completed phase records, ordered by start time. */\n getPhases(): PhaseRecord[];\n /** Total elapsed time from first start() to last end(). */\n totalMs(): number;\n /** Format a human-readable summary string. */\n formatSummary(): string;\n}\n\n/**\n * Create a startup timer that records phase durations.\n */\nexport function createStartupTimer(): StartupTimer {\n const pending = new Map<string, number>();\n const phases: PhaseRecord[] = [];\n\n return {\n start(phase: string): void {\n pending.set(phase, performance.now());\n },\n\n end(phase: string): number {\n const startMs = pending.get(phase);\n if (startMs === undefined) {\n return 0;\n }\n pending.delete(phase);\n const durationMs = performance.now() - startMs;\n phases.push({ name: phase, startMs, durationMs });\n return durationMs;\n },\n\n getPhases(): PhaseRecord[] {\n return [...phases].sort((a, b) => a.startMs - b.startMs);\n },\n\n totalMs(): number {\n if (phases.length === 0) return 0;\n const sorted = this.getPhases();\n const first = sorted[0];\n const last = sorted[sorted.length - 1];\n return last.startMs + last.durationMs - first.startMs;\n },\n\n formatSummary(): string {\n const sorted = this.getPhases();\n if (sorted.length === 0) return 'No phases recorded.';\n\n const lines = sorted.map((p) => {\n const ms = p.durationMs.toFixed(1);\n return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;\n });\n\n const total = this.totalMs().toFixed(1);\n lines.push(` ${'total'.padEnd(30)} ${total.padStart(8)}ms`);\n\n return ['[timber] startup timing:', ...lines].join('\\n');\n },\n };\n}\n\n/**\n * No-op timer for production builds — all methods are empty.\n */\nexport function createNoopTimer(): StartupTimer {\n return {\n start() {},\n end() {\n return 0;\n },\n getPhases() {\n return [];\n },\n totalMs() {\n return 0;\n },\n formatSummary() {\n return '';\n },\n };\n}\n","/**\n * Plugin context — internal types and helpers for timber sub-plugins.\n *\n * These are NOT part of the user-facing API. They are consumed by\n * sub-plugins (routing, entries, shims, etc.) via relative imports.\n * The root entry point (index.ts) does not re-export them.\n *\n * Design doc: 18-build-system.md §\"Shared Plugin Context\"\n */\n\nimport { existsSync } from 'node:fs';\nimport { join, resolve } from 'node:path';\nimport { createRequire } from 'node:module';\nimport { parseAst } from 'vite';\nimport type { RouteTree } from './routing/types.ts';\nimport type { BuildManifest } from './server/build-manifest.ts';\nimport type { StartupTimer } from './utils/startup-timer.ts';\nimport { createStartupTimer } from './utils/startup-timer.ts';\nimport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.ts';\nimport type { HoldingServer } from './dev-tools/holding-server.ts';\nimport type { ProgramNode } from './plugins/callsite-ast.ts';\nimport type { TimberConfig, TimberPlatformAdapter } from './adapters/types.ts';\nimport { normalizeBasePath } from './shared/base-path.ts';\n\n// Re-export for sub-plugin convenience — they import from plugin-context.ts\nexport type { TimberUserConfig, ClientJavascriptConfig } from './config-types.ts';\n\n// ── Client JavaScript config ─────────────────────────────────────────────\n\n/** Fully resolved client JavaScript configuration (no optionals). */\nexport interface ResolvedClientJavascript {\n disabled: boolean;\n enableHMRInDev: boolean;\n}\n\n/**\n * Resolve `clientJavascript` into a fully resolved config.\n */\nexport function resolveClientJavascript(config: TimberUserConfig): ResolvedClientJavascript {\n if (config.clientJavascript !== undefined) {\n if (typeof config.clientJavascript === 'boolean') {\n return {\n disabled: !config.clientJavascript,\n enableHMRInDev: !config.clientJavascript,\n };\n }\n return {\n disabled: config.clientJavascript.disabled,\n enableHMRInDev: config.clientJavascript.enableHMRInDev ?? config.clientJavascript.disabled,\n };\n }\n return { disabled: false, enableHMRInDev: false };\n}\n\n// ── Plugin Context ────────────────────────────────────────────────────────\n\n/**\n * Shared context object passed to all sub-plugins via closure.\n *\n * Sub-plugins communicate through this context — not through Vite's\n * plugin API or global state.\n * See design/18-build-system.md §\"Shared Plugin Context\".\n */\nexport interface PluginContext {\n config: TimberUserConfig;\n /** Resolved client JavaScript configuration */\n clientJavascript: ResolvedClientJavascript;\n /** The scanned route tree (populated by timber-routing, consumed by timber-entries) */\n routeTree: RouteTree | null;\n /** Absolute path to the app/ directory */\n appDir: string;\n /** Absolute path to the project root */\n root: string;\n /** Whether the dev server is running (set by timber-root-sync in configResolved) */\n dev: boolean;\n /**\n * Vite's resolved `base` (set by timber-root-sync in configResolved).\n * Always has a trailing slash; `'/'` unless the app is deployed under a\n * sub-path. Every URL the framework emits — assets, fonts, RSC payloads —\n * carries this prefix. This is the single copy: sub-plugins read it here\n * rather than capturing `config.base` in their own configResolved.\n *\n * **Availability:** written by timber-root-sync, which has no `enforce`, so\n * it is populated from `buildStart`/`load`/`generateBundle`/`configureServer`\n * onward — but NOT during the `configResolved` of an `enforce: 'pre'`\n * plugin, which Vite runs first. A pre plugin that needs the base inside\n * its own `configResolved` must read `config.base` there (timber-fonts\n * does). See TIM-1261.\n */\n base: string;\n /**\n * The build manifest. Created by timber-build-manifest from the client\n * bundle and finalized by the post-build phase (plugins/post-build.ts),\n * which is the only reader. Null in dev.\n */\n buildManifest: BuildManifest | null;\n /** Per-build deployment ID for version skew detection (null in dev) */\n deploymentId: string | null;\n /** Startup timer for profiling cold start phases (active in dev, no-op in prod) */\n timer: StartupTimer;\n /** Holding server that binds the port during dev startup (closed in timber-dev-server) */\n holdingServer?: HoldingServer | null;\n /**\n * Resolved absolute path to the build output directory.\n *\n * Defaults to `<root>/.timber/dist`. Can be overridden via\n * `timber.config.ts` `buildDir` or Vite's `build.outDir`.\n * Timber config takes precedence when both are set.\n *\n * The `config` hook resolves it into Vite's `build.outDir`, and\n * `configResolved` reads it back from Vite — the one assignment after\n * construction, so what the build writes to and what `timber preview`\n * serves (through {@link TimberPluginApi}) are the same directory.\n */\n buildDir: string;\n /**\n * Per-route client reference keys captured during the RSC build.\n * Maps absolute route file paths to the set of client reference\n * hash keys they transitively import. Used by the post-build phase to\n * populate per-route modulepreload entries from clientReferenceDeps.\n */\n routeClientRefKeys?: Map<string, Set<string>>;\n /**\n * Static request-dependency verdict per segment entry file, keyed by the\n * absolute `filePath` the route scanner produced. Computed once by\n * `timber-request-dep`'s RSC `buildEnd` and serialized into\n * `buildManifest.requestDep` by `timber-build-manifest` (build only — dev\n * answers the same question through the `Symbol.for('timber:request-dep')`\n * bridge instead).\n *\n * `undefined` and `{}` are different states and the difference is the whole\n * of TIM-1300: an empty object means the analysis ran and found no segments,\n * `undefined` means it never ran, and the runtime cannot tell them apart —\n * every key is absent either way, so every segment falls to the conservative\n * `true` and nothing is ever skipped. The build fails loudly on `undefined`\n * rather than shipping that silently.\n */\n requestDepMap?: Record<string, boolean>;\n /**\n * Maps absolute module IDs of client references in non-facade RSC\n * chunks to the absolute paths of their consuming facade modules.\n * Built during the RSC `generateBundle` phase by timber-chunks,\n * consumed by `clientChunkGroup` to reclassify `shared:` modules\n * that are only used by a single route boundary.\n * See design/27-chunking-strategy.md, TIM-1107.\n */\n sharedModuleFacades?: Map<string, string[]>;\n /**\n * Component ID → distinct static call-site prop objects captured from\n * JSX usage sites during the transform and generateBundle phases.\n * Consumed by the post-build capture pass to render with actual props\n * instead of empty `{}`. See design/44 §Props Derivation.\n */\n prebuiltCallSiteProps?: Map<string, Record<string, unknown>[]>;\n /**\n * Route file path (page or layout, absolute) → prebuilt component IDs\n * transitively reachable from it. Computed during RSC `generateBundle`,\n * consumed by the post-build capture pass (TIM-1119) to decide which\n * components to render under which route's param combos.\n */\n routePrebuiltComponents?: Map<string, string[]>;\n /**\n * Component ID → built chunk file name for prebuilt-declaring modules\n * that are only reachable via dynamic imports (import.meta.glob, lazy\n * routes). The capture pass imports these chunks to trigger\n * cache.component() registration before rendering. Populated during\n * RSC generateBundle alongside routePrebuiltComponents.\n */\n prebuiltLazyChunks?: Map<string, string>;\n /**\n * Content-keyed AST parse memo shared across cache/prebuilt/prerender-sugar\n * transforms. Collapses both cross-plugin and cross-environment duplicate\n * parses: identical code hits, rewritten code misses (correct because Vite\n * transforms chain — a rewrite produces different code). Bounded LRU (32\n * entries) so it doesn't hold programs for the whole build.\n *\n * Consumers MUST treat the returned ProgramNode as read-only — they collect\n * ranges and rewrite CODE via MagicString; the AST itself is never mutated.\n */\n parseCached?: ParseMemo;\n}\n\n// ── AST parse memo ───────────────────────────────────────────────────────\n\nconst PARSE_MEMO_CAPACITY = 32;\n\n/**\n * Content-keyed LRU memo for `parseAst`. Transforms for the same module run\n * back-to-back across plugins and environments, so a tiny window captures the\n * wins. The returned ProgramNode is shared — callers MUST NOT mutate it.\n */\nexport class ParseMemo {\n private cache = new Map<string, ProgramNode>();\n\n /**\n * Parse code, returning a cached ProgramNode if the same code was parsed\n * before. The returned AST MUST NOT be mutated — it may be shared across\n * multiple consumers.\n */\n parse(code: string): ProgramNode {\n const existing = this.cache.get(code);\n if (existing) {\n // LRU refresh: move to end\n this.cache.delete(code);\n this.cache.set(code, existing);\n return existing;\n }\n const program = parseAst(code, { lang: 'tsx' }) as unknown as ProgramNode;\n if (this.cache.size >= PARSE_MEMO_CAPACITY) {\n // Evict oldest (first inserted)\n const firstKey = this.cache.keys().next().value!;\n this.cache.delete(firstKey);\n }\n this.cache.set(code, program);\n return program;\n }\n\n get size(): number {\n return this.cache.size;\n }\n}\n\n// ── App directory resolution ──────────────────────────────────────────────\n\n// ── Plugin API (Vite `plugin.api`) ────────────────────────────────────────\n\n/**\n * What `timber-root-sync` publishes on its Vite `plugin.api`.\n *\n * The single supported way to read timber's *resolved* configuration from\n * outside the plugin pipeline. Re-reading `timber.config.ts` is not equivalent:\n * config passed inline as `timber({ ... })` in `vite.config.ts` exists only in\n * the plugin's closure, and `root`/`appDir` are resolved against Vite's root\n * rather than the caller's cwd.\n */\nexport interface TimberPluginApi {\n /** The live plugin context. Fully populated from `configResolved` onward. */\n getContext(): PluginContext;\n}\n\n/** Narrow an unknown Vite `plugin.api` to timber's. */\nexport function isTimberPluginApi(api: unknown): api is TimberPluginApi {\n return (\n typeof api === 'object' &&\n api !== null &&\n 'getContext' in api &&\n typeof api.getContext === 'function'\n );\n}\n\n/**\n * The configured platform adapter. `adapter` is typed `unknown` on the user\n * config so that `timber.config.ts` does not depend on the adapter types; the\n * adapter factories are what produce values of this shape.\n */\nexport function contextAdapter(ctx: PluginContext): TimberPlatformAdapter | undefined {\n return ctx.config.adapter as TimberPlatformAdapter | undefined;\n}\n\n/**\n * The configuration an adapter is handed, derived once from the resolved\n * context — by the post-build phase for `buildOutput` and by `timber preview`\n * for `preview`.\n */\nexport function adapterConfig(ctx: PluginContext): TimberConfig {\n return {\n root: ctx.root,\n // Normalized once here, not in each adapter: the platform files\n // adapters write address deployed-URL space (TIM-1274).\n base: normalizeBasePath(ctx.base),\n clientJavascriptDisabled: ctx.clientJavascript.disabled,\n };\n}\n\n/**\n * Resolve the app directory. Checks (in order):\n * 1. Explicit `configAppDir` from timber.config.ts\n * 2. `<root>/app`\n * 3. `<root>/src/app`\n *\n * Throws if none exist.\n */\nexport function resolveAppDir(root: string, configAppDir?: string): string {\n if (configAppDir) {\n const explicit = join(root, configAppDir);\n if (!existsSync(explicit)) {\n throw new Error(`[timber] Configured appDir \"${configAppDir}\" does not exist at ${explicit}`);\n }\n return explicit;\n }\n\n const rootApp = join(root, 'app');\n if (existsSync(rootApp)) return rootApp;\n\n const srcApp = join(root, 'src', 'app');\n if (existsSync(srcApp)) return srcApp;\n\n throw new Error(\n `[timber] Could not find app directory. Expected \"app/\" or \"src/app/\" in ${root}. ` +\n `You can set appDir in timber.config.ts to specify a custom location.`\n );\n}\n\n// ── Plugin context factory ────────────────────────────────────────────────\n\nexport function createPluginContext(config?: TimberUserConfig, root?: string): PluginContext {\n const projectRoot = root ?? process.cwd();\n const resolvedConfig: TimberUserConfig = { ...config };\n return {\n config: resolvedConfig,\n clientJavascript: resolveClientJavascript(resolvedConfig),\n routeTree: null,\n appDir: join(projectRoot, 'app'),\n root: projectRoot,\n dev: false,\n base: '/',\n buildManifest: null,\n deploymentId: null,\n timer: createStartupTimer(),\n holdingServer: null,\n buildDir: resolveBuildDir(projectRoot, resolvedConfig.buildDir),\n parseCached: new ParseMemo(),\n };\n}\n\n// ── Build directory resolution ────────────────────────────────────────────\n\n/** Default build output directory (relative to root). */\nexport const DEFAULT_BUILD_DIR = join('.timber', 'dist');\n\n/**\n * Resolve the build output directory.\n *\n * Priority:\n * 1. Explicit `timberBuildDir` from timber.config.ts\n * 2. Explicit `viteBuildOutDir` from Vite's build.outDir (if not the default 'dist')\n * 3. Default: `.timber/dist`\n *\n * Returns an absolute path.\n */\nexport function resolveBuildDir(\n root: string,\n timberBuildDir?: string,\n viteBuildOutDir?: string\n): string {\n if (timberBuildDir) {\n return resolve(root, timberBuildDir);\n }\n if (viteBuildOutDir && viteBuildOutDir !== 'dist') {\n return resolve(root, viteBuildOutDir);\n }\n return join(root, DEFAULT_BUILD_DIR);\n}\n\n// ── Config file loading ───────────────────────────────────────────────────\n\n/**\n * Map known timber.config load failures to actionable hints (TIM-1040).\n * Returns null when the error has no known remedy beyond its own message.\n *\n * Known failure modes of loading timber.config.ts via native require():\n * - Node < 22.18 has no default type stripping → ERR_UNKNOWN_FILE_EXTENSION\n * - Non-erasable TS syntax (enums, namespaces, parameter properties)\n * → ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX / ERR_INVALID_TYPESCRIPT_SYNTAX\n * - ESM syntax in a `\"type\": \"commonjs\"` package → the stripped .ts is\n * parsed as CJS and throws SyntaxError: \"Unexpected token 'export'\"\n * (export statements) or \"Cannot use import statement outside a\n * module\" (import statements)\n */\nexport function configLoadHint(configName: string, err: unknown): string | null {\n const code = (err as NodeJS.ErrnoException | null)?.code;\n const isTs = configName.endsWith('.ts');\n\n if (isTs && code === 'ERR_UNKNOWN_FILE_EXTENSION') {\n return (\n 'Loading a TypeScript config requires Node.js 22.18+ (built-in type stripping). ' +\n 'Upgrade Node.js or rewrite the config as timber.config.js.'\n );\n }\n if (code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' || code === 'ERR_INVALID_TYPESCRIPT_SYNTAX') {\n return (\n 'Node.js loads the config with type stripping, which only supports erasable TypeScript syntax — ' +\n 'replace enums, namespaces, and constructor parameter properties with plain objects and types.'\n );\n }\n if (\n isTs &&\n err instanceof SyntaxError &&\n /Unexpected token '(export|import)'|Cannot use import statement outside a module/.test(\n err.message\n )\n ) {\n return (\n 'The config uses ESM syntax but Node.js parsed it as CommonJS — ' +\n 'set \"type\": \"module\" in package.json, or use module.exports instead of export default.'\n );\n }\n return null;\n}\n\n/**\n * Load timber.config.ts (or .js, .mjs) from the project root.\n * Returns the config object or null if no config file is found.\n * Throws a descriptive error naming the file when a config file exists\n * but fails to load — a broken config must fail loudly, never silently\n * degrade (see TIM-1066). Known failure modes get an actionable hint\n * appended (see configLoadHint, TIM-1040).\n *\n * Uses require() — Node 22.12+ can require() ESM modules, and Node\n * 22.18+ strips types from .ts files by default (hence the engines\n * field requiring >=22.18.0). This keeps timber() synchronous — no\n * async config loading needed.\n * This is the single config loader — dev, build, and preview all use it.\n */\nexport function loadTimberConfigFile(root: string): TimberUserConfig | null {\n const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];\n const req = createRequire(join(root, 'package.json'));\n\n for (const name of configNames) {\n const configPath = join(root, name);\n if (existsSync(configPath)) {\n try {\n const mod = req(configPath);\n return (mod.default ?? mod) as TimberUserConfig;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n const hint = configLoadHint(name, err);\n throw new Error(\n `[timber] Failed to load ${configPath}: ${message}${hint ? `\\n${hint}` : ''}`,\n { cause: err }\n );\n }\n }\n }\n return null;\n}\n\n/**\n * Detect config keys set in both inline (vite.config.ts) and file (timber.config.ts)\n * and warn the user. Only keys the inline config actually sets count — the\n * `output` default is applied after the merge (timber-root-sync), so it never\n * appears here as an inline key.\n *\n * Returns the list of conflicting key names (for testing).\n */\nexport function warnConfigConflicts(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): string[] {\n const conflicts: string[] = [];\n for (const key of Object.keys(fileConfig) as (keyof TimberUserConfig)[]) {\n if (key in inline && inline[key] !== undefined) {\n conflicts.push(key);\n }\n }\n if (conflicts.length > 0) {\n console.warn(\n `[timber] Config conflict: ${conflicts.map((k) => `\"${k}\"`).join(', ')} set in both ` +\n `vite.config.ts (inline) and timber.config.ts. ` +\n `Move all config to timber.config.ts to avoid confusion. ` +\n `The inline value from vite.config.ts will be used.`\n );\n }\n return conflicts;\n}\n\n/**\n * Drop own keys whose value is `undefined` so they don't clobber file\n * config during spread merging. `timber({ buildDir: process.env.BUILD_DIR })`\n * with the var unset must not erase the timber.config.ts value.\n */\nfunction stripUndefined<T extends object>(obj: T): T {\n return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as T;\n}\n\n/**\n * Merge file-based config (timber.config.ts) with inline config (vite.config.ts).\n * Inline config takes precedence — file config only fills in missing fields.\n * Inline keys explicitly set to `undefined` are treated as absent, matching\n * the `!== undefined` check in warnConfigConflicts.\n */\nexport function mergeFileConfig(\n inline: TimberUserConfig,\n fileConfig: TimberUserConfig\n): TimberUserConfig {\n warnConfigConflicts(inline, fileConfig);\n const definedInline = stripUndefined(inline);\n\n return {\n ...fileConfig,\n ...definedInline,\n ...(fileConfig.limits && definedInline.limits\n ? { limits: { ...fileConfig.limits, ...stripUndefined(definedInline.limits) } }\n : {}),\n ...(fileConfig.dev && definedInline.dev\n ? { dev: { ...fileConfig.dev, ...stripUndefined(definedInline.dev) } }\n : {}),\n ...(fileConfig.mdx && definedInline.mdx\n ? { mdx: { ...fileConfig.mdx, ...stripUndefined(definedInline.mdx) } }\n : {}),\n };\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { n as INFERRED_RULES, t as EXPLICIT_MARKERS } from "./poison-rules-DoEhbqaY.js";
2
- import { o as langForFile } from "./chains-B4bgOl2g.js";
2
+ import { o as langForFile } from "./chains-CpFg56UB.js";
3
3
  import { parseAst } from "vite";
4
4
  //#region src/analyze/poison-scan.ts
5
5
  /**
@@ -98,4 +98,4 @@ function poisoningsFor(kind, pending, signals) {
98
98
  //#endregion
99
99
  export { importSignals as n, poisoningsFor as r, collectImportSpecifiers as t };
100
100
 
101
- //# sourceMappingURL=poison-scan-DG5eI2_z.js.map
101
+ //# sourceMappingURL=poison-scan-CpeT6_OJ.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"poison-scan-DG5eI2_z.js","names":[],"sources":["../../src/analyze/poison-scan.ts"],"sourcesContent":["/**\n * poison-scan — report-only poisoning detection for `timber graph`.\n *\n * Matches a module's static import specifiers against the shared rule\n * lists in poison-rules.ts (the single source of truth — design/47 §3)\n * and reports a poisoning only when the implied exclusivity CONFLICTS\n * with where the module actually runs: a server-only signal in a\n * client-reachable module, or a client-only signal in a server-reachable\n * one. A `node:fs` import in a file only the RSC environment loads is\n * working code, not a finding.\n *\n * Scope: import-specifier signals only (explicit markers + the inferred\n * rules' `matchesImport`). The inferred browser-global identifier rules\n * need shadow-safe allowlisted-position AST matching and are a separate\n * ticket — reporting them from a text scan would flag legitimate code.\n *\n * Design docs: 47-module-environment-tooling.md §3.\n */\n\nimport { parseAst } from 'vite';\nimport { EXPLICIT_MARKERS, INFERRED_RULES } from './poison-rules.ts';\nimport { langForFile, type ModuleKind } from './classify.ts';\n\n/** One poisoning finding on a module, in the JSON-contract shape. */\nexport interface Poisoning {\n /** Rule id — an inferred rule's id, or the explicit marker specifier. */\n rule: string;\n /** The import specifier that matched. */\n specifier: string;\n /** Which exclusivity the signal implies for the containing module. */\n implies: 'server-only' | 'client-only';\n /** Human-readable description for reporter output. */\n description: string;\n}\n\n/** File extensions the specifier scan can parse. */\nconst PARSEABLE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;\n\n/**\n * A NON-EMPTY specifier list where every entry is type-only. Empty or\n * absent lists return false — a bare `import 'pkg'` is a side-effect\n * import and very much runs.\n */\nfunction isAllSpecifiersTypeOnly(\n specifiers: Array<{ importKind?: string; exportKind?: string }> | undefined\n): boolean {\n if (!specifiers || specifiers.length === 0) return false;\n return specifiers.every((s) => s.importKind === 'type' || s.exportKind === 'type');\n}\n\n/**\n * Collect every static import/export source and string-literal dynamic\n * import from a module's original source. Sources that fail to parse\n * (or are not JS/TS at all) return [] — no specifiers is a safe\n * default for a report-only scan.\n */\nexport function collectImportSpecifiers(code: string, file: string): string[] {\n if (!PARSEABLE_EXTENSIONS.test(file)) return [];\n let program;\n try {\n // Language from the extension, never forced tsx — see langForFile.\n program = parseAst(code, { lang: langForFile(file) });\n } catch {\n return [];\n }\n const specifiers: string[] = [];\n const visit = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const child of node) visit(child);\n return;\n }\n if (typeof node !== 'object' || node === null) return;\n const record = node as Record<string, unknown> & {\n type?: string;\n importKind?: string;\n exportKind?: string;\n specifiers?: Array<{ importKind?: string; exportKind?: string }>;\n };\n if (\n (record.type === 'ImportDeclaration' ||\n record.type === 'ExportNamedDeclaration' ||\n record.type === 'ExportAllDeclaration') &&\n // Type-only imports/exports are erased at runtime — `import type\n // { Stats } from 'node:fs'` in client code is working code, and\n // flagging it would be a false positive in a report-only scan.\n // Covers both the declaration-level form (`import type {…}`,\n // importKind on the declaration) and the inline form\n // (`import { type Stats }`, where the declaration stays 'value'\n // and each SPECIFIER carries the kind) — a declaration whose\n // specifiers are all type-only is erased just the same.\n record.importKind !== 'type' &&\n record.exportKind !== 'type' &&\n !isAllSpecifiersTypeOnly(record.specifiers) &&\n typeof (record.source as { value?: unknown } | null)?.value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n } else if (\n record.type === 'ImportExpression' &&\n (record.source as { type?: string; value?: unknown })?.type === 'Literal' &&\n typeof (record.source as { value?: unknown }).value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n }\n for (const key of Object.keys(record)) {\n if (key === 'type') continue;\n visit(record[key]);\n }\n };\n visit(program.body);\n return specifiers;\n}\n\n/** Exclusivity signals a module's import specifiers carry. */\nexport function importSignals(specifiers: string[]): Poisoning[] {\n const signals: Poisoning[] = [];\n for (const specifier of specifiers) {\n for (const marker of EXPLICIT_MARKERS) {\n if (specifier === marker.specifier) {\n signals.push({\n rule: marker.specifier,\n specifier,\n implies: marker.specifier === 'server-only' ? 'server-only' : 'client-only',\n description: `imports the ${marker.specifier} poison-pill marker`,\n });\n }\n }\n for (const rule of INFERRED_RULES) {\n if (rule.matchesImport?.(specifier)) {\n signals.push({\n rule: rule.id,\n specifier,\n implies: rule.implies,\n description: rule.description,\n });\n }\n }\n }\n return signals;\n}\n\n/**\n * Filter a module's signals down to actual conflicts with its\n * classification. Environment reach follows the taxonomy: boundary /\n * client-internal / shared modules run client-side; server / shared /\n * server-action modules run server-side. Pending (provisional) modules\n * run nowhere confirmed, so nothing conflicts yet.\n */\nexport function poisoningsFor(\n kind: ModuleKind,\n pending: boolean,\n signals: Poisoning[]\n): Poisoning[] {\n if (pending || signals.length === 0) return [];\n const clientReachable =\n kind === 'client-boundary' || kind === 'client-internal' || kind === 'shared';\n const serverReachable = kind === 'server' || kind === 'shared' || kind === 'server-action';\n return signals.filter(\n (signal) =>\n (signal.implies === 'server-only' && clientReachable) ||\n (signal.implies === 'client-only' && serverReachable)\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAM,uBAAuB;;;;;;AAO7B,SAAS,wBACP,YACS;CACT,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO;CACnD,OAAO,WAAW,OAAO,MAAM,EAAE,eAAe,UAAU,EAAE,eAAe,MAAM;AACnF;;;;;;;AAQA,SAAgB,wBAAwB,MAAc,MAAwB;CAC5E,IAAI,CAAC,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC;CAC9C,IAAI;CACJ,IAAI;EAEF,UAAU,SAAS,MAAM,EAAE,MAAM,YAAY,IAAI,EAAE,CAAC;CACtD,QAAQ;EACN,OAAO,CAAC;CACV;CACA,MAAM,aAAuB,CAAC;CAC9B,MAAM,SAAS,SAAwB;EACrC,IAAI,MAAM,QAAQ,IAAI,GAAG;GACvB,KAAK,MAAM,SAAS,MAAM,MAAM,KAAK;GACrC;EACF;EACA,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;EAC/C,MAAM,SAAS;EAMf,KACG,OAAO,SAAS,uBACf,OAAO,SAAS,4BAChB,OAAO,SAAS,2BASlB,OAAO,eAAe,UACtB,OAAO,eAAe,UACtB,CAAC,wBAAwB,OAAO,UAAU,KAC1C,OAAQ,OAAO,QAAuC,UAAU,UAEhE,WAAW,KAAM,OAAO,OAA6B,KAAK;OACrD,IACL,OAAO,SAAS,sBACf,OAAO,QAA+C,SAAS,aAChE,OAAQ,OAAO,OAA+B,UAAU,UAExD,WAAW,KAAM,OAAO,OAA6B,KAAK;EAE5D,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;GACrC,IAAI,QAAQ,QAAQ;GACpB,MAAM,OAAO,IAAI;EACnB;CACF;CACA,MAAM,QAAQ,IAAI;CAClB,OAAO;AACT;;AAGA,SAAgB,cAAc,YAAmC;CAC/D,MAAM,UAAuB,CAAC;CAC9B,KAAK,MAAM,aAAa,YAAY;EAClC,KAAK,MAAM,UAAU,kBACnB,IAAI,cAAc,OAAO,WACvB,QAAQ,KAAK;GACX,MAAM,OAAO;GACb;GACA,SAAS,OAAO,cAAc,gBAAgB,gBAAgB;GAC9D,aAAa,eAAe,OAAO,UAAU;EAC/C,CAAC;EAGL,KAAK,MAAM,QAAQ,gBACjB,IAAI,KAAK,gBAAgB,SAAS,GAChC,QAAQ,KAAK;GACX,MAAM,KAAK;GACX;GACA,SAAS,KAAK;GACd,aAAa,KAAK;EACpB,CAAC;CAGP;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,cACd,MACA,SACA,SACa;CACb,IAAI,WAAW,QAAQ,WAAW,GAAG,OAAO,CAAC;CAC7C,MAAM,kBACJ,SAAS,qBAAqB,SAAS,qBAAqB,SAAS;CACvE,MAAM,kBAAkB,SAAS,YAAY,SAAS,YAAY,SAAS;CAC3E,OAAO,QAAQ,QACZ,WACE,OAAO,YAAY,iBAAiB,mBACpC,OAAO,YAAY,iBAAiB,eACzC;AACF"}
1
+ {"version":3,"file":"poison-scan-CpeT6_OJ.js","names":[],"sources":["../../src/analyze/poison-scan.ts"],"sourcesContent":["/**\n * poison-scan — report-only poisoning detection for `timber graph`.\n *\n * Matches a module's static import specifiers against the shared rule\n * lists in poison-rules.ts (the single source of truth — design/47 §3)\n * and reports a poisoning only when the implied exclusivity CONFLICTS\n * with where the module actually runs: a server-only signal in a\n * client-reachable module, or a client-only signal in a server-reachable\n * one. A `node:fs` import in a file only the RSC environment loads is\n * working code, not a finding.\n *\n * Scope: import-specifier signals only (explicit markers + the inferred\n * rules' `matchesImport`). The inferred browser-global identifier rules\n * need shadow-safe allowlisted-position AST matching and are a separate\n * ticket — reporting them from a text scan would flag legitimate code.\n *\n * Design docs: 47-module-environment-tooling.md §3.\n */\n\nimport { parseAst } from 'vite';\nimport { EXPLICIT_MARKERS, INFERRED_RULES } from './poison-rules.ts';\nimport { langForFile, type ModuleKind } from './classify.ts';\n\n/** One poisoning finding on a module, in the JSON-contract shape. */\nexport interface Poisoning {\n /** Rule id — an inferred rule's id, or the explicit marker specifier. */\n rule: string;\n /** The import specifier that matched. */\n specifier: string;\n /** Which exclusivity the signal implies for the containing module. */\n implies: 'server-only' | 'client-only';\n /** Human-readable description for reporter output. */\n description: string;\n}\n\n/** File extensions the specifier scan can parse. */\nconst PARSEABLE_EXTENSIONS = /\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts)$/;\n\n/**\n * A NON-EMPTY specifier list where every entry is type-only. Empty or\n * absent lists return false — a bare `import 'pkg'` is a side-effect\n * import and very much runs.\n */\nfunction isAllSpecifiersTypeOnly(\n specifiers: Array<{ importKind?: string; exportKind?: string }> | undefined\n): boolean {\n if (!specifiers || specifiers.length === 0) return false;\n return specifiers.every((s) => s.importKind === 'type' || s.exportKind === 'type');\n}\n\n/**\n * Collect every static import/export source and string-literal dynamic\n * import from a module's original source. Sources that fail to parse\n * (or are not JS/TS at all) return [] — no specifiers is a safe\n * default for a report-only scan.\n */\nexport function collectImportSpecifiers(code: string, file: string): string[] {\n if (!PARSEABLE_EXTENSIONS.test(file)) return [];\n let program;\n try {\n // Language from the extension, never forced tsx — see langForFile.\n program = parseAst(code, { lang: langForFile(file) });\n } catch {\n return [];\n }\n const specifiers: string[] = [];\n const visit = (node: unknown): void => {\n if (Array.isArray(node)) {\n for (const child of node) visit(child);\n return;\n }\n if (typeof node !== 'object' || node === null) return;\n const record = node as Record<string, unknown> & {\n type?: string;\n importKind?: string;\n exportKind?: string;\n specifiers?: Array<{ importKind?: string; exportKind?: string }>;\n };\n if (\n (record.type === 'ImportDeclaration' ||\n record.type === 'ExportNamedDeclaration' ||\n record.type === 'ExportAllDeclaration') &&\n // Type-only imports/exports are erased at runtime — `import type\n // { Stats } from 'node:fs'` in client code is working code, and\n // flagging it would be a false positive in a report-only scan.\n // Covers both the declaration-level form (`import type {…}`,\n // importKind on the declaration) and the inline form\n // (`import { type Stats }`, where the declaration stays 'value'\n // and each SPECIFIER carries the kind) — a declaration whose\n // specifiers are all type-only is erased just the same.\n record.importKind !== 'type' &&\n record.exportKind !== 'type' &&\n !isAllSpecifiersTypeOnly(record.specifiers) &&\n typeof (record.source as { value?: unknown } | null)?.value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n } else if (\n record.type === 'ImportExpression' &&\n (record.source as { type?: string; value?: unknown })?.type === 'Literal' &&\n typeof (record.source as { value?: unknown }).value === 'string'\n ) {\n specifiers.push((record.source as { value: string }).value);\n }\n for (const key of Object.keys(record)) {\n if (key === 'type') continue;\n visit(record[key]);\n }\n };\n visit(program.body);\n return specifiers;\n}\n\n/** Exclusivity signals a module's import specifiers carry. */\nexport function importSignals(specifiers: string[]): Poisoning[] {\n const signals: Poisoning[] = [];\n for (const specifier of specifiers) {\n for (const marker of EXPLICIT_MARKERS) {\n if (specifier === marker.specifier) {\n signals.push({\n rule: marker.specifier,\n specifier,\n implies: marker.specifier === 'server-only' ? 'server-only' : 'client-only',\n description: `imports the ${marker.specifier} poison-pill marker`,\n });\n }\n }\n for (const rule of INFERRED_RULES) {\n if (rule.matchesImport?.(specifier)) {\n signals.push({\n rule: rule.id,\n specifier,\n implies: rule.implies,\n description: rule.description,\n });\n }\n }\n }\n return signals;\n}\n\n/**\n * Filter a module's signals down to actual conflicts with its\n * classification. Environment reach follows the taxonomy: boundary /\n * client-internal / shared modules run client-side; server / shared /\n * server-action modules run server-side. Pending (provisional) modules\n * run nowhere confirmed, so nothing conflicts yet.\n */\nexport function poisoningsFor(\n kind: ModuleKind,\n pending: boolean,\n signals: Poisoning[]\n): Poisoning[] {\n if (pending || signals.length === 0) return [];\n const clientReachable =\n kind === 'client-boundary' || kind === 'client-internal' || kind === 'shared';\n const serverReachable = kind === 'server' || kind === 'shared' || kind === 'server-action';\n return signals.filter(\n (signal) =>\n (signal.implies === 'server-only' && clientReachable) ||\n (signal.implies === 'client-only' && serverReachable)\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAM,uBAAuB;;;;;;AAO7B,SAAS,wBACP,YACS;CACT,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO;CACnD,OAAO,WAAW,OAAO,MAAM,EAAE,eAAe,UAAU,EAAE,eAAe,MAAM;AACnF;;;;;;;AAQA,SAAgB,wBAAwB,MAAc,MAAwB;CAC5E,IAAI,CAAC,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC;CAC9C,IAAI;CACJ,IAAI;EAEF,UAAU,SAAS,MAAM,EAAE,MAAM,YAAY,IAAI,EAAE,CAAC;CACtD,QAAQ;EACN,OAAO,CAAC;CACV;CACA,MAAM,aAAuB,CAAC;CAC9B,MAAM,SAAS,SAAwB;EACrC,IAAI,MAAM,QAAQ,IAAI,GAAG;GACvB,KAAK,MAAM,SAAS,MAAM,MAAM,KAAK;GACrC;EACF;EACA,IAAI,OAAO,SAAS,YAAY,SAAS,MAAM;EAC/C,MAAM,SAAS;EAMf,KACG,OAAO,SAAS,uBACf,OAAO,SAAS,4BAChB,OAAO,SAAS,2BASlB,OAAO,eAAe,UACtB,OAAO,eAAe,UACtB,CAAC,wBAAwB,OAAO,UAAU,KAC1C,OAAQ,OAAO,QAAuC,UAAU,UAEhE,WAAW,KAAM,OAAO,OAA6B,KAAK;OACrD,IACL,OAAO,SAAS,sBACf,OAAO,QAA+C,SAAS,aAChE,OAAQ,OAAO,OAA+B,UAAU,UAExD,WAAW,KAAM,OAAO,OAA6B,KAAK;EAE5D,KAAK,MAAM,OAAO,OAAO,KAAK,MAAM,GAAG;GACrC,IAAI,QAAQ,QAAQ;GACpB,MAAM,OAAO,IAAI;EACnB;CACF;CACA,MAAM,QAAQ,IAAI;CAClB,OAAO;AACT;;AAGA,SAAgB,cAAc,YAAmC;CAC/D,MAAM,UAAuB,CAAC;CAC9B,KAAK,MAAM,aAAa,YAAY;EAClC,KAAK,MAAM,UAAU,kBACnB,IAAI,cAAc,OAAO,WACvB,QAAQ,KAAK;GACX,MAAM,OAAO;GACb;GACA,SAAS,OAAO,cAAc,gBAAgB,gBAAgB;GAC9D,aAAa,eAAe,OAAO,UAAU;EAC/C,CAAC;EAGL,KAAK,MAAM,QAAQ,gBACjB,IAAI,KAAK,gBAAgB,SAAS,GAChC,QAAQ,KAAK;GACX,MAAM,KAAK;GACX;GACA,SAAS,KAAK;GACd,aAAa,KAAK;EACpB,CAAC;CAGP;CACA,OAAO;AACT;;;;;;;;AASA,SAAgB,cACd,MACA,SACA,SACa;CACb,IAAI,WAAW,QAAQ,WAAW,GAAG,OAAO,CAAC;CAC7C,MAAM,kBACJ,SAAS,qBAAqB,SAAS,qBAAqB,SAAS;CACvE,MAAM,kBAAkB,SAAS,YAAY,SAAS,YAAY,SAAS;CAC3E,OAAO,QAAQ,QACZ,WACE,OAAO,YAAY,iBAAiB,mBACpC,OAAO,YAAY,iBAAiB,eACzC;AACF"}
@@ -0,0 +1,46 @@
1
+ //#region src/server/render-timeout.ts
2
+ /**
3
+ * Render timeout utilities for SSR streaming pipeline.
4
+ *
5
+ * Provides a RenderTimeoutError class and a helper to create
6
+ * timeout-guarded AbortSignals. Used to defend against hung RSC
7
+ * streams and infinite SSR renders.
8
+ *
9
+ * Design doc: 02-rendering-pipeline.md §"Streaming Constraints"
10
+ */
11
+ /** `renderTimeoutMs` when timber.config.ts leaves it unset. */
12
+ var DEFAULT_RENDER_TIMEOUT_MS = 3e4;
13
+ /**
14
+ * What `renderTimeoutMs: 0` means: a safety ceiling, never "no timeout" —
15
+ * an unbounded render or singleflight wait can hang a connection or
16
+ * deadlock a cache key forever.
17
+ */
18
+ var RENDER_TIMEOUT_CEILING_MS = 12e4;
19
+ /**
20
+ * The one mapping from configured `renderTimeoutMs` to the timeout every
21
+ * consumer enforces. Config enters the runtime already resolved (the
22
+ * runtime config module, static generation's options), so readers never
23
+ * see 0 or undefined.
24
+ */
25
+ function resolveRenderTimeoutMs(configured) {
26
+ return (configured ?? 3e4) || 12e4;
27
+ }
28
+ /**
29
+ * Error thrown when an SSR render or RSC stream read exceeds the
30
+ * configured timeout. Callers can check `instanceof RenderTimeoutError`
31
+ * to distinguish timeout from other errors and return a 504 or close
32
+ * the connection cleanly.
33
+ */
34
+ var RenderTimeoutError = class extends Error {
35
+ timeoutMs;
36
+ constructor(timeoutMs, context) {
37
+ const message = context ? `Render timeout after ${timeoutMs}ms: ${context}` : `Render timeout after ${timeoutMs}ms`;
38
+ super(message);
39
+ this.name = "RenderTimeoutError";
40
+ this.timeoutMs = timeoutMs;
41
+ }
42
+ };
43
+ //#endregion
44
+ export { resolveRenderTimeoutMs as i, RENDER_TIMEOUT_CEILING_MS as n, RenderTimeoutError as r, DEFAULT_RENDER_TIMEOUT_MS as t };
45
+
46
+ //# sourceMappingURL=render-timeout-DOf_BBcO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-timeout-DOf_BBcO.js","names":[],"sources":["../../src/server/render-timeout.ts"],"sourcesContent":["/**\n * Render timeout utilities for SSR streaming pipeline.\n *\n * Provides a RenderTimeoutError class and a helper to create\n * timeout-guarded AbortSignals. Used to defend against hung RSC\n * streams and infinite SSR renders.\n *\n * Design doc: 02-rendering-pipeline.md §\"Streaming Constraints\"\n */\n\n/** `renderTimeoutMs` when timber.config.ts leaves it unset. */\nexport const DEFAULT_RENDER_TIMEOUT_MS = 30_000;\n\n/**\n * What `renderTimeoutMs: 0` means: a safety ceiling, never \"no timeout\" —\n * an unbounded render or singleflight wait can hang a connection or\n * deadlock a cache key forever.\n */\nexport const RENDER_TIMEOUT_CEILING_MS = 120_000;\n\n/**\n * The one mapping from configured `renderTimeoutMs` to the timeout every\n * consumer enforces. Config enters the runtime already resolved (the\n * runtime config module, static generation's options), so readers never\n * see 0 or undefined.\n */\nexport function resolveRenderTimeoutMs(configured: number | undefined): number {\n return (configured ?? DEFAULT_RENDER_TIMEOUT_MS) || RENDER_TIMEOUT_CEILING_MS;\n}\n\n/**\n * Error thrown when an SSR render or RSC stream read exceeds the\n * configured timeout. Callers can check `instanceof RenderTimeoutError`\n * to distinguish timeout from other errors and return a 504 or close\n * the connection cleanly.\n */\nexport class RenderTimeoutError extends Error {\n readonly timeoutMs: number;\n\n constructor(timeoutMs: number, context?: string) {\n const message = context\n ? `Render timeout after ${timeoutMs}ms: ${context}`\n : `Render timeout after ${timeoutMs}ms`;\n super(message);\n this.name = 'RenderTimeoutError';\n this.timeoutMs = timeoutMs;\n }\n}\n\n/**\n * Result of createRenderTimeout — an AbortSignal that fires after\n * the given duration, plus a cancel function to clear the timer\n * when the render completes normally.\n */\nexport interface RenderTimeout {\n /** AbortSignal that aborts after timeoutMs. */\n signal: AbortSignal;\n /** Cancel the timeout timer. Call this when the render completes. */\n cancel: () => void;\n}\n\n/**\n * Create a render timeout that aborts after the given duration.\n *\n * Returns an AbortSignal and a cancel function. The signal fires\n * with a RenderTimeoutError as the abort reason after `timeoutMs`.\n * Call `cancel()` when the render completes to prevent the timeout\n * from firing.\n *\n * If an existing `parentSignal` is provided, the returned signal\n * aborts when either the parent signal or the timeout fires —\n * whichever comes first.\n */\nexport function createRenderTimeout(timeoutMs: number, parentSignal?: AbortSignal): RenderTimeout {\n const controller = new AbortController();\n const reason = new RenderTimeoutError(timeoutMs, 'RSC stream read timed out');\n\n const timer = setTimeout(() => controller.abort(reason), timeoutMs);\n\n let onParentAbort: (() => void) | null = null;\n\n if (parentSignal) {\n if (parentSignal.aborted) {\n clearTimeout(timer);\n controller.abort(parentSignal.reason);\n } else {\n onParentAbort = () => {\n clearTimeout(timer);\n controller.abort(parentSignal.reason);\n };\n parentSignal.addEventListener('abort', onParentAbort, { once: true });\n }\n }\n\n return {\n signal: controller.signal,\n cancel: () => {\n clearTimeout(timer);\n if (onParentAbort && parentSignal) {\n parentSignal.removeEventListener('abort', onParentAbort);\n onParentAbort = null;\n }\n },\n };\n}\n\n/**\n * Race a promise against a timeout. Rejects with RenderTimeoutError\n * if the promise does not resolve within `timeoutMs`.\n *\n * Used to guard individual `rscReader.read()` calls inside pullLoop.\n */\nexport function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n context?: string\n): Promise<T> {\n let timer: ReturnType<typeof setTimeout>;\n const timeoutPromise = new Promise<never>((_resolve, reject) => {\n timer = setTimeout(() => {\n reject(new RenderTimeoutError(timeoutMs, context));\n }, timeoutMs);\n });\n\n return Promise.race([promise, timeoutPromise]).finally(() => {\n clearTimeout(timer!);\n });\n}\n"],"mappings":";;;;;;;;;;;AAWA,IAAa,4BAA4B;;;;;;AAOzC,IAAa,4BAA4B;;;;;;;AAQzC,SAAgB,uBAAuB,YAAwC;CAC7E,QAAQ,cAAA,QAAA;AACV;;;;;;;AAQA,IAAa,qBAAb,cAAwC,MAAM;CAC5C;CAEA,YAAY,WAAmB,SAAkB;EAC/C,MAAM,UAAU,UACZ,wBAAwB,UAAU,MAAM,YACxC,wBAAwB,UAAU;EACtC,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,YAAY;CACnB;AACF"}
@@ -39,10 +39,16 @@
39
39
  * normalize; this function does not, because the two callers normalize from
40
40
  * different sources (a browser URL, a build-time route entry).
41
41
  */
42
+ /**
43
+ * The directory, relative to the site root, that holds every static RSC
44
+ * payload. The one spelling: the build writes, hashes, and reports it as
45
+ * content-hashed from here.
46
+ */
47
+ var RSC_PAYLOAD_DIR = "_rsc";
42
48
  function rscPayloadPath(routePath) {
43
- return `/_rsc${routePath === "/" ? "" : routePath}/index.rsc`;
49
+ return `/${RSC_PAYLOAD_DIR}${routePath === "/" ? "" : routePath}/index.rsc`;
44
50
  }
45
51
  //#endregion
46
- export { rscPayloadPath as t };
52
+ export { rscPayloadPath as n, RSC_PAYLOAD_DIR as t };
47
53
 
48
- //# sourceMappingURL=rsc-payload-path-B_LBodc2.js.map
54
+ //# sourceMappingURL=rsc-payload-path-dffcs4jp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rsc-payload-path-B_LBodc2.js","names":[],"sources":["../../src/shared/rsc-payload-path.ts"],"sourcesContent":["/**\n * Where a route's Flight payload lives in `output: 'static'`.\n *\n * The static build writes these files and the client fetches them, so the two\n * halves must agree byte for byte. They used to state the naming rule\n * separately — `plugins/static-build.ts` for the write and\n * `client/rsc-fetch.ts` for the read — which is a duplicate representation with\n * nothing checking it. One function now, called by both.\n *\n * ## The shape\n *\n * The payload path mirrors the HTML path: every route gets a directory and the\n * artifact is the `index` inside it.\n *\n * / → /_rsc/index.rsc (cf. index.html)\n * /about → /_rsc/about/index.rsc (cf. about/index.html)\n * /blog/hello → /_rsc/blog/hello/index.rsc\n *\n * That shape is **injective**, which the previous one (`/about` →\n * `/_rsc/about.rsc`, root → `/_rsc/index.rsc`) was not: a site with both `/`\n * and `/index` produced two payloads at `_rsc/index.rsc`, and the second write\n * won silently — a client navigation to one route was served the other route's\n * payload, same content type and same shape, undetectable downstream\n * (TIM-1266).\n *\n * Injectivity holds because two distinct non-root routes give distinct\n * directories, and the root's `/_rsc/index.rsc` has no directory component for\n * a non-root route to occupy — reaching it would need an empty route path,\n * which is not a route.\n *\n * Isomorphic: no server, client, or build-time imports.\n */\n\n/**\n * The route-space path of a route's static Flight payload.\n *\n * @param routePath A route-space pathname: leading slash, no deployment base,\n * no query or fragment, no trailing slash (except the root `/`). Callers\n * normalize; this function does not, because the two callers normalize from\n * different sources (a browser URL, a build-time route entry).\n */\nexport function rscPayloadPath(routePath: string): string {\n const prefix = routePath === '/' ? '' : routePath;\n return `/_rsc${prefix}/index.rsc`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAgB,eAAe,WAA2B;CAExD,OAAO,QADQ,cAAc,MAAM,KAAK,UAClB;AACxB"}
1
+ {"version":3,"file":"rsc-payload-path-dffcs4jp.js","names":[],"sources":["../../src/shared/rsc-payload-path.ts"],"sourcesContent":["/**\n * Where a route's Flight payload lives in `output: 'static'`.\n *\n * The static build writes these files and the client fetches them, so the two\n * halves must agree byte for byte. They used to state the naming rule\n * separately — `plugins/static-build.ts` for the write and\n * `client/rsc-fetch.ts` for the read — which is a duplicate representation with\n * nothing checking it. One function now, called by both.\n *\n * ## The shape\n *\n * The payload path mirrors the HTML path: every route gets a directory and the\n * artifact is the `index` inside it.\n *\n * / → /_rsc/index.rsc (cf. index.html)\n * /about → /_rsc/about/index.rsc (cf. about/index.html)\n * /blog/hello → /_rsc/blog/hello/index.rsc\n *\n * That shape is **injective**, which the previous one (`/about` →\n * `/_rsc/about.rsc`, root → `/_rsc/index.rsc`) was not: a site with both `/`\n * and `/index` produced two payloads at `_rsc/index.rsc`, and the second write\n * won silently — a client navigation to one route was served the other route's\n * payload, same content type and same shape, undetectable downstream\n * (TIM-1266).\n *\n * Injectivity holds because two distinct non-root routes give distinct\n * directories, and the root's `/_rsc/index.rsc` has no directory component for\n * a non-root route to occupy — reaching it would need an empty route path,\n * which is not a route.\n *\n * Isomorphic: no server, client, or build-time imports.\n */\n\n/**\n * The route-space path of a route's static Flight payload.\n *\n * @param routePath A route-space pathname: leading slash, no deployment base,\n * no query or fragment, no trailing slash (except the root `/`). Callers\n * normalize; this function does not, because the two callers normalize from\n * different sources (a browser URL, a build-time route entry).\n */\n/**\n * The directory, relative to the site root, that holds every static RSC\n * payload. The one spelling: the build writes, hashes, and reports it as\n * content-hashed from here.\n */\nexport const RSC_PAYLOAD_DIR = '_rsc';\n\nexport function rscPayloadPath(routePath: string): string {\n const prefix = routePath === '/' ? '' : routePath;\n return `/${RSC_PAYLOAD_DIR}${prefix}/index.rsc`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,kBAAkB;AAE/B,SAAgB,eAAe,WAA2B;CAExD,OAAO,IAAI,kBADI,cAAc,MAAM,KAAK,UACJ;AACtC"}
@@ -3,7 +3,7 @@ import { n as NULL_BYTE_RE, t as ENCODED_SEPARATOR_RE } from "./canonicalize-CgH
3
3
  import { t as matchUrlParts } from "./tree-match-6yihorhk.js";
4
4
  import { i as DEFAULT_PAGE_EXTENSIONS, n as classifyUrlSegment, r as effectiveUrlSegment, t as classifySegment } from "./segment-classify-C539Pa2O.js";
5
5
  import { n as treePathDepth, t as computeSegmentTreePaths } from "./segment-keys-D5hu1hz4.js";
6
- import { h as swallow } from "./logger-k2DQ4EUf.js";
6
+ import { _ as swallow } from "./logger-DiDt5ppH.js";
7
7
  import { basename, extname, join } from "node:path";
8
8
  import { readdirSync, statSync } from "node:fs";
9
9
  //#region src/routing/collision-probe.ts
@@ -1668,4 +1668,4 @@ function findScriptExtFile(dirPath, name) {
1668
1668
  //#endregion
1669
1669
  export { scanRoutes as n, collectInterceptionRewrites as r, isRouteConventionFile as t };
1670
1670
 
1671
- //# sourceMappingURL=scanner-BQ5sPIJ7.js.map
1671
+ //# sourceMappingURL=scanner-Bw0oq1HB.js.map