@timber-js/app 0.2.0-alpha.97 → 0.2.0-alpha.99

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 (372) hide show
  1. package/dist/_chunks/actions-CQ8Z8VGL.js +1061 -0
  2. package/dist/_chunks/actions-CQ8Z8VGL.js.map +1 -0
  3. package/dist/_chunks/build-output-helper-DXnW0qjz.js +61 -0
  4. package/dist/_chunks/build-output-helper-DXnW0qjz.js.map +1 -0
  5. package/dist/_chunks/{define-Itxvcd7F.js → define-B-Q_UMOD.js} +19 -23
  6. package/dist/_chunks/define-B-Q_UMOD.js.map +1 -0
  7. package/dist/_chunks/{define-C77ScO0m.js → define-CfBPoJb0.js} +24 -7
  8. package/dist/_chunks/define-CfBPoJb0.js.map +1 -0
  9. package/dist/_chunks/define-cookie-BjpIt4UC.js +194 -0
  10. package/dist/_chunks/define-cookie-BjpIt4UC.js.map +1 -0
  11. package/dist/_chunks/{format-CYBGxKtc.js → format-Bcn-Iv1x.js} +1 -1
  12. package/dist/_chunks/{format-CYBGxKtc.js.map → format-Bcn-Iv1x.js.map} +1 -1
  13. package/dist/_chunks/handler-store-B-lqaGyh.js +54 -0
  14. package/dist/_chunks/handler-store-B-lqaGyh.js.map +1 -0
  15. package/dist/_chunks/logger-0m8MsKdc.js +291 -0
  16. package/dist/_chunks/logger-0m8MsKdc.js.map +1 -0
  17. package/dist/_chunks/merge-search-params-BphMdht_.js +122 -0
  18. package/dist/_chunks/merge-search-params-BphMdht_.js.map +1 -0
  19. package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
  20. package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  21. package/dist/_chunks/navigation-root-BCYczjml.js +96 -0
  22. package/dist/_chunks/navigation-root-BCYczjml.js.map +1 -0
  23. package/dist/_chunks/registry-I2ss-lvy.js +20 -0
  24. package/dist/_chunks/registry-I2ss-lvy.js.map +1 -0
  25. package/dist/_chunks/router-ref-h3-UaCQv.js +28 -0
  26. package/dist/_chunks/router-ref-h3-UaCQv.js.map +1 -0
  27. package/dist/_chunks/{schema-bridge-C3xl_vfb.js → schema-bridge-Cxu4l-7p.js} +1 -1
  28. package/dist/_chunks/{schema-bridge-C3xl_vfb.js.map → schema-bridge-Cxu4l-7p.js.map} +1 -1
  29. package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
  30. package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
  31. package/dist/_chunks/{segment-context-fHFLF1PE.js → segment-context-Dx_OizxD.js} +1 -1
  32. package/dist/_chunks/{segment-context-fHFLF1PE.js.map → segment-context-Dx_OizxD.js.map} +1 -1
  33. package/dist/_chunks/{router-ref-C8OCm7g7.js → ssr-data-B4CdH7rE.js} +2 -26
  34. package/dist/_chunks/ssr-data-B4CdH7rE.js.map +1 -0
  35. package/dist/_chunks/{stale-reload-BX5gL1r-.js → stale-reload-Bab885FO.js} +1 -1
  36. package/dist/_chunks/{stale-reload-BX5gL1r-.js.map → stale-reload-Bab885FO.js.map} +1 -1
  37. package/dist/_chunks/tracing-C8V-YGsP.js +329 -0
  38. package/dist/_chunks/tracing-C8V-YGsP.js.map +1 -0
  39. package/dist/_chunks/{use-query-states-BiV5GJgm.js → use-query-states-B2XTqxDR.js} +3 -19
  40. package/dist/_chunks/use-query-states-B2XTqxDR.js.map +1 -0
  41. package/dist/_chunks/{use-params-IOPu7E8t.js → use-segment-params-BkpKAQ7D.js} +9 -95
  42. package/dist/_chunks/use-segment-params-BkpKAQ7D.js.map +1 -0
  43. package/dist/_chunks/{interception-BbqMCVXa.js → walkers-Tg0Alwcg.js} +66 -87
  44. package/dist/_chunks/walkers-Tg0Alwcg.js.map +1 -0
  45. package/dist/_chunks/{dev-warnings-DpGRGoDi.js → warnings-Cg47l5sk.js} +3 -3
  46. package/dist/_chunks/warnings-Cg47l5sk.js.map +1 -0
  47. package/dist/adapters/build-output-helper.d.ts +28 -0
  48. package/dist/adapters/build-output-helper.d.ts.map +1 -0
  49. package/dist/adapters/cloudflare.d.ts.map +1 -1
  50. package/dist/adapters/cloudflare.js +8 -28
  51. package/dist/adapters/cloudflare.js.map +1 -1
  52. package/dist/adapters/nitro.d.ts.map +1 -1
  53. package/dist/adapters/nitro.js +63 -31
  54. package/dist/adapters/nitro.js.map +1 -1
  55. package/dist/adapters/shared.d.ts +16 -0
  56. package/dist/adapters/shared.d.ts.map +1 -0
  57. package/dist/cache/index.js +9 -2
  58. package/dist/cache/index.js.map +1 -1
  59. package/dist/cache/timber-cache.d.ts.map +1 -1
  60. package/dist/client/error-boundary.js +2 -1
  61. package/dist/client/error-boundary.js.map +1 -1
  62. package/dist/client/form.d.ts +10 -24
  63. package/dist/client/form.d.ts.map +1 -1
  64. package/dist/client/index.d.ts +1 -5
  65. package/dist/client/index.d.ts.map +1 -1
  66. package/dist/client/index.js +41 -91
  67. package/dist/client/index.js.map +1 -1
  68. package/dist/client/internal.d.ts +2 -1
  69. package/dist/client/internal.d.ts.map +1 -1
  70. package/dist/client/internal.js +81 -7
  71. package/dist/client/internal.js.map +1 -1
  72. package/dist/client/rsc-fetch.d.ts.map +1 -1
  73. package/dist/client/state.d.ts +1 -1
  74. package/dist/client/use-cookie.d.ts +8 -0
  75. package/dist/client/use-cookie.d.ts.map +1 -1
  76. package/dist/client/{use-params.d.ts → use-segment-params.d.ts} +1 -1
  77. package/dist/client/use-segment-params.d.ts.map +1 -0
  78. package/dist/codec.d.ts +1 -1
  79. package/dist/codec.d.ts.map +1 -1
  80. package/dist/codec.js +2 -2
  81. package/dist/config-types.d.ts +28 -0
  82. package/dist/config-types.d.ts.map +1 -1
  83. package/dist/cookies/define-cookie.d.ts +87 -35
  84. package/dist/cookies/define-cookie.d.ts.map +1 -1
  85. package/dist/cookies/index.d.ts +2 -1
  86. package/dist/cookies/index.d.ts.map +1 -1
  87. package/dist/cookies/index.js +48 -2
  88. package/dist/cookies/index.js.map +1 -0
  89. package/dist/cookies/json-cookie.d.ts +64 -0
  90. package/dist/cookies/json-cookie.d.ts.map +1 -0
  91. package/dist/cookies/validation.d.ts +46 -0
  92. package/dist/cookies/validation.d.ts.map +1 -0
  93. package/dist/{plugins/dev-404-page.d.ts → dev-tools/404-page.d.ts} +9 -19
  94. package/dist/dev-tools/404-page.d.ts.map +1 -0
  95. package/dist/{plugins/dev-browser-logs.d.ts → dev-tools/browser-logs.d.ts} +1 -1
  96. package/dist/dev-tools/browser-logs.d.ts.map +1 -0
  97. package/dist/{plugins/dev-error-page.d.ts → dev-tools/error-page.d.ts} +2 -2
  98. package/dist/dev-tools/error-page.d.ts.map +1 -0
  99. package/dist/{server/dev-holding-server.d.ts → dev-tools/holding-server.d.ts} +5 -3
  100. package/dist/dev-tools/holding-server.d.ts.map +1 -0
  101. package/dist/dev-tools/index.d.ts +31 -0
  102. package/dist/dev-tools/index.d.ts.map +1 -0
  103. package/dist/{server/dev-span-processor.d.ts → dev-tools/instrumentation.d.ts} +26 -6
  104. package/dist/dev-tools/instrumentation.d.ts.map +1 -0
  105. package/dist/{server/dev-logger.d.ts → dev-tools/logger.d.ts} +1 -1
  106. package/dist/dev-tools/logger.d.ts.map +1 -0
  107. package/dist/{plugins/dev-logs.d.ts → dev-tools/logs.d.ts} +1 -1
  108. package/dist/dev-tools/logs.d.ts.map +1 -0
  109. package/dist/{plugins/dev-error-overlay.d.ts → dev-tools/overlay.d.ts} +3 -12
  110. package/dist/dev-tools/overlay.d.ts.map +1 -0
  111. package/dist/dev-tools/stack-classifier.d.ts +34 -0
  112. package/dist/dev-tools/stack-classifier.d.ts.map +1 -0
  113. package/dist/{plugins/dev-terminal-error.d.ts → dev-tools/terminal.d.ts} +2 -2
  114. package/dist/dev-tools/terminal.d.ts.map +1 -0
  115. package/dist/{server/dev-warnings.d.ts → dev-tools/warnings.d.ts} +1 -1
  116. package/dist/dev-tools/warnings.d.ts.map +1 -0
  117. package/dist/index.d.ts +1 -0
  118. package/dist/index.d.ts.map +1 -1
  119. package/dist/index.js +285 -133
  120. package/dist/index.js.map +1 -1
  121. package/dist/plugin-context.d.ts +1 -1
  122. package/dist/plugin-context.d.ts.map +1 -1
  123. package/dist/plugins/adapter-build.d.ts.map +1 -1
  124. package/dist/plugins/build-report.d.ts +6 -4
  125. package/dist/plugins/build-report.d.ts.map +1 -1
  126. package/dist/routing/convention-lint.d.ts.map +1 -1
  127. package/dist/routing/index.d.ts +5 -3
  128. package/dist/routing/index.d.ts.map +1 -1
  129. package/dist/routing/index.js +3 -3
  130. package/dist/routing/scanner.d.ts +1 -10
  131. package/dist/routing/scanner.d.ts.map +1 -1
  132. package/dist/routing/segment-classify.d.ts +37 -8
  133. package/dist/routing/segment-classify.d.ts.map +1 -1
  134. package/dist/routing/status-file-lint.d.ts.map +1 -1
  135. package/dist/routing/types.d.ts +63 -23
  136. package/dist/routing/types.d.ts.map +1 -1
  137. package/dist/routing/walkers.d.ts +51 -0
  138. package/dist/routing/walkers.d.ts.map +1 -0
  139. package/dist/search-params/define.d.ts +25 -7
  140. package/dist/search-params/define.d.ts.map +1 -1
  141. package/dist/search-params/index.js +5 -3
  142. package/dist/search-params/index.js.map +1 -1
  143. package/dist/search-params/wrappers.d.ts +2 -2
  144. package/dist/search-params/wrappers.d.ts.map +1 -1
  145. package/dist/segment-params/define.d.ts +23 -6
  146. package/dist/segment-params/define.d.ts.map +1 -1
  147. package/dist/segment-params/index.js +1 -1
  148. package/dist/server/access-gate.d.ts +4 -3
  149. package/dist/server/access-gate.d.ts.map +1 -1
  150. package/dist/server/action-handler.d.ts +15 -6
  151. package/dist/server/action-handler.d.ts.map +1 -1
  152. package/dist/server/als-registry.d.ts +5 -5
  153. package/dist/server/als-registry.d.ts.map +1 -1
  154. package/dist/server/asset-headers.d.ts +1 -15
  155. package/dist/server/asset-headers.d.ts.map +1 -1
  156. package/dist/server/cookie-context.d.ts +170 -0
  157. package/dist/server/cookie-context.d.ts.map +1 -0
  158. package/dist/server/cookie-parsing.d.ts +51 -0
  159. package/dist/server/cookie-parsing.d.ts.map +1 -0
  160. package/dist/server/deny-boundary.d.ts +90 -0
  161. package/dist/server/deny-boundary.d.ts.map +1 -0
  162. package/dist/server/deny-renderer.d.ts.map +1 -1
  163. package/dist/server/early-hints-sender.d.ts.map +1 -1
  164. package/dist/server/html-injector-core.d.ts +212 -0
  165. package/dist/server/html-injector-core.d.ts.map +1 -0
  166. package/dist/server/html-injectors.d.ts +59 -59
  167. package/dist/server/html-injectors.d.ts.map +1 -1
  168. package/dist/server/index.d.ts +5 -4
  169. package/dist/server/index.d.ts.map +1 -1
  170. package/dist/server/index.js +4 -149
  171. package/dist/server/index.js.map +1 -1
  172. package/dist/server/internal.d.ts +6 -4
  173. package/dist/server/internal.d.ts.map +1 -1
  174. package/dist/server/internal.js +852 -852
  175. package/dist/server/internal.js.map +1 -1
  176. package/dist/server/logger.d.ts +14 -0
  177. package/dist/server/logger.d.ts.map +1 -1
  178. package/dist/server/middleware-runner.d.ts +17 -0
  179. package/dist/server/middleware-runner.d.ts.map +1 -1
  180. package/dist/server/node-stream-transforms.d.ts +46 -49
  181. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  182. package/dist/server/param-coercion.d.ts +26 -0
  183. package/dist/server/param-coercion.d.ts.map +1 -0
  184. package/dist/server/pipeline-helpers.d.ts +95 -0
  185. package/dist/server/pipeline-helpers.d.ts.map +1 -0
  186. package/dist/server/pipeline-outcome.d.ts +49 -0
  187. package/dist/server/pipeline-outcome.d.ts.map +1 -0
  188. package/dist/server/pipeline-phases.d.ts +52 -0
  189. package/dist/server/pipeline-phases.d.ts.map +1 -0
  190. package/dist/server/pipeline.d.ts +51 -32
  191. package/dist/server/pipeline.d.ts.map +1 -1
  192. package/dist/server/port-resolution.d.ts +117 -0
  193. package/dist/server/port-resolution.d.ts.map +1 -0
  194. package/dist/server/request-context.d.ts +22 -159
  195. package/dist/server/request-context.d.ts.map +1 -1
  196. package/dist/server/route-element-builder.d.ts.map +1 -1
  197. package/dist/server/route-matcher.d.ts +20 -47
  198. package/dist/server/route-matcher.d.ts.map +1 -1
  199. package/dist/server/rsc-entry/action-middleware-runner.d.ts +66 -0
  200. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -0
  201. package/dist/server/rsc-entry/helpers.d.ts +1 -1
  202. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  203. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  204. package/dist/server/rsc-entry/render-route.d.ts +50 -0
  205. package/dist/server/rsc-entry/render-route.d.ts.map +1 -0
  206. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +119 -0
  207. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
  208. package/dist/server/state-tree-diff.d.ts.map +1 -1
  209. package/dist/server/status-code-resolver.d.ts +16 -11
  210. package/dist/server/status-code-resolver.d.ts.map +1 -1
  211. package/dist/server/tracing.d.ts +1 -1
  212. package/dist/server/tracing.d.ts.map +1 -1
  213. package/dist/server/tree-builder.d.ts +45 -16
  214. package/dist/server/tree-builder.d.ts.map +1 -1
  215. package/dist/server/types.d.ts +48 -0
  216. package/dist/server/types.d.ts.map +1 -1
  217. package/dist/server/utils/escape-html.d.ts +14 -0
  218. package/dist/server/utils/escape-html.d.ts.map +1 -0
  219. package/dist/shims/headers.d.ts +2 -2
  220. package/dist/shims/headers.d.ts.map +1 -1
  221. package/dist/shims/navigation-client.d.ts +3 -1
  222. package/dist/shims/navigation-client.d.ts.map +1 -1
  223. package/dist/shims/navigation.d.ts +9 -4
  224. package/dist/shims/navigation.d.ts.map +1 -1
  225. package/dist/utils/directive-parser.d.ts +0 -45
  226. package/dist/utils/directive-parser.d.ts.map +1 -1
  227. package/package.json +1 -1
  228. package/src/adapters/build-output-helper.ts +77 -0
  229. package/src/adapters/cloudflare.ts +10 -50
  230. package/src/adapters/nitro.ts +66 -50
  231. package/src/adapters/shared.ts +40 -0
  232. package/src/cache/timber-cache.ts +3 -2
  233. package/src/client/form.tsx +17 -25
  234. package/src/client/index.ts +16 -9
  235. package/src/client/internal.ts +3 -2
  236. package/src/client/router.ts +1 -1
  237. package/src/client/rsc-fetch.ts +15 -0
  238. package/src/client/state.ts +2 -2
  239. package/src/client/use-cookie.ts +29 -0
  240. package/src/codec.ts +3 -7
  241. package/src/config-types.ts +28 -0
  242. package/src/cookies/define-cookie.ts +271 -78
  243. package/src/cookies/index.ts +11 -8
  244. package/src/cookies/json-cookie.ts +105 -0
  245. package/src/cookies/validation.ts +134 -0
  246. package/src/{plugins/dev-404-page.ts → dev-tools/404-page.ts} +17 -48
  247. package/src/{plugins/dev-error-page.ts → dev-tools/error-page.ts} +5 -32
  248. package/src/{server/dev-holding-server.ts → dev-tools/holding-server.ts} +4 -2
  249. package/src/dev-tools/index.ts +90 -0
  250. package/src/dev-tools/instrumentation.ts +176 -0
  251. package/src/{plugins/dev-logs.ts → dev-tools/logs.ts} +2 -2
  252. package/src/{plugins/dev-error-overlay.ts → dev-tools/overlay.ts} +5 -23
  253. package/src/dev-tools/stack-classifier.ts +75 -0
  254. package/src/{plugins/dev-terminal-error.ts → dev-tools/terminal.ts} +4 -38
  255. package/src/{server/dev-warnings.ts → dev-tools/warnings.ts} +1 -1
  256. package/src/index.ts +95 -34
  257. package/src/plugin-context.ts +1 -1
  258. package/src/plugins/adapter-build.ts +3 -1
  259. package/src/plugins/build-report.ts +13 -22
  260. package/src/plugins/dev-server.ts +3 -3
  261. package/src/plugins/routing.ts +14 -12
  262. package/src/plugins/shims.ts +1 -1
  263. package/src/plugins/static-build.ts +1 -1
  264. package/src/routing/codegen.ts +1 -1
  265. package/src/routing/convention-lint.ts +9 -8
  266. package/src/routing/index.ts +5 -3
  267. package/src/routing/interception.ts +1 -1
  268. package/src/routing/scanner.ts +22 -95
  269. package/src/routing/segment-classify.ts +107 -8
  270. package/src/routing/status-file-lint.ts +7 -5
  271. package/src/routing/types.ts +63 -23
  272. package/src/routing/walkers.ts +90 -0
  273. package/src/search-params/define.ts +71 -15
  274. package/src/search-params/wrappers.ts +9 -2
  275. package/src/segment-params/define.ts +66 -13
  276. package/src/server/access-gate.tsx +9 -8
  277. package/src/server/action-handler.ts +34 -38
  278. package/src/server/als-registry.ts +5 -5
  279. package/src/server/asset-headers.ts +8 -34
  280. package/src/server/cookie-context.ts +468 -0
  281. package/src/server/cookie-parsing.ts +135 -0
  282. package/src/server/{deny-page-resolver.ts → deny-boundary.ts} +78 -14
  283. package/src/server/deny-renderer.ts +7 -12
  284. package/src/server/early-hints-sender.ts +3 -2
  285. package/src/server/fallback-error.ts +2 -2
  286. package/src/server/html-injector-core.ts +403 -0
  287. package/src/server/html-injectors.ts +158 -297
  288. package/src/server/index.ts +13 -14
  289. package/src/server/internal.ts +10 -3
  290. package/src/server/logger.ts +23 -0
  291. package/src/server/middleware-runner.ts +44 -0
  292. package/src/server/node-stream-transforms.ts +108 -248
  293. package/src/server/param-coercion.ts +76 -0
  294. package/src/server/pipeline-helpers.ts +204 -0
  295. package/src/server/pipeline-outcome.ts +167 -0
  296. package/src/server/pipeline-phases.ts +409 -0
  297. package/src/server/pipeline.ts +70 -540
  298. package/src/server/port-resolution.ts +215 -0
  299. package/src/server/request-context.ts +46 -451
  300. package/src/server/route-element-builder.ts +8 -4
  301. package/src/server/route-matcher.ts +28 -60
  302. package/src/server/rsc-entry/action-middleware-runner.ts +167 -0
  303. package/src/server/rsc-entry/api-handler.ts +2 -2
  304. package/src/server/rsc-entry/error-renderer.ts +2 -2
  305. package/src/server/rsc-entry/helpers.ts +2 -7
  306. package/src/server/rsc-entry/index.ts +81 -366
  307. package/src/server/rsc-entry/render-route.ts +304 -0
  308. package/src/server/rsc-entry/rsc-payload.ts +1 -1
  309. package/src/server/rsc-entry/ssr-renderer.ts +2 -2
  310. package/src/server/rsc-entry/wrap-action-dispatch.ts +449 -0
  311. package/src/server/sitemap-generator.ts +1 -1
  312. package/src/server/slot-resolver.ts +1 -1
  313. package/src/server/ssr-entry.ts +1 -1
  314. package/src/server/state-tree-diff.ts +4 -1
  315. package/src/server/status-code-resolver.ts +112 -128
  316. package/src/server/tracing.ts +3 -3
  317. package/src/server/tree-builder.ts +134 -56
  318. package/src/server/types.ts +52 -0
  319. package/src/server/utils/escape-html.ts +20 -0
  320. package/src/shims/headers.ts +3 -3
  321. package/src/shims/navigation-client.ts +4 -3
  322. package/src/shims/navigation.ts +9 -7
  323. package/src/utils/directive-parser.ts +0 -392
  324. package/dist/_chunks/actions-DLnUaR65.js +0 -421
  325. package/dist/_chunks/actions-DLnUaR65.js.map +0 -1
  326. package/dist/_chunks/als-registry-HS0LGUl2.js +0 -41
  327. package/dist/_chunks/als-registry-HS0LGUl2.js.map +0 -1
  328. package/dist/_chunks/debug-ECi_61pb.js +0 -108
  329. package/dist/_chunks/debug-ECi_61pb.js.map +0 -1
  330. package/dist/_chunks/define-C77ScO0m.js.map +0 -1
  331. package/dist/_chunks/define-Itxvcd7F.js.map +0 -1
  332. package/dist/_chunks/define-cookie-BowvzoP0.js +0 -94
  333. package/dist/_chunks/define-cookie-BowvzoP0.js.map +0 -1
  334. package/dist/_chunks/dev-warnings-DpGRGoDi.js.map +0 -1
  335. package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
  336. package/dist/_chunks/merge-search-params-Cm_KIWDX.js +0 -41
  337. package/dist/_chunks/merge-search-params-Cm_KIWDX.js.map +0 -1
  338. package/dist/_chunks/request-context-CK5tZqIP.js +0 -478
  339. package/dist/_chunks/request-context-CK5tZqIP.js.map +0 -1
  340. package/dist/_chunks/router-ref-C8OCm7g7.js.map +0 -1
  341. package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
  342. package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
  343. package/dist/_chunks/tracing-CCYbKn5n.js +0 -238
  344. package/dist/_chunks/tracing-CCYbKn5n.js.map +0 -1
  345. package/dist/_chunks/use-params-IOPu7E8t.js.map +0 -1
  346. package/dist/_chunks/use-query-states-BiV5GJgm.js.map +0 -1
  347. package/dist/client/use-params.d.ts.map +0 -1
  348. package/dist/plugins/dev-404-page.d.ts.map +0 -1
  349. package/dist/plugins/dev-browser-logs.d.ts.map +0 -1
  350. package/dist/plugins/dev-error-overlay.d.ts.map +0 -1
  351. package/dist/plugins/dev-error-page.d.ts.map +0 -1
  352. package/dist/plugins/dev-logs.d.ts.map +0 -1
  353. package/dist/plugins/dev-terminal-error.d.ts.map +0 -1
  354. package/dist/server/deny-page-resolver.d.ts +0 -52
  355. package/dist/server/deny-page-resolver.d.ts.map +0 -1
  356. package/dist/server/dev-fetch-instrumentation.d.ts +0 -22
  357. package/dist/server/dev-fetch-instrumentation.d.ts.map +0 -1
  358. package/dist/server/dev-holding-server.d.ts.map +0 -1
  359. package/dist/server/dev-logger.d.ts.map +0 -1
  360. package/dist/server/dev-span-processor.d.ts.map +0 -1
  361. package/dist/server/dev-warnings.d.ts.map +0 -1
  362. package/dist/server/manifest-status-resolver.d.ts +0 -58
  363. package/dist/server/manifest-status-resolver.d.ts.map +0 -1
  364. package/dist/server/page-deny-boundary.d.ts +0 -31
  365. package/dist/server/page-deny-boundary.d.ts.map +0 -1
  366. package/src/server/dev-fetch-instrumentation.ts +0 -96
  367. package/src/server/dev-span-processor.ts +0 -78
  368. package/src/server/manifest-status-resolver.ts +0 -215
  369. package/src/server/page-deny-boundary.tsx +0 -56
  370. /package/src/client/{use-params.ts → use-segment-params.ts} +0 -0
  371. /package/src/{plugins/dev-browser-logs.ts → dev-tools/browser-logs.ts} +0 -0
  372. /package/src/{server/dev-logger.ts → dev-tools/logger.ts} +0 -0
