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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/dist/_chunks/{actions-C-Rw9vPc.js → actions-35jnMdeJ.js} +4 -3
  2. package/dist/_chunks/{actions-C-Rw9vPc.js.map → actions-35jnMdeJ.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
  5. package/dist/_chunks/{als-slots-mFweg276.js → als-slots-BEEIPKYm.js} +3 -4
  6. package/dist/_chunks/{als-slots-mFweg276.js.map → als-slots-BEEIPKYm.js.map} +1 -1
  7. package/dist/_chunks/{cache-api-Cd0VZ_Pd.js → cache-api-DjNrIWRR.js} +7 -13
  8. package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
  9. package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
  10. package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
  11. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
  12. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
  13. package/dist/_chunks/{cloudflare-Cs0uZXea.js → cloudflare-CGP6BZKO.js} +4 -3
  14. package/dist/_chunks/{cloudflare-Cs0uZXea.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
  15. package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
  16. package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
  17. package/dist/_chunks/{error-boundary-DpYRI_I1.js → error-boundary-D-ODYX41.js} +25 -2
  18. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
  19. package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
  20. package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
  21. package/dist/_chunks/{logger-N7e5auP0.js → logger-D8xJZXIN.js} +27 -40
  22. package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
  23. package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
  24. package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
  25. package/dist/_chunks/{plugin-context-DEGLSJs3.js → plugin-context-rCinWLiE.js} +7 -2
  26. package/dist/_chunks/{plugin-context-DEGLSJs3.js.map → plugin-context-rCinWLiE.js.map} +1 -1
  27. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  28. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  29. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
  30. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
  31. package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
  32. package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
  33. package/dist/_chunks/{cli-schema-sync-CKgHC2MB.js → scanner-C8b0Gcw3.js} +5 -298
  34. package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
  35. package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
  36. package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
  37. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
  38. package/dist/_chunks/{param-value-C8TNYchQ.js → segment-context-CjOlyB8Y.js} +33 -2
  39. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
  40. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  41. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  42. package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
  43. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  44. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  45. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  46. package/dist/adapters/cloudflare-dev.js +1 -1
  47. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  48. package/dist/adapters/cloudflare.d.ts.map +1 -1
  49. package/dist/adapters/cloudflare.js +1 -1
  50. package/dist/adapters/compress-module.d.ts +12 -0
  51. package/dist/adapters/compress-module.d.ts.map +1 -1
  52. package/dist/adapters/nitro.js +54 -2
  53. package/dist/adapters/nitro.js.map +1 -1
  54. package/dist/cache/index.js +1 -1
  55. package/dist/cdn/cloudflare-purge.js +30 -0
  56. package/dist/cdn/cloudflare-purge.js.map +1 -0
  57. package/dist/cdn/fastly-purge.js +33 -0
  58. package/dist/cdn/fastly-purge.js.map +1 -0
  59. package/dist/cdn/index.js +94 -0
  60. package/dist/cdn/index.js.map +1 -0
  61. package/dist/cdn/workers-cache-purge.js +35 -0
  62. package/dist/cdn/workers-cache-purge.js.map +1 -0
  63. package/dist/cli-check.d.ts +153 -0
  64. package/dist/cli-check.d.ts.map +1 -0
  65. package/dist/cli.d.ts +34 -8
  66. package/dist/cli.d.ts.map +1 -1
  67. package/dist/cli.js +46 -21
  68. package/dist/cli.js.map +1 -1
  69. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  70. package/dist/client/browser-entry/index.d.ts +1 -1
  71. package/dist/client/browser-entry/index.d.ts.map +1 -1
  72. package/dist/client/error-boundary.d.ts +6 -0
  73. package/dist/client/error-boundary.d.ts.map +1 -1
  74. package/dist/client/error-boundary.js +1 -1
  75. package/dist/client/index.d.ts +1 -0
  76. package/dist/client/index.d.ts.map +1 -1
  77. package/dist/client/index.js +27 -33
  78. package/dist/client/index.js.map +1 -1
  79. package/dist/client/internal.js +21 -14
  80. package/dist/client/internal.js.map +1 -1
  81. package/dist/client/link.d.ts +1 -7
  82. package/dist/client/link.d.ts.map +1 -1
  83. package/dist/client/navigation-commit.d.ts +16 -0
  84. package/dist/client/navigation-commit.d.ts.map +1 -1
  85. package/dist/client/router-pipeline.d.ts.map +1 -1
  86. package/dist/client/router.d.ts.map +1 -1
  87. package/dist/client/rsc-fetch.d.ts +9 -1
  88. package/dist/client/rsc-fetch.d.ts.map +1 -1
  89. package/dist/client/segment-cache.d.ts +8 -0
  90. package/dist/client/segment-cache.d.ts.map +1 -1
  91. package/dist/client/use-query-states.d.ts +9 -3
  92. package/dist/client/use-query-states.d.ts.map +1 -1
  93. package/dist/codec.js +1 -1
  94. package/dist/cookies/define-cookie.d.ts.map +1 -1
  95. package/dist/cookies/index.js +2 -2
  96. package/dist/cookies/index.js.map +1 -1
  97. package/dist/index.d.ts +4 -1
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +142 -477
  100. package/dist/index.js.map +1 -1
  101. package/dist/params/index.js +1 -1
  102. package/dist/plugin-context.d.ts +15 -0
  103. package/dist/plugin-context.d.ts.map +1 -1
  104. package/dist/plugins/routing.d.ts +0 -9
  105. package/dist/plugins/routing.d.ts.map +1 -1
  106. package/dist/plugins/shims.d.ts.map +1 -1
  107. package/dist/plugins/static-build.d.ts +24 -0
  108. package/dist/plugins/static-build.d.ts.map +1 -1
  109. package/dist/routing/codegen-shared.d.ts +3 -44
  110. package/dist/routing/codegen-shared.d.ts.map +1 -1
  111. package/dist/routing/codegen-types.d.ts +10 -31
  112. package/dist/routing/codegen-types.d.ts.map +1 -1
  113. package/dist/routing/codegen-write.d.ts +51 -0
  114. package/dist/routing/codegen-write.d.ts.map +1 -0
  115. package/dist/routing/codegen.d.ts.map +1 -1
  116. package/dist/routing/convention-lint.d.ts +18 -4
  117. package/dist/routing/convention-lint.d.ts.map +1 -1
  118. package/dist/routing/export-detect.d.ts +16 -0
  119. package/dist/routing/export-detect.d.ts.map +1 -1
  120. package/dist/routing/index.js +3 -2
  121. package/dist/routing/link-codegen.d.ts +19 -4
  122. package/dist/routing/link-codegen.d.ts.map +1 -1
  123. package/dist/routing/manifest-codegen.d.ts +1 -7
  124. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  125. package/dist/routing/types.d.ts +0 -6
  126. package/dist/routing/types.d.ts.map +1 -1
  127. package/dist/schema-bridge.d.ts +60 -9
  128. package/dist/schema-bridge.d.ts.map +1 -1
  129. package/dist/search-params/define.d.ts +62 -8
  130. package/dist/search-params/define.d.ts.map +1 -1
  131. package/dist/search-params/index.d.ts +0 -1
  132. package/dist/search-params/index.d.ts.map +1 -1
  133. package/dist/search-params/index.js +66 -29
  134. package/dist/search-params/index.js.map +1 -1
  135. package/dist/search-params/parse-total.d.ts +70 -0
  136. package/dist/search-params/parse-total.d.ts.map +1 -0
  137. package/dist/search-params/wrappers.d.ts +26 -3
  138. package/dist/search-params/wrappers.d.ts.map +1 -1
  139. package/dist/server/access-gate.d.ts +19 -8
  140. package/dist/server/access-gate.d.ts.map +1 -1
  141. package/dist/server/action-handler.d.ts.map +1 -1
  142. package/dist/server/als-registry.d.ts +16 -0
  143. package/dist/server/als-registry.d.ts.map +1 -1
  144. package/dist/server/compress.d.ts.map +1 -1
  145. package/dist/server/deny-boundary.d.ts +148 -15
  146. package/dist/server/deny-boundary.d.ts.map +1 -1
  147. package/dist/server/deny-renderer.d.ts +2 -2
  148. package/dist/server/deny-renderer.d.ts.map +1 -1
  149. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  150. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  151. package/dist/server/index.d.ts +0 -1
  152. package/dist/server/index.d.ts.map +1 -1
  153. package/dist/server/index.js +3 -2
  154. package/dist/server/index.js.map +1 -1
  155. package/dist/server/internal.d.ts +3 -1
  156. package/dist/server/internal.d.ts.map +1 -1
  157. package/dist/server/internal.js +343 -230
  158. package/dist/server/internal.js.map +1 -1
  159. package/dist/server/metadata-collector.d.ts +52 -0
  160. package/dist/server/metadata-collector.d.ts.map +1 -0
  161. package/dist/server/param-coercion.d.ts +12 -5
  162. package/dist/server/param-coercion.d.ts.map +1 -1
  163. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  164. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  165. package/dist/server/pipeline-phases.d.ts.map +1 -1
  166. package/dist/server/primitives.d.ts +23 -0
  167. package/dist/server/primitives.d.ts.map +1 -1
  168. package/dist/server/route-element-builder.d.ts +1 -12
  169. package/dist/server/route-element-builder.d.ts.map +1 -1
  170. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  171. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  172. package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
  173. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  174. package/dist/server/rsc-entry/helpers.d.ts +0 -7
  175. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/index.d.ts +0 -1
  177. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  179. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
  181. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  182. package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
  183. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  184. package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
  185. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  186. package/dist/server/slot-resolver.d.ts.map +1 -1
  187. package/dist/server/ssr-bridge-types.d.ts +11 -0
  188. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  189. package/dist/server/ssr-entry.d.ts +0 -1
  190. package/dist/server/ssr-entry.d.ts.map +1 -1
  191. package/dist/server/static-generator.d.ts.map +1 -1
  192. package/dist/server/status-code-resolver.d.ts +8 -1
  193. package/dist/server/status-code-resolver.d.ts.map +1 -1
  194. package/dist/server/tree-builder.d.ts +28 -36
  195. package/dist/server/tree-builder.d.ts.map +1 -1
  196. package/dist/server/types.d.ts +12 -7
  197. package/dist/server/types.d.ts.map +1 -1
  198. package/dist/server/utils/element-type.d.ts +40 -0
  199. package/dist/server/utils/element-type.d.ts.map +1 -0
  200. package/dist/shared/rsc-media-type.d.ts +40 -0
  201. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  202. package/docs/api/30-api-server.mdx +1 -1
  203. package/docs/api/33-api-search-params.mdx +38 -16
  204. package/docs/api/35-api-typescript.mdx +3 -3
  205. package/docs/api/36-cli.mdx +34 -7
  206. package/docs/learn/00-introduction.mdx +1 -1
  207. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  208. package/docs/learn/05-typed-params.mdx +8 -8
  209. package/docs/learn/07-typed-routes.mdx +12 -7
  210. package/docs/learn/11-error-handling.mdx +16 -0
  211. package/docs/more/01-advanced-routing.mdx +1 -1
  212. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  213. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  214. package/package.json +8 -5
  215. package/src/adapters/cloudflare.ts +4 -1
  216. package/src/adapters/compress-module.ts +79 -1
  217. package/src/cli-check.ts +458 -0
  218. package/src/cli.ts +59 -24
  219. package/src/client/browser-entry/action-dispatch.ts +2 -1
  220. package/src/client/browser-entry/index.ts +0 -5
  221. package/src/client/error-boundary.tsx +65 -1
  222. package/src/client/index.ts +14 -3
  223. package/src/client/link.tsx +65 -64
  224. package/src/client/navigation-commit.ts +27 -4
  225. package/src/client/params-context.ts +4 -4
  226. package/src/client/router-pipeline.ts +4 -0
  227. package/src/client/router.ts +1 -0
  228. package/src/client/rsc-fetch.ts +14 -4
  229. package/src/client/segment-cache.ts +8 -0
  230. package/src/client/use-query-states.ts +102 -39
  231. package/src/cookies/define-cookie.ts +6 -1
  232. package/src/index.ts +20 -3
  233. package/src/plugin-context.ts +26 -0
  234. package/src/plugins/routing.ts +84 -146
  235. package/src/plugins/shims.ts +0 -1
  236. package/src/plugins/static-build.ts +78 -24
  237. package/src/routing/codegen-shared.ts +3 -79
  238. package/src/routing/codegen-types.ts +10 -31
  239. package/src/routing/codegen-write.ts +139 -0
  240. package/src/routing/codegen.ts +56 -182
  241. package/src/routing/convention-lint.ts +139 -40
  242. package/src/routing/export-detect.ts +151 -7
  243. package/src/routing/link-codegen.ts +32 -65
  244. package/src/routing/manifest-codegen.ts +1 -59
  245. package/src/routing/scanner.ts +3 -3
  246. package/src/routing/types.ts +0 -6
  247. package/src/schema-bridge.ts +180 -58
  248. package/src/search-params/define.ts +102 -37
  249. package/src/search-params/index.ts +0 -1
  250. package/src/search-params/parse-total.ts +78 -0
  251. package/src/search-params/wrappers.ts +60 -11
  252. package/src/server/access-gate.tsx +60 -40
  253. package/src/server/action-handler.ts +1 -4
  254. package/src/server/als-registry.ts +16 -0
  255. package/src/server/compress.ts +9 -1
  256. package/src/server/deny-boundary.ts +269 -41
  257. package/src/server/deny-renderer.ts +32 -21
  258. package/src/server/error-boundary-wrapper.ts +166 -79
  259. package/src/server/index.ts +1 -3
  260. package/src/server/internal.ts +2 -2
  261. package/src/server/metadata-collector.ts +115 -0
  262. package/src/server/param-coercion.ts +13 -61
  263. package/src/server/pipeline-helpers.ts +2 -2
  264. package/src/server/pipeline-outcome.ts +2 -1
  265. package/src/server/pipeline-phases.ts +9 -9
  266. package/src/server/primitives.ts +25 -0
  267. package/src/server/route-element-builder.ts +167 -170
  268. package/src/server/rsc-cache-key-guard.ts +2 -8
  269. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  270. package/src/server/rsc-entry/error-renderer.ts +31 -11
  271. package/src/server/rsc-entry/helpers.ts +2 -12
  272. package/src/server/rsc-entry/index.ts +0 -5
  273. package/src/server/rsc-entry/render-route.ts +14 -11
  274. package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
  275. package/src/server/rsc-entry/rsc-payload.ts +104 -20
  276. package/src/server/rsc-entry/rsc-stream.ts +25 -2
  277. package/src/server/rsc-entry/ssr-renderer.ts +28 -16
  278. package/src/server/slot-resolver.ts +10 -2
  279. package/src/server/ssr-bridge-types.ts +12 -0
  280. package/src/server/ssr-entry.ts +8 -6
  281. package/src/server/static-generator.ts +3 -2
  282. package/src/server/status-code-resolver.ts +28 -11
  283. package/src/server/tree-builder.ts +35 -218
  284. package/src/server/types.ts +12 -7
  285. package/src/server/utils/element-type.ts +72 -0
  286. package/src/shared/rsc-media-type.ts +43 -0
  287. package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
  288. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
  289. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
  290. package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
  291. package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
  292. package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
  293. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  294. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  295. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  296. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  297. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  298. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  299. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  300. package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
  301. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
  302. package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
  303. package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
  304. package/dist/search-params/registry.d.ts +0 -20
  305. package/dist/search-params/registry.d.ts.map +0 -1
  306. package/dist/segment-params/define.d.ts +0 -83
  307. package/dist/segment-params/define.d.ts.map +0 -1
  308. package/dist/segment-params/index.d.ts +0 -3
  309. package/dist/segment-params/index.d.ts.map +0 -1
  310. package/dist/segment-params/index.js +0 -70
  311. package/dist/segment-params/index.js.map +0 -1
  312. package/src/search-params/registry.ts +0 -31
  313. package/src/segment-params/define.ts +0 -226
  314. package/src/segment-params/index.ts +0 -9
@@ -20,18 +20,12 @@ import { createElement, Fragment } from 'react';
20
20
  import { withSpan } from './tracing.js';
21
21
  import type { RouteMatch } from './pipeline.js';
22
22
  import type { ManifestSegmentNode } from './route-matcher.js';
23
- import { resolveMetadata, renderMetadataToElements } from './metadata.js';
24
23
  import type { Metadata } from './types.js';
25
- import { collectMetadataRouteHeadElements } from './metadata-routes.js';
26
24
  import { DenySignal, RedirectSignal } from './primitives.js';
25
+ import { MetadataCollector, MetadataHead } from './metadata-collector.js';
27
26
  import { AccessGate } from './access-gate.js';
28
27
  import { requestContextAls } from './als-registry.js';
29
- import {
30
- PageDenyBoundary,
31
- buildDenyPageChain,
32
- renderMatchingDenyPage,
33
- setDenyStatus,
34
- } from './deny-boundary.js';
28
+ import { PageDenyBoundary, buildDenyPageChain, handleCaughtDeny } from './deny-boundary.js';
35
29
  import type { DenyPageEntry } from './deny-boundary.js';
36
30
  import { LAYOUT_CHILDREN_SLOT } from './prebuilt/slots.js';
37
31
  import { resolveSlotProps, publishSlotSegmentParams, type SlotSkipEntry } from './slot-resolver.js';
@@ -43,6 +37,7 @@ import { SegmentOutlet } from '../client/segment-outlet.js';
43
37
  import { SlotOutlet } from '../client/slot-outlet.js';
44
38
  import { SlotsProvider } from '../client/slot-provider.js';
45
39
 
40
+ import { TimberErrorBoundary } from '../client/error-boundary.js';
46
41
  import { wrapSegmentWithErrorBoundaries } from './error-boundary-wrapper.js';
47
42
  import type { InterceptionContext } from './pipeline.js';
48
43
  import { shouldSkipSegment, type ClientStateTree } from './state-tree-diff.js';
@@ -52,7 +47,7 @@ import {
52
47
  computeSegmentTreePaths,
53
48
  computeSlotKey,
54
49
  } from '../routing/segment-keys.js';
55
- import { headElementsToReact } from './metadata.js';
50
+ import { setFirstDeniedSegmentIndex } from './deny-boundary.js';
56
51
  import { loadModule } from './safe-load.js';
57
52
 
58
53
  /**
@@ -67,9 +62,9 @@ function replaceOutermostSegmentOutlet(
67
62
  if (element.type === SegmentOutlet) {
68
63
  return replacement;
69
64
  }
70
- // AccessGate wraps outside SegmentOutlet — recurse through it.
65
+ // AccessGate and MetadataCollector wrap outside SegmentOutlet — recurse through them.
71
66
  const props = element.props as Record<string, unknown>;
72
- if (element.type === AccessGate && props.children) {
67
+ if ((element.type === AccessGate || element.type === MetadataCollector) && props.children) {
73
68
  const inner = replaceOutermostSegmentOutlet(props.children as React.ReactElement, replacement);
74
69
  /* eslint-disable react/no-children-prop -- createElement API */
75
70
  return createElement(element.type as React.FunctionComponent<Record<string, unknown>>, {
@@ -134,7 +129,7 @@ export function isClientReference(component: unknown): boolean {
134
129
  // ─── Typed Errors ────────────────────────────────────────────────────────
135
130
 
136
131
  /**
137
- * Thrown when a defineSegmentParams codec's parse() fails.
132
+ * Thrown when an app/schema.ts segment-param codec's parse() fails.
138
133
  * The pipeline catches this and responds with 404.
139
134
  */
140
135
  export class ParamCoercionError extends Error {
@@ -199,17 +194,6 @@ export interface RouteElementResult {
199
194
  * enabling the client to populate its segment cache with slot entries.
200
195
  */
201
196
  slotSkipInfo: SlotSkipEntry[];
202
- /**
203
- * Index of the first segment whose `access.ts` denied, or `Infinity`.
204
- *
205
- * AccessGate sits OUTSIDE the layout, so a denial means this segment and
206
- * everything below it never rendered — no layout, no slots. It is the
207
- * boundary of the tree that actually exists, which is why
208
- * `collectMetadataRouteHeadElements` already filters on it and why
209
- * `buildSegmentInfo` must too: the layouts were still *loaded*, so a list
210
- * built from `layoutComponents` describes more than what mounted.
211
- */
212
- firstDeniedIndex: number;
213
197
  }
214
198
 
215
199
  // ─── Module Processing Helpers ─────────────────────────────────────────────
@@ -297,47 +281,19 @@ export async function buildRouteElement(
297
281
  ): Promise<RouteElementResult> {
298
282
  const segments = match.segments;
299
283
 
300
- // ── Access pre-pass ──────────────────────────────────────────────────
301
- // Run access checks eagerly to determine which segments are denied.
302
- // This gates metadata() resolution: denied segments' metadata functions
303
- // are never called, preventing unauthorized side effects (DB queries)
304
- // and metadata content leaking into the <head> of denied responses.
305
- //
306
- // Verdicts are passed to AccessGate for synchronous replay (DenySignal
307
- // renders the deny page in-tree; RedirectSignal propagates). For PASSING
308
- // segments, no verdict is passed — AccessGate re-runs access during
309
- // renderToReadableStream to populate React.cache for layout dedup.
310
- //
311
- // See TIM-1027, design/04-authorization.md, design/13-security.md.
312
- const accessVerdicts = new Map<number, DenySignal | RedirectSignal>();
313
- let firstDeniedIndex = Infinity;
314
-
315
- for (let i = 0; i < segments.length; i++) {
316
- const segment = segments[i];
317
- if (!segment.access || i >= firstDeniedIndex) continue;
318
-
319
- try {
320
- const accessMod = await loadModule(segment.access);
321
- const accessFn = accessMod.default as (() => Promise<void>) | undefined;
322
- if (accessFn) {
323
- await withSpan(
324
- 'timber.access.pre',
325
- { 'timber.segment': segment.segmentName ?? 'unknown' },
326
- () => accessFn()
327
- );
328
- }
329
- } catch (error: unknown) {
330
- if (error instanceof DenySignal || error instanceof RedirectSignal) {
331
- accessVerdicts.set(i, error);
332
- firstDeniedIndex = i;
333
- } else {
334
- throw error;
335
- }
336
- }
284
+ // ── Module loading ──────────────────────────────────────────────────
285
+ // Load layout/page modules and collect metadata sources. Metadata
286
+ // resolution is deferred to render time via MetadataCollector components
287
+ // placed below each AccessGate — denied segments' metadata() never
288
+ // executes because AccessGate prevents MetadataCollector from rendering.
289
+ // No pre-render access pass needed. See TIM-1363.
290
+ interface MetadataSource {
291
+ metadataExport: Metadata | (() => Promise<Metadata>);
292
+ segmentName: string;
293
+ isPage: boolean;
294
+ segmentIndex: number;
337
295
  }
338
-
339
- // ── Module loading + metadata resolution ─────────────────────────────
340
- const metadataEntries: Array<{ metadata: Metadata; isPage: boolean }> = [];
296
+ const metadataSources: MetadataSource[] = [];
341
297
  const layoutComponents: LayoutComponentEntry[] = [];
342
298
  let PageComponent: ((...args: unknown[]) => unknown) | null = null;
343
299
  let deferSuspenseFor = 0;
@@ -345,7 +301,6 @@ export async function buildRouteElement(
345
301
  for (let i = 0; i < segments.length; i++) {
346
302
  const segment = segments[i];
347
303
  const isLeaf = i === segments.length - 1;
348
- const isDenied = i >= firstDeniedIndex;
349
304
 
350
305
  // Load layout
351
306
  if (segment.layout) {
@@ -357,15 +312,14 @@ export async function buildRouteElement(
357
312
  });
358
313
  }
359
314
 
360
- // Param coercion is handled in the pipeline (Stage 2c) before
361
- // middleware and rendering. See coerceSegmentParams() in pipeline.ts.
362
-
363
315
  rejectLegacyGenerateMetadata(mod, segment.layout.filePath ?? segment.urlPath);
364
- if (!isDenied) {
365
- const layoutMetadata = await extractMetadata(mod, segment);
366
- if (layoutMetadata) {
367
- metadataEntries.push({ metadata: layoutMetadata, isPage: false });
368
- }
316
+ if (mod.metadata) {
317
+ metadataSources.push({
318
+ metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
319
+ segmentName: segment.segmentName ?? segment.urlPath,
320
+ isPage: false,
321
+ segmentIndex: i,
322
+ });
369
323
  }
370
324
  deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
371
325
  }
@@ -374,18 +328,17 @@ export async function buildRouteElement(
374
328
  if (isLeaf && segment.page) {
375
329
  const mod = await loadModule(segment.page);
376
330
 
377
- // Param coercion is handled in the pipeline (Stage 2c) before
378
- // middleware and rendering. See coerceSegmentParams() in pipeline.ts.
379
-
380
331
  if (mod.default) {
381
332
  PageComponent = mod.default as (...args: unknown[]) => unknown;
382
333
  }
383
334
  rejectLegacyGenerateMetadata(mod, segment.page.filePath ?? segment.urlPath);
384
- if (!isDenied) {
385
- const pageMetadata = await extractMetadata(mod, segment);
386
- if (pageMetadata) {
387
- metadataEntries.push({ metadata: pageMetadata, isPage: true });
388
- }
335
+ if (mod.metadata) {
336
+ metadataSources.push({
337
+ metadataExport: mod.metadata as Metadata | (() => Promise<Metadata>),
338
+ segmentName: segment.segmentName ?? segment.urlPath,
339
+ isPage: true,
340
+ segmentIndex: i,
341
+ });
389
342
  }
390
343
  deferSuspenseFor = extractDeferSuspenseFor(mod, deferSuspenseFor);
391
344
  }
@@ -403,45 +356,36 @@ export async function buildRouteElement(
403
356
  );
404
357
  }
405
358
 
359
+ // Compute state-tree keys for all segments. Route groups accumulate
360
+ // ancestor group names for globally unique keys (e.g., "/(a)/(shared)"
361
+ // instead of just "/(shared)"). This is the single key computation —
362
+ // the same keys are used for skip decisions, SegmentOutlet props, the
363
+ // X-Timber-Segments header (via buildSegmentInfo), the error boundaries,
364
+ // and deny page ownership.
365
+ //
366
+ // Computed here, before the deny chains below, because each chain entry
367
+ // records the key of the segment that owns it.
368
+ const segmentKeys = computeSegmentKeys(segments);
369
+ // The directory path of each segment — the key `setSlotParams` and codegen
370
+ // both use for a slot beneath it, and the identity interception scoping
371
+ // compares against. One derivation, so a slot cannot be keyed two ways.
372
+ const segmentTreePaths = computeSegmentTreePaths(segments);
373
+
406
374
  // Build deny page fallback chains for each segment position.
407
375
  // When AccessGate or PageDenyBoundary catches a DenySignal, they render
408
376
  // the matching deny page in-tree instead of throwing into React Flight.
409
377
  // The chain walks from the current segment outward to root, collecting
410
- // status-code files (403.tsx → 4xx.tsx → error.tsx) in fallback order.
411
- // See TIM-666.
378
+ // status-code files (403.tsx → 4xx.tsx → error.tsx) in fallback order,
379
+ // each tagged with the key of the segment it came from so the catch site
380
+ // can tell a file it owns from an ancestor's. See TIM-666, TIM-1356.
412
381
  const denyPageChains = new Map<number, DenyPageEntry[]>();
413
382
  for (let i = 0; i < segments.length; i++) {
414
- const chain = await buildDenyPageChain(segments, i);
383
+ const chain = await buildDenyPageChain(segments, i, segmentKeys);
415
384
  if (chain.length > 0) {
416
385
  denyPageChains.set(i, chain);
417
386
  }
418
387
  }
419
388
 
420
- // Resolve metadata and convert to React elements that React 19 Float
421
- // hoists into <head> during both SSR and client rendering. This replaces
422
- // the previous HeadElement descriptor system (string splice for SSR,
423
- // X-Timber-Head header for RSC nav, imperative DOM updates on client).
424
- // React manages <title>/<meta>/<link> lifecycle across navigations.
425
- // See design/16-metadata.md, TIM-1151.
426
- const resolvedMetadata = resolveMetadata(metadataEntries);
427
- const headElements = renderMetadataToElements(resolvedMetadata);
428
-
429
- // Auto-link metadata route files (icon, apple-icon, manifest, opengraph-image).
430
- // See design/16-metadata.md §"Auto-Linking"
431
- const autoLinkedElements = collectMetadataRouteHeadElements(
432
- segments,
433
- firstDeniedIndex,
434
- resolvedMetadata,
435
- new URL(req.url),
436
- metadataRouteHashes
437
- );
438
- headElements.push(...autoLinkedElements);
439
-
440
- // Convert HeadElement descriptors to React elements for Float hoisting.
441
- // Placed at the page segment level so they're always re-rendered on
442
- // SPA navigation (sync layouts may be skipped by segment tree diffing).
443
- const metadataElement = headElementsToReact(headElements);
444
-
445
389
  // Build element tree: page wrapped in layouts (innermost to outermost)
446
390
  const h = createElement as (...args: unknown[]) => React.ReactElement;
447
391
 
@@ -482,6 +426,7 @@ export async function buildRouteElement(
482
426
  Page: PageComponent,
483
427
  route: match.segments[leafIndex]?.urlPath ?? '/',
484
428
  denyPages: leafDenyPages,
429
+ segmentKey: segmentKeys[leafIndex],
485
430
  onSettled,
486
431
  });
487
432
  } else {
@@ -496,11 +441,18 @@ export async function buildRouteElement(
496
441
  element = h(TracedPage, {});
497
442
  }
498
443
 
499
- // Wrap page + metadata in a Fragment so Float-hoisted elements are at
500
- // the page segment level (always re-rendered, never skipped by segment
501
- // tree diffing). React 19 Float hoists the <title>/<meta>/<link>
502
- // children into <head> regardless of where they appear in the tree.
503
- element = h(Fragment, null, metadataElement, element);
444
+ // Wrap page in MetadataHead — a server component that reads all
445
+ // metadata entries collected by MetadataCollector components above it,
446
+ // merges them, and renders Float-hoisted head elements as siblings of
447
+ // the page. Placed at the page level so metadata is always re-rendered
448
+ // on SPA navigation (never skipped by segment tree diffing).
449
+ // See TIM-1363, design/16-metadata.md.
450
+ element = h(MetadataHead, {
451
+ segments,
452
+ requestUrl: req.url,
453
+ metadataRouteHashes,
454
+ children: element,
455
+ });
504
456
 
505
457
  // Build a lookup of layout components by segment for O(1) access.
506
458
  const layoutBySegment = new Map(
@@ -511,17 +463,6 @@ export async function buildRouteElement(
511
463
  // The client uses this to merge the partial payload with its cached segments.
512
464
  const skippedSegments: string[] = [];
513
465
 
514
- // Compute state-tree keys for all segments. Route groups accumulate
515
- // ancestor group names for globally unique keys (e.g., "/(a)/(shared)"
516
- // instead of just "/(shared)"). This is the single key computation —
517
- // the same keys are used for skip decisions, SegmentOutlet props, and
518
- // the X-Timber-Segments header (via buildSegmentInfo).
519
- const segmentKeys = computeSegmentKeys(segments);
520
- // The directory path of each segment — the key `setSlotParams` and codegen
521
- // both use for a slot beneath it, and the identity interception scoping
522
- // compares against. One derivation, so a slot cannot be keyed two ways.
523
- const segmentTreePaths = computeSegmentTreePaths(segments);
524
-
525
466
  // Pre-compute the contiguous skippable prefix (top-down).
526
467
  //
527
468
  // Skipped segments must form a contiguous prefix from root to avoid
@@ -539,10 +480,6 @@ export async function buildRouteElement(
539
480
  const isLeaf = i === segments.length - 1;
540
481
  const layoutComponent = layoutBySegment.get(segments[i]);
541
482
 
542
- // Never skip denied segments — their AccessGate must render the
543
- // deny page, not reuse the client's cached (passing) layout.
544
- if (i >= firstDeniedIndex) break;
545
-
546
483
  if (shouldSkipSegment(segmentKeys[i], layoutComponent, isLeaf, clientStateTree)) {
547
484
  skippableSet.add(i);
548
485
  } else if (layoutComponent) {
@@ -655,20 +592,55 @@ export async function buildRouteElement(
655
592
  // because an unchanged slot is what made this layout skippable.
656
593
  publishSlotSegmentParams(segment, match, destinationUrl, segmentTreePaths[i]);
657
594
 
595
+ // No error boundaries are wrapped for a skipped segment — the `continue`
596
+ // below returns before that walk. A deny addressed to this segment
597
+ // therefore finds no boundary in the payload, and since TIM-1356 the
598
+ // payload path no longer falls back to a re-render for an addressed
599
+ // deny. What makes that sound is the precondition for skipping at all:
600
+ // `shouldSkipSegment` only returns true when this exact segment key is
601
+ // in the client's state tree, which means the client rendered this
602
+ // segment on an earlier navigation and still has its boundaries mounted,
603
+ // carrying the same key this build would have stamped. The payload
604
+ // splices in below them, so the throw passes through them on its way up.
605
+ //
606
+ // Metadata from skipped segments still needs to be collected (e.g.,
607
+ // root layout title templates). MetadataCollector wraps the element
608
+ // inside AccessGate — if access denies, metadata is excluded.
609
+ const skippedMeta = metadataSources.filter((s) => s.segmentIndex === i).reverse();
610
+ for (const meta of skippedMeta) {
611
+ const isDynamic = typeof meta.metadataExport === 'function';
612
+ element = h(MetadataCollector, {
613
+ metadataExport: meta.metadataExport,
614
+ segmentName: meta.segmentName,
615
+ isPage: meta.isPage,
616
+ ...(isDynamic ? { onSettled: trackShellComponent() } : {}),
617
+ children: element,
618
+ });
619
+ }
620
+
658
621
  // SECURITY: Even though the layout is skipped, AccessGate MUST still
659
622
  // wrap the element tree. access.ts runs on every navigation regardless
660
623
  // of cached layouts or state tree content.
661
624
  // See design/13-security.md §"Auth always runs" (test #11).
625
+ //
626
+ // Skipped segments omit denyPages and set skippedSegment=true. If
627
+ // access denies at a skipped segment, AccessGate records the deny in
628
+ // ALS and rethrows — the response path returns rscErrorEnvelope so the
629
+ // client hard-navigates for a clean full render with correct deny page
630
+ // placement. Rendering the deny page in-tree here would appear inside
631
+ // the client's cached layout chrome (TIM-1074). See TIM-1363.
662
632
  if (segment.access) {
663
633
  const accessMod = await loadModule(segment.access);
664
634
  const accessFn = accessMod.default as (() => unknown) | undefined;
665
635
  if (accessFn) {
666
- const verdict = accessVerdicts.get(i);
636
+ const onGateSettled = trackShellComponent();
667
637
  element = h(AccessGate, {
668
638
  accessFn,
669
639
  segmentName: segment.segmentName,
670
- denyPages: denyPageChains.get(i),
671
- ...(verdict ? { verdict } : {}),
640
+ segmentKey: segmentKeys[i],
641
+ segmentIndex: i,
642
+ skippedSegment: true,
643
+ onSettled: onGateSettled,
672
644
  children: element,
673
645
  });
674
646
  }
@@ -677,12 +649,42 @@ export async function buildRouteElement(
677
649
  continue;
678
650
  }
679
651
 
652
+ // Wrap with MetadataCollector(s) INSIDE error boundaries so a
653
+ // metadata() throw is caught by this segment's status-file boundaries,
654
+ // not an ancestor's. Still inside AccessGate (placed below) — when
655
+ // access denies, neither MetadataCollector nor error boundaries render.
656
+ //
657
+ // Reversed: the loop wraps innermost-to-outermost, so the last-wrapped
658
+ // collector renders first. Reversing ensures layout metadata is pushed
659
+ // before page metadata, preserving the root→page merge order required
660
+ // by resolveMetadata (page-authoritative). See TIM-1363.
661
+ const segmentMetadata = metadataSources.filter((s) => s.segmentIndex === i).reverse();
662
+ for (const meta of segmentMetadata) {
663
+ const isDynamic = typeof meta.metadataExport === 'function';
664
+ element = h(MetadataCollector, {
665
+ metadataExport: meta.metadataExport,
666
+ segmentName: meta.segmentName,
667
+ isPage: meta.isPage,
668
+ ...(isDynamic ? { onSettled: trackShellComponent() } : {}),
669
+ children: element,
670
+ });
671
+ }
672
+
680
673
  // Wrap with error boundaries from this segment (inside layout).
681
674
  // Keep ALL error boundaries (including 4xx) — they're the safety net for
682
675
  // DenySignal from nested server components that escape AccessGate/PageDenyBoundary
683
676
  // try/catch. Status-code files must be 'use client' TSX or MDX to serialize
684
677
  // as error boundary fallbacks. See TIM-666.
685
- element = await wrapSegmentWithErrorBoundaries(segment, element, h);
678
+ // `segmentKey` makes each boundary addressable: a deny hoisted to this
679
+ // segment carries its owner's key, and every other boundary declines it
680
+ // rather than catching it first and rendering at the wrong depth
681
+ // (TIM-1356).
682
+ element = await wrapSegmentWithErrorBoundaries(segment, element, {
683
+ loadModule,
684
+ createElement: h,
685
+ errorBoundaryComponent: TimberErrorBoundary,
686
+ segmentKey: segmentKeys[i],
687
+ });
686
688
 
687
689
  // Wrap with layout BEFORE AccessGate — AccessGate is OUTSIDE the layout.
688
690
  // When AccessGate denies, the layout never renders. The deny page appears
@@ -690,31 +692,24 @@ export async function buildRouteElement(
690
692
  // This prevents leaking layout UI (sidebars, nav) on denied pages.
691
693
  // See design/04-authorization.md §"Access Failure".
692
694
  if (layoutComponent) {
693
- // Resolve parallel slots for this layout, wrapping each in
694
- // SegmentOutlet for client-side caching. Unchanged slots get
695
- // skip=true so the client keeps its cached content.
696
- //
697
- // Skip slot resolution for denied segments (TIM-1229): when a
698
- // parent segment's AccessGate denies, the layout never renders,
699
- // so slot props are never consumed. Resolving them is wasted work
700
- // and could trigger side effects in slot access.ts functions.
695
+ // Resolve parallel slots for this layout. If a parent AccessGate
696
+ // denies during render, the layout never renders and slot props
697
+ // are never consumed. Slot access functions may still fire eagerly
698
+ // for skip-candidate slots — a follow-up will make that lazy.
701
699
  const segmentId = segmentKeys[i];
702
700
  const parentTreePath = segmentTreePaths[i];
703
- const slotProps =
704
- i >= firstDeniedIndex
705
- ? {}
706
- : await resolveSlotProps({
707
- segment,
708
- segmentId,
709
- match,
710
- h,
711
- interception,
712
- parentTreePath,
713
- departingUrl,
714
- destinationUrl,
715
- clientStateTree: clientStateTree as ClientStateTree | null,
716
- slotSkipInfo,
717
- });
701
+ const slotProps = await resolveSlotProps({
702
+ segment,
703
+ segmentId,
704
+ match,
705
+ h,
706
+ interception,
707
+ parentTreePath,
708
+ departingUrl,
709
+ destinationUrl,
710
+ clientStateTree: clientStateTree as ClientStateTree | null,
711
+ slotSkipInfo,
712
+ });
718
713
 
719
714
  const segmentPath = computeLayoutSegmentPath(segments, i, match);
720
715
  const parallelRouteKeys = Object.keys(segment.slots ?? {});
@@ -750,11 +745,14 @@ export async function buildRouteElement(
750
745
  (layoutComponentRef as (props: Record<string, unknown>) => unknown)(props)
751
746
  );
752
747
  } catch (error: unknown) {
753
- if (error instanceof DenySignal && layoutDenyPages) {
754
- const denyElement = renderMatchingDenyPage(layoutDenyPages, error.status, error.data);
755
- if (denyElement) {
756
- setDenyStatus(error.status);
757
- return denyElement;
748
+ // A layout that denied never renders its own chrome — it threw
749
+ // instead of returning it — so an entry this segment owns is
750
+ // already correctly placed here. Anything owned further up is
751
+ // hoisted (handleCaughtDeny throws). See TIM-1356.
752
+ if (error instanceof DenySignal) {
753
+ setFirstDeniedSegmentIndex(i);
754
+ if (layoutDenyPages) {
755
+ return handleCaughtDeny(layoutDenyPages, error, segmentId);
758
756
  }
759
757
  }
760
758
  throw error;
@@ -795,23 +793,23 @@ export async function buildRouteElement(
795
793
  outermostSegmentProvider = segmentProviderElement;
796
794
  }
797
795
 
798
- // Wrap in AccessGate OUTSIDE the layout.
799
- // If access denies, the deny page renders here — the layout above
800
- // never executes. Parent layouts (from outer iterations) form the shell.
801
- // See TIM-662, TIM-666, design/04-authorization.md §"Access Failure".
796
+ // Wrap in AccessGate OUTSIDE the layout, error boundaries, and MetadataCollector.
797
+ // If access denies, neither the layout nor MetadataCollector renders.
798
+ // Parent layouts (from outer iterations) form the shell. Access runs
799
+ // exactly once, during render, inside React.cache scope — layouts
800
+ // share the cache. See TIM-1363, design/04-authorization.md.
802
801
  if (segment.access) {
803
802
  const accessMod = await loadModule(segment.access);
804
803
  const accessFn = accessMod.default as (() => unknown) | undefined;
805
804
  if (accessFn) {
806
- // Pass verdict for denied/redirected segments so AccessGate replays
807
- // without re-execution. Passing segments omit verdict so AccessGate
808
- // re-runs access during render for React.cache population.
809
- const verdict = accessVerdicts.get(i);
805
+ const onGateSettled = trackShellComponent();
810
806
  element = h(AccessGate, {
811
807
  accessFn,
812
808
  segmentName: segment.segmentName,
809
+ segmentKey: segmentKeys[i],
810
+ segmentIndex: i,
813
811
  denyPages: denyPageChains.get(i),
814
- ...(verdict ? { verdict } : {}),
812
+ onSettled: onGateSettled,
815
813
  children: element,
816
814
  });
817
815
  }
@@ -852,6 +850,5 @@ export async function buildRouteElement(
852
850
  skippedSegments,
853
851
  shellSettled,
854
852
  slotSkipInfo,
855
- firstDeniedIndex,
856
853
  };
857
854
  }
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { rscCacheKey, isRscCacheKeyShape, RSC_KEY_PARAM } from '../shared/rsc-cache-key.js';
29
+ import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
29
30
 
30
31
  /**
31
32
  * Legacy shared-cache control fields that predate RFC 9213 and so do not
@@ -105,13 +106,6 @@ export async function hasValidRscCacheKey(req: Request): Promise<boolean> {
105
106
  return claims.every((claim) => claim === expected);
106
107
  }
107
108
 
108
- /**
109
- * Media type of a Flight payload. Also spelled in `rsc-entry/helpers.ts`,
110
- * `deny-renderer.ts`, `action-handler.ts`, and `client/rsc-fetch.ts` — that
111
- * duplication predates this module and is tracked separately.
112
- */
113
- const RSC_MEDIA_TYPE = 'text/x-component';
114
-
115
109
  /**
116
110
  * Ensure a Flight payload can never be interpreted as a document.
117
111
  *
@@ -130,7 +124,7 @@ const RSC_MEDIA_TYPE = 'text/x-component';
130
124
  export function hardenRscPayloadResponse(response: Response): void {
131
125
  const contentType = response.headers.get('content-type');
132
126
  if (!contentType) return;
133
- if (contentType.split(';')[0].trim().toLowerCase() !== RSC_MEDIA_TYPE) return;
127
+ if (contentType.split(';')[0].trim().toLowerCase() !== RSC_CONTENT_TYPE) return;
134
128
  response.headers.set('X-Content-Type-Options', 'nosniff');
135
129
  }
136
130
 
@@ -17,7 +17,8 @@ import { logRenderError } from '../logger.js';
17
17
  import type { PipelineConfig, RouteMatch } from '../pipeline.js';
18
18
  import type { ManifestSegmentNode } from '../route-matcher.js';
19
19
  import { loadModule } from '../safe-load.js';
20
- import { createDebugChannelSink, isRscPayloadRequest } from './helpers.js';
20
+ import { createDebugChannelSink } from './helpers.js';
21
+ import { isRscRequest } from '../../shared/rsc-media-type.js';
21
22
  import { callSsr } from './ssr-bridge.js';
22
23
 
23
24
  /**
@@ -67,7 +68,7 @@ export function createDenyFallbackRenderer(options: {
67
68
  // decoder breaks navigation, so dispatch to the RSC renderer when the
68
69
  // request is an RSC payload request — mirroring the normal render path.
69
70
  // See design/19-client-navigation.md §"RSC Payload Handling" and TIM-823.
70
- if (isRscPayloadRequest(req)) {
71
+ if (isRscRequest(req)) {
71
72
  return renderDenyPageAsRsc(
72
73
  deny,
73
74
  chain,