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

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 (331) 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/segment-keys-BawYuNFO.js.map +1 -1
  41. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  42. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  43. package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
  44. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  45. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  46. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  47. package/dist/adapters/cloudflare-dev.js +1 -1
  48. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  49. package/dist/adapters/cloudflare.d.ts.map +1 -1
  50. package/dist/adapters/cloudflare.js +1 -1
  51. package/dist/adapters/compress-module.d.ts +12 -0
  52. package/dist/adapters/compress-module.d.ts.map +1 -1
  53. package/dist/adapters/nitro.js +54 -2
  54. package/dist/adapters/nitro.js.map +1 -1
  55. package/dist/cache/index.js +1 -1
  56. package/dist/cdn/cloudflare-purge.js +30 -0
  57. package/dist/cdn/cloudflare-purge.js.map +1 -0
  58. package/dist/cdn/fastly-purge.js +33 -0
  59. package/dist/cdn/fastly-purge.js.map +1 -0
  60. package/dist/cdn/index.js +94 -0
  61. package/dist/cdn/index.js.map +1 -0
  62. package/dist/cdn/workers-cache-purge.js +35 -0
  63. package/dist/cdn/workers-cache-purge.js.map +1 -0
  64. package/dist/cli-check.d.ts +153 -0
  65. package/dist/cli-check.d.ts.map +1 -0
  66. package/dist/cli.d.ts +34 -8
  67. package/dist/cli.d.ts.map +1 -1
  68. package/dist/cli.js +46 -21
  69. package/dist/cli.js.map +1 -1
  70. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  71. package/dist/client/browser-entry/index.d.ts +1 -1
  72. package/dist/client/browser-entry/index.d.ts.map +1 -1
  73. package/dist/client/error-boundary.d.ts +6 -0
  74. package/dist/client/error-boundary.d.ts.map +1 -1
  75. package/dist/client/error-boundary.js +1 -1
  76. package/dist/client/index.d.ts +1 -0
  77. package/dist/client/index.d.ts.map +1 -1
  78. package/dist/client/index.js +27 -33
  79. package/dist/client/index.js.map +1 -1
  80. package/dist/client/internal.js +33 -17
  81. package/dist/client/internal.js.map +1 -1
  82. package/dist/client/link.d.ts +1 -7
  83. package/dist/client/link.d.ts.map +1 -1
  84. package/dist/client/navigation-commit.d.ts +16 -0
  85. package/dist/client/navigation-commit.d.ts.map +1 -1
  86. package/dist/client/router-pipeline.d.ts.map +1 -1
  87. package/dist/client/router.d.ts.map +1 -1
  88. package/dist/client/rsc-fetch.d.ts +9 -1
  89. package/dist/client/rsc-fetch.d.ts.map +1 -1
  90. package/dist/client/segment-cache.d.ts +14 -0
  91. package/dist/client/segment-cache.d.ts.map +1 -1
  92. package/dist/client/use-query-states.d.ts +9 -3
  93. package/dist/client/use-query-states.d.ts.map +1 -1
  94. package/dist/codec.js +1 -1
  95. package/dist/cookies/define-cookie.d.ts.map +1 -1
  96. package/dist/cookies/index.js +2 -2
  97. package/dist/cookies/index.js.map +1 -1
  98. package/dist/index.d.ts +4 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +142 -477
  101. package/dist/index.js.map +1 -1
  102. package/dist/params/index.js +1 -1
  103. package/dist/plugin-context.d.ts +15 -0
  104. package/dist/plugin-context.d.ts.map +1 -1
  105. package/dist/plugins/routing.d.ts +0 -9
  106. package/dist/plugins/routing.d.ts.map +1 -1
  107. package/dist/plugins/shims.d.ts.map +1 -1
  108. package/dist/plugins/static-build.d.ts +24 -0
  109. package/dist/plugins/static-build.d.ts.map +1 -1
  110. package/dist/routing/codegen-shared.d.ts +3 -44
  111. package/dist/routing/codegen-shared.d.ts.map +1 -1
  112. package/dist/routing/codegen-types.d.ts +10 -31
  113. package/dist/routing/codegen-types.d.ts.map +1 -1
  114. package/dist/routing/codegen-write.d.ts +51 -0
  115. package/dist/routing/codegen-write.d.ts.map +1 -0
  116. package/dist/routing/codegen.d.ts.map +1 -1
  117. package/dist/routing/convention-lint.d.ts +18 -4
  118. package/dist/routing/convention-lint.d.ts.map +1 -1
  119. package/dist/routing/export-detect.d.ts +16 -0
  120. package/dist/routing/export-detect.d.ts.map +1 -1
  121. package/dist/routing/index.js +3 -2
  122. package/dist/routing/link-codegen.d.ts +19 -4
  123. package/dist/routing/link-codegen.d.ts.map +1 -1
  124. package/dist/routing/manifest-codegen.d.ts +1 -7
  125. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  126. package/dist/routing/segment-keys.d.ts +22 -0
  127. package/dist/routing/segment-keys.d.ts.map +1 -1
  128. package/dist/routing/types.d.ts +0 -6
  129. package/dist/routing/types.d.ts.map +1 -1
  130. package/dist/schema-bridge.d.ts +60 -9
  131. package/dist/schema-bridge.d.ts.map +1 -1
  132. package/dist/search-params/define.d.ts +62 -8
  133. package/dist/search-params/define.d.ts.map +1 -1
  134. package/dist/search-params/index.d.ts +0 -1
  135. package/dist/search-params/index.d.ts.map +1 -1
  136. package/dist/search-params/index.js +66 -29
  137. package/dist/search-params/index.js.map +1 -1
  138. package/dist/search-params/parse-total.d.ts +70 -0
  139. package/dist/search-params/parse-total.d.ts.map +1 -0
  140. package/dist/search-params/wrappers.d.ts +26 -3
  141. package/dist/search-params/wrappers.d.ts.map +1 -1
  142. package/dist/server/access-gate.d.ts +19 -8
  143. package/dist/server/access-gate.d.ts.map +1 -1
  144. package/dist/server/action-handler.d.ts.map +1 -1
  145. package/dist/server/als-registry.d.ts +16 -0
  146. package/dist/server/als-registry.d.ts.map +1 -1
  147. package/dist/server/chain-url-parts.d.ts +2 -3
  148. package/dist/server/chain-url-parts.d.ts.map +1 -1
  149. package/dist/server/compress.d.ts.map +1 -1
  150. package/dist/server/deny-boundary.d.ts +148 -15
  151. package/dist/server/deny-boundary.d.ts.map +1 -1
  152. package/dist/server/deny-renderer.d.ts +2 -2
  153. package/dist/server/deny-renderer.d.ts.map +1 -1
  154. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  155. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  156. package/dist/server/index.d.ts +0 -1
  157. package/dist/server/index.d.ts.map +1 -1
  158. package/dist/server/index.js +3 -2
  159. package/dist/server/index.js.map +1 -1
  160. package/dist/server/internal.d.ts +3 -1
  161. package/dist/server/internal.d.ts.map +1 -1
  162. package/dist/server/internal.js +344 -237
  163. package/dist/server/internal.js.map +1 -1
  164. package/dist/server/metadata-collector.d.ts +52 -0
  165. package/dist/server/metadata-collector.d.ts.map +1 -0
  166. package/dist/server/param-coercion.d.ts +12 -5
  167. package/dist/server/param-coercion.d.ts.map +1 -1
  168. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  169. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  170. package/dist/server/pipeline-phases.d.ts.map +1 -1
  171. package/dist/server/primitives.d.ts +23 -0
  172. package/dist/server/primitives.d.ts.map +1 -1
  173. package/dist/server/route-element-builder.d.ts +1 -12
  174. package/dist/server/route-element-builder.d.ts.map +1 -1
  175. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
  178. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  179. package/dist/server/rsc-entry/helpers.d.ts +0 -7
  180. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  181. package/dist/server/rsc-entry/index.d.ts +0 -1
  182. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  183. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  184. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  185. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
  186. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  187. package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
  188. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  189. package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
  190. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  191. package/dist/server/skippable-prefix.d.ts +3 -0
  192. package/dist/server/skippable-prefix.d.ts.map +1 -1
  193. package/dist/server/slot-resolver.d.ts +18 -19
  194. package/dist/server/slot-resolver.d.ts.map +1 -1
  195. package/dist/server/ssr-bridge-types.d.ts +11 -0
  196. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  197. package/dist/server/ssr-entry.d.ts +0 -1
  198. package/dist/server/ssr-entry.d.ts.map +1 -1
  199. package/dist/server/state-tree-diff.d.ts +9 -16
  200. package/dist/server/state-tree-diff.d.ts.map +1 -1
  201. package/dist/server/static-generator.d.ts.map +1 -1
  202. package/dist/server/status-code-resolver.d.ts +8 -1
  203. package/dist/server/status-code-resolver.d.ts.map +1 -1
  204. package/dist/server/tree-builder.d.ts +28 -43
  205. package/dist/server/tree-builder.d.ts.map +1 -1
  206. package/dist/server/types.d.ts +12 -7
  207. package/dist/server/types.d.ts.map +1 -1
  208. package/dist/server/utils/element-type.d.ts +40 -0
  209. package/dist/server/utils/element-type.d.ts.map +1 -0
  210. package/dist/shared/rsc-media-type.d.ts +40 -0
  211. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  212. package/dist/shared/segment-info.d.ts +7 -0
  213. package/dist/shared/segment-info.d.ts.map +1 -1
  214. package/docs/api/30-api-server.mdx +1 -1
  215. package/docs/api/33-api-search-params.mdx +38 -16
  216. package/docs/api/35-api-typescript.mdx +3 -3
  217. package/docs/api/36-cli.mdx +34 -7
  218. package/docs/learn/00-introduction.mdx +1 -1
  219. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  220. package/docs/learn/05-typed-params.mdx +8 -8
  221. package/docs/learn/07-typed-routes.mdx +12 -7
  222. package/docs/learn/11-error-handling.mdx +16 -0
  223. package/docs/more/01-advanced-routing.mdx +1 -1
  224. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  225. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  226. package/package.json +8 -5
  227. package/src/adapters/cloudflare.ts +4 -1
  228. package/src/adapters/compress-module.ts +79 -1
  229. package/src/cli-check.ts +458 -0
  230. package/src/cli.ts +59 -24
  231. package/src/client/browser-entry/action-dispatch.ts +2 -1
  232. package/src/client/browser-entry/index.ts +0 -5
  233. package/src/client/error-boundary.tsx +65 -1
  234. package/src/client/index.ts +14 -3
  235. package/src/client/link.tsx +65 -64
  236. package/src/client/navigation-commit.ts +27 -4
  237. package/src/client/params-context.ts +4 -4
  238. package/src/client/router-pipeline.ts +4 -0
  239. package/src/client/router.ts +1 -0
  240. package/src/client/rsc-fetch.ts +14 -4
  241. package/src/client/segment-cache.ts +26 -5
  242. package/src/client/use-query-states.ts +102 -39
  243. package/src/cookies/define-cookie.ts +6 -1
  244. package/src/index.ts +20 -3
  245. package/src/plugin-context.ts +26 -0
  246. package/src/plugins/routing.ts +84 -146
  247. package/src/plugins/shims.ts +0 -1
  248. package/src/plugins/static-build.ts +78 -24
  249. package/src/routing/codegen-shared.ts +3 -79
  250. package/src/routing/codegen-types.ts +10 -31
  251. package/src/routing/codegen-write.ts +139 -0
  252. package/src/routing/codegen.ts +56 -182
  253. package/src/routing/convention-lint.ts +139 -40
  254. package/src/routing/export-detect.ts +151 -7
  255. package/src/routing/link-codegen.ts +32 -65
  256. package/src/routing/manifest-codegen.ts +1 -59
  257. package/src/routing/scanner.ts +3 -3
  258. package/src/routing/segment-keys.ts +37 -0
  259. package/src/routing/types.ts +0 -6
  260. package/src/schema-bridge.ts +180 -58
  261. package/src/search-params/define.ts +102 -37
  262. package/src/search-params/index.ts +0 -1
  263. package/src/search-params/parse-total.ts +78 -0
  264. package/src/search-params/wrappers.ts +60 -11
  265. package/src/server/access-gate.tsx +61 -68
  266. package/src/server/action-handler.ts +1 -4
  267. package/src/server/als-registry.ts +16 -0
  268. package/src/server/chain-url-parts.ts +2 -3
  269. package/src/server/compress.ts +9 -1
  270. package/src/server/deny-boundary.ts +269 -41
  271. package/src/server/deny-renderer.ts +32 -21
  272. package/src/server/error-boundary-wrapper.ts +166 -79
  273. package/src/server/index.ts +1 -3
  274. package/src/server/internal.ts +2 -2
  275. package/src/server/metadata-collector.ts +115 -0
  276. package/src/server/param-coercion.ts +13 -61
  277. package/src/server/pipeline-helpers.ts +2 -2
  278. package/src/server/pipeline-outcome.ts +2 -1
  279. package/src/server/pipeline-phases.ts +9 -9
  280. package/src/server/primitives.ts +25 -0
  281. package/src/server/route-element-builder.ts +169 -194
  282. package/src/server/rsc-cache-key-guard.ts +2 -8
  283. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  284. package/src/server/rsc-entry/error-renderer.ts +31 -11
  285. package/src/server/rsc-entry/helpers.ts +3 -12
  286. package/src/server/rsc-entry/index.ts +0 -5
  287. package/src/server/rsc-entry/render-route.ts +14 -11
  288. package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
  289. package/src/server/rsc-entry/rsc-payload.ts +104 -20
  290. package/src/server/rsc-entry/rsc-stream.ts +25 -2
  291. package/src/server/rsc-entry/ssr-renderer.ts +28 -16
  292. package/src/server/skippable-prefix.ts +21 -39
  293. package/src/server/slot-resolver.ts +69 -189
  294. package/src/server/ssr-bridge-types.ts +12 -0
  295. package/src/server/ssr-entry.ts +8 -6
  296. package/src/server/state-tree-diff.ts +11 -62
  297. package/src/server/static-generator.ts +3 -2
  298. package/src/server/status-code-resolver.ts +28 -11
  299. package/src/server/tree-builder.ts +35 -228
  300. package/src/server/types.ts +12 -7
  301. package/src/server/utils/element-type.ts +72 -0
  302. package/src/shared/rsc-media-type.ts +43 -0
  303. package/src/shared/segment-info.ts +7 -0
  304. package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
  305. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
  306. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
  307. package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
  308. package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
  309. package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
  310. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  311. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  312. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  313. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  314. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  315. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  316. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  317. package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
  318. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
  319. package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
  320. package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
  321. package/dist/search-params/registry.d.ts +0 -20
  322. package/dist/search-params/registry.d.ts.map +0 -1
  323. package/dist/segment-params/define.d.ts +0 -83
  324. package/dist/segment-params/define.d.ts.map +0 -1
  325. package/dist/segment-params/index.d.ts +0 -3
  326. package/dist/segment-params/index.d.ts.map +0 -1
  327. package/dist/segment-params/index.js +0 -70
  328. package/dist/segment-params/index.js.map +0 -1
  329. package/src/search-params/registry.ts +0 -31
  330. package/src/segment-params/define.ts +0 -226
  331. package/src/segment-params/index.ts +0 -9