@@ -4,25 +4,21 @@
4
4
  * Pipeline stages (in order):
5
5
  * proxy.ts → canonicalize → route match → 103 Early Hints → middleware.ts → render
6
6
  *
7
- * Each stage is a pure function or returns a Response to short-circuit.
8
- * Each request gets a trace ID, structured logging, and OTEL spans.
7
+ * The phase functions live in `pipeline-phases.ts` so each phase can be
8
+ * tested in isolation. The terminal `outcomeToResponse` translator and
9
+ * stateless helpers live in `pipeline-phases.ts` and `pipeline-helpers.ts`
10
+ * respectively. This file owns only the public type surface and the
11
+ * `createPipeline` entry point: trace ID setup, request-context ALS,
12
+ * Server-Timing wrapping, and the activeRequests counter.
9
13
  *
10
14
  * See design/07-routing.md §"Request Lifecycle", design/02-rendering-pipeline.md §"Request Flow",
11
15
  * and design/17-logging.md §"Production Logging"
12
16
  */
13
17
 
14
- import { canonicalize } from './canonicalize.js';
15
- import { runProxy, type ProxyExport } from './proxy.js';
16
- import { runMiddlewareChain, type MiddlewareFn } from './middleware-runner.js';
17
- import { runWithTimingCollector, withTiming, getServerTimingHeader } from './server-timing.js';
18
- import {
19
- runWithRequestContext,
20
- applyRequestHeaderOverlay,
21
- setMutableCookieContext,
22
- getSetCookieHeaders,
23
- markResponseFlushed,
24
- setSegmentParams,
25
- } from './request-context.js';
18
+ import type { ProxyExport } from './proxy.js';
19
+ import type { MiddlewareFn } from './middleware-runner.js';
20
+ import { runWithTimingCollector, getServerTimingHeader } from './server-timing.js';
21
+ import { runWithRequestContext } from './request-context.js';
26
22
  import {
27
23
  generateTraceId,
28
24
  runWithTraceId,
@@ -30,55 +26,28 @@ import {
30
26
  replaceTraceId,
31
27
  withSpan,
32
28
  setSpanAttribute,
33
- getTraceId,
34
29
  } from './tracing.js';
35
- import {
36
- logRequestReceived,
37
- logRequestCompleted,
38
- logSlowRequest,
39
- logProxyError,
40
- logMiddlewareError,
41
- logMiddlewareShortCircuit,
42
- logRenderError,
43
- } from './logger.js';
44
- import { callOnRequestError } from './instrumentation.js';
45
- import { RedirectSignal, DenySignal } from './primitives.js';
46
- import { ParamCoercionError } from './route-element-builder.js';
47
- import { checkVersionSkew, applyReloadHeaders } from './version-skew.js';
48
- import { serveStaticMetadataFile, serializeSitemap } from './pipeline-metadata.js';
49
- import { loadModule } from './safe-load.js';
50
- import { findInterceptionMatch } from './pipeline-interception.js';
51
- import type { MiddlewareContext } from './types.js';
52
- import type { SegmentNode } from '../routing/types.js';
53
-
54
- // ─── Prototype-Pollution-Safe Merge ────────────────────────────────────────
30
+ import { logRequestReceived, logRequestCompleted, logSlowRequest } from './logger.js';
31
+ import { DenySignal } from './primitives.js';
32
+ import type { ManifestSegmentNode } from './route-matcher.js';
33
+ import { makeProxyResolver } from './pipeline-helpers.js';
34
+ import { handleRequest, runProxyPhase } from './pipeline-phases.js';
35
+ import { outcomeToResponse } from './pipeline-outcome.js';
55
36
 
56
- /** Keys that must never be merged via Object.assign — they pollute Object.prototype. */
57
- const DANGEROUS_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
37
+ // ─── Route Match Result ────────────────────────────────────────────────────
58
38
 
59
39
  /**
60
- * Shallow merge that skips prototype-polluting keys.
61
- *
62
- * Used instead of Object.assign when the source object comes from
63
- * user-authored codec output (segmentParams.parse), which could
64
- * contain __proto__, constructor, or prototype keys.
40
+ * Result of matching a canonical pathname against the route tree.
65
41
  *
66
- * See TIM-655, design/13-security.md
42
+ * `segments` is the runtime (`ManifestFile`-specialized) shape — the same
43
+ * nodes carried in the virtual route manifest. TIM-863 unified this: the
44
+ * matcher produces `ManifestSegmentNode[]` directly and every consumer
45
+ * (render, slots, params coercion, early hints, deny fallback) sees the
46
+ * same structural type with no `as unknown as` laundering.
67
47
  */
68
- export function safeMerge(target: Record<string, unknown>, source: Record<string, unknown>): void {
69
- for (const key of Object.keys(source)) {
70
- if (!DANGEROUS_KEYS.has(key)) {
71
- target[key] = source[key];
72
- }
73
- }
74
- }
75
-
76
- // ─── Route Match Result ────────────────────────────────────────────────────
77
-
78
- /** Result of matching a canonical pathname against the route tree. */
79
48
  export interface RouteMatch {
80
49
  /** The matched segment chain from root to leaf. */
81
- segments: SegmentNode[];
50
+ segments: ManifestSegmentNode[];
82
51
  /** Extracted segment params (catch-all segments produce string[]). */
83
52
  segmentParams: Record<string, string | string[]>;
84
53
  /** Middleware chain from the segment tree, ordered root-to-leaf. */
@@ -117,11 +86,32 @@ export type EarlyHintsEmitter = (
117
86
 
118
87
  // ─── Pipeline Configuration ────────────────────────────────────────────────
119
88
 
89
+ /**
90
+ * Proxy source — a tagged union so the choice between "already-resolved
91
+ * export" and "lazy HMR-friendly loader" is encoded in the type, not
92
+ * inferred per-request.
93
+ *
94
+ * - `static` — the proxy export is already resolved (production, tests).
95
+ * - `lazy` — a loader is called per-request for HMR freshness (dev).
96
+ *
97
+ * `PipelineConfig.proxy` also accepts a bare `ProxyExport` (a function or
98
+ * function array) as shorthand for the static variant — convenient for tests
99
+ * that construct a `createPipeline` config inline. Omit the field entirely
100
+ * when the app has no `proxy.ts`.
101
+ *
102
+ * See design/07-routing.md §"proxy.ts — Global Middleware".
103
+ */
104
+ export type ProxyConfig =
105
+ | { kind: 'static'; export: ProxyExport }
106
+ | { kind: 'lazy'; loader: () => Promise<{ default: ProxyExport }> };
107
+
120
108
  export interface PipelineConfig {
121
- /** The proxy.ts default export (function or array). Undefined if no proxy.ts. */
122
- proxy?: ProxyExport;
123
- /** Lazy loader for proxy.ts called per-request so HMR updates take effect. */
124
- proxyLoader?: () => Promise<{ default: ProxyExport }>;
109
+ /**
110
+ * proxy.ts source. Undefined if the app has no proxy.ts. Accepts either a
111
+ * tagged `ProxyConfig` (canonical) or a bare `ProxyExport` as sugar for the
112
+ * static variant.
113
+ */
114
+ proxy?: ProxyConfig | ProxyExport;
125
115
  /** Route matcher — resolves a canonical pathname to a RouteMatch. */
126
116
  matchRoute: RouteMatcher;
127
117
  /** Metadata route matcher — resolves metadata route pathnames (sitemap.xml, robots.txt, etc.) */
@@ -209,70 +199,26 @@ export interface PipelineConfig {
209
199
  ) => Response | Promise<Response>;
210
200
  }
211
201
 
212
- // ─── Param Coercion ────────────────────────────────────────────────────────
213
-
214
- /**
215
- * Run segment param coercion on the matched route's segments.
216
- *
217
- * Loads params.ts modules from segments that have them, extracts the
218
- * segmentParams definition, and coerces raw string params through codecs.
219
- * Throws ParamCoercionError if any codec fails (→ 404).
220
- *
221
- * This runs BEFORE middleware, so ctx.segmentParams is already typed.
222
- * See design/07-routing.md §"Where Coercion Runs"
223
- */
224
- export async function coerceSegmentParams(match: RouteMatch): Promise<void> {
225
- const segments = match.segments as unknown as import('./route-matcher.js').ManifestSegmentNode[];
226
-
227
- for (const segment of segments) {
228
- // Only process segments that have a params.ts convention file
229
- if (!segment.params) continue;
230
-
231
- let mod: Record<string, unknown>;
232
- try {
233
- mod = await loadModule(segment.params);
234
- } catch (err) {
235
- throw new ParamCoercionError(
236
- `Failed to load params module for segment "${segment.segmentName}": ${err instanceof Error ? err.message : String(err)}`
237
- );
238
- }
239
-
240
- const segmentParamsDef = mod.segmentParams as
241
- | { parse(raw: Record<string, string | string[]>): Record<string, unknown> }
242
- | undefined;
243
-
244
- if (!segmentParamsDef || typeof segmentParamsDef.parse !== 'function') continue;
245
-
246
- try {
247
- const coerced = segmentParamsDef.parse(match.segmentParams);
248
- // Merge coerced values back — use safeMerge to prevent prototype pollution
249
- // from malicious/buggy codec output. See TIM-655.
250
- safeMerge(match.segmentParams, coerced as Record<string, unknown>);
251
- } catch (err) {
252
- throw new ParamCoercionError(err instanceof Error ? err.message : String(err));
253
- }
254
- }
255
- }
256
-
257
202
  // ─── Pipeline ──────────────────────────────────────────────────────────────
258
203
 
259
204
  /**
260
205
  * Create the request handler from a pipeline configuration.
261
206
  *
262
- * Returns a function that processes an incoming Request through all pipeline stages
263
- * and produces a Response. This is the top-level entry point for the server.
207
+ * Returns a function that processes an incoming Request through all pipeline
208
+ * stages and produces a Response. This is the top-level entry point for the
209
+ * server. The body is intentionally small — phase logic lives in
210
+ * `pipeline-phases.ts`. This function only owns the per-request setup that
211
+ * has to wrap the entire dispatch: trace ID, request context ALS, span
212
+ * scope, Server-Timing header emission, and the active-request counter.
264
213
  */
265
214
  export function createPipeline(config: PipelineConfig): (req: Request) => Promise<Response> {
266
- const {
267
- proxy,
268
- matchRoute,
269
- render,
270
- earlyHints,
271
- stripTrailingSlash = true,
272
- slowRequestMs = 3000,
273
- serverTiming = 'total',
274
- onPipelineError,
275
- } = config;
215
+ // Resolve the proxy source once. The request hot path calls this closure
216
+ // directly with no discriminant check — the branch is taken here during
217
+ // setup. For the lazy variant, `loader()` still runs per-request so HMR
218
+ // continues to re-import the user's proxy.ts.
219
+ const proxyResolver = makeProxyResolver(config.proxy);
220
+ const slowRequestMs = config.slowRequestMs ?? 3000;
221
+ const serverTiming = config.serverTiming ?? 'total';
276
222
 
277
223
  // Concurrent request counter — tracks how many requests are in-flight.
278
224
  // Logged with each request for diagnosing resource contention.
@@ -311,10 +257,11 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
311
257
  }
312
258
 
313
259
  let result: Response;
314
- if (proxy || config.proxyLoader) {
315
- result = await runProxyPhase(req, method, path);
260
+ if (proxyResolver) {
261
+ const outcome = await runProxyPhase(config, proxyResolver, req, method, path);
262
+ result = await outcomeToResponse(config, outcome, { req, method, path });
316
263
  } else {
317
- result = await handleRequest(req, method, path);
264
+ result = await handleRequest(config, req, method, path);
318
265
  }
319
266
 
320
267
  // Set response status on the root span before it ends —
@@ -322,13 +269,14 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
322
269
  await setSpanAttribute('http.response.status_code', result.status);
323
270
 
324
271
  // Append Server-Timing header based on configured mode.
325
- // Response.redirect() creates immutable headers, so we must
326
- // ensure mutability before writing Server-Timing.
272
+ // Header mutability is guaranteed by the producer-side clone
273
+ // in `outcomeToResponse` and the metadata-route / auto-sitemap
274
+ // user-handler clones in `handleRequest`, so we can write
275
+ // directly without a runtime probe. See TIM-866.
327
276
  if (serverTiming === 'detailed') {
328
277
  // Detailed: per-phase breakdown (proxy, middleware, render).
329
278
  const timingHeader = getServerTimingHeader();
330
279
  if (timingHeader) {
331
- result = ensureMutableResponse(result);
332
280
  result.headers.set('Server-Timing', timingHeader);
333
281
  }
334
282
  } else if (serverTiming === 'total') {
@@ -336,7 +284,6 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
336
284
  // Prevents information disclosure while giving browser
337
285
  // DevTools useful timing data.
338
286
  const totalMs = Math.round(performance.now() - startTime);
339
- result = ensureMutableResponse(result);
340
287
  result.headers.set('Server-Timing', `total;dur=${totalMs}`);
341
288
  }
342
289
  // serverTiming === false: no header at all
@@ -363,421 +310,4 @@ export function createPipeline(config: PipelineConfig): (req: Request) => Promis
363
310
  });
364
311
  });
365
312
  };
366
-
367
- async function runProxyPhase(req: Request, method: string, path: string): Promise<Response> {
368
- try {
369
- // Resolve the proxy export. When a proxyLoader is provided (lazy import),
370
- // it is called per-request so HMR updates in dev take effect immediately.
371
- let proxyExport: ProxyExport;
372
- if (config.proxyLoader) {
373
- const mod = await config.proxyLoader();
374
- proxyExport = mod.default;
375
- } else {
376
- proxyExport = config.proxy!;
377
- }
378
- const proxyFn = () => runProxy(proxyExport, req, () => handleRequest(req, method, path));
379
- return await withSpan('timber.proxy', {}, () =>
380
- serverTiming === 'detailed' ? withTiming('proxy', 'proxy.ts', proxyFn) : proxyFn()
381
- );
382
- } catch (error) {
383
- // Uncaught proxy.ts error → bare HTTP 500
384
- logProxyError({ error });
385
- await fireOnRequestError(error, req, 'proxy');
386
- if (onPipelineError && error instanceof Error) onPipelineError(error, 'proxy');
387
- return new Response(null, { status: 500 });
388
- }
389
- }
390
-
391
- /**
392
- * Build a redirect Response from a RedirectSignal.
393
- *
394
- * For RSC payload requests (client navigation), returns 204 + X-Timber-Redirect
395
- * so the client router can perform a soft SPA redirect. A raw 302 would be
396
- * turned into an opaque redirect by fetch({redirect:'manual'}), crashing
397
- * createFromFetch. See design/19-client-navigation.md.
398
- */
399
- function buildRedirectResponse(signal: RedirectSignal, req: Request, headers: Headers): Response {
400
- const isRsc = (req.headers.get('Accept') ?? '').includes('text/x-component');
401
- if (isRsc) {
402
- headers.set('X-Timber-Redirect', signal.location);
403
- return new Response(null, { status: 204, headers });
404
- }
405
- headers.set('Location', signal.location);
406
- return new Response(null, { status: signal.status, headers });
407
- }
408
-
409
- async function handleRequest(req: Request, method: string, path: string): Promise<Response> {
410
- // Stage 1: URL canonicalization
411
- const url = new URL(req.url);
412
- const result = canonicalize(url.pathname, stripTrailingSlash);
413
- if (!result.ok) {
414
- return new Response(null, { status: result.status });
415
- }
416
- const canonicalPathname = result.pathname;
417
-
418
- // Stage 1b: Metadata route matching — runs before regular route matching.
419
- // Metadata routes skip middleware.ts and access.ts (public endpoints for crawlers).
420
- // See design/16-metadata.md §"Pipeline Integration"
421
- if (config.matchMetadataRoute) {
422
- const metaMatch = config.matchMetadataRoute(canonicalPathname);
423
- if (metaMatch) {
424
- try {
425
- // Static metadata files (.xml, .txt, .png, .ico, etc.) are served
426
- // directly from disk. Dynamic metadata routes (.ts, .tsx) export a
427
- // handler function that generates the response.
428
- if (metaMatch.isStatic) {
429
- return await serveStaticMetadataFile(metaMatch);
430
- }
431
-
432
- const mod = await loadModule<{ default?: Function }>(metaMatch.file);
433
- if (typeof mod.default !== 'function') {
434
- return new Response('Metadata route must export a default function', { status: 500 });
435
- }
436
- const handlerResult = await mod.default();
437
- // If the handler returns a Response, use it directly
438
- if (handlerResult instanceof Response) {
439
- return handlerResult;
440
- }
441
- // Otherwise, serialize based on content type
442
- const contentType = metaMatch.contentType;
443
- let body: string;
444
- if (typeof handlerResult === 'string') {
445
- body = handlerResult;
446
- } else if (contentType === 'application/xml') {
447
- body = serializeSitemap(handlerResult);
448
- } else if (contentType === 'application/manifest+json') {
449
- body = JSON.stringify(handlerResult, null, 2);
450
- } else {
451
- body = typeof handlerResult === 'string' ? handlerResult : String(handlerResult);
452
- }
453
- return new Response(body, {
454
- status: 200,
455
- headers: { 'Content-Type': `${contentType}; charset=utf-8` },
456
- });
457
- } catch (error) {
458
- logRenderError({ method, path, error });
459
- if (onPipelineError && error instanceof Error) onPipelineError(error, 'metadata-route');
460
- return new Response(null, { status: 500 });
461
- }
462
- }
463
- }
464
-
465
- // Stage 1b.2: Auto-generated sitemap — serves /sitemap.xml and /sitemap/N.xml
466
- // when sitemap generation is enabled and no user-authored sitemap exists.
467
- // Runs after metadata route matching so user sitemaps always take precedence.
468
- // See design/16-metadata.md §"Auto-generated Sitemap"
469
- if (config.autoSitemapHandler) {
470
- try {
471
- const sitemapResponse = await config.autoSitemapHandler(canonicalPathname);
472
- if (sitemapResponse) return sitemapResponse;
473
- } catch (error) {
474
- logRenderError({ method, path, error });
475
- if (onPipelineError && error instanceof Error) onPipelineError(error, 'auto-sitemap');
476
- return new Response(null, { status: 500 });
477
- }
478
- }
479
-
480
- // Stage 1c: Version skew detection (TIM-446).
481
- // For RSC payload requests (client navigation), check if the client's
482
- // deployment ID matches the current build. On mismatch, signal the
483
- // client to do a full page reload instead of returning an RSC payload
484
- // that references mismatched module IDs.
485
- const isRscRequest = (req.headers.get('Accept') ?? '').includes('text/x-component');
486
- if (isRscRequest) {
487
- const skewCheck = checkVersionSkew(req);
488
- if (!skewCheck.ok) {
489
- const reloadHeaders = new Headers();
490
- applyReloadHeaders(reloadHeaders);
491
- return new Response(null, { status: 204, headers: reloadHeaders });
492
- }
493
- }
494
-
495
- // Stage 2: Route matching
496
- let match = matchRoute(canonicalPathname);
497
- let interception: InterceptionContext | undefined;
498
-
499
- // Stage 2a: Intercepting route resolution (modal pattern).
500
- // On soft navigation, check if an intercepting route should render instead.
501
- // The client sends X-Timber-URL with the current pathname (where they're
502
- // navigating FROM). If a rewrite matches, re-route to the source URL so
503
- // the source layout renders with the intercepted content in the slot.
504
- const sourceUrl = req.headers.get('X-Timber-URL');
505
- if (sourceUrl && config.interceptionRewrites?.length) {
506
- const intercepted = findInterceptionMatch(
507
- canonicalPathname,
508
- sourceUrl,
509
- config.interceptionRewrites
510
- );
511
- if (intercepted) {
512
- const sourceMatch = matchRoute(intercepted.sourcePathname);
513
- if (sourceMatch) {
514
- match = sourceMatch;
515
- interception = { targetPathname: canonicalPathname };
516
- }
517
- }
518
- }
519
-
520
- if (!match) {
521
- // No route matched — render 404.tsx in root layout if available,
522
- // otherwise fall back to a bare 404 Response.
523
- if (config.renderNoMatch) {
524
- const responseHeaders = new Headers();
525
- return config.renderNoMatch(req, responseHeaders);
526
- }
527
- return new Response(null, { status: 404 });
528
- }
529
-
530
- // Response and request header containers — created before early hints so
531
- // the emitter can append Link headers (e.g. for Cloudflare CDN → 103).
532
- const responseHeaders = new Headers();
533
- const requestHeaderOverlay = new Headers();
534
-
535
- // Set Cache-Control for dynamic HTML responses. Without this header,
536
- // CDNs (particularly Cloudflare) may attempt to buffer/process the
537
- // response differently, causing intermittent multi-second delays.
538
- // This matches Next.js's default behavior.
539
- responseHeaders.set('Cache-Control', 'private, no-cache, no-store, max-age=0, must-revalidate');
540
-
541
- // Stage 2b: 103 Early Hints (before middleware, after match)
542
- // Fires before middleware so the browser can begin fetching critical
543
- // assets while middleware runs. Non-fatal — a failing emitter never
544
- // blocks the request.
545
- if (earlyHints) {
546
- try {
547
- await earlyHints(match, req, responseHeaders);
548
- } catch {
549
- // Early hints failure is non-fatal
550
- }
551
- }
552
-
553
- // Stage 2c: Param coercion (before middleware)
554
- // Load params.ts modules from matched segments and coerce raw string
555
- // params through defineSegmentParams codecs. Coercion failure → 404
556
- // (middleware never runs). See design/07-routing.md §"Where Coercion Runs"
557
- try {
558
- await coerceSegmentParams(match);
559
- } catch (error) {
560
- if (error instanceof ParamCoercionError) {
561
- // For API routes (route.ts), return a bare 404 — not an HTML page.
562
- // API consumers expect JSON/empty responses, not rendered HTML.
563
- const leafSegment = match.segments[match.segments.length - 1];
564
- if (
565
- (leafSegment as { route?: unknown }).route &&
566
- !(leafSegment as { page?: unknown }).page
567
- ) {
568
- return new Response(null, { status: 404 });
569
- }
570
- // Route through the app's 404 page (404.tsx in root layout) instead of
571
- // returning a bare empty 404 Response. Falls back to bare 404 only if
572
- // no renderNoMatch renderer is configured.
573
- if (config.renderNoMatch) {
574
- return config.renderNoMatch(req, responseHeaders);
575
- }
576
- return new Response(null, { status: 404 });
577
- }
578
- throw error;
579
- }
580
-
581
- // Store coerced segment params in ALS so components can access them
582
- // via getSegmentParams() instead of receiving them as a prop.
583
- // See design/07-routing.md §"params.ts — Convention File for Typed Params"
584
- setSegmentParams(match.segmentParams);
585
-
586
- // Stage 3: Middleware chain (root-to-leaf, short-circuits on first Response)
587
- if (match.middlewareChain.length > 0) {
588
- const ctx: MiddlewareContext = {
589
- req,
590
- requestHeaders: requestHeaderOverlay,
591
- headers: responseHeaders,
592
- segmentParams: match.segmentParams,
593
- earlyHints: (hints) => {
594
- for (const hint of hints) {
595
- // Match Cloudflare's cached Early Hints attribute order: `as` before `rel`.
596
- // Cloudflare caches Link headers and re-emits them on subsequent 200s.
597
- // If our order differs, the browser sees duplicate preloads and warns.
598
- let value: string;
599
- if (hint.as !== undefined) {
600
- value = `<${hint.href}>; as=${hint.as}; rel=${hint.rel}`;
601
- } else {
602
- value = `<${hint.href}>; rel=${hint.rel}`;
603
- }
604
- if (hint.crossOrigin !== undefined) value += `; crossorigin=${hint.crossOrigin}`;
605
- if (hint.fetchPriority !== undefined) value += `; fetchpriority=${hint.fetchPriority}`;
606
- responseHeaders.append('Link', value);
607
- }
608
- },
609
- };
610
-
611
- try {
612
- // Enable cookie mutation during middleware (design/29-cookies.md §"Context Tracking")
613
- setMutableCookieContext(true);
614
- const chainFn = () => runMiddlewareChain(match.middlewareChain, ctx);
615
- const middlewareResponse = await withSpan('timber.middleware', {}, () =>
616
- serverTiming === 'detailed' ? withTiming('mw', 'middleware.ts', chainFn) : chainFn()
617
- );
618
- setMutableCookieContext(false);
619
- if (middlewareResponse) {
620
- // Apply cookie jar to short-circuit response.
621
- // Response.redirect() creates immutable headers, so ensure
622
- // mutability before appending Set-Cookie entries.
623
- const finalResponse = ensureMutableResponse(middlewareResponse);
624
- applyCookieJar(finalResponse.headers);
625
- // Merge parent-set responseHeaders onto the short-circuit response.
626
- // Child-set headers take precedence — only add headers not already present.
627
- // Snapshot existing keys first so multi-value headers (Set-Cookie, Link)
628
- // from the parent are all appended when the child didn't set that key.
629
- const existingKeys = new Set(
630
- [...finalResponse.headers.keys()].map((k) => k.toLowerCase())
631
- );
632
- for (const [key, value] of responseHeaders.entries()) {
633
- if (!existingKeys.has(key.toLowerCase())) {
634
- finalResponse.headers.append(key, value);
635
- }
636
- }
637
- logMiddlewareShortCircuit({ method, path, status: finalResponse.status });
638
- return finalResponse;
639
- }
640
- // Middleware chain completed without short-circuiting — apply any
641
- // injected request headers so getHeaders() returns them downstream.
642
- applyRequestHeaderOverlay(requestHeaderOverlay);
643
- } catch (error) {
644
- setMutableCookieContext(false);
645
- // RedirectSignal from middleware → HTTP redirect (not an error)
646
- if (error instanceof RedirectSignal) {
647
- applyCookieJar(responseHeaders);
648
- return buildRedirectResponse(error, req, responseHeaders);
649
- }
650
- // DenySignal from middleware → render deny page with correct status code.
651
- // Previously returned bare Response(null) — now renders 403.tsx etc.
652
- if (error instanceof DenySignal) {
653
- applyCookieJar(responseHeaders);
654
- if (config.renderDenyFallback) {
655
- try {
656
- return await config.renderDenyFallback(error, req, responseHeaders, match);
657
- } catch {
658
- // Deny page rendering failed — fall through to bare response
659
- }
660
- }
661
- return new Response(null, { status: error.status, headers: responseHeaders });
662
- }
663
- // Middleware throw → HTTP 500 (middleware runs before rendering,
664
- // no error boundary to catch it)
665
- logMiddlewareError({ method, path, error });
666
- await fireOnRequestError(error, req, 'handler');
667
- if (onPipelineError && error instanceof Error) onPipelineError(error, 'middleware');
668
- return new Response(null, { status: 500 });
669
- }
670
- }
671
-
672
- // Apply cookie jar to response headers before render commits them.
673
- // Middleware may have set cookies; they need to be on responseHeaders
674
- // before flushResponse creates the Response object.
675
- applyCookieJar(responseHeaders);
676
-
677
- // Stage 4: Render (access gates + element tree + renderToReadableStream)
678
- try {
679
- const renderFn = () =>
680
- render(req, match, responseHeaders, requestHeaderOverlay, interception);
681
- const response = await withSpan('timber.render', { 'http.route': canonicalPathname }, () =>
682
- serverTiming === 'detailed'
683
- ? withTiming('render', 'RSC + SSR render', renderFn)
684
- : renderFn()
685
- );
686
- markResponseFlushed();
687
- return response;
688
- } catch (error) {
689
- // DenySignal leaked from render (e.g. notFound() in metadata()).
690
- // Render the deny page with the correct status code.
691
- if (error instanceof DenySignal) {
692
- if (config.renderDenyFallback) {
693
- try {
694
- return await config.renderDenyFallback(error, req, responseHeaders, match);
695
- } catch {
696
- // Deny page rendering failed — fall through to bare response
697
- }
698
- }
699
- return new Response(null, { status: error.status, headers: responseHeaders });
700
- }
701
- // RedirectSignal leaked from render — honour the redirect
702
- if (error instanceof RedirectSignal) {
703
- return buildRedirectResponse(error, req, responseHeaders);
704
- }
705
- logRenderError({ method, path, error });
706
- await fireOnRequestError(error, req, 'render');
707
- if (onPipelineError && error instanceof Error) onPipelineError(error, 'render');
708
- // Try fallback error page before bare 500
709
- if (config.renderFallbackError) {
710
- try {
711
- return await config.renderFallbackError(error, req, responseHeaders);
712
- } catch {
713
- // Fallback rendering itself failed — fall through to bare 500
714
- }
715
- }
716
- return new Response(null, { status: 500 });
717
- }
718
- }
719
- }
720
-
721
- /**
722
- * Fire the user's onRequestError hook with request context.
723
- * Extracts request info from the Request object and calls the instrumentation hook.
724
- */
725
- async function fireOnRequestError(
726
- error: unknown,
727
- req: Request,
728
- phase: 'proxy' | 'handler' | 'render' | 'action' | 'route'
729
- ): Promise<void> {
730
- const url = new URL(req.url);
731
- const headersObj: Record<string, string> = {};
732
- req.headers.forEach((v, k) => {
733
- headersObj[k] = v;
734
- });
735
-
736
- await callOnRequestError(
737
- error,
738
- { method: req.method, path: url.pathname, headers: headersObj },
739
- { phase, routePath: url.pathname, routeType: 'page', traceId: getTraceId() }
740
- );
741
- }
742
-
743
- // ─── Cookie Helpers ──────────────────────────────────────────────────────
744
-
745
- /**
746
- * Apply all Set-Cookie headers from the cookie jar to a Headers object.
747
- * Each cookie gets its own Set-Cookie header per RFC 6265 §4.1.
748
- */
749
- function applyCookieJar(headers: Headers): void {
750
- for (const value of getSetCookieHeaders()) {
751
- headers.append('Set-Cookie', value);
752
- }
753
- }
754
-
755
- // ─── Immutable Response Helpers ──────────────────────────────────────────
756
-
757
- /**
758
- * Ensure a Response has mutable headers so the pipeline can safely append
759
- * Set-Cookie and Server-Timing entries.
760
- *
761
- * `Response.redirect()` and some platform-level responses return objects
762
- * with immutable headers. Calling `.set()` or `.append()` on them throws
763
- * `TypeError: immutable`. This helper detects the immutable case by
764
- * attempting a no-op write and, on failure, clones into a fresh Response
765
- * with mutable headers.
766
- */
767
- function ensureMutableResponse(response: Response): Response {
768
- try {
769
- // Probe mutability with a benign operation that we immediately undo.
770
- // We pick a header name that is extremely unlikely to collide with
771
- // anything meaningful and delete it right away.
772
- response.headers.set('X-Timber-Probe', '1');
773
- response.headers.delete('X-Timber-Probe');
774
- return response;
775
- } catch {
776
- // Headers are immutable — rebuild with mutable headers.
777
- return new Response(response.body, {
778
- status: response.status,
779
- statusText: response.statusText,
780
- headers: new Headers(response.headers),
781
- });
782
- }
783
313
  }