@@ -15,14 +15,32 @@ import type {
15
15
  SearchParamField,
16
16
  } from './define.js';
17
17
  import { isCodec, isStandardSchema, fromSchema } from '../schema-bridge.js';
18
+ import { parseTotal } from './parse-total.js';
18
19
 
19
20
  // ---------------------------------------------------------------------------
20
21
  // withDefault
21
22
  // ---------------------------------------------------------------------------
22
23
 
23
24
  /**
24
- * Wrap a nullable codec with a default value. When the inner codec returns
25
- * null, the default is used instead. The output type becomes non-nullable.
25
+ * Wrap a nullable codec with a default value. The output type becomes
26
+ * non-nullable, and the wrapper is TOTAL over `string | string[] |
27
+ * undefined` even when the inner codec is not.
28
+ *
29
+ * The default is substituted for `null` — the documented "no value"
30
+ * answer — and for `undefined`, which is what an implicitly-optional
31
+ * Standard Schema field and a bare `parseAsString` produce for an absent
32
+ * param. Substituting only for `null` left a field typed non-nullable
33
+ * `string` holding `undefined` (TIM-1350).
34
+ *
35
+ * The inner codec is invoked through `parseTotal`, so a nuqs parser gets
36
+ * its own normalization (absent → null, repeated → first value, inner
37
+ * throw → null) before either check applies. That is what makes
38
+ * `withDefault(parseAsBoolean, false)` total; the wrapper itself does NOT
39
+ * catch. A throw from a codec is a deliberate signal — `fromSchema` throws
40
+ * on an async schema, and an app codec may `redirect()` or `notFound()` on
41
+ * a value it refuses — and swallowing it would convert a loud failure into
42
+ * a permanently-default field. design/09 §"The SearchParamCodec Protocol":
43
+ * a codec that throws bubbles as a render-phase error, by design.
26
44
  *
27
45
  * Works with any codec — nuqs parsers, custom codecs, fromSchema results.
28
46
  *
@@ -34,20 +52,35 @@ import { isCodec, isStandardSchema, fromSchema } from '../schema-bridge.js';
34
52
  * // page.parse(undefined) → 1 (not null)
35
53
  * // page.parse('5') → 5
36
54
  * ```
55
+ *
56
+ * The signature strips BOTH nullish constituents from the result, not just
57
+ * `null`, because the runtime substitutes for both. An implicitly-optional
58
+ * schema field is a `SearchParamCodec<string | undefined>`, and wrapping it
59
+ * used to yield `SearchParamCodec<string | undefined>` — a field the
60
+ * wrapper guarantees is always present, still typed as maybe-absent.
37
61
  */
38
62
  export function withDefault<T>(
39
- codec: SearchParamCodec<T | null>,
40
- defaultValue: T
41
- ): SearchParamCodec<T> {
42
- return {
43
- parse(value: string | string[] | undefined): T {
44
- const result = codec.parse(value);
45
- return result === null ? defaultValue : result;
63
+ codec: SearchParamCodec<T | null | undefined>,
64
+ defaultValue: NonNullable<T>
65
+ ): SearchParamCodec<NonNullable<T>> {
66
+ // A wrapper that rebuilds a codec from two methods drops everything else
67
+ // the codec carries. `withUrlKey` lost `parseServerSide` that way; this
68
+ // one lost `urlKey`, so `withDefault(withUrlKey(parseAsInteger, 'p'), 1)`
69
+ // silently read `?page=` instead of `?p=`. Carry it explicitly rather
70
+ // than spreading: this wrapper's own `parse` IS the total entry point,
71
+ // and re-publishing the inner `parseServerSide` would let `parseTotal`
72
+ // reach past it and skip the default entirely.
73
+ const wrapped: SearchParamCodec<NonNullable<T>> = {
74
+ parse(value: string | string[] | undefined): NonNullable<T> {
75
+ const result = parseTotal(codec, value);
76
+ return result === null || result === undefined ? defaultValue : result;
46
77
  },
47
- serialize(value: T): string | null {
78
+ serialize(value: NonNullable<T>): string | null {
48
79
  return codec.serialize(value);
49
80
  },
50
81
  };
82
+ if (codec.urlKey !== undefined) wrapped.urlKey = codec.urlKey;
83
+ return wrapped;
51
84
  }
52
85
 
53
86
  // ---------------------------------------------------------------------------
@@ -93,9 +126,25 @@ export function withUrlKey<F extends SearchParamField>(
93
126
  : isStandardSchema(codecOrSchema)
94
127
  ? (fromSchema(codecOrSchema) as SearchParamCodec<T>)
95
128
  : (codecOrSchema as SearchParamCodec<T>);
96
- return {
129
+ // Carry the total entry point, bound like the other two methods. A
130
+ // codec's totality lives in a THIRD property, `parseServerSide` (see
131
+ // parse-total.ts); picking only `parse`/`serialize` silently produced a
132
+ // non-total alias, so `withUrlKey(parseAsBoolean, 'b')` threw on an
133
+ // absent param while the bare parser did not (TIM-1350).
134
+ //
135
+ // Bound explicitly rather than spread: a spread copies own enumerable
136
+ // properties only, so a class-based codec — whose `parse` and
137
+ // `serialize` this wrapper already reaches through the prototype —
138
+ // would have kept its methods and lost exactly the one that makes it
139
+ // total. Three properties, one rule, no dependence on how the inner
140
+ // codec was constructed.
141
+ const wrapped: SearchParamCodecWithUrlKey<T> = {
97
142
  parse: codec.parse.bind(codec),
98
143
  serialize: codec.serialize.bind(codec),
99
144
  urlKey,
100
145
  };
146
+ if (typeof codec.parseServerSide === 'function') {
147
+ wrapped.parseServerSide = codec.parseServerSide.bind(codec);
148
+ }
149
+ return wrapped;
101
150
  }
@@ -18,7 +18,7 @@ import type { AccessGateProps, SlotAccessGateProps } from './tree-builder.js';
18
18
  import { withSpan, setSpanAttribute } from './tracing.js';
19
19
  import { isDebug } from './debug.js';
20
20
  import type { DenyPageEntry } from './deny-boundary.js';
21
- import { renderMatchingDenyPage, setDenyStatus } from './deny-boundary.js';
21
+ import { handleCaughtDeny, setFirstDeniedSegmentIndex } from './deny-boundary.js';
22
22
  import type { ReactNode } from 'react';
23
23
 
24
24
  // ─── AccessGate ─────────────────────────────────────────────────────────────
@@ -26,50 +26,64 @@ import type { ReactNode } from 'react';
26
26
  /**
27
27
  * Framework-injected access gate for segments.
28
28
  *
29
- * When a pre-computed `verdict` prop is provided (from the pre-render pass
30
- * in route-element-builder.ts), AccessGate replays it synchronously — no
31
- * async, no re-execution of access.ts, immune to Suspense timing. The OTEL
32
- * span was already emitted during the pre-render pass.
33
- *
34
- * When no verdict is provided (backward compat with tree-builder.ts),
35
- * AccessGate calls accessFn directly with OTEL instrumentation.
29
+ * Calls accessFn during render — inside `renderToReadableStream`, sharing
30
+ * the same `React.cache` scope as layouts and pages. A `requireUser()`
31
+ * call in access.ts populates the cache; the same call in any layout is
32
+ * a free hit. Access executes exactly once per request. See TIM-1363.
36
33
  *
37
34
  * access.ts is a pure gate — return values are discarded. The layout below
38
35
  * gets the same data by calling the same cached functions (React.cache dedup).
36
+ *
37
+ * On denial the gate renders the matching deny page only when this segment
38
+ * owns it. Because the gate is wrapped OUTSIDE its own layout, that render
39
+ * omits the denying segment's layout — deliberate, and the one place the
40
+ * in-tree path diverges from `renderDenyPage`'s inclusive slice: a layout
41
+ * whose access.ts denied must never run and serialize protected data into
42
+ * the Flight stream (TIM-1074). An ancestor-owned page is hoisted instead,
43
+ * because the gate also sits inside every layout above it and rendering
44
+ * here would wrap the deny page in all of them (TIM-1356).
45
+ *
46
+ * For skipped segments (skippedSegment=true), deny pages are NOT rendered
47
+ * in-tree — the client's cached layout chrome would wrap them incorrectly.
48
+ * Instead, the deny is recorded in ALS and rethrown so the response path
49
+ * can return rscErrorEnvelope for a client hard-navigate.
39
50
  */
40
- export function AccessGate(props: AccessGateProps): ReactNode | Promise<ReactNode> {
41
- const { accessFn, segmentName, verdict, denyPages, children } = props;
42
-
43
- // Fast path: replay pre-computed verdict from the pre-render pass.
44
- if (verdict !== undefined) {
45
- if (verdict === 'pass') {
46
- return children;
47
- }
48
- // Render deny page in-tree when possible (same as the fallback path).
49
- if (verdict instanceof DenySignal && denyPages) {
50
- const denyElement = renderMatchingDenyPage(denyPages, verdict.status, verdict.data);
51
- if (denyElement) {
52
- setDenyStatus(verdict.status);
53
- return denyElement;
54
- }
55
- }
56
- throw verdict;
57
- }
51
+ export function AccessGate(props: AccessGateProps): Promise<ReactNode> {
52
+ const {
53
+ accessFn,
54
+ segmentName,
55
+ segmentKey,
56
+ segmentIndex,
57
+ denyPages,
58
+ skippedSegment,
59
+ onSettled,
60
+ children,
61
+ } = props;
58
62
 
59
- // Primary path: call accessFn directly during render.
60
- // If denyPages is provided, catch DenySignal and render the deny page
61
- // in-tree — no throw reaches React Flight, no second render pass.
62
- return accessGateFallback(accessFn, segmentName, denyPages, children);
63
+ return accessGateFallback(
64
+ accessFn,
65
+ segmentName,
66
+ segmentKey,
67
+ segmentIndex,
68
+ skippedSegment ? undefined : denyPages,
69
+ skippedSegment,
70
+ onSettled,
71
+ children
72
+ );
63
73
  }
64
74
 
65
75
  /**
66
- * Async fallback for AccessGate when no pre-computed verdict is available.
67
- * Calls accessFn with OTEL instrumentation.
76
+ * Calls accessFn during render with OTEL instrumentation.
77
+ * Records the first denied segment index in ALS for post-render consumers.
68
78
  */
69
79
  async function accessGateFallback(
70
80
  accessFn: AccessGateProps['accessFn'],
71
81
  segmentName: AccessGateProps['segmentName'],
82
+ segmentKey: AccessGateProps['segmentKey'],
83
+ segmentIndex: AccessGateProps['segmentIndex'],
72
84
  denyPages: DenyPageEntry[] | undefined,
85
+ skippedSegment: boolean | undefined,
86
+ onSettled: (() => void) | undefined,
73
87
  children: ReactNode
74
88
  ): Promise<ReactNode> {
75
89
  try {
@@ -91,17 +105,23 @@ async function accessGateFallback(
91
105
  }
92
106
  });
93
107
  } catch (error: unknown) {
94
- // Catch DenySignal and render the deny page in-tree.
95
- // No throw reaches React Flight — clean stream, single render pass.
96
- // RedirectSignal and other errors propagate normally.
97
- if (error instanceof DenySignal && denyPages) {
98
- const denyElement = renderMatchingDenyPage(denyPages, error.status, error.data);
99
- if (denyElement) {
100
- setDenyStatus(error.status);
101
- return denyElement;
108
+ if (error instanceof DenySignal) {
109
+ if (segmentIndex !== undefined) {
110
+ setFirstDeniedSegmentIndex(segmentIndex);
111
+ }
112
+
113
+ if (skippedSegment) {
114
+ error._hardNavigate = true;
115
+ throw error;
116
+ }
117
+
118
+ if (denyPages) {
119
+ return handleCaughtDeny(denyPages, error, segmentKey);
102
120
  }
103
121
  }
104
122
  throw error;
123
+ } finally {
124
+ onSettled?.();
105
125
  }
106
126
 
107
127
  return children;
@@ -124,35 +144,8 @@ async function accessGateFallback(
124
144
  * slot doesn't make architectural sense.
125
145
  */
126
146
  export async function SlotAccessGate(props: SlotAccessGateProps): Promise<ReactNode> {
127
- const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children, verdict } =
128
- props;
129
-
130
- // Fast path: replay pre-computed verdict from eager evaluation in resolveSlotProps.
131
- if (verdict !== undefined) {
132
- if (verdict === 'pass') return children;
133
- if (verdict instanceof DenySignal) {
134
- return (
135
- buildDeniedFallback(DeniedComponent, slotName, verdict.data, createElement) ??
136
- defaultFallback ??
137
- null
138
- );
139
- }
140
- // RedirectSignal: treat as deny in production (same as existing behavior)
141
- if (isDebug()) {
142
- console.error(
143
- '[timber] redirect() is not allowed in slot access.ts. ' +
144
- 'Slots use deny() for graceful degradation — denied.tsx → default.tsx → null. ' +
145
- "If you need to redirect, move the logic to the parent segment's access.ts."
146
- );
147
- }
148
- return (
149
- buildDeniedFallback(DeniedComponent, slotName, undefined, createElement) ??
150
- defaultFallback ??
151
- null
152
- );
153
- }
147
+ const { accessFn, DeniedComponent, slotName, createElement, defaultFallback, children } = props;
154
148
 
155
- // Fallback path: call accessFn during render (no pre-computed verdict).
156
149
  try {
157
150
  await accessFn();
158
151
  } catch (error: unknown) {
@@ -36,6 +36,7 @@ import {
36
36
  import type { FormFlashData } from './form-flash.js';
37
37
  import { checkVersionSkew, applyReloadHeaders } from './version-skew.js';
38
38
  import { logActionError } from './logger.js';
39
+ import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
39
40
 
40
41
  // ─── Types ────────────────────────────────────────────────────────────────
41
42
 
@@ -55,10 +56,6 @@ export interface ActionDispatchConfig {
55
56
  sensitiveFields?: SensitiveFieldsOption;
56
57
  }
57
58
 
58
- // ─── Constants ────────────────────────────────────────────────────────────
59
-
60
- const RSC_CONTENT_TYPE = 'text/x-component';
61
-
62
59
  // ─── Detection ────────────────────────────────────────────────────────────
63
60
 
64
61
  /**
@@ -141,6 +141,22 @@ export interface RequestContextStore {
141
141
  * context for render-phase errors without module-level shared state.
142
142
  */
143
143
  debugComponentsGetter?: () => DebugComponentEntry[];
144
+ /**
145
+ * Index of the first segment whose AccessGate denied during render.
146
+ * Recorded by AccessGate when it catches a DenySignal so post-render
147
+ * consumers (buildSegmentInfo, X-Timber-Segments header) can bound
148
+ * the segment list without a pre-render access pass. Infinity when
149
+ * no segment denied. See TIM-1363.
150
+ */
151
+ _firstDeniedSegmentIndex?: number;
152
+ /**
153
+ * Metadata entries collected during render by MetadataCollector.
154
+ * Populated per-segment in tree order (root→page) during
155
+ * renderToReadableStream. MetadataHead reads all entries,
156
+ * merges them, and renders Float-hoisted head elements.
157
+ * See TIM-1363, design/16-metadata.md.
158
+ */
159
+ _metadataEntries?: import('./metadata.js').SegmentMetadataEntry[];
144
160
  }
145
161
 
146
162
  /** A single outgoing cookie entry in the cookie jar. */
@@ -7,9 +7,8 @@
7
7
  * - the layout depth handed to `SegmentProvider` (`route-element-builder.ts`),
8
8
  * which `useSelectedLayoutSegment(s)` indexes the live pathname with;
9
9
  * - `slotUrlParts`, which slices the destination pathname for slot matching;
10
- * - `resolveSlotProps`, which slices the *departing* pathname the same way to
11
- * decide whether a slot can be skipped;
12
- * - `skippable-prefix.ts`, which does the same for the contiguous skip prefix.
10
+ * - `resolveSlotProps` and `skippable-prefix.ts`, which derive content keys
11
+ * for slot skip decisions (TIM-1370).
13
12
  *
14
13
  * They were four separate `extractUrlParts(...).length` calls. That was fine
15
14
  * while the answer was a plain walk, and stopped being fine when interception
@@ -7,6 +7,14 @@
7
7
  // this module is only used on Node.js/Bun.
8
8
  //
9
9
  // See design/25-production-deployments.md.
10
+ //
11
+ // This module is also emitted standalone as `_compress.mjs` by
12
+ // `adapters/compress-module.ts`, which strips types but does not bundle. Its
13
+ // imports must therefore be interpolated there — a new import that is not
14
+ // fails the build with an explanatory error rather than resolving to nothing
15
+ // on the deployed server.
16
+
17
+ import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
10
18
 
11
19
  // ─── Constants ────────────────────────────────────────────────────────────
12
20
 
@@ -21,7 +29,7 @@ export const COMPRESSIBLE_TYPES = new Set([
21
29
  'text/plain',
22
30
  'text/xml',
23
31
  'text/javascript',
24
- 'text/x-component',
32
+ RSC_CONTENT_TYPE,
25
33
  'application/json',
26
34
  'application/javascript',
27
35
  'application/xml',