@timber-js/app 0.2.0-alpha.192 → 0.2.0-alpha.194

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 (667) hide show
  1. package/agent-skill.md +2 -0
  2. package/dist/_chunks/{actions-BjbbNRFN.js → actions-CWYtq6ii.js} +3 -3
  3. package/dist/_chunks/actions-CWYtq6ii.js.map +1 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  5. package/dist/_chunks/als-slots-BEEIPKYm.js.map +1 -1
  6. package/dist/_chunks/{build-output-helper-C3DrfzZR.js → build-output-helper-9dqCv1pZ.js} +3 -2
  7. package/dist/_chunks/build-output-helper-9dqCv1pZ.js.map +1 -0
  8. package/dist/_chunks/{cache-api-DllJ-Lyw.js → cache-api-CQeYzA5g.js} +6 -8
  9. package/dist/_chunks/cache-api-CQeYzA5g.js.map +1 -0
  10. package/dist/_chunks/chains-h7EO-u3n.js +412 -0
  11. package/dist/_chunks/chains-h7EO-u3n.js.map +1 -0
  12. package/dist/_chunks/{cli-check-DJZDc22E.js → cli-check-BVthpfLS.js} +43 -6
  13. package/dist/_chunks/cli-check-BVthpfLS.js.map +1 -0
  14. package/dist/_chunks/{cli-schema-sync-D4_AZVUb.js → cli-schema-sync-ZwM9u_ob.js} +6 -5
  15. package/dist/_chunks/{cli-schema-sync-D4_AZVUb.js.map → cli-schema-sync-ZwM9u_ob.js.map} +1 -1
  16. package/dist/_chunks/{cloudflare-CGP6BZKO.js → cloudflare-BKJC3SC_.js} +28 -22
  17. package/dist/_chunks/{cloudflare-CGP6BZKO.js.map → cloudflare-BKJC3SC_.js.map} +1 -1
  18. package/dist/_chunks/codec-registry-oOUxugz3.js.map +1 -1
  19. package/dist/_chunks/{convention-lint-CpteIpTm.js → convention-lint-DO10_pVl.js} +93 -7
  20. package/dist/_chunks/convention-lint-DO10_pVl.js.map +1 -0
  21. package/dist/_chunks/dist-DtrJyBbw.js +386 -0
  22. package/dist/_chunks/dist-DtrJyBbw.js.map +1 -0
  23. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -1
  24. package/dist/_chunks/{file-cache-DmX7OqZP.js → file-cache-Dw6BJPG7.js} +5 -1
  25. package/dist/_chunks/{file-cache-DmX7OqZP.js.map → file-cache-Dw6BJPG7.js.map} +1 -1
  26. package/dist/_chunks/{format-CfwjgPz9.js → format-BKclgVbk.js} +3 -3
  27. package/dist/_chunks/{format-CfwjgPz9.js.map → format-BKclgVbk.js.map} +1 -1
  28. package/dist/_chunks/graph-cache-CP4GEmf9.js +304 -0
  29. package/dist/_chunks/graph-cache-CP4GEmf9.js.map +1 -0
  30. package/dist/_chunks/href-validation-CMc5JRls.js.map +1 -1
  31. package/dist/_chunks/live-graph-Bx4HodF1.js +150 -0
  32. package/dist/_chunks/live-graph-Bx4HodF1.js.map +1 -0
  33. package/dist/_chunks/{logger-CH7IcMmg.js → logger-pumCm3Il.js} +25 -7
  34. package/dist/_chunks/{logger-CH7IcMmg.js.map → logger-pumCm3Il.js.map} +1 -1
  35. package/dist/_chunks/{mdx-file-C005ay-P.js → mdx-file-CXyHGUpS.js} +3 -3
  36. package/dist/_chunks/mdx-file-CXyHGUpS.js.map +1 -0
  37. package/dist/_chunks/{plugin-context-rCinWLiE.js → plugin-context-8idfR3UZ.js} +2 -2
  38. package/dist/_chunks/plugin-context-8idfR3UZ.js.map +1 -0
  39. package/dist/_chunks/poison-rules-DoEhbqaY.js +110 -0
  40. package/dist/_chunks/poison-rules-DoEhbqaY.js.map +1 -0
  41. package/dist/_chunks/poison-scan-BAxfTT5L.js +101 -0
  42. package/dist/_chunks/poison-scan-BAxfTT5L.js.map +1 -0
  43. package/dist/_chunks/resolve-schema-CBR6Lm4i.js.map +1 -1
  44. package/dist/_chunks/rolldown-runtime-D7D4PA-g.js +13 -0
  45. package/dist/_chunks/router-ref-DuYuV_0Q.js.map +1 -1
  46. package/dist/_chunks/{rsc-media-type-DRqE_lD_.js → rsc-media-type-DDc7duTD.js} +17 -2
  47. package/dist/_chunks/{rsc-media-type-DRqE_lD_.js.map → rsc-media-type-DDc7duTD.js.map} +1 -1
  48. package/dist/_chunks/scan-BoP17W3w.js +140 -0
  49. package/dist/_chunks/scan-BoP17W3w.js.map +1 -0
  50. package/dist/_chunks/{scanner-CAietmj4.js → scanner-BRIOmHE2.js} +43 -4
  51. package/dist/_chunks/scanner-BRIOmHE2.js.map +1 -0
  52. package/dist/_chunks/schema-bridge-C83xa9lT.js.map +1 -1
  53. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
  54. package/dist/_chunks/{segment-keys-BawYuNFO.js → segment-keys-BhqoHiLc.js} +14 -2
  55. package/dist/_chunks/{segment-keys-BawYuNFO.js.map → segment-keys-BhqoHiLc.js.map} +1 -1
  56. package/dist/_chunks/slot-params-BCTmZkQB.js.map +1 -1
  57. package/dist/_chunks/ssr-data-14MXm7Pj.js.map +1 -1
  58. package/dist/_chunks/use-query-states-I3JMng6J.js.map +1 -1
  59. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -1
  60. package/dist/_chunks/{walkers-BsVLmD1S.js → walkers-uCu3WW6_.js} +9 -6
  61. package/dist/_chunks/{walkers-BsVLmD1S.js.map → walkers-uCu3WW6_.js.map} +1 -1
  62. package/dist/adapters/build-output-helper.d.ts +1 -1
  63. package/dist/adapters/build-output-helper.d.ts.map +1 -1
  64. package/dist/adapters/cloudflare-dev.d.ts.map +1 -1
  65. package/dist/adapters/cloudflare-dev.js +2 -1
  66. package/dist/adapters/cloudflare-dev.js.map +1 -1
  67. package/dist/adapters/cloudflare-kv-cache.d.ts +1 -1
  68. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  69. package/dist/adapters/cloudflare-kv-cache.js +7 -8
  70. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  71. package/dist/adapters/cloudflare.d.ts +1 -1
  72. package/dist/adapters/cloudflare.d.ts.map +1 -1
  73. package/dist/adapters/cloudflare.js +1 -1
  74. package/dist/adapters/nitro-presets.d.ts +37 -0
  75. package/dist/adapters/nitro-presets.d.ts.map +1 -0
  76. package/dist/adapters/nitro-preview.d.ts +32 -0
  77. package/dist/adapters/nitro-preview.d.ts.map +1 -0
  78. package/dist/adapters/nitro.d.ts +28 -51
  79. package/dist/adapters/nitro.d.ts.map +1 -1
  80. package/dist/adapters/nitro.js +224 -171
  81. package/dist/adapters/nitro.js.map +1 -1
  82. package/dist/analyze/chains.d.ts +124 -0
  83. package/dist/analyze/chains.d.ts.map +1 -0
  84. package/dist/analyze/classify.d.ts +150 -0
  85. package/dist/analyze/classify.d.ts.map +1 -0
  86. package/dist/analyze/crawl-entry.d.ts +17 -0
  87. package/dist/analyze/crawl-entry.d.ts.map +1 -0
  88. package/dist/analyze/crawl-entry.js +313 -0
  89. package/dist/analyze/crawl-entry.js.map +1 -0
  90. package/dist/analyze/crawl.d.ts +134 -0
  91. package/dist/analyze/crawl.d.ts.map +1 -0
  92. package/dist/analyze/graph-cache.d.ts +104 -0
  93. package/dist/analyze/graph-cache.d.ts.map +1 -0
  94. package/dist/analyze/graph-command.d.ts +43 -0
  95. package/dist/analyze/graph-command.d.ts.map +1 -0
  96. package/dist/analyze/graph-command.js +472 -0
  97. package/dist/analyze/graph-command.js.map +1 -0
  98. package/dist/analyze/live-graph.d.ts +81 -0
  99. package/dist/analyze/live-graph.d.ts.map +1 -0
  100. package/dist/analyze/poison-rules.d.ts +72 -0
  101. package/dist/analyze/poison-rules.d.ts.map +1 -0
  102. package/dist/analyze/poison-scan.d.ts +48 -0
  103. package/dist/analyze/poison-scan.d.ts.map +1 -0
  104. package/dist/analyze/scan.d.ts +73 -0
  105. package/dist/analyze/scan.d.ts.map +1 -0
  106. package/dist/cache/cache-api.d.ts +2 -2
  107. package/dist/cache/cache-api.d.ts.map +1 -1
  108. package/dist/cache/index.d.ts +8 -8
  109. package/dist/cache/index.d.ts.map +1 -1
  110. package/dist/cache/index.js +1 -1
  111. package/dist/cache/redis-handler.d.ts +1 -1
  112. package/dist/cache/redis-handler.d.ts.map +1 -1
  113. package/dist/cache/stores/cloudflare-kv.d.ts +1 -1
  114. package/dist/cache/stores/cloudflare-kv.d.ts.map +1 -1
  115. package/dist/cache/stores/cloudflare-kv.js.map +1 -1
  116. package/dist/cache/stores/memory.d.ts +1 -1
  117. package/dist/cache/stores/memory.d.ts.map +1 -1
  118. package/dist/cache/stores/memory.js.map +1 -1
  119. package/dist/cache/stores/redis.d.ts +1 -1
  120. package/dist/cache/stores/redis.d.ts.map +1 -1
  121. package/dist/cache/stores/redis.js.map +1 -1
  122. package/dist/cache/stores/vercel.d.ts +1 -1
  123. package/dist/cache/stores/vercel.d.ts.map +1 -1
  124. package/dist/cache/stores/vercel.js.map +1 -1
  125. package/dist/cache/tag-aware-handler.d.ts +2 -2
  126. package/dist/cache/tag-aware-handler.d.ts.map +1 -1
  127. package/dist/cache/timber-cache.d.ts +1 -1
  128. package/dist/cache/timber-cache.d.ts.map +1 -1
  129. package/dist/cdn/cloudflare-purge.d.ts +1 -1
  130. package/dist/cdn/cloudflare-purge.d.ts.map +1 -1
  131. package/dist/cdn/cloudflare-purge.js.map +1 -1
  132. package/dist/cdn/fastly-purge.d.ts +1 -1
  133. package/dist/cdn/fastly-purge.d.ts.map +1 -1
  134. package/dist/cdn/fastly-purge.js.map +1 -1
  135. package/dist/cdn/index.d.ts +4 -4
  136. package/dist/cdn/index.d.ts.map +1 -1
  137. package/dist/cdn/workers-cache-purge.d.ts +5 -1
  138. package/dist/cdn/workers-cache-purge.d.ts.map +1 -1
  139. package/dist/cdn/workers-cache-purge.js.map +1 -1
  140. package/dist/cli-check.d.ts +10 -0
  141. package/dist/cli-check.d.ts.map +1 -1
  142. package/dist/cli.d.ts +12 -4
  143. package/dist/cli.d.ts.map +1 -1
  144. package/dist/cli.js +30 -6
  145. package/dist/cli.js.map +1 -1
  146. package/dist/client/browser-entry/hydrate.d.ts +2 -2
  147. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  148. package/dist/client/browser-entry/post-hydration.d.ts +2 -2
  149. package/dist/client/browser-entry/router-init.d.ts +2 -2
  150. package/dist/client/browser-entry/rsc-stream.d.ts +1 -1
  151. package/dist/client/form.d.ts +2 -2
  152. package/dist/client/form.d.ts.map +1 -1
  153. package/dist/client/history.d.ts +2 -2
  154. package/dist/client/index.d.ts +18 -18
  155. package/dist/client/index.d.ts.map +1 -1
  156. package/dist/client/index.js.map +1 -1
  157. package/dist/client/internal.d.ts +19 -19
  158. package/dist/client/internal.d.ts.map +1 -1
  159. package/dist/client/internal.js +11 -15
  160. package/dist/client/internal.js.map +1 -1
  161. package/dist/client/link.d.ts +3 -3
  162. package/dist/client/navigation-commit.d.ts +5 -5
  163. package/dist/client/navigation-root.d.ts +1 -1
  164. package/dist/client/navigation-root.d.ts.map +1 -1
  165. package/dist/client/params-context.d.ts +3 -3
  166. package/dist/client/router-pipeline.d.ts +7 -7
  167. package/dist/client/router-ref.d.ts +1 -1
  168. package/dist/client/router-types.d.ts +6 -6
  169. package/dist/client/router-types.d.ts.map +1 -1
  170. package/dist/client/router.d.ts +1 -1
  171. package/dist/client/rsc-fetch.d.ts +4 -4
  172. package/dist/client/rsc-fetch.d.ts.map +1 -1
  173. package/dist/client/segment-cache.d.ts +2 -2
  174. package/dist/client/slot-provider.d.ts +1 -1
  175. package/dist/client/ssr-data.d.ts +2 -2
  176. package/dist/client/state.d.ts +4 -4
  177. package/dist/client/use-query-states.d.ts +1 -1
  178. package/dist/client/use-segment-params.d.ts +3 -3
  179. package/dist/codec.d.ts +2 -2
  180. package/dist/codec.js.map +1 -1
  181. package/dist/config-validation.d.ts +1 -1
  182. package/dist/cookies/define-cookie.d.ts +3 -3
  183. package/dist/cookies/index.d.ts +3 -3
  184. package/dist/cookies/index.js +3 -4
  185. package/dist/cookies/index.js.map +1 -1
  186. package/dist/cookies/json-cookie.d.ts +1 -1
  187. package/dist/dev-tools/404-page.d.ts +2 -2
  188. package/dist/dev-tools/error-page.d.ts +2 -2
  189. package/dist/dev-tools/index.d.ts +12 -12
  190. package/dist/dev-tools/instrumentation.d.ts +1 -1
  191. package/dist/dev-tools/logs.d.ts +1 -1
  192. package/dist/dev-tools/overlay.d.ts +2 -2
  193. package/dist/dev-tools/terminal.d.ts +1 -1
  194. package/dist/fonts/ast.d.ts +4 -4
  195. package/dist/fonts/bundle.d.ts +3 -3
  196. package/dist/fonts/css.d.ts +1 -1
  197. package/dist/fonts/dev-middleware.d.ts +1 -1
  198. package/dist/fonts/google.d.ts +1 -1
  199. package/dist/fonts/local.d.ts +1 -1
  200. package/dist/fonts/pipeline.d.ts +3 -3
  201. package/dist/fonts/transform.d.ts +2 -2
  202. package/dist/fonts/types.d.ts +1 -1
  203. package/dist/fonts/virtual-modules.d.ts +1 -1
  204. package/dist/index.d.ts +3 -3
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +785 -282
  207. package/dist/index.js.map +1 -1
  208. package/dist/params/codec-registry.d.ts +1 -1
  209. package/dist/params/define-schema.d.ts +2 -2
  210. package/dist/params/index.d.ts +4 -4
  211. package/dist/params/index.js.map +1 -1
  212. package/dist/params/resolve-schema.d.ts +1 -1
  213. package/dist/plugin-context.d.ts +9 -9
  214. package/dist/plugin-context.d.ts.map +1 -1
  215. package/dist/plugins/adapter-build.d.ts +2 -2
  216. package/dist/plugins/build-manifest.d.ts +2 -2
  217. package/dist/plugins/build-report.d.ts +5 -5
  218. package/dist/plugins/cache.d.ts +1 -1
  219. package/dist/plugins/callsite-jsx.d.ts +2 -2
  220. package/dist/plugins/chunks.d.ts +1 -1
  221. package/dist/plugins/chunks.d.ts.map +1 -1
  222. package/dist/plugins/cloudflare-wasm.d.ts +1 -1
  223. package/dist/plugins/content.d.ts +1 -1
  224. package/dist/plugins/content.d.ts.map +1 -1
  225. package/dist/plugins/dev-discovery.d.ts +123 -0
  226. package/dist/plugins/dev-discovery.d.ts.map +1 -0
  227. package/dist/plugins/dev-server.d.ts +1 -1
  228. package/dist/plugins/dev-server.d.ts.map +1 -1
  229. package/dist/plugins/entries.d.ts +1 -1
  230. package/dist/plugins/fonts.d.ts +3 -3
  231. package/dist/plugins/fonts.d.ts.map +1 -1
  232. package/dist/plugins/graph-endpoint.d.ts +112 -0
  233. package/dist/plugins/graph-endpoint.d.ts.map +1 -0
  234. package/dist/plugins/mdx.d.ts +1 -1
  235. package/dist/plugins/mdx.d.ts.map +1 -1
  236. package/dist/plugins/prebuilt-capture.d.ts +2 -2
  237. package/dist/plugins/prebuilt-options-analysis.d.ts +1 -1
  238. package/dist/plugins/prebuilt.d.ts +1 -1
  239. package/dist/plugins/prerender-sugar.d.ts +11 -14
  240. package/dist/plugins/prerender-sugar.d.ts.map +1 -1
  241. package/dist/plugins/request-dep/analysis.d.ts +1 -1
  242. package/dist/plugins/request-dep/index.d.ts +5 -5
  243. package/dist/plugins/request-dep/origin.d.ts +1 -1
  244. package/dist/plugins/routing.d.ts +1 -1
  245. package/dist/plugins/routing.d.ts.map +1 -1
  246. package/dist/plugins/server-bundle.d.ts.map +1 -1
  247. package/dist/plugins/shims.d.ts +1 -1
  248. package/dist/plugins/shims.d.ts.map +1 -1
  249. package/dist/plugins/static-build.d.ts +5 -5
  250. package/dist/routing/codegen-shared.d.ts +1 -1
  251. package/dist/routing/codegen-write.d.ts +1 -1
  252. package/dist/routing/codegen.d.ts +1 -1
  253. package/dist/routing/collision-probe.d.ts +1 -1
  254. package/dist/routing/collision-spaces.d.ts +1 -1
  255. package/dist/routing/convention-lint.d.ts +1 -1
  256. package/dist/routing/convention-lint.d.ts.map +1 -1
  257. package/dist/routing/index.d.ts +13 -13
  258. package/dist/routing/index.js +3 -3
  259. package/dist/routing/interception.d.ts +1 -1
  260. package/dist/routing/interception.d.ts.map +1 -1
  261. package/dist/routing/link-codegen.d.ts +1 -1
  262. package/dist/routing/manifest-codegen.d.ts +1 -1
  263. package/dist/routing/scanner.d.ts +14 -1
  264. package/dist/routing/scanner.d.ts.map +1 -1
  265. package/dist/routing/schema-validation.d.ts +1 -1
  266. package/dist/routing/segment-classify.d.ts +1 -1
  267. package/dist/routing/segment-codegen.d.ts +1 -1
  268. package/dist/routing/segment-keys.d.ts +1 -1
  269. package/dist/routing/slot-placement.d.ts +1 -1
  270. package/dist/routing/walkers.d.ts +1 -1
  271. package/dist/rsc-runtime/transforms.d.ts +13 -0
  272. package/dist/rsc-runtime/transforms.d.ts.map +1 -0
  273. package/dist/schema-bridge.d.ts +1 -1
  274. package/dist/search-params/define.d.ts +3 -3
  275. package/dist/search-params/index.d.ts +3 -3
  276. package/dist/search-params/index.js.map +1 -1
  277. package/dist/search-params/wrappers.d.ts +1 -1
  278. package/dist/server/access-gate.d.ts +1 -1
  279. package/dist/server/action-client.d.ts +1 -1
  280. package/dist/server/action-handler.d.ts +5 -5
  281. package/dist/server/actions.d.ts +2 -2
  282. package/dist/server/als-registry.d.ts +8 -8
  283. package/dist/server/asset-headers.d.ts +1 -1
  284. package/dist/server/chain-url-parts.d.ts +3 -3
  285. package/dist/server/children-interception.d.ts +2 -2
  286. package/dist/server/cookie-context.d.ts +1 -1
  287. package/dist/server/cookie-parsing.d.ts +2 -2
  288. package/dist/server/default-logger.d.ts +1 -1
  289. package/dist/server/deny-boundary.d.ts +2 -2
  290. package/dist/server/deny-renderer.d.ts +5 -5
  291. package/dist/server/early-hints.d.ts +1 -1
  292. package/dist/server/error-boundary-wrapper.d.ts +2 -4
  293. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  294. package/dist/server/fallback-error.d.ts +5 -5
  295. package/dist/server/flight-injection-state.d.ts +1 -1
  296. package/dist/server/form-flash.d.ts +1 -1
  297. package/dist/server/html-injector-core.d.ts +2 -2
  298. package/dist/server/html-injectors.d.ts +1 -1
  299. package/dist/server/index.d.ts +18 -18
  300. package/dist/server/index.d.ts.map +1 -1
  301. package/dist/server/index.js +33 -32
  302. package/dist/server/index.js.map +1 -1
  303. package/dist/server/instrumentation.d.ts +1 -1
  304. package/dist/server/internal.d.ts +50 -50
  305. package/dist/server/internal.d.ts.map +1 -1
  306. package/dist/server/internal.js +1023 -1025
  307. package/dist/server/internal.js.map +1 -1
  308. package/dist/server/metadata-collector.d.ts +2 -2
  309. package/dist/server/metadata-platform.d.ts +2 -2
  310. package/dist/server/metadata-render.d.ts +2 -2
  311. package/dist/server/metadata-routes.d.ts +3 -3
  312. package/dist/server/metadata-social.d.ts +2 -2
  313. package/dist/server/metadata.d.ts +2 -2
  314. package/dist/server/middleware-runner.d.ts +1 -1
  315. package/dist/server/node-stream-transforms.d.ts +1 -1
  316. package/dist/server/param-coercion.d.ts +4 -4
  317. package/dist/server/pipeline-helpers.d.ts +3 -3
  318. package/dist/server/pipeline-interception.d.ts +1 -1
  319. package/dist/server/pipeline-metadata.d.ts +1 -1
  320. package/dist/server/pipeline-outcome.d.ts +2 -2
  321. package/dist/server/pipeline-phases.d.ts +3 -3
  322. package/dist/server/pipeline-phases.d.ts.map +1 -1
  323. package/dist/server/pipeline.d.ts +9 -9
  324. package/dist/server/prebuilt/cache-key.d.ts +1 -1
  325. package/dist/server/prebuilt/capture-state.d.ts +1 -1
  326. package/dist/server/prebuilt/key-discipline.d.ts +1 -1
  327. package/dist/server/prebuilt/manifest-tag-index.d.ts +1 -1
  328. package/dist/server/prebuilt/overlay.d.ts +2 -2
  329. package/dist/server/prebuilt/payload-source.d.ts +1 -1
  330. package/dist/server/prebuilt/slots.d.ts +1 -1
  331. package/dist/server/prebuilt/synthetic-store.d.ts +2 -2
  332. package/dist/server/prebuilt-builder.d.ts +2 -2
  333. package/dist/server/primitives.d.ts +2 -2
  334. package/dist/server/publish-params.d.ts +1 -1
  335. package/dist/server/request-context.d.ts +3 -3
  336. package/dist/server/route-element-builder.d.ts +16 -6
  337. package/dist/server/route-element-builder.d.ts.map +1 -1
  338. package/dist/server/route-handler.d.ts +1 -1
  339. package/dist/server/route-matcher.d.ts +4 -4
  340. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  341. package/dist/server/rsc-entry/action-middleware-runner.d.ts +2 -2
  342. package/dist/server/rsc-entry/api-handler.d.ts +1 -1
  343. package/dist/server/rsc-entry/api-handler.d.ts.map +1 -1
  344. package/dist/server/rsc-entry/deny-fallback.d.ts +3 -3
  345. package/dist/server/rsc-entry/error-renderer.d.ts +4 -4
  346. package/dist/server/rsc-entry/helpers.d.ts +6 -6
  347. package/dist/server/rsc-entry/index.d.ts +7 -6
  348. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  349. package/dist/server/rsc-entry/render-route.d.ts +4 -4
  350. package/dist/server/rsc-entry/revalidate-renderer.d.ts +4 -4
  351. package/dist/server/rsc-entry/rsc-payload.d.ts +4 -4
  352. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  353. package/dist/server/rsc-entry/rsc-stream.d.ts +3 -3
  354. package/dist/server/rsc-entry/ssr-bridge.d.ts +1 -1
  355. package/dist/server/rsc-entry/ssr-renderer.d.ts +7 -7
  356. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +5 -5
  357. package/dist/server/sitemap-generator.d.ts +2 -2
  358. package/dist/server/sitemap-handler.d.ts +1 -1
  359. package/dist/server/skippable-prefix.d.ts +3 -3
  360. package/dist/server/slot-interception.d.ts +2 -2
  361. package/dist/server/slot-resolver.d.ts +3 -7
  362. package/dist/server/slot-resolver.d.ts.map +1 -1
  363. package/dist/server/ssr-bridge-types.d.ts +2 -2
  364. package/dist/server/ssr-entry.d.ts +1 -1
  365. package/dist/server/static-generator.d.ts +1 -1
  366. package/dist/server/static-not-found.d.ts +1 -1
  367. package/dist/server/status-code-resolver.d.ts +1 -1
  368. package/dist/server/tracing.d.ts +3 -3
  369. package/dist/server/tree-builder.d.ts +3 -3
  370. package/dist/server/tree-match.d.ts +1 -1
  371. package/dist/server/types.d.ts +2 -2
  372. package/dist/server/utils/mdx-file.d.ts +2 -0
  373. package/dist/server/utils/mdx-file.d.ts.map +1 -1
  374. package/dist/server/workers-cache-bridge.d.ts +1 -1
  375. package/dist/shared/als-slots.d.ts +1 -1
  376. package/dist/shared/payload-root.d.ts +2 -2
  377. package/dist/shared/rsc-media-type.d.ts +12 -0
  378. package/dist/shared/rsc-media-type.d.ts.map +1 -1
  379. package/dist/shared/slot-params.d.ts +1 -1
  380. package/dist/shims/font-google.d.ts +1 -1
  381. package/dist/shims/font-google.js.map +1 -1
  382. package/dist/shims/font-local.d.ts +1 -1
  383. package/dist/shims/font-local.js.map +1 -1
  384. package/dist/shims/navigation-client.d.ts +4 -4
  385. package/dist/shims/navigation.d.ts +2 -2
  386. package/docs/api/36-cli.mdx +23 -3
  387. package/docs/learn/11-error-handling.mdx +11 -0
  388. package/docs/more/01-advanced-routing.mdx +1 -0
  389. package/package.json +17 -16
  390. package/src/adapters/build-output-helper.ts +4 -4
  391. package/src/adapters/cloudflare-dev.ts +7 -1
  392. package/src/adapters/cloudflare-kv-cache.ts +11 -8
  393. package/src/adapters/cloudflare.ts +21 -15
  394. package/src/adapters/compress-module.ts +2 -2
  395. package/src/adapters/nitro-presets.ts +139 -0
  396. package/src/adapters/nitro-preview.ts +340 -0
  397. package/src/adapters/nitro.ts +100 -465
  398. package/src/adapters/shared.ts +1 -1
  399. package/src/analyze/chains.ts +411 -0
  400. package/src/analyze/classify.ts +258 -0
  401. package/src/analyze/crawl-entry.ts +68 -0
  402. package/src/analyze/crawl.ts +500 -0
  403. package/src/analyze/graph-cache.ts +332 -0
  404. package/src/analyze/graph-command.ts +762 -0
  405. package/src/analyze/live-graph.ts +232 -0
  406. package/src/analyze/poison-rules.ts +165 -0
  407. package/src/analyze/poison-scan.ts +162 -0
  408. package/src/analyze/scan.ts +149 -0
  409. package/src/cache/cache-api.ts +7 -7
  410. package/src/cache/index.ts +9 -9
  411. package/src/cache/redis-handler.ts +2 -2
  412. package/src/cache/stores/cloudflare-kv.ts +1 -1
  413. package/src/cache/stores/memory.ts +2 -2
  414. package/src/cache/stores/redis.ts +1 -1
  415. package/src/cache/stores/vercel.ts +1 -1
  416. package/src/cache/tag-aware-handler.ts +2 -2
  417. package/src/cache/timber-cache.ts +8 -8
  418. package/src/cdn/cloudflare-purge.ts +1 -1
  419. package/src/cdn/fastly-purge.ts +1 -1
  420. package/src/cdn/index.ts +10 -4
  421. package/src/cdn/workers-cache-purge.ts +6 -2
  422. package/src/cli-check.ts +53 -4
  423. package/src/cli-schema-sync.ts +3 -3
  424. package/src/cli.ts +50 -11
  425. package/src/client/browser-dev.ts +1 -1
  426. package/src/client/browser-entry/action-dispatch.ts +5 -5
  427. package/src/client/browser-entry/hmr.ts +1 -1
  428. package/src/client/browser-entry/hydrate.ts +9 -9
  429. package/src/client/browser-entry/index.ts +10 -10
  430. package/src/client/browser-entry/post-hydration.ts +5 -5
  431. package/src/client/browser-entry/router-init.ts +9 -9
  432. package/src/client/browser-entry/rsc-stream.ts +3 -3
  433. package/src/client/error-boundary.tsx +4 -4
  434. package/src/client/form.tsx +7 -2
  435. package/src/client/history.ts +2 -2
  436. package/src/client/index.ts +21 -18
  437. package/src/client/internal.ts +19 -19
  438. package/src/client/link.tsx +9 -9
  439. package/src/client/navigation-api.ts +1 -1
  440. package/src/client/navigation-commit.ts +5 -5
  441. package/src/client/navigation-context.ts +1 -1
  442. package/src/client/navigation-root.tsx +1 -1
  443. package/src/client/nuqs-adapter.tsx +2 -2
  444. package/src/client/params-context.ts +4 -4
  445. package/src/client/router-effects.ts +3 -3
  446. package/src/client/router-lifecycle.ts +2 -2
  447. package/src/client/router-pipeline.ts +10 -10
  448. package/src/client/router-ref.ts +2 -2
  449. package/src/client/router-skew.ts +2 -2
  450. package/src/client/router-types.ts +6 -6
  451. package/src/client/router.ts +13 -13
  452. package/src/client/rsc-fetch.ts +11 -16
  453. package/src/client/segment-cache.ts +2 -2
  454. package/src/client/segment-outlet.tsx +1 -1
  455. package/src/client/shallow-url.ts +1 -1
  456. package/src/client/slot-outlet.tsx +1 -1
  457. package/src/client/slot-provider.tsx +1 -1
  458. package/src/client/ssr-data.ts +3 -3
  459. package/src/client/stale-client.ts +1 -1
  460. package/src/client/state.ts +4 -4
  461. package/src/client/top-loader.tsx +1 -1
  462. package/src/client/unload-guard.ts +1 -1
  463. package/src/client/use-cookie.ts +2 -2
  464. package/src/client/use-pathname.ts +2 -2
  465. package/src/client/use-pending-navigation.ts +1 -1
  466. package/src/client/use-query-states.ts +3 -3
  467. package/src/client/use-router.ts +2 -2
  468. package/src/client/use-search-params.ts +3 -3
  469. package/src/client/use-segment-params.ts +6 -6
  470. package/src/client/use-selected-layout-segment.ts +2 -2
  471. package/src/codec.ts +2 -2
  472. package/src/config-validation.ts +1 -1
  473. package/src/cookies/define-cookie.ts +6 -6
  474. package/src/cookies/index.ts +3 -3
  475. package/src/cookies/json-cookie.ts +1 -1
  476. package/src/dev-tools/404-page.ts +4 -4
  477. package/src/dev-tools/dev-page-shell.ts +1 -1
  478. package/src/dev-tools/error-page.ts +4 -4
  479. package/src/dev-tools/index.ts +12 -12
  480. package/src/dev-tools/instrumentation.ts +1 -1
  481. package/src/dev-tools/logs.ts +1 -1
  482. package/src/dev-tools/overlay.ts +3 -3
  483. package/src/dev-tools/terminal.ts +2 -2
  484. package/src/fonts/ast.ts +5 -5
  485. package/src/fonts/bundle.ts +5 -5
  486. package/src/fonts/css.ts +1 -1
  487. package/src/fonts/dev-middleware.ts +2 -2
  488. package/src/fonts/google.ts +2 -2
  489. package/src/fonts/local.ts +4 -4
  490. package/src/fonts/pipeline.ts +8 -8
  491. package/src/fonts/transform.ts +5 -5
  492. package/src/fonts/types.ts +1 -1
  493. package/src/fonts/virtual-modules.ts +1 -1
  494. package/src/index.ts +46 -37
  495. package/src/params/codec-registry.ts +1 -1
  496. package/src/params/define-schema.ts +2 -2
  497. package/src/params/index.ts +4 -4
  498. package/src/params/resolve-schema.ts +2 -2
  499. package/src/plugin-context.ts +10 -10
  500. package/src/plugins/adapter-build.ts +5 -5
  501. package/src/plugins/build-manifest.ts +4 -4
  502. package/src/plugins/build-report.ts +6 -6
  503. package/src/plugins/cache.ts +3 -3
  504. package/src/plugins/callsite-ast.ts +1 -1
  505. package/src/plugins/callsite-jsx.ts +2 -2
  506. package/src/plugins/chunks.ts +1 -1
  507. package/src/plugins/cloudflare-wasm.ts +1 -1
  508. package/src/plugins/content.ts +5 -1
  509. package/src/plugins/dev-discovery.ts +218 -0
  510. package/src/plugins/dev-server.ts +97 -12
  511. package/src/plugins/entries.ts +1 -1
  512. package/src/plugins/fonts.ts +17 -10
  513. package/src/plugins/graph-endpoint.ts +532 -0
  514. package/src/plugins/mdx.ts +4 -3
  515. package/src/plugins/prebuilt-capture.ts +3 -3
  516. package/src/plugins/prebuilt-options-analysis.ts +1 -1
  517. package/src/plugins/prebuilt.ts +7 -7
  518. package/src/plugins/prerender-sugar.ts +11 -19
  519. package/src/plugins/request-dep/analysis.ts +1 -1
  520. package/src/plugins/request-dep/index.ts +8 -8
  521. package/src/plugins/request-dep/origin.ts +1 -1
  522. package/src/plugins/routing.ts +12 -7
  523. package/src/plugins/server-bundle.ts +9 -2
  524. package/src/plugins/shims.ts +16 -31
  525. package/src/plugins/static-build.ts +10 -10
  526. package/src/routing/codegen-shared.ts +1 -1
  527. package/src/routing/codegen-write.ts +3 -3
  528. package/src/routing/codegen.ts +14 -5
  529. package/src/routing/collision-probe.ts +3 -3
  530. package/src/routing/collision-spaces.ts +3 -3
  531. package/src/routing/convention-lint.ts +136 -6
  532. package/src/routing/export-detect.ts +1 -1
  533. package/src/routing/index.ts +13 -13
  534. package/src/routing/interception.ts +41 -4
  535. package/src/routing/link-codegen.ts +3 -3
  536. package/src/routing/manifest-codegen.ts +2 -2
  537. package/src/routing/scanner.ts +33 -9
  538. package/src/routing/schema-validation.ts +4 -4
  539. package/src/routing/segment-classify.ts +2 -2
  540. package/src/routing/segment-codegen.ts +1 -1
  541. package/src/routing/segment-keys.ts +1 -1
  542. package/src/routing/slot-placement.ts +1 -1
  543. package/src/routing/walkers.ts +1 -1
  544. package/src/rsc-runtime/transforms.ts +13 -0
  545. package/src/rsc-runtime/vendor-types.d.ts +12 -0
  546. package/src/schema-bridge.ts +1 -1
  547. package/src/search-params/define.ts +8 -8
  548. package/src/search-params/index.ts +3 -3
  549. package/src/search-params/wrappers.ts +3 -3
  550. package/src/server/access-gate.tsx +6 -6
  551. package/src/server/action-client.ts +5 -5
  552. package/src/server/action-handler.ts +16 -16
  553. package/src/server/actions.ts +5 -5
  554. package/src/server/als-registry.ts +8 -8
  555. package/src/server/asset-headers.ts +2 -2
  556. package/src/server/chain-url-parts.ts +5 -5
  557. package/src/server/children-interception.ts +4 -4
  558. package/src/server/compress.ts +1 -1
  559. package/src/server/cookie-context.ts +4 -4
  560. package/src/server/cookie-parsing.ts +2 -2
  561. package/src/server/default-logger.ts +3 -3
  562. package/src/server/deny-boundary.ts +10 -10
  563. package/src/server/deny-renderer.ts +38 -25
  564. package/src/server/early-hints-sender.ts +2 -2
  565. package/src/server/early-hints.ts +2 -2
  566. package/src/server/error-boundary-wrapper.ts +4 -12
  567. package/src/server/fallback-error.ts +13 -13
  568. package/src/server/flight-injection-state.ts +1 -1
  569. package/src/server/form-flash.ts +2 -2
  570. package/src/server/html-injector-core.ts +4 -4
  571. package/src/server/html-injectors.ts +5 -5
  572. package/src/server/index.ts +18 -18
  573. package/src/server/instrumentation.ts +1 -1
  574. package/src/server/internal.ts +50 -50
  575. package/src/server/logger.ts +4 -4
  576. package/src/server/metadata-collector.ts +7 -7
  577. package/src/server/metadata-platform.ts +2 -2
  578. package/src/server/metadata-render.ts +4 -4
  579. package/src/server/metadata-routes.ts +3 -3
  580. package/src/server/metadata-social.ts +2 -2
  581. package/src/server/metadata.ts +2 -2
  582. package/src/server/middleware-runner.ts +1 -1
  583. package/src/server/node-stream-transforms.ts +4 -4
  584. package/src/server/param-coercion.ts +8 -8
  585. package/src/server/pipeline-helpers.ts +7 -7
  586. package/src/server/pipeline-interception.ts +2 -2
  587. package/src/server/pipeline-metadata.ts +1 -1
  588. package/src/server/pipeline-outcome.ts +8 -8
  589. package/src/server/pipeline-phases.ts +24 -28
  590. package/src/server/pipeline.ts +22 -22
  591. package/src/server/prebuilt/cache-key.ts +1 -1
  592. package/src/server/prebuilt/capture-state.ts +1 -1
  593. package/src/server/prebuilt/key-discipline.ts +1 -1
  594. package/src/server/prebuilt/manifest-tag-index.ts +1 -1
  595. package/src/server/prebuilt/overlay.ts +6 -6
  596. package/src/server/prebuilt/payload-source.ts +2 -2
  597. package/src/server/prebuilt/slots.ts +5 -5
  598. package/src/server/prebuilt/synthetic-store.ts +2 -2
  599. package/src/server/prebuilt-builder.ts +16 -16
  600. package/src/server/prebuilt-runtime.ts +12 -12
  601. package/src/server/primitives.ts +7 -7
  602. package/src/server/publish-params.ts +3 -3
  603. package/src/server/request-context.ts +7 -7
  604. package/src/server/route-element-builder.ts +108 -50
  605. package/src/server/route-handler.ts +4 -4
  606. package/src/server/route-matcher.ts +5 -5
  607. package/src/server/rsc-cache-key-guard.ts +3 -5
  608. package/src/server/rsc-entry/action-middleware-runner.ts +7 -7
  609. package/src/server/rsc-entry/api-handler.ts +18 -9
  610. package/src/server/rsc-entry/deny-fallback.ts +10 -10
  611. package/src/server/rsc-entry/error-renderer.ts +27 -27
  612. package/src/server/rsc-entry/helpers.ts +10 -10
  613. package/src/server/rsc-entry/index.ts +51 -47
  614. package/src/server/rsc-entry/render-route.ts +20 -20
  615. package/src/server/rsc-entry/revalidate-renderer.ts +12 -12
  616. package/src/server/rsc-entry/rsc-payload.ts +19 -15
  617. package/src/server/rsc-entry/rsc-stream.ts +8 -8
  618. package/src/server/rsc-entry/ssr-bridge.ts +3 -3
  619. package/src/server/rsc-entry/ssr-renderer.ts +23 -23
  620. package/src/server/rsc-entry/wrap-action-dispatch.ts +9 -9
  621. package/src/server/rsc-prop-warnings.ts +1 -1
  622. package/src/server/sensitive-fields.ts +1 -1
  623. package/src/server/server-timing.ts +1 -1
  624. package/src/server/sitemap-generator.ts +3 -3
  625. package/src/server/sitemap-handler.ts +3 -3
  626. package/src/server/skippable-prefix.ts +7 -7
  627. package/src/server/slot-interception.ts +5 -5
  628. package/src/server/slot-resolver.ts +20 -28
  629. package/src/server/ssr-bridge-types.ts +2 -2
  630. package/src/server/ssr-entry.ts +18 -18
  631. package/src/server/ssr-render.ts +4 -4
  632. package/src/server/ssr-wrappers.tsx +1 -1
  633. package/src/server/state-tree-diff.ts +1 -1
  634. package/src/server/static-generator.ts +6 -6
  635. package/src/server/static-not-found.ts +3 -3
  636. package/src/server/status-code-resolver.ts +1 -1
  637. package/src/server/tracing.ts +5 -5
  638. package/src/server/tree-builder.ts +4 -4
  639. package/src/server/tree-match.ts +1 -1
  640. package/src/server/types.ts +2 -2
  641. package/src/server/utils/mdx-file.ts +2 -2
  642. package/src/server/waituntil-bridge.ts +1 -1
  643. package/src/server/workers-cache-bridge.ts +2 -2
  644. package/src/shared/als-slots.ts +1 -1
  645. package/src/shared/payload-root.ts +2 -2
  646. package/src/shared/rsc-media-type.ts +16 -0
  647. package/src/shared/slot-params.ts +1 -1
  648. package/src/shims/font-google.ts +1 -1
  649. package/src/shims/font-local.ts +1 -1
  650. package/src/shims/navigation-client.ts +5 -5
  651. package/src/shims/navigation.ts +2 -2
  652. package/LICENSE +0 -8
  653. package/dist/_chunks/actions-BjbbNRFN.js.map +0 -1
  654. package/dist/_chunks/build-output-helper-C3DrfzZR.js.map +0 -1
  655. package/dist/_chunks/cache-api-DllJ-Lyw.js.map +0 -1
  656. package/dist/_chunks/cli-check-DJZDc22E.js.map +0 -1
  657. package/dist/_chunks/convention-lint-CpteIpTm.js.map +0 -1
  658. package/dist/_chunks/dist-BA3u1z3W.js +0 -423
  659. package/dist/_chunks/dist-BA3u1z3W.js.map +0 -1
  660. package/dist/_chunks/mdx-file-C005ay-P.js.map +0 -1
  661. package/dist/_chunks/plugin-context-rCinWLiE.js.map +0 -1
  662. package/dist/_chunks/scanner-CAietmj4.js.map +0 -1
  663. package/dist/content/index.d.ts +0 -2
  664. package/dist/content/index.d.ts.map +0 -1
  665. package/dist/content/index.js +0 -0
  666. package/src/content/index.ts +0 -5
  667. package/src/shims/server-only-noop.js +0 -6
@@ -1 +1 @@
1
- {"version":3,"file":"segment-classify-C539Pa2O.js","names":[],"sources":["../../src/routing/types.ts","../../src/routing/segment-classify.ts"],"sourcesContent":["/**\n * Route tree types for timber.js file-system routing.\n *\n * The route tree is built by scanning the app/ directory and recognizing\n * file conventions (page.*, layout.*, middleware.ts, access.ts, route.ts, etc.).\n *\n * **Single shape, two specializations** (TIM-848):\n *\n * `SegmentNode<TFile>` is the one canonical in-memory shape for the\n * timber route tree. The same interface is used at build time (with\n * `TFile = RouteFile`) and at request time (with `TFile = ManifestFile`,\n * see `server/route-matcher.ts`). Walkers parameterized over `TFile`\n * work on either, eliminating the previous duplication between\n * `SegmentNode` (Map-based) and `ManifestSegmentNode` (object-based).\n *\n * Keyed groups (`slots`, `statusFiles`, `jsonStatusFiles`,\n * `metadataRoutes`) are plain `Record<string, …>`\n * objects rather than `Map`s so that the build-time tree can be\n * serialized into the virtual route manifest with no shape transform.\n *\n * See design/07-routing.md §\"Route Tree Shape\" and design/18-build-system.md\n * §\"Route Manifest Shape\".\n */\n\n/** Segment type classification */\nexport type SegmentType =\n | 'static' // e.g. \"dashboard\"\n | 'dynamic' // e.g. \"[id]\"\n | 'catch-all' // e.g. \"[...slug]\"\n | 'optional-catch-all' // e.g. \"[[...slug]]\"\n | 'group' // e.g. \"(marketing)\"\n | 'slot' // e.g. \"@sidebar\"\n | 'intercepting' // e.g. \"(.)photo\", \"(..)photo\", \"(...)photo\"\n | 'private'; // e.g. \"_components\", \"_lib\" — excluded from routing\n\n/**\n * Intercepting route marker — indicates how many levels up to resolve the\n * intercepted route from the intercepting route's location.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\nexport type InterceptionMarker = '(.)' | '(..)' | '(...)' | '(..)(..)';\n\n/** All recognized interception markers, ordered longest-first for parsing. */\nexport const INTERCEPTION_MARKERS: InterceptionMarker[] = ['(..)(..)', '(.)', '(..)', '(...)'];\n\n/**\n * A single file discovered in a route segment at build time.\n *\n * The runtime equivalent (`ManifestFile`, defined in\n * `server/route-matcher.ts`) replaces `extension` with a lazy `load`\n * function. Walkers that only need `filePath` are parameterized over\n * `TFile` and accept either.\n */\nexport interface RouteFile {\n /** Absolute path to the file */\n filePath: string;\n /** File extension without leading dot (e.g. \"tsx\", \"ts\", \"mdx\") */\n extension: string;\n}\n\n/**\n * A node in the segment tree.\n *\n * Generic over `TFile` so the same interface describes both the\n * build-time tree (`SegmentNode<RouteFile>`, the default) and the\n * runtime manifest tree (`SegmentNode<ManifestFile>`, aliased as\n * `ManifestSegmentNode`). All keyed groups use `Record` (not `Map`)\n * so the build-time tree serializes to the virtual route manifest\n * with no shape transform.\n */\nexport interface SegmentNode<TFile = RouteFile> {\n /** The raw directory name (e.g. \"dashboard\", \"[id]\", \"(auth)\", \"@sidebar\") */\n segmentName: string;\n /** Classified segment type */\n segmentType: SegmentType;\n /** The dynamic param name, if dynamic (e.g. \"id\" for \"[id]\", \"slug\" for \"[...slug]\") */\n paramName?: string;\n /** Literal prefix before the dynamic bracket (e.g. \"img-\" for \"img-[id].png\") */\n paramPrefix?: string;\n /** Literal suffix after the dynamic bracket (e.g. \".png\" for \"img-[id].png\") */\n paramSuffix?: string;\n /** The URL path prefix at this segment level (e.g. \"/dashboard\") */\n urlPath: string;\n /** For intercepting segments: the marker used, e.g. \"(.)\". */\n interceptionMarker?: InterceptionMarker;\n /**\n * For intercepting segments: the segment name after stripping the marker.\n * E.g., for \"(.)photo\" this is \"photo\".\n */\n interceptedSegmentName?: string;\n\n // --- File conventions ---\n page?: TFile;\n layout?: TFile;\n middleware?: TFile;\n access?: TFile;\n route?: TFile;\n error?: TFile;\n default?: TFile;\n /** Status-code files: 4xx.tsx, 5xx.tsx, {status}.tsx (component format) */\n statusFiles?: Record<string, TFile>;\n /** JSON status-code files: 4xx.json, 5xx.json, {status}.json */\n jsonStatusFiles?: Record<string, TFile>;\n /** denied.tsx — slot-only denial rendering */\n denied?: TFile;\n\n /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */\n metadataRoutes?: Record<string, TFile>;\n\n // --- Children ---\n children: SegmentNode<TFile>[];\n /** Parallel route slots (keyed by slot name without @) */\n slots: Record<string, SegmentNode<TFile>>;\n}\n\n/**\n * The full route tree output from the scanner (or the root of the\n * runtime route manifest, when `TFile = ManifestFile`).\n *\n * Generic so the same wrapper carries app-root metadata for both\n * shapes. The runtime manifest extends this with `viteRoot` (see\n * `ManifestRoot` in `server/route-matcher.ts`).\n */\nexport interface RouteTree<TFile = RouteFile> {\n /** The root segment node (representing app/) */\n root: SegmentNode<TFile>;\n /** All discovered proxy.ts files (should be at most one, in app/) */\n proxy?: TFile;\n /**\n * Global error page: app/global-error.{tsx,ts,jsx,js}\n *\n * Rendered as a standalone full-page replacement (no layout wrapping)\n * when no segment-level error file is found. SSR-only render path.\n * Must provide its own <html> and <body>.\n *\n * See design/10-error-handling.md §\"Tier 2 — Global Error Page\"\n */\n globalError?: TFile;\n}\n\n/** Configuration passed to the scanner */\nexport interface ScannerConfig {\n /** Recognized page/layout extensions (without dots). Default: ['tsx', 'ts', 'jsx', 'js'] */\n pageExtensions?: string[];\n}\n\n/** Default page extensions */\nexport const DEFAULT_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'];\n","/**\n * Shared segment classifier — both URL tokens and filesystem directory names.\n *\n * `classifyUrlSegment(token)` is a pure single-pass character parser that\n * classifies a route segment token (e.g. \"dashboard\", \"[id]\", \"[...slug]\",\n * \"[[...path]]\") into a typed discriminated union. NO regex, NO Node.js-only\n * APIs — safe to import from browser code (used by `Link` interpolation).\n *\n * `classifySegment(dirName)` is the build-time directory-name classifier\n * used by the scanner. It recognizes timber-only conventions (private\n * `_*`, parallel `@*`, route groups `(name)`, intercepting routes\n * `(.)`/`(..)`/`(...)`/`(..)(..)`) and delegates bracket syntax to\n * `classifyUrlSegment`. It is the **single source of truth** for what\n * counts as a routing segment — there is no separate copy in the\n * scanner. (TIM-848.)\n *\n * Malformed input falls through to `{ kind: 'static' }` — the safe default.\n *\n * If you change the bracket syntax, update ONLY this file. Every\n * consumer imports from here.\n *\n * See design/07-routing.md §\"Route Segments\"\n */\n\nimport type { InterceptionMarker, SegmentType } from './types.js';\nimport { INTERCEPTION_MARKERS } from './types.js';\n\nexport type UrlSegment =\n | { kind: 'static'; value: string }\n | { kind: 'dynamic'; name: string; prefix?: string; suffix?: string }\n | { kind: 'catch-all'; name: string }\n | { kind: 'optional-catch-all'; name: string };\n\n/**\n * Classify a URL path segment token.\n *\n * Walks the string left-to-right in one pass:\n * 1. Find the first '[' — characters before it are the prefix.\n * 2. Count opening brackets (1 or 2) to detect optional.\n * 3. Check for '...' to detect catch-all.\n * 4. Read the param name up to the closing bracket.\n * 5. Validate the expected closing sequence (']' or ']]').\n * 6. Characters after the close are the suffix.\n * 7. Reject affixes on catch-all/optional-catch-all.\n * 8. Reject affixes that contain '[' or ']' (no multi-param segments).\n *\n * Any structural violation → static (safe default).\n */\nexport function classifyUrlSegment(token: string): UrlSegment {\n const len = token.length;\n if (len === 0) return { kind: 'static', value: token };\n\n // Find first '[' — characters before it are the prefix\n const bracketStart = token.indexOf('[');\n if (bracketStart === -1) return { kind: 'static', value: token };\n\n const prefix = token.slice(0, bracketStart);\n\n let i = bracketStart + 1;\n\n // Check for optional: '[[...'\n const optional = i < len && token[i] === '[';\n if (optional) i++;\n\n // Check for catch-all: '...'\n const catchAll = i + 2 < len && token[i] === '.' && token[i + 1] === '.' && token[i + 2] === '.';\n if (catchAll) i += 3;\n\n // Read param name — everything up to ']'\n const nameStart = i;\n while (i < len && token[i] !== ']') i++;\n\n // Must have found a ']' and name must be non-empty\n if (i >= len || i === nameStart) {\n return { kind: 'static', value: token };\n }\n\n const name = token.slice(nameStart, i);\n i++; // skip first ']'\n\n // Optional requires a second ']'\n if (optional) {\n if (i >= len || token[i] !== ']') {\n return { kind: 'static', value: token };\n }\n i++;\n }\n\n const suffix = token.slice(i);\n\n // Reject affixes containing brackets (no multi-param segments like [foo]-[bar])\n if (suffix.includes('[') || suffix.includes(']')) {\n return { kind: 'static', value: token };\n }\n if (prefix.includes(']')) {\n return { kind: 'static', value: token };\n }\n\n const hasAffixes = prefix.length > 0 || suffix.length > 0;\n\n if (optional && catchAll) {\n // No affixes on optional catch-all\n if (hasAffixes) return { kind: 'static', value: token };\n return { kind: 'optional-catch-all', name };\n }\n if (catchAll) {\n // No affixes on catch-all\n if (hasAffixes) return { kind: 'static', value: token };\n return { kind: 'catch-all', name };\n }\n if (optional) {\n // '[[name]]' without '...' is malformed — not a valid segment syntax\n return { kind: 'static', value: token };\n }\n\n if (hasAffixes) {\n return {\n kind: 'dynamic',\n name,\n prefix: prefix || undefined,\n suffix: suffix || undefined,\n };\n }\n return { kind: 'dynamic', name };\n}\n\n// ─── Directory-name classifier (build-time scanner) ─────────────────────────\n\n/** Result of classifying a filesystem directory name. */\nexport interface SegmentClassification {\n type: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n interceptionMarker?: InterceptionMarker;\n interceptedSegmentName?: string;\n}\n\n/**\n * Classify a directory name into its segment type.\n *\n * Recognizes all timber file-system conventions in priority order:\n * 1. Private folders: `_name` (excluded from routing)\n * 2. Parallel route slots: `@name`\n * 3. Intercepting routes: `(.)name`, `(..)name`, `(...)name`, `(..)(..)name`\n * 4. Route groups: `(name)`\n * 5. Bracket syntax: `[id]`, `[...slug]`, `[[...path]]` (delegated to\n * `classifyUrlSegment`)\n * 6. Static: anything else\n *\n * If you change the bracket syntax, update only `classifyUrlSegment`.\n * If you change the directory-prefix conventions, update this function.\n */\nexport function classifySegment(dirName: string): SegmentClassification {\n // Private folder: _name (excluded from routing)\n if (dirName.startsWith('_')) {\n return { type: 'private' };\n }\n\n // Parallel route slot: @name\n if (dirName.startsWith('@')) {\n return { type: 'slot' };\n }\n\n // Intercepting routes: (.)name, (..)name, (...)name, (..)(..)name\n // Check before route groups since intercepting markers also start with (\n const interception = parseInterceptionMarker(dirName);\n if (interception) {\n return {\n type: 'intercepting',\n interceptionMarker: interception.marker,\n interceptedSegmentName: interception.segmentName,\n };\n }\n\n // Route group: (name)\n if (dirName.startsWith('(') && dirName.endsWith(')')) {\n return { type: 'group' };\n }\n\n // Bracket-syntax segments: [param], [...param], [[...param]]\n const urlSeg = classifyUrlSegment(dirName);\n if (urlSeg.kind !== 'static') {\n const result: SegmentClassification = { type: urlSeg.kind, paramName: urlSeg.name };\n if (urlSeg.kind === 'dynamic') {\n if (urlSeg.prefix) result.paramPrefix = urlSeg.prefix;\n if (urlSeg.suffix) result.paramSuffix = urlSeg.suffix;\n }\n return result;\n }\n\n return { type: 'static' };\n}\n\n/**\n * The URL-matching identity of a segment node.\n *\n * For every node except an intercepting one this is the node itself. An\n * intercepting node is different: its directory name carries a marker\n * (`(.)photo`, `(.)[id]`), so the node's own `segmentType` is\n * `'intercepting'` and the bracket syntax of the segment it intercepts was\n * never classified. Everything that has to reason about *the URL part this\n * node stands for* — matching it, and keying its param for codec coercion —\n * needs that classification.\n *\n * Deriving it here, from `interceptedSegmentName`, keeps interception out\n * of `classifySegment`'s output and out of the serialized manifest: there\n * is no second copy of the classification to drift from this one. The\n * returned `segmentName` is the intercepted name (`photo`, `[id]`), NOT the\n * node's directory name — callers that need the directory name for tree\n * paths must keep reading the node. See TIM-1281.\n */\nexport interface UrlSegmentIdentity {\n segmentName: string;\n segmentType: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n}\n\nexport function effectiveUrlSegment(node: {\n segmentName: string;\n segmentType: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n interceptedSegmentName?: string;\n}): UrlSegmentIdentity {\n if (node.segmentType !== 'intercepting' || !node.interceptedSegmentName) {\n return {\n segmentName: node.segmentName,\n segmentType: node.segmentType,\n paramName: node.paramName,\n paramPrefix: node.paramPrefix,\n paramSuffix: node.paramSuffix,\n };\n }\n\n const seg = classifyUrlSegment(node.interceptedSegmentName);\n if (seg.kind === 'static') {\n return { segmentName: seg.value, segmentType: 'static' };\n }\n return {\n segmentName: node.interceptedSegmentName,\n segmentType: seg.kind,\n paramName: seg.name,\n paramPrefix: seg.kind === 'dynamic' ? seg.prefix : undefined,\n paramSuffix: seg.kind === 'dynamic' ? seg.suffix : undefined,\n };\n}\n\n/**\n * Parse an interception marker from a directory name.\n *\n * Returns the marker and the remaining segment name, or null if not an\n * intercepting route. Markers are checked longest-first to avoid `(..)`\n * matching before `(..)(..)`.\n *\n * Examples:\n * \"(.)photo\" → { marker: \"(.)\", segmentName: \"photo\" }\n * \"(..)feed\" → { marker: \"(..)\", segmentName: \"feed\" }\n * \"(...)photos\" → { marker: \"(...)\", segmentName: \"photos\" }\n * \"(..)(..)admin\" → { marker: \"(..)(..)\", segmentName: \"admin\" }\n * \"(marketing)\" → null (route group, not interception)\n */\nfunction parseInterceptionMarker(\n dirName: string\n): { marker: InterceptionMarker; segmentName: string } | null {\n for (const marker of INTERCEPTION_MARKERS) {\n if (dirName.startsWith(marker)) {\n const rest = dirName.slice(marker.length);\n // Must have a segment name after the marker, and the rest must not\n // be empty or end with ) (which would be a route group like \"(auth)\")\n if (rest.length > 0 && !rest.endsWith(')')) {\n return { marker, segmentName: rest };\n }\n }\n }\n return null;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"segment-classify-C539Pa2O.js","names":[],"sources":["../../src/routing/types.ts","../../src/routing/segment-classify.ts"],"sourcesContent":["/**\n * Route tree types for timber.js file-system routing.\n *\n * The route tree is built by scanning the app/ directory and recognizing\n * file conventions (page.*, layout.*, middleware.ts, access.ts, route.ts, etc.).\n *\n * **Single shape, two specializations** (TIM-848):\n *\n * `SegmentNode<TFile>` is the one canonical in-memory shape for the\n * timber route tree. The same interface is used at build time (with\n * `TFile = RouteFile`) and at request time (with `TFile = ManifestFile`,\n * see `server/route-matcher.ts`). Walkers parameterized over `TFile`\n * work on either, eliminating the previous duplication between\n * `SegmentNode` (Map-based) and `ManifestSegmentNode` (object-based).\n *\n * Keyed groups (`slots`, `statusFiles`, `jsonStatusFiles`,\n * `metadataRoutes`) are plain `Record<string, …>`\n * objects rather than `Map`s so that the build-time tree can be\n * serialized into the virtual route manifest with no shape transform.\n *\n * See design/07-routing.md §\"Route Tree Shape\" and design/18-build-system.md\n * §\"Route Manifest Shape\".\n */\n\n/** Segment type classification */\nexport type SegmentType =\n | 'static' // e.g. \"dashboard\"\n | 'dynamic' // e.g. \"[id]\"\n | 'catch-all' // e.g. \"[...slug]\"\n | 'optional-catch-all' // e.g. \"[[...slug]]\"\n | 'group' // e.g. \"(marketing)\"\n | 'slot' // e.g. \"@sidebar\"\n | 'intercepting' // e.g. \"(.)photo\", \"(..)photo\", \"(...)photo\"\n | 'private'; // e.g. \"_components\", \"_lib\" — excluded from routing\n\n/**\n * Intercepting route marker — indicates how many levels up to resolve the\n * intercepted route from the intercepting route's location.\n *\n * See design/07-routing.md §\"Intercepting Routes\"\n */\nexport type InterceptionMarker = '(.)' | '(..)' | '(...)' | '(..)(..)';\n\n/** All recognized interception markers, ordered longest-first for parsing. */\nexport const INTERCEPTION_MARKERS: InterceptionMarker[] = ['(..)(..)', '(.)', '(..)', '(...)'];\n\n/**\n * A single file discovered in a route segment at build time.\n *\n * The runtime equivalent (`ManifestFile`, defined in\n * `server/route-matcher.ts`) replaces `extension` with a lazy `load`\n * function. Walkers that only need `filePath` are parameterized over\n * `TFile` and accept either.\n */\nexport interface RouteFile {\n /** Absolute path to the file */\n filePath: string;\n /** File extension without leading dot (e.g. \"tsx\", \"ts\", \"mdx\") */\n extension: string;\n}\n\n/**\n * A node in the segment tree.\n *\n * Generic over `TFile` so the same interface describes both the\n * build-time tree (`SegmentNode<RouteFile>`, the default) and the\n * runtime manifest tree (`SegmentNode<ManifestFile>`, aliased as\n * `ManifestSegmentNode`). All keyed groups use `Record` (not `Map`)\n * so the build-time tree serializes to the virtual route manifest\n * with no shape transform.\n */\nexport interface SegmentNode<TFile = RouteFile> {\n /** The raw directory name (e.g. \"dashboard\", \"[id]\", \"(auth)\", \"@sidebar\") */\n segmentName: string;\n /** Classified segment type */\n segmentType: SegmentType;\n /** The dynamic param name, if dynamic (e.g. \"id\" for \"[id]\", \"slug\" for \"[...slug]\") */\n paramName?: string;\n /** Literal prefix before the dynamic bracket (e.g. \"img-\" for \"img-[id].png\") */\n paramPrefix?: string;\n /** Literal suffix after the dynamic bracket (e.g. \".png\" for \"img-[id].png\") */\n paramSuffix?: string;\n /** The URL path prefix at this segment level (e.g. \"/dashboard\") */\n urlPath: string;\n /** For intercepting segments: the marker used, e.g. \"(.)\". */\n interceptionMarker?: InterceptionMarker;\n /**\n * For intercepting segments: the segment name after stripping the marker.\n * E.g., for \"(.)photo\" this is \"photo\".\n */\n interceptedSegmentName?: string;\n\n // --- File conventions ---\n page?: TFile;\n layout?: TFile;\n middleware?: TFile;\n access?: TFile;\n route?: TFile;\n error?: TFile;\n default?: TFile;\n /** Status-code files: 4xx.tsx, 5xx.tsx, {status}.tsx (component format) */\n statusFiles?: Record<string, TFile>;\n /** JSON status-code files: 4xx.json, 5xx.json, {status}.json */\n jsonStatusFiles?: Record<string, TFile>;\n /** denied.tsx — slot-only denial rendering */\n denied?: TFile;\n\n /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */\n metadataRoutes?: Record<string, TFile>;\n\n // --- Children ---\n children: SegmentNode<TFile>[];\n /** Parallel route slots (keyed by slot name without @) */\n slots: Record<string, SegmentNode<TFile>>;\n}\n\n/**\n * The full route tree output from the scanner (or the root of the\n * runtime route manifest, when `TFile = ManifestFile`).\n *\n * Generic so the same wrapper carries app-root metadata for both\n * shapes. The runtime manifest extends this with `viteRoot` (see\n * `ManifestRoot` in `server/route-matcher.ts`).\n */\nexport interface RouteTree<TFile = RouteFile> {\n /** The root segment node (representing app/) */\n root: SegmentNode<TFile>;\n /** All discovered proxy.ts files (should be at most one, in app/) */\n proxy?: TFile;\n /**\n * Global error page: app/global-error.{tsx,ts,jsx,js}\n *\n * Rendered as a standalone full-page replacement (no layout wrapping)\n * when no segment-level error file is found. SSR-only render path.\n * Must provide its own <html> and <body>.\n *\n * See design/10-error-handling.md §\"Tier 2 — Global Error Page\"\n */\n globalError?: TFile;\n}\n\n/** Configuration passed to the scanner */\nexport interface ScannerConfig {\n /** Recognized page/layout extensions (without dots). Default: ['tsx', 'ts', 'jsx', 'js'] */\n pageExtensions?: string[];\n}\n\n/** Default page extensions */\nexport const DEFAULT_PAGE_EXTENSIONS = ['tsx', 'ts', 'jsx', 'js'];\n","/**\n * Shared segment classifier — both URL tokens and filesystem directory names.\n *\n * `classifyUrlSegment(token)` is a pure single-pass character parser that\n * classifies a route segment token (e.g. \"dashboard\", \"[id]\", \"[...slug]\",\n * \"[[...path]]\") into a typed discriminated union. NO regex, NO Node.js-only\n * APIs — safe to import from browser code (used by `Link` interpolation).\n *\n * `classifySegment(dirName)` is the build-time directory-name classifier\n * used by the scanner. It recognizes timber-only conventions (private\n * `_*`, parallel `@*`, route groups `(name)`, intercepting routes\n * `(.)`/`(..)`/`(...)`/`(..)(..)`) and delegates bracket syntax to\n * `classifyUrlSegment`. It is the **single source of truth** for what\n * counts as a routing segment — there is no separate copy in the\n * scanner. (TIM-848.)\n *\n * Malformed input falls through to `{ kind: 'static' }` — the safe default.\n *\n * If you change the bracket syntax, update ONLY this file. Every\n * consumer imports from here.\n *\n * See design/07-routing.md §\"Route Segments\"\n */\n\nimport type { InterceptionMarker, SegmentType } from './types.ts';\nimport { INTERCEPTION_MARKERS } from './types.ts';\n\nexport type UrlSegment =\n | { kind: 'static'; value: string }\n | { kind: 'dynamic'; name: string; prefix?: string; suffix?: string }\n | { kind: 'catch-all'; name: string }\n | { kind: 'optional-catch-all'; name: string };\n\n/**\n * Classify a URL path segment token.\n *\n * Walks the string left-to-right in one pass:\n * 1. Find the first '[' — characters before it are the prefix.\n * 2. Count opening brackets (1 or 2) to detect optional.\n * 3. Check for '...' to detect catch-all.\n * 4. Read the param name up to the closing bracket.\n * 5. Validate the expected closing sequence (']' or ']]').\n * 6. Characters after the close are the suffix.\n * 7. Reject affixes on catch-all/optional-catch-all.\n * 8. Reject affixes that contain '[' or ']' (no multi-param segments).\n *\n * Any structural violation → static (safe default).\n */\nexport function classifyUrlSegment(token: string): UrlSegment {\n const len = token.length;\n if (len === 0) return { kind: 'static', value: token };\n\n // Find first '[' — characters before it are the prefix\n const bracketStart = token.indexOf('[');\n if (bracketStart === -1) return { kind: 'static', value: token };\n\n const prefix = token.slice(0, bracketStart);\n\n let i = bracketStart + 1;\n\n // Check for optional: '[[...'\n const optional = i < len && token[i] === '[';\n if (optional) i++;\n\n // Check for catch-all: '...'\n const catchAll = i + 2 < len && token[i] === '.' && token[i + 1] === '.' && token[i + 2] === '.';\n if (catchAll) i += 3;\n\n // Read param name — everything up to ']'\n const nameStart = i;\n while (i < len && token[i] !== ']') i++;\n\n // Must have found a ']' and name must be non-empty\n if (i >= len || i === nameStart) {\n return { kind: 'static', value: token };\n }\n\n const name = token.slice(nameStart, i);\n i++; // skip first ']'\n\n // Optional requires a second ']'\n if (optional) {\n if (i >= len || token[i] !== ']') {\n return { kind: 'static', value: token };\n }\n i++;\n }\n\n const suffix = token.slice(i);\n\n // Reject affixes containing brackets (no multi-param segments like [foo]-[bar])\n if (suffix.includes('[') || suffix.includes(']')) {\n return { kind: 'static', value: token };\n }\n if (prefix.includes(']')) {\n return { kind: 'static', value: token };\n }\n\n const hasAffixes = prefix.length > 0 || suffix.length > 0;\n\n if (optional && catchAll) {\n // No affixes on optional catch-all\n if (hasAffixes) return { kind: 'static', value: token };\n return { kind: 'optional-catch-all', name };\n }\n if (catchAll) {\n // No affixes on catch-all\n if (hasAffixes) return { kind: 'static', value: token };\n return { kind: 'catch-all', name };\n }\n if (optional) {\n // '[[name]]' without '...' is malformed — not a valid segment syntax\n return { kind: 'static', value: token };\n }\n\n if (hasAffixes) {\n return {\n kind: 'dynamic',\n name,\n prefix: prefix || undefined,\n suffix: suffix || undefined,\n };\n }\n return { kind: 'dynamic', name };\n}\n\n// ─── Directory-name classifier (build-time scanner) ─────────────────────────\n\n/** Result of classifying a filesystem directory name. */\nexport interface SegmentClassification {\n type: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n interceptionMarker?: InterceptionMarker;\n interceptedSegmentName?: string;\n}\n\n/**\n * Classify a directory name into its segment type.\n *\n * Recognizes all timber file-system conventions in priority order:\n * 1. Private folders: `_name` (excluded from routing)\n * 2. Parallel route slots: `@name`\n * 3. Intercepting routes: `(.)name`, `(..)name`, `(...)name`, `(..)(..)name`\n * 4. Route groups: `(name)`\n * 5. Bracket syntax: `[id]`, `[...slug]`, `[[...path]]` (delegated to\n * `classifyUrlSegment`)\n * 6. Static: anything else\n *\n * If you change the bracket syntax, update only `classifyUrlSegment`.\n * If you change the directory-prefix conventions, update this function.\n */\nexport function classifySegment(dirName: string): SegmentClassification {\n // Private folder: _name (excluded from routing)\n if (dirName.startsWith('_')) {\n return { type: 'private' };\n }\n\n // Parallel route slot: @name\n if (dirName.startsWith('@')) {\n return { type: 'slot' };\n }\n\n // Intercepting routes: (.)name, (..)name, (...)name, (..)(..)name\n // Check before route groups since intercepting markers also start with (\n const interception = parseInterceptionMarker(dirName);\n if (interception) {\n return {\n type: 'intercepting',\n interceptionMarker: interception.marker,\n interceptedSegmentName: interception.segmentName,\n };\n }\n\n // Route group: (name)\n if (dirName.startsWith('(') && dirName.endsWith(')')) {\n return { type: 'group' };\n }\n\n // Bracket-syntax segments: [param], [...param], [[...param]]\n const urlSeg = classifyUrlSegment(dirName);\n if (urlSeg.kind !== 'static') {\n const result: SegmentClassification = { type: urlSeg.kind, paramName: urlSeg.name };\n if (urlSeg.kind === 'dynamic') {\n if (urlSeg.prefix) result.paramPrefix = urlSeg.prefix;\n if (urlSeg.suffix) result.paramSuffix = urlSeg.suffix;\n }\n return result;\n }\n\n return { type: 'static' };\n}\n\n/**\n * The URL-matching identity of a segment node.\n *\n * For every node except an intercepting one this is the node itself. An\n * intercepting node is different: its directory name carries a marker\n * (`(.)photo`, `(.)[id]`), so the node's own `segmentType` is\n * `'intercepting'` and the bracket syntax of the segment it intercepts was\n * never classified. Everything that has to reason about *the URL part this\n * node stands for* — matching it, and keying its param for codec coercion —\n * needs that classification.\n *\n * Deriving it here, from `interceptedSegmentName`, keeps interception out\n * of `classifySegment`'s output and out of the serialized manifest: there\n * is no second copy of the classification to drift from this one. The\n * returned `segmentName` is the intercepted name (`photo`, `[id]`), NOT the\n * node's directory name — callers that need the directory name for tree\n * paths must keep reading the node. See TIM-1281.\n */\nexport interface UrlSegmentIdentity {\n segmentName: string;\n segmentType: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n}\n\nexport function effectiveUrlSegment(node: {\n segmentName: string;\n segmentType: SegmentType;\n paramName?: string;\n paramPrefix?: string;\n paramSuffix?: string;\n interceptedSegmentName?: string;\n}): UrlSegmentIdentity {\n if (node.segmentType !== 'intercepting' || !node.interceptedSegmentName) {\n return {\n segmentName: node.segmentName,\n segmentType: node.segmentType,\n paramName: node.paramName,\n paramPrefix: node.paramPrefix,\n paramSuffix: node.paramSuffix,\n };\n }\n\n const seg = classifyUrlSegment(node.interceptedSegmentName);\n if (seg.kind === 'static') {\n return { segmentName: seg.value, segmentType: 'static' };\n }\n return {\n segmentName: node.interceptedSegmentName,\n segmentType: seg.kind,\n paramName: seg.name,\n paramPrefix: seg.kind === 'dynamic' ? seg.prefix : undefined,\n paramSuffix: seg.kind === 'dynamic' ? seg.suffix : undefined,\n };\n}\n\n/**\n * Parse an interception marker from a directory name.\n *\n * Returns the marker and the remaining segment name, or null if not an\n * intercepting route. Markers are checked longest-first to avoid `(..)`\n * matching before `(..)(..)`.\n *\n * Examples:\n * \"(.)photo\" → { marker: \"(.)\", segmentName: \"photo\" }\n * \"(..)feed\" → { marker: \"(..)\", segmentName: \"feed\" }\n * \"(...)photos\" → { marker: \"(...)\", segmentName: \"photos\" }\n * \"(..)(..)admin\" → { marker: \"(..)(..)\", segmentName: \"admin\" }\n * \"(marketing)\" → null (route group, not interception)\n */\nfunction parseInterceptionMarker(\n dirName: string\n): { marker: InterceptionMarker; segmentName: string } | null {\n for (const marker of INTERCEPTION_MARKERS) {\n if (dirName.startsWith(marker)) {\n const rest = dirName.slice(marker.length);\n // Must have a segment name after the marker, and the rest must not\n // be empty or end with ) (which would be a route group like \"(auth)\")\n if (rest.length > 0 && !rest.endsWith(')')) {\n return { marker, segmentName: rest };\n }\n }\n }\n return null;\n}\n"],"mappings":"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"}
@@ -75,6 +75,18 @@ var METADATA_ROUTE_CONVENTIONS = {
75
75
  }
76
76
  };
77
77
  /**
78
+ * Check if a file extension represents a static (non-code) metadata route file.
79
+ *
80
+ * @param baseName - The base file name without extension (e.g. "sitemap", "icon")
81
+ * @param extension - The file extension without leading dot (e.g. "xml", "png", "ts")
82
+ * @returns true if this is a static file, false if dynamic or unrecognized
83
+ */
84
+ function isStaticMetadataExtension(baseName, extension) {
85
+ const convention = METADATA_ROUTE_CONVENTIONS[baseName];
86
+ if (!convention) return false;
87
+ return convention.staticExtensions.includes(extension);
88
+ }
89
+ /**
78
90
  * Check if a file extension represents a dynamic (code) metadata route file.
79
91
  *
80
92
  * @param baseName - The base file name without extension (e.g. "sitemap", "icon")
@@ -302,6 +314,6 @@ function treePathDepth(treePath) {
302
314
  return treePathNames(treePath).length;
303
315
  }
304
316
  //#endregion
305
- export { canonicalize as a, getMetadataRouteAutoLink as c, isMetadataRouteServePath as d, NULL_BYTE_RE as i, getMetadataRouteServePath as l, treePathDepth as n, METADATA_ROUTE_CONVENTIONS as o, ENCODED_SEPARATOR_RE as r, classifyMetadataRoute as s, computeSegmentTreePaths as t, isDynamicMetadataExtension as u };
317
+ export { canonicalize as a, getMetadataRouteAutoLink as c, isMetadataRouteServePath as d, isStaticMetadataExtension as f, NULL_BYTE_RE as i, getMetadataRouteServePath as l, treePathDepth as n, METADATA_ROUTE_CONVENTIONS as o, ENCODED_SEPARATOR_RE as r, classifyMetadataRoute as s, computeSegmentTreePaths as t, isDynamicMetadataExtension as u };
306
318
 
307
- //# sourceMappingURL=segment-keys-BawYuNFO.js.map
319
+ //# sourceMappingURL=segment-keys-BhqoHiLc.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"segment-keys-BawYuNFO.js","names":[],"sources":["../../src/server/metadata-routes.ts","../../src/server/canonicalize.ts","../../src/routing/segment-keys.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { HeadElement } from './metadata.js';\nimport type { Metadata } from './types.js';\nimport type { ManifestSegmentNode } from './route-matcher.js';\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /**\n * The URL path basename this file serves at (relative to segment).\n * For image routes, the full serve path includes an extension via\n * `resolveServePathForFile()`.\n */\n servePath: string;\n /**\n * When set, image routes append `.{serveExtension}` to the serve path.\n * Dynamic handlers (`.ts`/`.tsx`) use this as the default. Static files\n * use their own extension instead. Non-image routes leave this undefined.\n */\n serveExtension?: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n serveExtension: 'png',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n serveExtension: 'png',\n },\n\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n serveExtension: 'png',\n },\n};\n\n// ─── MIME Type Resolution ─────────────────────────────────────────────────────\n\n/**\n * Map of file extensions to MIME types for static metadata route files.\n * Used to resolve the generic `image/*` content type for static image files.\n */\nconst EXTENSION_MIME_TYPES: Record<string, string> = {\n xml: 'application/xml',\n txt: 'text/plain',\n json: 'application/json',\n ico: 'image/x-icon',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n svg: 'image/svg+xml',\n webp: 'image/webp',\n};\n\n/**\n * Resolve the concrete MIME type for a static metadata route file.\n *\n * For generic content types like `image/*`, this resolves to the actual\n * MIME type based on the file extension (e.g. `image/png` for `.png`).\n *\n * @param conventionContentType - The content type from the convention table (may be generic like `image/*`)\n * @param extension - The file extension without leading dot (e.g. \"png\", \"xml\")\n * @returns The resolved MIME type\n */\nexport function resolveStaticContentType(conventionContentType: string, extension: string): string {\n if (conventionContentType.includes('*')) {\n return EXTENSION_MIME_TYPES[extension] ?? 'application/octet-stream';\n }\n return conventionContentType;\n}\n\n/**\n * Check if a file extension represents a static (non-code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"xml\", \"png\", \"ts\")\n * @returns true if this is a static file, false if dynamic or unrecognized\n */\nexport function isStaticMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.staticExtensions.includes(extension);\n}\n\n/**\n * Check if a file extension represents a dynamic (code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"ts\", \"tsx\")\n * @returns true if this is a dynamic file, false if static or unrecognized\n */\nexport function isDynamicMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.dynamicExtensions.includes(extension);\n}\n\n// ─── Classification ──────────────────────────────────────────────────────────\n\n/**\n * Classify a file name as a metadata route, or return null if it's not one.\n *\n * @param fileName - The full file name including extension (e.g. \"sitemap.xml\", \"icon.tsx\")\n * @returns Classification info, or null if not a metadata route\n */\nexport function classifyMetadataRoute(fileName: string): MetadataRouteInfo | null {\n const dotIndex = fileName.lastIndexOf('.');\n if (dotIndex === -1) return null;\n\n const baseName = fileName.slice(0, dotIndex);\n const ext = fileName.slice(dotIndex + 1);\n\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return null;\n\n const isStatic = convention.staticExtensions.includes(ext);\n const isDynamic = convention.dynamicExtensions.includes(ext);\n\n if (!isStatic && !isDynamic) return null;\n\n return {\n type: convention.type,\n contentType: convention.contentType,\n nestable: convention.nestable,\n };\n}\n\n/**\n * Resolve the serve path for a metadata route file.\n *\n * For image routes (icon, opengraph-image, apple-icon), the serve path includes\n * a file extension so CDNs cache correctly:\n * - Dynamic handlers (.ts/.tsx) use the convention's `serveExtension` (default: .png)\n * - Static files use their own extension (e.g., icon.svg → icon.svg)\n *\n * Non-image routes return the convention's `servePath` as-is (already includes\n * extension: sitemap.xml, robots.txt, etc.).\n */\nexport function resolveServePathForFile(baseName: string, filePath: string): string {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return baseName;\n\n if (!convention.serveExtension) return convention.servePath;\n\n const ext = filePath.slice(filePath.lastIndexOf('.') + 1);\n if (convention.staticExtensions.includes(ext)) {\n return `${convention.servePath}.${ext}`;\n }\n return `${convention.servePath}.${convention.serveExtension}`;\n}\n\n/**\n * Get the default serve path for a metadata route type (using default extension\n * for image routes). Used for auto-link generation when only the type is known.\n */\nexport function getMetadataRouteServePath(type: MetadataRouteType): string {\n for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.type === type) {\n if (convention.serveExtension) {\n return `${convention.servePath}.${convention.serveExtension}`;\n }\n return convention.servePath;\n }\n }\n throw new Error(`[timber] Unknown metadata route type: ${type}`);\n}\n\n/**\n * All possible serve path segments for metadata routes (includes extension\n * variants for image routes).\n */\nconst METADATA_SERVE_PATHS = new Set<string>();\nfor (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.serveExtension) {\n for (const ext of [...convention.staticExtensions, convention.serveExtension]) {\n METADATA_SERVE_PATHS.add(`${convention.servePath}.${ext}`);\n }\n } else {\n METADATA_SERVE_PATHS.add(convention.servePath);\n }\n}\n\nexport function isMetadataRouteServePath(pathname: string): boolean {\n let lastSegment = pathname.slice(pathname.lastIndexOf('/') + 1);\n const qIdx = lastSegment.indexOf('?');\n let query = '';\n if (qIdx !== -1) {\n query = lastSegment.slice(qIdx + 1);\n lastSegment = lastSegment.slice(0, qIdx);\n }\n if (!METADATA_SERVE_PATHS.has(lastSegment)) return false;\n // Vite module requests (e.g., /src/icon.svg?import) use special query params.\n // These are source assets, not metadata routes.\n if (/(?:^|&)(?:import|url|raw|worker|inline)(?:&|$)/.test(query)) return false;\n return true;\n}\n\n/** A <link> auto-link tag. */\nexport interface AutoLinkLink {\n tag: 'link';\n rel: string;\n href: string;\n type?: string;\n}\n\n/** A <meta> auto-link tag. */\nexport interface AutoLinkMeta {\n tag: 'meta';\n property?: string;\n name?: string;\n content: string;\n}\n\nexport type AutoLinkTag = AutoLinkLink | AutoLinkMeta;\n\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * Returns link tags for icon/apple-icon/manifest, and meta tags for\n * opengraph-image (emits both og:image and twitter:image). Returns null\n * for types that don't auto-link (favicon, sitemap, robots).\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns Tag descriptor(s) for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(type: MetadataRouteType, href: string): AutoLinkTag[] {\n switch (type) {\n case 'icon':\n return [{ tag: 'link', rel: 'icon', href }];\n case 'apple-icon':\n return [{ tag: 'link', rel: 'apple-touch-icon', href }];\n case 'manifest':\n return [{ tag: 'link', rel: 'manifest', href }];\n case 'opengraph-image':\n return [\n { tag: 'meta', property: 'og:image', content: href },\n { tag: 'meta', name: 'twitter:image', content: href },\n ];\n default:\n return [];\n }\n}\n\n// ─── Auto-Linking ──────────────────────────────────────────────────────────\n\n// In dev mode, use a per-startup nonce for metadata route cache busting\n// instead of per-file content hashes (avoids rehashing on every request).\nlet _devNonce: string | undefined;\nfunction getDevNonce(): string {\n _devNonce ??= randomUUID().slice(0, 8);\n return _devNonce;\n}\n\n/**\n * Collect auto-linked head elements from metadata route files in the segment chain.\n *\n * Walks each segment's metadataRoutes, resolves serve paths and URLs, and\n * emits HeadElement descriptors for <link> and <meta> tags that React Float\n * hoists into <head>.\n *\n * See design/16-metadata.md §\"Auto-Linking\"\n */\nexport function collectMetadataRouteHeadElements(\n segments: ManifestSegmentNode[],\n firstDeniedIndex: number,\n resolvedMetadata: Metadata,\n requestUrl: URL,\n metadataRouteHashes?: Record<string, string>\n): HeadElement[] {\n const elements: HeadElement[] = [];\n const hasUserOgImage = Boolean(resolvedMetadata.openGraph?.images);\n const hasUserTwitterImage = Boolean(resolvedMetadata.twitter?.images);\n const requestPathname = requestUrl.pathname;\n // In dev mode, use the request origin so OG URLs resolve to localhost.\n // In production, use metadataBase (the canonical domain).\n const ogBase =\n process.env.NODE_ENV !== 'production'\n ? new URL(requestUrl.origin)\n : resolvedMetadata.metadataBase;\n\n for (let si = 0; si < segments.length; si++) {\n const segment = segments[si];\n if (!segment.metadataRoutes) continue;\n if (si >= firstDeniedIndex) continue;\n for (const baseName of Object.keys(segment.metadataRoutes)) {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) continue;\n if (!convention.nestable && segment.urlPath !== '/') continue;\n if (convention.type === 'opengraph-image' && hasUserOgImage) continue;\n const resolvedPrefix = convention.nestable\n ? requestPathname === '/'\n ? ''\n : requestPathname\n : '';\n const metaFile = segment.metadataRoutes[baseName];\n const fileServePath = metaFile?.filePath\n ? resolveServePathForFile(baseName, metaFile.filePath)\n : convention.serveExtension\n ? `${convention.servePath}.${convention.serveExtension}`\n : convention.servePath;\n let href = `${resolvedPrefix}/${fileServePath}`;\n if (convention.type === 'opengraph-image') {\n const fileHash = metaFile?.filePath ? metadataRouteHashes?.[metaFile.filePath] : undefined;\n const cacheBust = fileHash ?? getDevNonce();\n href = `${href}?${cacheBust}`;\n }\n if (ogBase && convention.type === 'opengraph-image') {\n href = new URL(href, ogBase).toString();\n }\n for (const autoLink of getMetadataRouteAutoLink(convention.type, href)) {\n if (\n hasUserTwitterImage &&\n autoLink.tag === 'meta' &&\n 'name' in autoLink &&\n autoLink.name === 'twitter:image'\n )\n continue;\n if (autoLink.tag === 'link') {\n const attrs: Record<string, string> = { rel: autoLink.rel, href: autoLink.href };\n if (autoLink.type) attrs.type = autoLink.type;\n elements.push({ tag: 'link', attrs });\n } else {\n const attrs: Record<string, string> = { content: autoLink.content };\n if (autoLink.property) attrs.property = autoLink.property;\n if (autoLink.name) attrs.name = autoLink.name;\n elements.push({ tag: 'meta', attrs });\n }\n }\n }\n }\n return elements;\n}\n","/**\n * URL canonicalization — runs once at the request boundary.\n *\n * Every layer (proxy.ts, middleware.ts, access.ts, components) sees the same\n * canonical path. No re-decoding occurs at any later stage.\n *\n * See design/07-routing.md §\"URL Canonicalization & Security\"\n */\n\n/** Result of canonicalization — either a clean path or a rejection. */\nexport type CanonicalizeResult = { ok: true; pathname: string } | { ok: false; status: 400 };\n\n/**\n * Encoded separators that produce a 400 rejection.\n * %2f (/) and %5c (\\) cause path-confusion attacks.\n *\n * Shared between the runtime canonicalizer and the build-time route scanner\n * to ensure both enforce identical security rules. See design/13-security.md.\n */\nexport const ENCODED_SEPARATOR_RE = /%2f|%5c/i;\n\n/** Null byte — rejected. Shared with the route scanner. */\nexport const NULL_BYTE_RE = /%00/i;\n\n/**\n * Canonicalize a URL pathname.\n *\n * 1. Reject encoded separators (%2f, %5c) and null bytes (%00)\n * 2. Single percent-decode\n * 3. Collapse // → /\n * 4. Resolve .. segments (reject if escaping root)\n * 5. Strip trailing slash (except root \"/\")\n *\n * @param rawPathname - The raw pathname from the request URL (percent-encoded)\n * @param stripTrailingSlash - Whether to strip trailing slashes. Default: true.\n */\nexport function canonicalize(rawPathname: string, stripTrailingSlash = true): CanonicalizeResult {\n // Step 1: Reject dangerous encoded sequences BEFORE decoding.\n // This must happen on the raw input so %252f doesn't bypass after a single decode.\n if (ENCODED_SEPARATOR_RE.test(rawPathname)) {\n return { ok: false, status: 400 };\n }\n if (NULL_BYTE_RE.test(rawPathname)) {\n return { ok: false, status: 400 };\n }\n\n // Step 2: Single percent-decode.\n // Double-encoded input (%2561 → %61) stays as %61 — not decoded again.\n let decoded: string;\n try {\n decoded = decodeURIComponent(rawPathname);\n } catch {\n // Malformed percent-encoding → 400\n return { ok: false, status: 400 };\n }\n\n // Reject null bytes that appeared after decoding (from valid %00-like sequences\n // that weren't caught above — belt and suspenders).\n if (decoded.includes('\\0')) {\n return { ok: false, status: 400 };\n }\n\n // Backslash is NOT a path separator — keep as literal character.\n // But reject if it would create // after normalization (e.g., /\\evil.com).\n // We do NOT convert \\ to / — it stays as a literal.\n\n // Step 3: Collapse consecutive slashes.\n let pathname = decoded.replace(/\\/\\/+/g, '/');\n\n // Step 4: Resolve .. and . segments.\n const segments = pathname.split('/');\n const resolved: string[] = [];\n for (const seg of segments) {\n if (seg === '..') {\n if (resolved.length <= 1) {\n // Trying to escape root — 400\n return { ok: false, status: 400 };\n }\n resolved.pop();\n } else if (seg !== '.') {\n resolved.push(seg);\n }\n }\n\n pathname = resolved.join('/') || '/';\n\n // Step 5: Strip trailing slash (except root \"/\").\n if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n\n return { ok: true, pathname };\n}\n","/**\n * Segment key computation — the stable identity of a segment in the route tree.\n *\n * A segment key is the string that names a segment across the RSC/SSR/client\n * boundary: it appears in the X-Timber-State-Tree header, the X-Timber-Segments\n * header, SegmentOutlet props, the client segment cache, and (since TIM-1279)\n * interception scoping.\n *\n * This module lives in `routing/` rather than `server/` because the keys are a\n * property of the route tree itself — both the build-time scanner tree and the\n * runtime manifest tree produce identical keys for the same directory, which is\n * what lets a build-time value (an interception scope) be compared against a\n * request-time matched chain.\n *\n * See design/19-client-navigation.md §\"X-Timber-State-Tree Header\"\n */\n\n/**\n * Segment node shape expected by computeSegmentKeys.\n *\n * Structurally satisfied by both `SegmentNode` (build time) and\n * `ManifestSegmentNode` (request time) — keys depend only on the\n * URL path and the segment classification, never on file payloads.\n */\n\nimport type { CoercedParams } from '../shared/param-value.js';\n\nexport interface SegmentKeyInput {\n urlPath: string;\n segmentName?: string;\n segmentType?: string;\n}\n\n/**\n * Compute state-tree keys for a segment chain.\n *\n * Non-group segments use their urlPath as-is. Route groups accumulate\n * ancestor group names to produce globally unique keys:\n * app/(a)/(shared)/dashboard → keys: ['/', '/(a)', '/(a)/(shared)', '/dashboard']\n *\n * **An intercepting segment switches the chain to name accumulation for good.**\n * A children-path interception (TIM-1280) puts an intercepting node and its\n * descendants into the *main* rendered chain, and their `urlPath`s are not\n * URLs: interception adds no URL depth, so `app/(browse)/(...)[artistSlug]/[year]`\n * carries `/` and `/[year]`. Keyed by urlPath the intercepting node would\n * collide with its own owner, and `[year]` with any top-level `[year]` route —\n * so a client that had one mounted would reuse the wrong cached layout. The\n * switch is sticky rather than per-node because the *descendants* are where\n * the false URLs are; the intercepting node alone is not the problem.\n *\n * This is the single source of truth for segment keys — used by the\n * element builder (skip decisions, SegmentOutlet props), segment info\n * (X-Timber-Segments header), the client cache/state tree, and\n * interception scoping (`routing/interception.ts`).\n */\nexport function computeSegmentKeys(segments: SegmentKeyInput[]): string[] {\n const keys: string[] = [];\n let prevKey = '';\n let insideIntercepting = false;\n\n for (const segment of segments) {\n if (segment.segmentType === 'intercepting') insideIntercepting = true;\n if (segment.segmentType === 'group' || insideIntercepting) {\n const base = prevKey === '/' ? '' : prevKey;\n const key = `${base}/${segment.segmentName}`;\n keys.push(key);\n prevKey = key;\n } else {\n keys.push(segment.urlPath);\n prevKey = segment.urlPath;\n }\n }\n\n return keys;\n}\n\n/**\n * Compute tree paths for a segment chain — the directory path of each segment\n * with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.\n *\n * This is `computeSegmentKeys` with the group branch applied to *every*\n * segment rather than only to groups. That one difference is the whole point:\n * `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so\n * `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories\n * that render different layouts must not share an identity.\n *\n * Used for interception scoping (`routing/interception.ts`), where the\n * question is \"does this route pass through *that* directory?\" — not \"does it\n * render this URL?\". Not interchangeable with `computeSegmentKeys`: the state\n * tree is keyed by URL on purpose, because the client caches by URL.\n */\nexport function computeSegmentTreePaths(segments: SegmentKeyInput[]): string[] {\n const paths: string[] = [];\n let prev = '';\n\n for (const segment of segments) {\n const name = segment.segmentName ?? '';\n if (!name) {\n // The app root — no directory name of its own.\n paths.push('/');\n prev = '/';\n continue;\n }\n const base = prev === '/' ? '' : prev;\n const path = `${base}/${name}`;\n paths.push(path);\n prev = path;\n }\n\n return paths;\n}\n\n/**\n * The directory names a tree path is built from — `/` is none, `/feed/(a)` is\n * `['feed', '(a)']`.\n *\n * The inverse of the walk above, and it lives beside it so the format is\n * stated once: `findChainByTreePath` (`server/children-interception.ts`) walks\n * these names back down the tree to the directory the path addresses, and\n * `treePathDepth` counts them. Both are readings of the same string, and a\n * reader that disagreed with the producer about what separates two names\n * resolves an interception to the wrong node or to none.\n */\nexport function treePathNames(treePath: string): string[] {\n return treePath === '/' ? [] : treePath.slice(1).split('/');\n}\n\n/**\n * How many directories a tree path names — `/` is 0, `/feed` is 1,\n * `/feed/(a)` is 2.\n *\n * Two tree paths on a single rendered chain are strictly nested, so on that\n * chain depth orders them totally — which is what makes it usable as \"the\n * deeper of these two directories\" wherever a route passes through both\n * (`routing/interception.ts`).\n */\nexport function treePathDepth(treePath: string): number {\n return treePathNames(treePath).length;\n}\n\n/**\n * Compute a unique key for a parallel route slot.\n * Format: `{parentSegmentId}/@{slotName}`, e.g. `/@sidebar` or `/dashboard/@modal`.\n */\nexport function computeSlotKey(parentSegmentId: string, slotName: string): string {\n const name = slotName.startsWith('@') ? slotName : `@${slotName}`;\n const prefix = parentSegmentId === '/' ? '' : parentSegmentId;\n return `${prefix}/${name}`;\n}\n\n/**\n * Compute a content key for a parallel route slot.\n *\n * The content key encodes everything that determines the slot's rendered\n * output for a given navigation: the slot key (which includes the owning\n * segment's identity and the slot name), the URL parts consumed by the\n * owning segment (which determine parent params the slot page might\n * read), the matched page file (or a sentinel for `default.tsx`), and\n * the slot's own extracted params.\n *\n * The slot key prevents cross-group collisions: two route groups at the\n * same URL level each owning a `@sidebar` slot produce different keys\n * even when both fall back to `default.tsx`.\n *\n * The key is opaque to the client — it stores whatever the server sent\n * and advertises it back. The server computes the destination key and\n * checks membership. This eliminates the need for the server to\n * reconstruct the client's state from a departing URL.\n *\n * See design/07-routing.md §\"Segment Tree Diffing on Navigation\"\n */\nexport function computeSlotContentKey(\n slotKey: string,\n ownerParts: string[],\n entryFile: string | null,\n slotParams: CoercedParams\n): string {\n const owner = ownerParts.join('/');\n const entry = entryFile ?? '\\x01';\n const paramKeys = Object.keys(slotParams).sort();\n const paramParts = paramKeys.map((k) => {\n const v = slotParams[k];\n return Array.isArray(v) ? `${k}=${v.map(String).join('\\x02')}` : `${k}=${String(v)}`;\n });\n return [slotKey, owner, entry, ...paramParts].join('\\0');\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"segment-keys-BhqoHiLc.js","names":[],"sources":["../../src/server/metadata-routes.ts","../../src/server/canonicalize.ts","../../src/routing/segment-keys.ts"],"sourcesContent":["/**\n * Metadata route classification for timber.js.\n *\n * Metadata routes are file-based endpoints that generate well-known URLs for\n * crawlers and browsers (sitemap.xml, robots.txt, OG images, etc.).\n *\n * These routes run through proxy.ts but NOT through middleware.ts or access.ts —\n * they are public endpoints by nature.\n *\n * See design/16-metadata.md §\"Metadata Routes\"\n */\n\nimport { randomUUID } from 'node:crypto';\nimport type { HeadElement } from './metadata.ts';\nimport type { Metadata } from './types.ts';\nimport type { ManifestSegmentNode } from './route-matcher.ts';\n\n// ─── Types ───────────────────────────────────────────────────────────────────\n\n/** Classification of a metadata route file. */\nexport interface MetadataRouteInfo {\n /** The metadata route type. */\n type: MetadataRouteType;\n /** The content type to serve this route with. */\n contentType: string;\n /** Whether this route can appear in nested segments (not just app root). */\n nestable: boolean;\n}\n\nexport type MetadataRouteType =\n | 'sitemap'\n | 'robots'\n | 'manifest'\n | 'favicon'\n | 'icon'\n | 'opengraph-image'\n | 'apple-icon';\n\n// ─── Convention Table ────────────────────────────────────────────────────────\n\n/**\n * All recognized metadata route file conventions.\n *\n * Each entry maps a base file name (without extension) to its route info.\n * The extensions determine whether the file is static or dynamic.\n *\n * Static extensions: .xml, .txt, .json, .png, .jpg, .ico, .svg\n * Dynamic extensions: .ts, .tsx\n */\nexport const METADATA_ROUTE_CONVENTIONS: Record<\n string,\n {\n type: MetadataRouteType;\n contentType: string;\n nestable: boolean;\n staticExtensions: string[];\n dynamicExtensions: string[];\n /**\n * The URL path basename this file serves at (relative to segment).\n * For image routes, the full serve path includes an extension via\n * `resolveServePathForFile()`.\n */\n servePath: string;\n /**\n * When set, image routes append `.{serveExtension}` to the serve path.\n * Dynamic handlers (`.ts`/`.tsx`) use this as the default. Static files\n * use their own extension instead. Non-image routes leave this undefined.\n */\n serveExtension?: string;\n }\n> = {\n 'sitemap': {\n type: 'sitemap',\n contentType: 'application/xml',\n nestable: true,\n staticExtensions: ['xml'],\n dynamicExtensions: ['ts'],\n servePath: 'sitemap.xml',\n },\n 'robots': {\n type: 'robots',\n contentType: 'text/plain',\n nestable: false,\n staticExtensions: ['txt'],\n dynamicExtensions: ['ts'],\n servePath: 'robots.txt',\n },\n 'manifest': {\n type: 'manifest',\n contentType: 'application/manifest+json',\n nestable: false,\n staticExtensions: ['json'],\n dynamicExtensions: ['ts'],\n servePath: 'manifest.webmanifest',\n },\n 'favicon': {\n type: 'favicon',\n contentType: 'image/x-icon',\n nestable: false,\n staticExtensions: ['ico'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'favicon.ico',\n },\n 'icon': {\n type: 'icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg', 'svg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'icon',\n serveExtension: 'png',\n },\n 'opengraph-image': {\n type: 'opengraph-image',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png', 'jpg'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'opengraph-image',\n serveExtension: 'png',\n },\n\n 'apple-icon': {\n type: 'apple-icon',\n contentType: 'image/*',\n nestable: true,\n staticExtensions: ['png'],\n dynamicExtensions: ['ts', 'tsx'],\n servePath: 'apple-icon',\n serveExtension: 'png',\n },\n};\n\n// ─── MIME Type Resolution ─────────────────────────────────────────────────────\n\n/**\n * Map of file extensions to MIME types for static metadata route files.\n * Used to resolve the generic `image/*` content type for static image files.\n */\nconst EXTENSION_MIME_TYPES: Record<string, string> = {\n xml: 'application/xml',\n txt: 'text/plain',\n json: 'application/json',\n ico: 'image/x-icon',\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n svg: 'image/svg+xml',\n webp: 'image/webp',\n};\n\n/**\n * Resolve the concrete MIME type for a static metadata route file.\n *\n * For generic content types like `image/*`, this resolves to the actual\n * MIME type based on the file extension (e.g. `image/png` for `.png`).\n *\n * @param conventionContentType - The content type from the convention table (may be generic like `image/*`)\n * @param extension - The file extension without leading dot (e.g. \"png\", \"xml\")\n * @returns The resolved MIME type\n */\nexport function resolveStaticContentType(conventionContentType: string, extension: string): string {\n if (conventionContentType.includes('*')) {\n return EXTENSION_MIME_TYPES[extension] ?? 'application/octet-stream';\n }\n return conventionContentType;\n}\n\n/**\n * Check if a file extension represents a static (non-code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"xml\", \"png\", \"ts\")\n * @returns true if this is a static file, false if dynamic or unrecognized\n */\nexport function isStaticMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.staticExtensions.includes(extension);\n}\n\n/**\n * Check if a file extension represents a dynamic (code) metadata route file.\n *\n * @param baseName - The base file name without extension (e.g. \"sitemap\", \"icon\")\n * @param extension - The file extension without leading dot (e.g. \"ts\", \"tsx\")\n * @returns true if this is a dynamic file, false if static or unrecognized\n */\nexport function isDynamicMetadataExtension(baseName: string, extension: string): boolean {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return false;\n return convention.dynamicExtensions.includes(extension);\n}\n\n// ─── Classification ──────────────────────────────────────────────────────────\n\n/**\n * Classify a file name as a metadata route, or return null if it's not one.\n *\n * @param fileName - The full file name including extension (e.g. \"sitemap.xml\", \"icon.tsx\")\n * @returns Classification info, or null if not a metadata route\n */\nexport function classifyMetadataRoute(fileName: string): MetadataRouteInfo | null {\n const dotIndex = fileName.lastIndexOf('.');\n if (dotIndex === -1) return null;\n\n const baseName = fileName.slice(0, dotIndex);\n const ext = fileName.slice(dotIndex + 1);\n\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return null;\n\n const isStatic = convention.staticExtensions.includes(ext);\n const isDynamic = convention.dynamicExtensions.includes(ext);\n\n if (!isStatic && !isDynamic) return null;\n\n return {\n type: convention.type,\n contentType: convention.contentType,\n nestable: convention.nestable,\n };\n}\n\n/**\n * Resolve the serve path for a metadata route file.\n *\n * For image routes (icon, opengraph-image, apple-icon), the serve path includes\n * a file extension so CDNs cache correctly:\n * - Dynamic handlers (.ts/.tsx) use the convention's `serveExtension` (default: .png)\n * - Static files use their own extension (e.g., icon.svg → icon.svg)\n *\n * Non-image routes return the convention's `servePath` as-is (already includes\n * extension: sitemap.xml, robots.txt, etc.).\n */\nexport function resolveServePathForFile(baseName: string, filePath: string): string {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) return baseName;\n\n if (!convention.serveExtension) return convention.servePath;\n\n const ext = filePath.slice(filePath.lastIndexOf('.') + 1);\n if (convention.staticExtensions.includes(ext)) {\n return `${convention.servePath}.${ext}`;\n }\n return `${convention.servePath}.${convention.serveExtension}`;\n}\n\n/**\n * Get the default serve path for a metadata route type (using default extension\n * for image routes). Used for auto-link generation when only the type is known.\n */\nexport function getMetadataRouteServePath(type: MetadataRouteType): string {\n for (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.type === type) {\n if (convention.serveExtension) {\n return `${convention.servePath}.${convention.serveExtension}`;\n }\n return convention.servePath;\n }\n }\n throw new Error(`[timber] Unknown metadata route type: ${type}`);\n}\n\n/**\n * All possible serve path segments for metadata routes (includes extension\n * variants for image routes).\n */\nconst METADATA_SERVE_PATHS = new Set<string>();\nfor (const convention of Object.values(METADATA_ROUTE_CONVENTIONS)) {\n if (convention.serveExtension) {\n for (const ext of [...convention.staticExtensions, convention.serveExtension]) {\n METADATA_SERVE_PATHS.add(`${convention.servePath}.${ext}`);\n }\n } else {\n METADATA_SERVE_PATHS.add(convention.servePath);\n }\n}\n\nexport function isMetadataRouteServePath(pathname: string): boolean {\n let lastSegment = pathname.slice(pathname.lastIndexOf('/') + 1);\n const qIdx = lastSegment.indexOf('?');\n let query = '';\n if (qIdx !== -1) {\n query = lastSegment.slice(qIdx + 1);\n lastSegment = lastSegment.slice(0, qIdx);\n }\n if (!METADATA_SERVE_PATHS.has(lastSegment)) return false;\n // Vite module requests (e.g., /src/icon.svg?import) use special query params.\n // These are source assets, not metadata routes.\n if (/(?:^|&)(?:import|url|raw|worker|inline)(?:&|$)/.test(query)) return false;\n return true;\n}\n\n/** A <link> auto-link tag. */\nexport interface AutoLinkLink {\n tag: 'link';\n rel: string;\n href: string;\n type?: string;\n}\n\n/** A <meta> auto-link tag. */\nexport interface AutoLinkMeta {\n tag: 'meta';\n property?: string;\n name?: string;\n content: string;\n}\n\nexport type AutoLinkTag = AutoLinkLink | AutoLinkMeta;\n\n/**\n * Get the auto-link tags to inject into <head> for metadata route files\n * discovered in a segment.\n *\n * Returns link tags for icon/apple-icon/manifest, and meta tags for\n * opengraph-image (emits both og:image and twitter:image). Returns null\n * for types that don't auto-link (favicon, sitemap, robots).\n *\n * @param type - The metadata route type\n * @param href - The resolved URL path to the metadata route\n * @returns Tag descriptor(s) for the <head>, or null if no auto-link\n */\nexport function getMetadataRouteAutoLink(type: MetadataRouteType, href: string): AutoLinkTag[] {\n switch (type) {\n case 'icon':\n return [{ tag: 'link', rel: 'icon', href }];\n case 'apple-icon':\n return [{ tag: 'link', rel: 'apple-touch-icon', href }];\n case 'manifest':\n return [{ tag: 'link', rel: 'manifest', href }];\n case 'opengraph-image':\n return [\n { tag: 'meta', property: 'og:image', content: href },\n { tag: 'meta', name: 'twitter:image', content: href },\n ];\n default:\n return [];\n }\n}\n\n// ─── Auto-Linking ──────────────────────────────────────────────────────────\n\n// In dev mode, use a per-startup nonce for metadata route cache busting\n// instead of per-file content hashes (avoids rehashing on every request).\nlet _devNonce: string | undefined;\nfunction getDevNonce(): string {\n _devNonce ??= randomUUID().slice(0, 8);\n return _devNonce;\n}\n\n/**\n * Collect auto-linked head elements from metadata route files in the segment chain.\n *\n * Walks each segment's metadataRoutes, resolves serve paths and URLs, and\n * emits HeadElement descriptors for <link> and <meta> tags that React Float\n * hoists into <head>.\n *\n * See design/16-metadata.md §\"Auto-Linking\"\n */\nexport function collectMetadataRouteHeadElements(\n segments: ManifestSegmentNode[],\n firstDeniedIndex: number,\n resolvedMetadata: Metadata,\n requestUrl: URL,\n metadataRouteHashes?: Record<string, string>\n): HeadElement[] {\n const elements: HeadElement[] = [];\n const hasUserOgImage = Boolean(resolvedMetadata.openGraph?.images);\n const hasUserTwitterImage = Boolean(resolvedMetadata.twitter?.images);\n const requestPathname = requestUrl.pathname;\n // In dev mode, use the request origin so OG URLs resolve to localhost.\n // In production, use metadataBase (the canonical domain).\n const ogBase =\n process.env.NODE_ENV !== 'production'\n ? new URL(requestUrl.origin)\n : resolvedMetadata.metadataBase;\n\n for (let si = 0; si < segments.length; si++) {\n const segment = segments[si];\n if (!segment.metadataRoutes) continue;\n if (si >= firstDeniedIndex) continue;\n for (const baseName of Object.keys(segment.metadataRoutes)) {\n const convention = METADATA_ROUTE_CONVENTIONS[baseName];\n if (!convention) continue;\n if (!convention.nestable && segment.urlPath !== '/') continue;\n if (convention.type === 'opengraph-image' && hasUserOgImage) continue;\n const resolvedPrefix = convention.nestable\n ? requestPathname === '/'\n ? ''\n : requestPathname\n : '';\n const metaFile = segment.metadataRoutes[baseName];\n const fileServePath = metaFile?.filePath\n ? resolveServePathForFile(baseName, metaFile.filePath)\n : convention.serveExtension\n ? `${convention.servePath}.${convention.serveExtension}`\n : convention.servePath;\n let href = `${resolvedPrefix}/${fileServePath}`;\n if (convention.type === 'opengraph-image') {\n const fileHash = metaFile?.filePath ? metadataRouteHashes?.[metaFile.filePath] : undefined;\n const cacheBust = fileHash ?? getDevNonce();\n href = `${href}?${cacheBust}`;\n }\n if (ogBase && convention.type === 'opengraph-image') {\n href = new URL(href, ogBase).toString();\n }\n for (const autoLink of getMetadataRouteAutoLink(convention.type, href)) {\n if (\n hasUserTwitterImage &&\n autoLink.tag === 'meta' &&\n 'name' in autoLink &&\n autoLink.name === 'twitter:image'\n )\n continue;\n if (autoLink.tag === 'link') {\n const attrs: Record<string, string> = { rel: autoLink.rel, href: autoLink.href };\n if (autoLink.type) attrs.type = autoLink.type;\n elements.push({ tag: 'link', attrs });\n } else {\n const attrs: Record<string, string> = { content: autoLink.content };\n if (autoLink.property) attrs.property = autoLink.property;\n if (autoLink.name) attrs.name = autoLink.name;\n elements.push({ tag: 'meta', attrs });\n }\n }\n }\n }\n return elements;\n}\n","/**\n * URL canonicalization — runs once at the request boundary.\n *\n * Every layer (proxy.ts, middleware.ts, access.ts, components) sees the same\n * canonical path. No re-decoding occurs at any later stage.\n *\n * See design/07-routing.md §\"URL Canonicalization & Security\"\n */\n\n/** Result of canonicalization — either a clean path or a rejection. */\nexport type CanonicalizeResult = { ok: true; pathname: string } | { ok: false; status: 400 };\n\n/**\n * Encoded separators that produce a 400 rejection.\n * %2f (/) and %5c (\\) cause path-confusion attacks.\n *\n * Shared between the runtime canonicalizer and the build-time route scanner\n * to ensure both enforce identical security rules. See design/13-security.md.\n */\nexport const ENCODED_SEPARATOR_RE = /%2f|%5c/i;\n\n/** Null byte — rejected. Shared with the route scanner. */\nexport const NULL_BYTE_RE = /%00/i;\n\n/**\n * Canonicalize a URL pathname.\n *\n * 1. Reject encoded separators (%2f, %5c) and null bytes (%00)\n * 2. Single percent-decode\n * 3. Collapse // → /\n * 4. Resolve .. segments (reject if escaping root)\n * 5. Strip trailing slash (except root \"/\")\n *\n * @param rawPathname - The raw pathname from the request URL (percent-encoded)\n * @param stripTrailingSlash - Whether to strip trailing slashes. Default: true.\n */\nexport function canonicalize(rawPathname: string, stripTrailingSlash = true): CanonicalizeResult {\n // Step 1: Reject dangerous encoded sequences BEFORE decoding.\n // This must happen on the raw input so %252f doesn't bypass after a single decode.\n if (ENCODED_SEPARATOR_RE.test(rawPathname)) {\n return { ok: false, status: 400 };\n }\n if (NULL_BYTE_RE.test(rawPathname)) {\n return { ok: false, status: 400 };\n }\n\n // Step 2: Single percent-decode.\n // Double-encoded input (%2561 → %61) stays as %61 — not decoded again.\n let decoded: string;\n try {\n decoded = decodeURIComponent(rawPathname);\n } catch {\n // Malformed percent-encoding → 400\n return { ok: false, status: 400 };\n }\n\n // Reject null bytes that appeared after decoding (from valid %00-like sequences\n // that weren't caught above — belt and suspenders).\n if (decoded.includes('\\0')) {\n return { ok: false, status: 400 };\n }\n\n // Backslash is NOT a path separator — keep as literal character.\n // But reject if it would create // after normalization (e.g., /\\evil.com).\n // We do NOT convert \\ to / — it stays as a literal.\n\n // Step 3: Collapse consecutive slashes.\n let pathname = decoded.replace(/\\/\\/+/g, '/');\n\n // Step 4: Resolve .. and . segments.\n const segments = pathname.split('/');\n const resolved: string[] = [];\n for (const seg of segments) {\n if (seg === '..') {\n if (resolved.length <= 1) {\n // Trying to escape root — 400\n return { ok: false, status: 400 };\n }\n resolved.pop();\n } else if (seg !== '.') {\n resolved.push(seg);\n }\n }\n\n pathname = resolved.join('/') || '/';\n\n // Step 5: Strip trailing slash (except root \"/\").\n if (stripTrailingSlash && pathname.length > 1 && pathname.endsWith('/')) {\n pathname = pathname.slice(0, -1);\n }\n\n return { ok: true, pathname };\n}\n","/**\n * Segment key computation — the stable identity of a segment in the route tree.\n *\n * A segment key is the string that names a segment across the RSC/SSR/client\n * boundary: it appears in the X-Timber-State-Tree header, the X-Timber-Segments\n * header, SegmentOutlet props, the client segment cache, and (since TIM-1279)\n * interception scoping.\n *\n * This module lives in `routing/` rather than `server/` because the keys are a\n * property of the route tree itself — both the build-time scanner tree and the\n * runtime manifest tree produce identical keys for the same directory, which is\n * what lets a build-time value (an interception scope) be compared against a\n * request-time matched chain.\n *\n * See design/19-client-navigation.md §\"X-Timber-State-Tree Header\"\n */\n\n/**\n * Segment node shape expected by computeSegmentKeys.\n *\n * Structurally satisfied by both `SegmentNode` (build time) and\n * `ManifestSegmentNode` (request time) — keys depend only on the\n * URL path and the segment classification, never on file payloads.\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\n\nexport interface SegmentKeyInput {\n urlPath: string;\n segmentName?: string;\n segmentType?: string;\n}\n\n/**\n * Compute state-tree keys for a segment chain.\n *\n * Non-group segments use their urlPath as-is. Route groups accumulate\n * ancestor group names to produce globally unique keys:\n * app/(a)/(shared)/dashboard → keys: ['/', '/(a)', '/(a)/(shared)', '/dashboard']\n *\n * **An intercepting segment switches the chain to name accumulation for good.**\n * A children-path interception (TIM-1280) puts an intercepting node and its\n * descendants into the *main* rendered chain, and their `urlPath`s are not\n * URLs: interception adds no URL depth, so `app/(browse)/(...)[artistSlug]/[year]`\n * carries `/` and `/[year]`. Keyed by urlPath the intercepting node would\n * collide with its own owner, and `[year]` with any top-level `[year]` route —\n * so a client that had one mounted would reuse the wrong cached layout. The\n * switch is sticky rather than per-node because the *descendants* are where\n * the false URLs are; the intercepting node alone is not the problem.\n *\n * This is the single source of truth for segment keys — used by the\n * element builder (skip decisions, SegmentOutlet props), segment info\n * (X-Timber-Segments header), the client cache/state tree, and\n * interception scoping (`routing/interception.ts`).\n */\nexport function computeSegmentKeys(segments: SegmentKeyInput[]): string[] {\n const keys: string[] = [];\n let prevKey = '';\n let insideIntercepting = false;\n\n for (const segment of segments) {\n if (segment.segmentType === 'intercepting') insideIntercepting = true;\n if (segment.segmentType === 'group' || insideIntercepting) {\n const base = prevKey === '/' ? '' : prevKey;\n const key = `${base}/${segment.segmentName}`;\n keys.push(key);\n prevKey = key;\n } else {\n keys.push(segment.urlPath);\n prevKey = segment.urlPath;\n }\n }\n\n return keys;\n}\n\n/**\n * Compute tree paths for a segment chain — the directory path of each segment\n * with slots elided, e.g. `app/(browse)/feed` → `/(browse)/feed`.\n *\n * This is `computeSegmentKeys` with the group branch applied to *every*\n * segment rather than only to groups. That one difference is the whole point:\n * `computeSegmentKeys` resets to `urlPath` at each URL-visible segment, so\n * `(browse)/feed` and `(landing)/feed` both key as `/feed`. Two directories\n * that render different layouts must not share an identity.\n *\n * Used for interception scoping (`routing/interception.ts`), where the\n * question is \"does this route pass through *that* directory?\" — not \"does it\n * render this URL?\". Not interchangeable with `computeSegmentKeys`: the state\n * tree is keyed by URL on purpose, because the client caches by URL.\n */\nexport function computeSegmentTreePaths(segments: SegmentKeyInput[]): string[] {\n const paths: string[] = [];\n let prev = '';\n\n for (const segment of segments) {\n const name = segment.segmentName ?? '';\n if (!name) {\n // The app root — no directory name of its own.\n paths.push('/');\n prev = '/';\n continue;\n }\n const base = prev === '/' ? '' : prev;\n const path = `${base}/${name}`;\n paths.push(path);\n prev = path;\n }\n\n return paths;\n}\n\n/**\n * The directory names a tree path is built from — `/` is none, `/feed/(a)` is\n * `['feed', '(a)']`.\n *\n * The inverse of the walk above, and it lives beside it so the format is\n * stated once: `findChainByTreePath` (`server/children-interception.ts`) walks\n * these names back down the tree to the directory the path addresses, and\n * `treePathDepth` counts them. Both are readings of the same string, and a\n * reader that disagreed with the producer about what separates two names\n * resolves an interception to the wrong node or to none.\n */\nexport function treePathNames(treePath: string): string[] {\n return treePath === '/' ? [] : treePath.slice(1).split('/');\n}\n\n/**\n * How many directories a tree path names — `/` is 0, `/feed` is 1,\n * `/feed/(a)` is 2.\n *\n * Two tree paths on a single rendered chain are strictly nested, so on that\n * chain depth orders them totally — which is what makes it usable as \"the\n * deeper of these two directories\" wherever a route passes through both\n * (`routing/interception.ts`).\n */\nexport function treePathDepth(treePath: string): number {\n return treePathNames(treePath).length;\n}\n\n/**\n * Compute a unique key for a parallel route slot.\n * Format: `{parentSegmentId}/@{slotName}`, e.g. `/@sidebar` or `/dashboard/@modal`.\n */\nexport function computeSlotKey(parentSegmentId: string, slotName: string): string {\n const name = slotName.startsWith('@') ? slotName : `@${slotName}`;\n const prefix = parentSegmentId === '/' ? '' : parentSegmentId;\n return `${prefix}/${name}`;\n}\n\n/**\n * Compute a content key for a parallel route slot.\n *\n * The content key encodes everything that determines the slot's rendered\n * output for a given navigation: the slot key (which includes the owning\n * segment's identity and the slot name), the URL parts consumed by the\n * owning segment (which determine parent params the slot page might\n * read), the matched page file (or a sentinel for `default.tsx`), and\n * the slot's own extracted params.\n *\n * The slot key prevents cross-group collisions: two route groups at the\n * same URL level each owning a `@sidebar` slot produce different keys\n * even when both fall back to `default.tsx`.\n *\n * The key is opaque to the client — it stores whatever the server sent\n * and advertises it back. The server computes the destination key and\n * checks membership. This eliminates the need for the server to\n * reconstruct the client's state from a departing URL.\n *\n * See design/07-routing.md §\"Segment Tree Diffing on Navigation\"\n */\nexport function computeSlotContentKey(\n slotKey: string,\n ownerParts: string[],\n entryFile: string | null,\n slotParams: CoercedParams\n): string {\n const owner = ownerParts.join('/');\n const entry = entryFile ?? '\\x01';\n const paramKeys = Object.keys(slotParams).sort();\n const paramParts = paramKeys.map((k) => {\n const v = slotParams[k];\n return Array.isArray(v) ? `${k}=${v.map(String).join('\\x02')}` : `${k}=${String(v)}`;\n });\n return [slotKey, owner, entry, ...paramParts].join('\\0');\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"slot-params-BCTmZkQB.js","names":[],"sources":["../../src/shared/slot-params.ts"],"sourcesContent":["/**\n * Per-slot segment params — the one definition both halves of the API read.\n *\n * A parallel slot matches the URL through its *own* sub-tree, so it can derive\n * a param the main route never had, or derive the same name with a different\n * type (`[...year]` → `string[]` where the main route's `[year]` gave\n * `string`). The server answers `getSegmentParams(slotPath)` from a per-slot\n * map; the client answers `useSegmentParams(slotPath)` from the same map,\n * published on the RSC payload (TIM-1285).\n *\n * The merge lives here rather than in either half because the two must agree\n * about what \"the slot's params\" are — a divergence is invisible until\n * somebody reads a param of the wrong type, which is exactly the class of bug\n * this module closes.\n *\n * Isomorphic: no server or client imports.\n *\n * See design/41-global-params.md §\"Params in a parallel slot\".\n */\n\nimport type { CoercedParams } from './param-value.js';\n\n/** Slot tree path → that slot's own coerced params. */\nexport type SlotParamsRecord = Record<string, CoercedParams>;\n\n/**\n * The params a slot sees: the main route's as a base, the slot's own on top.\n *\n * Params from segments *above* the slot are not re-derived by the slot's own\n * chain, so they have to come from the main route; params the slot does\n * re-derive may have a different type than the main route gave them, so the\n * slot's win.\n *\n * Null-prototype, like every other param record the framework hands out\n * (`coerceSegmentParams` installs one unconditionally). A spread literal would\n * inherit `Object.prototype`, so a lookup for a param the route does not\n * define would resolve to an inherited member — `params.constructor` returning\n * a function instead of `undefined` — in the one param record that was built\n * by merging. See design/13-security.md #36c.\n */\nexport function mergeSlotParams(\n mainParams: CoercedParams,\n slotParams: CoercedParams\n): CoercedParams {\n return Object.assign(Object.create(null), mainParams, slotParams);\n}\n\n/**\n * Resolve the params for a segment path against a published slot map.\n *\n * Returns the merge when `segmentPath` names a slot that published params,\n * and the main route's record otherwise — an ordinary (non-slot) segment\n * path, an unknown path, or a request with no slots at all. Mirrors the\n * server's `getSegmentParams(segmentPath)` branch exactly.\n *\n * `Object.hasOwn` rather than `in` or a truthiness test: the map arrives from\n * JSON, so a segment path of `constructor` or `toString` would otherwise\n * resolve to an inherited member of `Object.prototype`.\n */\nexport function resolveSegmentParams(\n mainParams: CoercedParams,\n slotParams: SlotParamsRecord | null | undefined,\n segmentPath: string | undefined\n): CoercedParams {\n if (!segmentPath || !slotParams || !Object.hasOwn(slotParams, segmentPath)) return mainParams;\n return cachedMerge(mainParams, slotParams, segmentPath);\n}\n\n/**\n * The merged record for one (main record, slot map, path) triple, reused until\n * one of the three changes.\n *\n * `useSegmentParams(slotPath)` calls this on every render. Returning a fresh\n * object each time makes the hook's result change by reference on renders where\n * nothing navigated, so a component using it as a `useEffect` dependency\n * re-runs the effect — and re-renders in a loop if that effect sets state —\n * while memoized children below it lose their memoization.\n *\n * Keyed on the *identities* of the two records, not their contents: both are\n * replaced rather than mutated on every navigation (`setCurrentParams`,\n * `setCurrentSlotParams`, and the server's per-request store), so identity is\n * the correct equality here and a deep comparison would be slower and no more\n * accurate.\n *\n * A `WeakMap` chain rather than a `Map`, so a superseded navigation's records\n * and their merges become collectable as soon as nothing else holds them.\n */\nconst mergeCache = new WeakMap<object, WeakMap<object, Map<string, CoercedParams>>>();\n\nfunction cachedMerge(\n mainParams: CoercedParams,\n slotParams: SlotParamsRecord,\n segmentPath: string\n): CoercedParams {\n let bySlotMap = mergeCache.get(mainParams);\n if (!bySlotMap) {\n bySlotMap = new WeakMap();\n mergeCache.set(mainParams, bySlotMap);\n }\n let byPath = bySlotMap.get(slotParams);\n if (!byPath) {\n byPath = new Map();\n bySlotMap.set(slotParams, byPath);\n }\n const cached = byPath.get(segmentPath);\n if (cached) return cached;\n const merged = mergeSlotParams(mainParams, slotParams[segmentPath]);\n byPath.set(segmentPath, merged);\n return merged;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwCA,SAAgB,gBACd,YACA,YACe;CACf,OAAO,OAAO,OAAO,OAAO,OAAO,IAAI,GAAG,YAAY,UAAU;AAClE;;;;;;;;;;;;;AAcA,SAAgB,qBACd,YACA,YACA,aACe;CACf,IAAI,CAAC,eAAe,CAAC,cAAc,CAAC,OAAO,OAAO,YAAY,WAAW,GAAG,OAAO;CACnF,OAAO,YAAY,YAAY,YAAY,WAAW;AACxD;;;;;;;;;;;;;;;;;;;;AAqBA,IAAM,6BAAa,IAAI,QAA6D;AAEpF,SAAS,YACP,YACA,YACA,aACe;CACf,IAAI,YAAY,WAAW,IAAI,UAAU;CACzC,IAAI,CAAC,WAAW;EACd,4BAAY,IAAI,QAAQ;EACxB,WAAW,IAAI,YAAY,SAAS;CACtC;CACA,IAAI,SAAS,UAAU,IAAI,UAAU;CACrC,IAAI,CAAC,QAAQ;EACX,yBAAS,IAAI,IAAI;EACjB,UAAU,IAAI,YAAY,MAAM;CAClC;CACA,MAAM,SAAS,OAAO,IAAI,WAAW;CACrC,IAAI,QAAQ,OAAO;CACnB,MAAM,SAAS,gBAAgB,YAAY,WAAW,YAAY;CAClE,OAAO,IAAI,aAAa,MAAM;CAC9B,OAAO;AACT"}
1
+ {"version":3,"file":"slot-params-BCTmZkQB.js","names":[],"sources":["../../src/shared/slot-params.ts"],"sourcesContent":["/**\n * Per-slot segment params — the one definition both halves of the API read.\n *\n * A parallel slot matches the URL through its *own* sub-tree, so it can derive\n * a param the main route never had, or derive the same name with a different\n * type (`[...year]` → `string[]` where the main route's `[year]` gave\n * `string`). The server answers `getSegmentParams(slotPath)` from a per-slot\n * map; the client answers `useSegmentParams(slotPath)` from the same map,\n * published on the RSC payload (TIM-1285).\n *\n * The merge lives here rather than in either half because the two must agree\n * about what \"the slot's params\" are — a divergence is invisible until\n * somebody reads a param of the wrong type, which is exactly the class of bug\n * this module closes.\n *\n * Isomorphic: no server or client imports.\n *\n * See design/41-global-params.md §\"Params in a parallel slot\".\n */\n\nimport type { CoercedParams } from './param-value.ts';\n\n/** Slot tree path → that slot's own coerced params. */\nexport type SlotParamsRecord = Record<string, CoercedParams>;\n\n/**\n * The params a slot sees: the main route's as a base, the slot's own on top.\n *\n * Params from segments *above* the slot are not re-derived by the slot's own\n * chain, so they have to come from the main route; params the slot does\n * re-derive may have a different type than the main route gave them, so the\n * slot's win.\n *\n * Null-prototype, like every other param record the framework hands out\n * (`coerceSegmentParams` installs one unconditionally). A spread literal would\n * inherit `Object.prototype`, so a lookup for a param the route does not\n * define would resolve to an inherited member — `params.constructor` returning\n * a function instead of `undefined` — in the one param record that was built\n * by merging. See design/13-security.md #36c.\n */\nexport function mergeSlotParams(\n mainParams: CoercedParams,\n slotParams: CoercedParams\n): CoercedParams {\n return Object.assign(Object.create(null), mainParams, slotParams);\n}\n\n/**\n * Resolve the params for a segment path against a published slot map.\n *\n * Returns the merge when `segmentPath` names a slot that published params,\n * and the main route's record otherwise — an ordinary (non-slot) segment\n * path, an unknown path, or a request with no slots at all. Mirrors the\n * server's `getSegmentParams(segmentPath)` branch exactly.\n *\n * `Object.hasOwn` rather than `in` or a truthiness test: the map arrives from\n * JSON, so a segment path of `constructor` or `toString` would otherwise\n * resolve to an inherited member of `Object.prototype`.\n */\nexport function resolveSegmentParams(\n mainParams: CoercedParams,\n slotParams: SlotParamsRecord | null | undefined,\n segmentPath: string | undefined\n): CoercedParams {\n if (!segmentPath || !slotParams || !Object.hasOwn(slotParams, segmentPath)) return mainParams;\n return cachedMerge(mainParams, slotParams, segmentPath);\n}\n\n/**\n * The merged record for one (main record, slot map, path) triple, reused until\n * one of the three changes.\n *\n * `useSegmentParams(slotPath)` calls this on every render. Returning a fresh\n * object each time makes the hook's result change by reference on renders where\n * nothing navigated, so a component using it as a `useEffect` dependency\n * re-runs the effect — and re-renders in a loop if that effect sets state —\n * while memoized children below it lose their memoization.\n *\n * Keyed on the *identities* of the two records, not their contents: both are\n * replaced rather than mutated on every navigation (`setCurrentParams`,\n * `setCurrentSlotParams`, and the server's per-request store), so identity is\n * the correct equality here and a deep comparison would be slower and no more\n * accurate.\n *\n * A `WeakMap` chain rather than a `Map`, so a superseded navigation's records\n * and their merges become collectable as soon as nothing else holds them.\n */\nconst mergeCache = new WeakMap<object, WeakMap<object, Map<string, CoercedParams>>>();\n\nfunction cachedMerge(\n mainParams: CoercedParams,\n slotParams: SlotParamsRecord,\n segmentPath: string\n): CoercedParams {\n let bySlotMap = mergeCache.get(mainParams);\n if (!bySlotMap) {\n bySlotMap = new WeakMap();\n mergeCache.set(mainParams, bySlotMap);\n }\n let byPath = bySlotMap.get(slotParams);\n if (!byPath) {\n byPath = new Map();\n bySlotMap.set(slotParams, byPath);\n }\n const cached = byPath.get(segmentPath);\n if (cached) return cached;\n const merged = mergeSlotParams(mainParams, slotParams[segmentPath]);\n byPath.set(segmentPath, merged);\n return merged;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwCA,SAAgB,gBACd,YACA,YACe;CACf,OAAO,OAAO,OAAO,OAAO,OAAO,IAAI,GAAG,YAAY,UAAU;AAClE;;;;;;;;;;;;;AAcA,SAAgB,qBACd,YACA,YACA,aACe;CACf,IAAI,CAAC,eAAe,CAAC,cAAc,CAAC,OAAO,OAAO,YAAY,WAAW,GAAG,OAAO;CACnF,OAAO,YAAY,YAAY,YAAY,WAAW;AACxD;;;;;;;;;;;;;;;;;;;;AAqBA,IAAM,6BAAa,IAAI,QAA6D;AAEpF,SAAS,YACP,YACA,YACA,aACe;CACf,IAAI,YAAY,WAAW,IAAI,UAAU;CACzC,IAAI,CAAC,WAAW;EACd,4BAAY,IAAI,QAAQ;EACxB,WAAW,IAAI,YAAY,SAAS;CACtC;CACA,IAAI,SAAS,UAAU,IAAI,UAAU;CACrC,IAAI,CAAC,QAAQ;EACX,yBAAS,IAAI,IAAI;EACjB,UAAU,IAAI,YAAY,MAAM;CAClC;CACA,MAAM,SAAS,OAAO,IAAI,WAAW;CACrC,IAAI,QAAQ,OAAO;CACnB,MAAM,SAAS,gBAAgB,YAAY,WAAW,YAAY;CAClE,OAAO,IAAI,aAAa,MAAM;CAC9B,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-data-14MXm7Pj.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-segment-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { CoercedParams } from '../shared/param-value.js';\nimport type { SlotParamsRecord } from '../shared/slot-params.js';\nimport type { RouterInstance } from './router-types.js';\nimport type { SsrData } from './ssr-data.js';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-segment-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: CoercedParams = {};\n\nexport function _setCurrentParams(params: CoercedParams): void {\n currentParams = params;\n}\n\n/**\n * Per-slot params snapshot, keyed by slot tree path — the module-level\n * fallback behind `useSegmentParams(slotPath)` when no NavigationContext is\n * mounted (tests, calls outside a component). Replaced, never mutated, on\n * each navigation. See TIM-1285.\n */\nexport let currentSlotParams: SlotParamsRecord | null = null;\n\nexport function _setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {\n currentSlotParams = slotParams;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.js';\nimport type { CoercedParams } from '../shared/param-value.js';\nimport type { SlotParamsRecord } from '../shared/slot-params.js';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: CoercedParams;\n /**\n * Per-slot params keyed by slot tree path, absent when the request rendered\n * no slot with params of its own (TIM-1285).\n */\n slotParams?: SlotParamsRecord;\n /**\n * Mutable reference to NavContext for error boundary → pipeline communication.\n *\n * When TimberErrorBoundary catches a DenySignal during SSR, it:\n * 1. Sets `statusCode` to the deny status (e.g., 403) — so the HTTP\n * Response has the correct status code without a re-render.\n * 2. Sets `_denyHandledByBoundary = true` — so the pipeline skips\n * the redundant renderDenyPage() re-render.\n *\n * This runs synchronously during Fizz rendering, BEFORE onShellReady,\n * so the status code is committed before any bytes are sent.\n *\n * See TIM-664, design/04-authorization.md §\"React.cache Scope in Deny/Error Re-renders\"\n */\n _navContext?: { statusCode?: number; _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n"],"mappings":";;AA6BA,IAAW,eAAsC;AAEjD,SAAgB,iBAAiB,QAAqC;CACpE,eAAe;AACjB;;;;;AAQA,IAAW;;AAOX,IAAW;AAEX,SAAgB,mBAAmB,MAAiC;CAClE,iBAAiB;AACnB;;AAKA,IAAW,gBAA+B,CAAC;AAE3C,SAAgB,kBAAkB,QAA6B;CAC7D,gBAAgB;AAClB;;;;;;;AAQA,IAAW,oBAA6C;AAExD,SAAgB,sBAAsB,YAA2C;CAC/E,oBAAoB;AACtB;;AAQA,IAAW,eAAe;AAC1B,IAAW,qBAAqB,IAAI,gBAAgB;AAEpD,SAAgB,iBAAiB,QAAgB,QAA+B;CAC9E,eAAe;CACf,qBAAqB;AACvB;;AAKA,IAAW,YAAY;AAEvB,SAAgB,cAAc,OAAsB;CAClD,YAAY;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQA,SAAgB,WAAW,MAAqB;CAC9C,mBAAmB,IAAI;AACzB;;;;;;;AAQA,SAAgB,eAAqB;CACnC,mBAAmB,KAAA,CAAS;AAC9B;;;;;;;;;;AAWA,SAAgB,aAAkC;CAChD,IAAI,iBACF,OAAO,gBAAgB;CAEzB,OAAO;AACT"}
1
+ {"version":3,"file":"ssr-data-14MXm7Pj.js","names":[],"sources":["../../src/client/state.ts","../../src/client/ssr-data.ts"],"sourcesContent":["/**\n * Centralized client singleton state registry.\n *\n * ALL mutable module-level state that must have singleton semantics across\n * the client bundle lives here. Individual modules (router-ref.ts, ssr-data.ts,\n * use-segment-params.ts, use-search-params.ts, unload-guard.ts) import from this file\n * and re-export thin wrapper functions.\n *\n * Why: In Vite dev, a module is instantiated separately if reached via different\n * import paths (e.g., relative `./foo.js` vs barrel `@timber-js/app/client`).\n * By centralizing all mutable state in a single module that is always reached\n * through the same dependency chain (barrel → wrapper → state.ts), we guarantee\n * a single instance of every piece of shared state.\n *\n * DO NOT import this file from outside client/. Server code must never depend\n * on client state. The barrel (client/index.ts) is the public entry point.\n *\n * See design/18-build-system.md §\"Module Singleton Strategy\" and\n * §\"Singleton State Registry\".\n */\n\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\nimport type { RouterInstance } from './router-types.ts';\nimport type { SsrData } from './ssr-data.ts';\n\n// ─── Router (from router-ref.ts) ──────────────────────────────────────────\n\n/** The global router singleton — set once during bootstrap. */\nexport let globalRouter: RouterInstance | null = null;\n\nexport function _setGlobalRouter(router: RouterInstance | null): void {\n globalRouter = router;\n}\n\n// ─── SSR Data Provider (from ssr-data.ts) ──────────────────────────────────\n\n/**\n * ALS-backed SSR data provider. When registered, getSsrData() reads from\n * this function (ALS store) instead of module-level currentSsrData.\n */\nexport let ssrDataProvider: (() => SsrData | undefined) | undefined;\n\nexport function _setSsrDataProvider(provider: (() => SsrData | undefined) | undefined): void {\n ssrDataProvider = provider;\n}\n\n/** Fallback SSR data for tests and environments without ALS. */\nexport let currentSsrData: SsrData | undefined;\n\nexport function _setCurrentSsrData(data: SsrData | undefined): void {\n currentSsrData = data;\n}\n\n// ─── Route Params (from use-segment-params.ts) ──────────────────────────────────────\n\n/** Current route params snapshot — replaced (not mutated) on each navigation. */\nexport let currentParams: CoercedParams = {};\n\nexport function _setCurrentParams(params: CoercedParams): void {\n currentParams = params;\n}\n\n/**\n * Per-slot params snapshot, keyed by slot tree path — the module-level\n * fallback behind `useSegmentParams(slotPath)` when no NavigationContext is\n * mounted (tests, calls outside a component). Replaced, never mutated, on\n * each navigation. See TIM-1285.\n */\nexport let currentSlotParams: SlotParamsRecord | null = null;\n\nexport function _setCurrentSlotParams(slotParams: SlotParamsRecord | null): void {\n currentSlotParams = slotParams;\n}\n\n/** Listeners notified when currentParams changes. */\nexport const paramsListeners = new Set<() => void>();\n\n// ─── Search Params Cache (from use-search-params.ts) ────────────────────────\n\n/** Cached search string — avoids reparsing when URL hasn't changed. */\nexport let cachedSearch = '';\nexport let cachedSearchParams = new URLSearchParams();\n\nexport function _setCachedSearch(search: string, params: URLSearchParams): void {\n cachedSearch = search;\n cachedSearchParams = params;\n}\n\n// ─── Unload Guard (from unload-guard.ts) ─────────────────────────────────────\n\n/** Whether the page is currently being unloaded. */\nexport let unloading = false;\n\nexport function _setUnloading(value: boolean): void {\n unloading = value;\n}\n","/**\n * SSR Data — per-request state for client hooks during server-side rendering.\n *\n * RSC and SSR are separate Vite module graphs (see design/18-build-system.md),\n * so the RSC environment's request-context ALS is not visible to SSR modules.\n * This module provides getter/setter functions that ssr-entry.ts uses to\n * populate per-request data for React's render.\n *\n * Request isolation: On the server, ssr-entry.ts registers an ALS-backed\n * provider via registerSsrDataProvider(). getSsrData() reads from the ALS\n * store, ensuring correct per-request data even when Suspense boundaries\n * resolve asynchronously across concurrent requests. The module-level\n * setSsrData/clearSsrData functions are kept as a fallback for tests\n * and environments without ALS.\n *\n * IMPORTANT: This module must NOT import node:async_hooks or any Node.js-only\n * APIs, as it's imported by 'use client' hooks that are bundled for the browser.\n * The ALS instance lives in ssr-entry.ts (server-only); this module only holds\n * a reference to the provider function.\n *\n * All mutable state is delegated to client/state.ts for singleton guarantees.\n * See design/18-build-system.md §\"Singleton State Registry\"\n */\n\nimport {\n ssrDataProvider,\n currentSsrData,\n _setSsrDataProvider,\n _setCurrentSsrData,\n} from './state.ts';\nimport type { CoercedParams } from '../shared/param-value.ts';\nimport type { SlotParamsRecord } from '../shared/slot-params.ts';\n\n// ─── Types ────────────────────────────────────────────────────────\n\nexport interface SsrData {\n /** The request's URL pathname (e.g. '/dashboard/settings') */\n pathname: string;\n /** The request's search params as a plain record */\n searchParams: Record<string, string>;\n /** The request's cookies as name→value pairs */\n cookies: Map<string, string>;\n /** The request's route params (e.g. { id: '123' }) */\n params: CoercedParams;\n /**\n * Per-slot params keyed by slot tree path, absent when the request rendered\n * no slot with params of its own (TIM-1285).\n */\n slotParams?: SlotParamsRecord;\n /**\n * Mutable reference to NavContext for error boundary → pipeline communication.\n *\n * When TimberErrorBoundary catches a DenySignal during SSR, it:\n * 1. Sets `statusCode` to the deny status (e.g., 403) — so the HTTP\n * Response has the correct status code without a re-render.\n * 2. Sets `_denyHandledByBoundary = true` — so the pipeline skips\n * the redundant renderDenyPage() re-render.\n *\n * This runs synchronously during Fizz rendering, BEFORE onShellReady,\n * so the status code is committed before any bytes are sent.\n *\n * See TIM-664, design/04-authorization.md §\"React.cache Scope in Deny/Error Re-renders\"\n */\n _navContext?: { statusCode?: number; _denyHandledByBoundary?: boolean };\n}\n\n// ─── ALS-Backed Provider ─────────────────────────────────────────\n//\n// Server-side code (ssr-entry.ts) registers a provider that reads\n// from AsyncLocalStorage. This avoids importing node:async_hooks\n// in this browser-bundled module.\n//\n// Module singleton guarantee: In Vite's SSR environment, both\n// ssr-entry.ts (via #/client/ssr-data.js) and client component hooks\n// (via @timber-js/app/client) must resolve to the SAME module instance\n// of this file. The timber-shims plugin ensures this by remapping\n// @timber-js/app/client → src/client/index.ts in the SSR environment.\n// Without this remap, @timber-js/app/client resolves to dist/ (via\n// package.json exports), creating a split where registerSsrDataProvider\n// writes to one instance but getSsrData reads from another.\n// See timber-shims plugin resolveId for details.\n\n/**\n * Register an ALS-backed SSR data provider. Called once at module load\n * by ssr-entry.ts to wire up per-request data via AsyncLocalStorage.\n *\n * When registered, getSsrData() reads from the provider (ALS store)\n * instead of module-level state, ensuring correct isolation for\n * concurrent requests with streaming Suspense.\n */\nexport function registerSsrDataProvider(provider: () => SsrData | undefined): void {\n _setSsrDataProvider(provider);\n}\n\n// ─── Module-Level Fallback ────────────────────────────────────────\n//\n// Used by tests and as a fallback when no ALS provider is registered.\n\n/**\n * Set the SSR data for the current request via module-level state.\n *\n * In production, ssr-entry.ts uses ALS (runWithSsrData) instead.\n * This function is retained for tests and as a fallback.\n */\nexport function setSsrData(data: SsrData): void {\n _setCurrentSsrData(data);\n}\n\n/**\n * Clear the SSR data after rendering completes.\n *\n * In production, ALS scope handles cleanup automatically.\n * This function is retained for tests and as a fallback.\n */\nexport function clearSsrData(): void {\n _setCurrentSsrData(undefined);\n}\n\n/**\n * Read the current request's SSR data. Returns undefined when called\n * outside an SSR render (i.e. on the client after hydration).\n *\n * Prefers the ALS-backed provider when registered (server-side),\n * falling back to module-level state (tests, legacy).\n *\n * Used by client hooks' server snapshot functions.\n */\nexport function getSsrData(): SsrData | undefined {\n if (ssrDataProvider) {\n return ssrDataProvider();\n }\n return currentSsrData;\n}\n"],"mappings":";;AA6BA,IAAW,eAAsC;AAEjD,SAAgB,iBAAiB,QAAqC;CACpE,eAAe;AACjB;;;;;AAQA,IAAW;;AAOX,IAAW;AAEX,SAAgB,mBAAmB,MAAiC;CAClE,iBAAiB;AACnB;;AAKA,IAAW,gBAA+B,CAAC;AAE3C,SAAgB,kBAAkB,QAA6B;CAC7D,gBAAgB;AAClB;;;;;;;AAQA,IAAW,oBAA6C;AAExD,SAAgB,sBAAsB,YAA2C;CAC/E,oBAAoB;AACtB;;AAQA,IAAW,eAAe;AAC1B,IAAW,qBAAqB,IAAI,gBAAgB;AAEpD,SAAgB,iBAAiB,QAAgB,QAA+B;CAC9E,eAAe;CACf,qBAAqB;AACvB;;AAKA,IAAW,YAAY;AAEvB,SAAgB,cAAc,OAAsB;CAClD,YAAY;AACd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACQA,SAAgB,WAAW,MAAqB;CAC9C,mBAAmB,IAAI;AACzB;;;;;;;AAQA,SAAgB,eAAqB;CACnC,mBAAmB,KAAA,CAAS;AAC9B;;;;;;;;;;AAWA,SAAgB,aAAkC;CAChD,IAAI,iBACF,OAAO,gBAAgB;CAEzB,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"use-query-states-I3JMng6J.js","names":[],"sources":["../../src/search-params/parse-total.ts","../../src/search-params/serialize-equal.ts","../../src/client/use-query-states.ts"],"sourcesContent":["/**\n * parseTotal — invoke a codec over the FULL raw domain.\n *\n * `SearchParamCodec.parse` is documented to be total over\n * `string | string[] | undefined`, because that is exactly what a URL\n * hands it: a param can be absent (`undefined`) or repeated (`string[]`).\n * Timber's own codecs and the Standard Schema bridges honour that.\n *\n * nuqs parsers do not. Their `parse` expects a **present scalar string** —\n * nuqs checks presence itself before ever calling it — so `parseAsBoolean`\n * and `parseAsIsoDate` threw a render-phase 500 on an absent param and\n * `parseAsString` returned an array on a repeated one (TIM-1350).\n *\n * nuqs ships the missing adapter: every parser builder exposes\n * `parseServerSide(value: string | string[] | undefined)`, which maps\n * absent → `null` (or the parser's `withDefault` value), takes the FIRST\n * entry of a repeated param (matching `URLSearchParams.get()`), and wraps\n * the inner `parse` so a throw becomes `null`. That is precisely timber's\n * domain, so we call it in preference to `parse` rather than hand-rolling\n * a second normalization that could disagree with the client hook.\n *\n * Feature detection, not an instanceof check: any codec MAY publish\n * `parseServerSide` to declare \"this is my total entry point\" — it is an\n * optional member of `SearchParamCodec` — and a codec that does not is\n * assumed already total and called through `parse`. Timber codecs and\n * schema bridges take the second branch untouched; several of them rely on\n * `parse(undefined)` to produce their default.\n *\n * **Search params only.** Segment params (`server/param-coercion.ts`) call\n * `codec.parse` directly and must keep doing so: their domain is a value\n * the router matched, never absent, and a codec that REJECTS one is how a\n * route produces a 404. Routing a rejection through nuqs's `safeParse`\n * would turn that 404 into a silent `null` param. The two domains differ\n * in what \"no value\" means, not just in plumbing. Cookies are a third\n * domain (`cookies/define-cookie.ts`) and are likewise untouched.\n *\n * `parseServerSide` carries a `@deprecated` tag in nuqs (it steers users to\n * loaders, which timber does not use). It remains public, typed and\n * exercised; `tests/nuqs-codec-boundary.test.ts` asserts totality for every\n * parser through timber's own API, so a nuqs release that drops it fails\n * loudly rather than silently reinstating the 500s.\n *\n * Design doc: design/23-search-params.md §\"nuqs parsers, made total\"\n */\n\n/**\n * Minimal interface for anything `parseTotal` can dispatch. Only `parse` is\n * required; `parseServerSide` is the optional total entry point. `serialize`\n * is deliberately absent — `parseTotal` never calls it, and requiring it\n * would prevent `SearchParamCodec<T>` (whose serialize returns `string |\n * string[] | null`) from being passed where `Codec<T>` (whose serialize\n * returns `string | null`) is expected.\n */\nexport interface ParseableCodec<T> {\n parse(value: string | string[] | undefined): T;\n parseServerSide?(value: string | string[] | undefined): T;\n}\n\n/**\n * A codec that publishes a total entry point over the raw URL domain.\n *\n * The return is `T`, not `T | null`. This is the entry point timber calls,\n * so whatever it answers IS the field's type. A `null` for an absent param\n * belongs in `T` — a bare nuqs parser is a codec of `string | null`, and\n * `parseAsInteger.withDefault(1)` is a codec of `number`, because nuqs\n * narrows its own `parseServerSide` return to `NonNullable<T>`. Declaring\n * `T | null` here would let a codec annotated `SearchParamCodec<string>`\n * hand back `null` under a non-nullable type (TIM-1350 review).\n */\nexport interface TotalCodec<T> {\n parseServerSide(value: string | string[] | undefined): T;\n}\n\nfunction hasParseServerSide<T>(\n codec: ParseableCodec<T>\n): codec is ParseableCodec<T> & TotalCodec<T> {\n return typeof (codec as Partial<TotalCodec<T>>).parseServerSide === 'function';\n}\n\n/**\n * Parse a raw URL value through a codec, using the codec's total entry\n * point when it publishes one.\n *\n * Returns `T`, from both branches. A `null` for an absent param is part of\n * the codec's own `T` — see TotalCodec above — so this signature does not\n * widen it, and a caller that must handle \"no value\" (`withDefault`) sees\n * it because `T` carries it.\n */\nexport function parseTotal<T>(codec: ParseableCodec<T>, raw: string | string[] | undefined): T {\n return hasParseServerSide(codec) ? codec.parseServerSide(raw) : codec.parse(raw);\n}\n","/**\n * Compare two serialize results for equality. Needed because `string[]`\n * from repeated-key codecs does not compare by reference.\n *\n * Shared between define.ts (buildSearchParams default-omission) and\n * use-query-states.ts (bridgeCodec eq). One source of truth.\n */\nexport function serializedEqual(a: string | string[] | null, b: string | string[] | null): boolean {\n if (a === b) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n }\n return false;\n}\n","/**\n * useQueryStates — client-side hook for URL-synced search params.\n *\n * Delegates to nuqs for URL synchronization, batching, React 19 transitions,\n * and throttled URL writes. Bridges timber's SearchParamCodec protocol to\n * nuqs-compatible parsers.\n *\n * Design doc: design/23-search-params.md §\"Codec Bridge\"\n */\n\n'use client';\n\nimport { useQueryStates as nuqsUseQueryStates } from 'nuqs';\nimport type { MultiParser } from 'nuqs';\nimport type {\n SearchParamCodec,\n SearchParamsDefinition,\n SetParams,\n QueryStatesOptions,\n} from '../search-params/define.js';\nimport { parseTotal } from '../search-params/parse-total.js';\nimport { serializedEqual } from '../search-params/serialize-equal.js';\n\n// ─── Codec Bridge ─────────────────────────────────────────────────\n\n// nuqs's parser contract conflates values timber codecs distinguish:\n// parse() returning null means \"unparseable, substitute defaultValue\",\n// and undefined entries are skipped entirely. Timber codecs can\n// legitimately produce both — bare z.string() yields undefined for absent\n// params (implicit optionality), and a codec may map a present value to\n// null. Wrap those two values in sentinels across the nuqs boundary and\n// unwrap them before handing values back to the caller, so the client\n// hook returns exactly what server-side parse() returns.\n// Unique object references compared by identity — a codec can never\n// produce these from URL input, so user-controlled strings cannot collide\n// with them (unlike string sentinels), and unlike Symbols they survive\n// nuqs's internal string coercion without throwing.\nconst NULL_SENTINEL: object = { timberSentinel: 'null' };\nconst UNDEFINED_SENTINEL: object = { timberSentinel: 'undefined' };\n\nfunction wrapNuqsValue(value: unknown): unknown {\n if (value === null) return NULL_SENTINEL;\n if (value === undefined) return UNDEFINED_SENTINEL;\n return value;\n}\n\nfunction unwrapNuqsValue(value: unknown): unknown {\n if (value === NULL_SENTINEL) return null;\n if (value === UNDEFINED_SENTINEL) return undefined;\n return value;\n}\n\n/**\n * Bridge a timber SearchParamCodec to a nuqs-compatible MultiParser.\n *\n * nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }\n * timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }\n *\n * The defaultValue is computed eagerly, through `parseTotal` — the same\n * entry point server-side `parse()` uses, so the hook and the server agree\n * on what an absent param means (a bare nuqs parser answers `null`, not\n * `undefined`; TIM-1350). Codecs are documented to return a default rather\n * than throw, but a throwing codec must not crash every component that\n * mounts the hook — treat its default as undefined and let its error\n * surface from server-side parse() instead.\n *\n * A `null` absent-value is NOT registered as the nuqs default. nuqs\n * already represents an absent key as `null`, so the hook reads the same\n * value either way — but registering it makes `clearOnDefault` fire on\n * `setParams({ q: null })` and delete the key before the bridged\n * `serialize` runs. For a codec that encodes `null` as a real query value\n * (`serialize(null) === 'none'`), that silently disagrees with\n * `buildSearchParams({ q: null })`, which writes it. Same reasoning as\n * `getDefaultSerialized` on the server: a codec with no value for an\n * absent param has no default to register.\n */\nfunction bridgeCodec<T>(codec: SearchParamCodec<T>): MultiParser<T> & { defaultValue: T } {\n let absent: unknown;\n try {\n absent = parseTotal(codec, undefined);\n } catch {\n absent = undefined;\n }\n\n const parser = {\n // `multi`, so nuqs reads the key with `searchParams.getAll()` and hands\n // us EVERY value. A single parser reads `.get()` — the first value only\n // — which is not the domain a timber codec is defined over. The server\n // parses `?tags=a&tags=b` as `['a','b']`; a single parser made the hook\n // answer `['a']` for the same URL, under a declared `string[]` that\n // admitted no such disagreement (TIM-1352). Scalar codecs are unaffected:\n // they receive the array and take `value[0]`, exactly as they do on the\n // server, so first-value-wins is preserved through the same code path\n // rather than through nuqs's reader.\n //\n // nuqs never calls this with an empty array — `isAbsentFromUrl` treats\n // `[]` as absent and answers `defaultValue` directly — which is what\n // keeps the absent case agreeing with the server's `undefined`.\n //\n // Reading every value is only half of it: the values must arrive in the\n // SAME SHAPE the server would have produced, or the divergence just\n // moves. `normalizeRaw` (search-params/define.ts) collapses a\n // single-valued key to a bare string and keeps an array only for a\n // repeated one, so this mirrors that rule exactly. Handing a codec\n // `['3']` where the server hands it `'3'` breaks every codec whose\n // `parse` is written for the scalar case — which is most hand-written\n // ones, contract or no contract.\n type: 'multi' as const,\n // Through parseTotal, not codec.parse. nuqs's own `.withDefault(d)`\n // overrides ONLY `parseServerSide`, so `parseAsInteger.withDefault(1)`\n // on `?page=abc` returned 1 from the server and null from the hook —\n // a divergence the declared non-nullable `number` did not admit.\n parse: (v: readonly string[]) =>\n wrapNuqsValue(parseTotal(codec, v.length === 1 ? v[0] : [...v])),\n serialize: (v: unknown) => {\n const value = unwrapNuqsValue(v);\n if (value === undefined) return [''];\n // TIM-1354: catch TypeError from codecs whose serialize is not total\n // over null (e.g. parseAsIsoDate.serialize(null) → null.toISOString()).\n // Only TypeError — deliberate signals must not be swallowed.\n let result: string | string[] | null;\n try {\n result = codec.serialize(value as T);\n } catch (error) {\n if (error instanceof TypeError) return [''];\n throw error;\n }\n // TIM-1353: pass through string[] from codecs that emit repeated\n // keys. nuqs multi parsers append one key=value per array element.\n if (Array.isArray(result)) return result;\n return [result ?? ''];\n },\n eq: (a: unknown, b: unknown) => {\n if (a === b) return true;\n try {\n return serializedEqual(\n codec.serialize(unwrapNuqsValue(a) as T),\n codec.serialize(unwrapNuqsValue(b) as T)\n );\n } catch {\n return false;\n }\n },\n } as MultiParser<T> & { defaultValue: T };\n\n if (absent !== null) parser.defaultValue = wrapNuqsValue(absent) as T;\n return parser;\n}\n\n/**\n * Collect `withUrlKey` aliases off a codec map.\n *\n * `withUrlKey(codec, 'q')` returns a codec carrying `urlKey: 'q'`, so the map\n * alone is enough to reconstruct the aliases — `defineSearchParams` builds its\n * own `urlKeys` from exactly this property.\n */\nfunction deriveUrlKeys(codecs: Record<string, SearchParamCodec<unknown>>): Record<string, string> {\n const result: Record<string, string> = {};\n for (const key of Object.keys(codecs)) {\n const alias = codecs[key]?.urlKey;\n if (alias) result[key] = alias;\n }\n return result;\n}\n\n/**\n * Bridge an entire codec map to nuqs-compatible parsers.\n */\nfunction bridgeCodecs<T extends Record<string, unknown>>(codecs: {\n [K in keyof T]: SearchParamCodec<T[K]>;\n}) {\n const result: Record<string, MultiParser<unknown> & { defaultValue: unknown }> = {};\n for (const key of Object.keys(codecs)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result[key] = bridgeCodec(codecs[key as keyof T]) as any;\n }\n return result as { [K in keyof T]: MultiParser<T[K]> & { defaultValue: T[K] } };\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────\n\n/**\n * Read and write typed search params from/to the URL.\n *\n * Delegates to nuqs internally. The timber nuqs adapter (auto-injected in\n * browser-entry.ts) handles RSC navigation on non-shallow updates.\n *\n * Usage:\n * ```ts\n * // Via a SearchParamsDefinition imported from the route's params.ts\n * const [params, setParams] = definition.useQueryStates()\n *\n * // Standalone with inline codecs\n * const [params, setParams] = useQueryStates({\n * page: fromSchema(z.coerce.number().int().min(1).default(1)),\n * })\n * ```\n *\n * There is deliberately no route-string form (`useQueryStates('/products')`).\n * Importing the definition from `params.ts` is the documented way to reach\n * another route's codecs — it needs no runtime registry lookup and so has no\n * \"not registered yet\" failure mode. See design/23-search-params.md\n * §\"Client Access\".\n */\nexport function useQueryStates<T extends Record<string, unknown>>(\n codecs: { [K in keyof T]: SearchParamCodec<T[K]> },\n _options?: QueryStatesOptions,\n urlKeys?: Readonly<Record<string, string>>\n): [T, SetParams<T>] {\n const bridged = bridgeCodecs(codecs);\n\n // Forward hook-level options (shallow, scroll, history) to nuqs.\n // These become the default for all setter calls from this hook instance.\n // Per-call options in setParams(values, opts) override these defaults.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const nuqsOptions: any = {};\n if (_options?.shallow !== undefined) nuqsOptions.shallow = _options.shallow;\n if (_options?.scroll !== undefined) nuqsOptions.scroll = _options.scroll;\n if (_options?.history !== undefined) nuqsOptions.history = _options.history;\n // `withUrlKey` attaches the alias to the codec itself — that is the design's\n // \"URL keys travel with codecs\" principle — so the aliases are derivable\n // here and must be, for the inline codec-map form: nobody passes `urlKeys`\n // on that path, and without this an aliased bundle silently read and wrote\n // the property name instead of the alias. `bindUseQueryStates` still passes\n // the definition's precomputed map, which wins on conflict; it is built from\n // these same codecs, so the two agree by construction rather than by luck.\n const resolvedUrlKeys = { ...deriveUrlKeys(codecs), ...urlKeys };\n if (Object.keys(resolvedUrlKeys).length > 0) {\n nuqsOptions.urlKeys = resolvedUrlKeys;\n }\n\n let values: Record<string, unknown>;\n let setValues: Function;\n try {\n [values, setValues] = nuqsUseQueryStates(bridged, nuqsOptions);\n } catch (err) {\n if (\n err instanceof Error &&\n /Invalid hook call|cannot be called|Cannot read properties of null/i.test(err.message)\n ) {\n throw new Error(\n 'useQueryStates is a client component hook and cannot be called outside a React component. ' +\n 'Use definition.parse(searchParams) in server components instead.'\n );\n }\n throw err;\n }\n\n // Unwrap the null/undefined sentinels the bridge injected (see Codec\n // Bridge above) so callers see exactly what server-side parse() returns.\n // Copy-on-write preserves the identity of nuqs's memoized values object\n // when nothing needs unwrapping.\n let normalized = values;\n for (const key of Object.keys(bridged)) {\n const value = normalized[key];\n if (value === NULL_SENTINEL || value === UNDEFINED_SENTINEL) {\n if (normalized === values) normalized = { ...values };\n normalized[key] = unwrapNuqsValue(value);\n }\n }\n\n // Wrap the nuqs setter to match timber's SetParams<T> signature.\n // nuqs's setter accepts Partial<Nullable<Values>> | UpdaterFn | null.\n // timber's setter accepts Partial<T> with optional SetParamsOptions.\n const setParams: SetParams<T> = (partial, setOptions?) => {\n const nuqsSetOptions: Record<string, unknown> = {};\n if (setOptions?.shallow !== undefined) nuqsSetOptions.shallow = setOptions.shallow;\n if (setOptions?.scroll !== undefined) nuqsSetOptions.scroll = setOptions.scroll;\n if (setOptions?.history !== undefined) nuqsSetOptions.history = setOptions.history;\n // nuqs's update loop skips undefined entries and treats null as a\n // key deletion before serialize runs. Timber semantics:\n // - setParams({ q: undefined }) must clear ?q= (absent = undefined),\n // so explicit undefined maps to a null deletion.\n // - setParams({ q: null }) clears the key only when the codec encodes\n // null as \"omit\" (serialize(null) === null). If the codec encodes\n // null as a real query value, forward the sentinel so the bridged\n // serialize writes it — matching definition.serialize({ q: null }).\n let forwarded: Record<string, unknown> = partial;\n for (const key of Object.keys(partial)) {\n const value = partial[key as keyof T];\n if (value === undefined) {\n if (forwarded === partial) forwarded = { ...partial };\n forwarded[key] = null;\n } else if (value === null) {\n let encoded: string | string[] | null = null;\n try {\n const raw = codecs[key as keyof T]?.serialize(null as T[keyof T]);\n // string[] is non-null, but an empty array carries no value\n encoded = raw === null ? null : Array.isArray(raw) ? (raw.length > 0 ? raw : null) : raw;\n } catch {\n // Codec can't serialize null — treat as a deletion.\n }\n if (encoded !== null) {\n if (forwarded === partial) forwarded = { ...partial };\n forwarded[key] = NULL_SENTINEL;\n }\n }\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n void setValues(forwarded as any, nuqsSetOptions);\n };\n\n return [normalized as T, setParams];\n}\n\n// ─── Definition binding ───────────────────────────────────────────\n\n/**\n * Create a useQueryStates binding for a SearchParamsDefinition.\n * This is used internally by SearchParamsDefinition.useQueryStates().\n */\nexport function bindUseQueryStates<T extends Record<string, unknown>>(\n definition: SearchParamsDefinition<T>\n): (options?: QueryStatesOptions) => [T, SetParams<T>] {\n return (options?: QueryStatesOptions) => {\n return useQueryStates<T>(definition.codecs, options, definition.urlKeys);\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-query-states-I3JMng6J.js","names":[],"sources":["../../src/search-params/parse-total.ts","../../src/search-params/serialize-equal.ts","../../src/client/use-query-states.ts"],"sourcesContent":["/**\n * parseTotal — invoke a codec over the FULL raw domain.\n *\n * `SearchParamCodec.parse` is documented to be total over\n * `string | string[] | undefined`, because that is exactly what a URL\n * hands it: a param can be absent (`undefined`) or repeated (`string[]`).\n * Timber's own codecs and the Standard Schema bridges honour that.\n *\n * nuqs parsers do not. Their `parse` expects a **present scalar string** —\n * nuqs checks presence itself before ever calling it — so `parseAsBoolean`\n * and `parseAsIsoDate` threw a render-phase 500 on an absent param and\n * `parseAsString` returned an array on a repeated one (TIM-1350).\n *\n * nuqs ships the missing adapter: every parser builder exposes\n * `parseServerSide(value: string | string[] | undefined)`, which maps\n * absent → `null` (or the parser's `withDefault` value), takes the FIRST\n * entry of a repeated param (matching `URLSearchParams.get()`), and wraps\n * the inner `parse` so a throw becomes `null`. That is precisely timber's\n * domain, so we call it in preference to `parse` rather than hand-rolling\n * a second normalization that could disagree with the client hook.\n *\n * Feature detection, not an instanceof check: any codec MAY publish\n * `parseServerSide` to declare \"this is my total entry point\" — it is an\n * optional member of `SearchParamCodec` — and a codec that does not is\n * assumed already total and called through `parse`. Timber codecs and\n * schema bridges take the second branch untouched; several of them rely on\n * `parse(undefined)` to produce their default.\n *\n * **Search params only.** Segment params (`server/param-coercion.ts`) call\n * `codec.parse` directly and must keep doing so: their domain is a value\n * the router matched, never absent, and a codec that REJECTS one is how a\n * route produces a 404. Routing a rejection through nuqs's `safeParse`\n * would turn that 404 into a silent `null` param. The two domains differ\n * in what \"no value\" means, not just in plumbing. Cookies are a third\n * domain (`cookies/define-cookie.ts`) and are likewise untouched.\n *\n * `parseServerSide` carries a `@deprecated` tag in nuqs (it steers users to\n * loaders, which timber does not use). It remains public, typed and\n * exercised; `tests/nuqs-codec-boundary.test.ts` asserts totality for every\n * parser through timber's own API, so a nuqs release that drops it fails\n * loudly rather than silently reinstating the 500s.\n *\n * Design doc: design/23-search-params.md §\"nuqs parsers, made total\"\n */\n\n/**\n * Minimal interface for anything `parseTotal` can dispatch. Only `parse` is\n * required; `parseServerSide` is the optional total entry point. `serialize`\n * is deliberately absent — `parseTotal` never calls it, and requiring it\n * would prevent `SearchParamCodec<T>` (whose serialize returns `string |\n * string[] | null`) from being passed where `Codec<T>` (whose serialize\n * returns `string | null`) is expected.\n */\nexport interface ParseableCodec<T> {\n parse(value: string | string[] | undefined): T;\n parseServerSide?(value: string | string[] | undefined): T;\n}\n\n/**\n * A codec that publishes a total entry point over the raw URL domain.\n *\n * The return is `T`, not `T | null`. This is the entry point timber calls,\n * so whatever it answers IS the field's type. A `null` for an absent param\n * belongs in `T` — a bare nuqs parser is a codec of `string | null`, and\n * `parseAsInteger.withDefault(1)` is a codec of `number`, because nuqs\n * narrows its own `parseServerSide` return to `NonNullable<T>`. Declaring\n * `T | null` here would let a codec annotated `SearchParamCodec<string>`\n * hand back `null` under a non-nullable type (TIM-1350 review).\n */\nexport interface TotalCodec<T> {\n parseServerSide(value: string | string[] | undefined): T;\n}\n\nfunction hasParseServerSide<T>(\n codec: ParseableCodec<T>\n): codec is ParseableCodec<T> & TotalCodec<T> {\n return typeof (codec as Partial<TotalCodec<T>>).parseServerSide === 'function';\n}\n\n/**\n * Parse a raw URL value through a codec, using the codec's total entry\n * point when it publishes one.\n *\n * Returns `T`, from both branches. A `null` for an absent param is part of\n * the codec's own `T` — see TotalCodec above — so this signature does not\n * widen it, and a caller that must handle \"no value\" (`withDefault`) sees\n * it because `T` carries it.\n */\nexport function parseTotal<T>(codec: ParseableCodec<T>, raw: string | string[] | undefined): T {\n return hasParseServerSide(codec) ? codec.parseServerSide(raw) : codec.parse(raw);\n}\n","/**\n * Compare two serialize results for equality. Needed because `string[]`\n * from repeated-key codecs does not compare by reference.\n *\n * Shared between define.ts (buildSearchParams default-omission) and\n * use-query-states.ts (bridgeCodec eq). One source of truth.\n */\nexport function serializedEqual(a: string | string[] | null, b: string | string[] | null): boolean {\n if (a === b) return true;\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((v, i) => v === b[i]);\n }\n return false;\n}\n","/**\n * useQueryStates — client-side hook for URL-synced search params.\n *\n * Delegates to nuqs for URL synchronization, batching, React 19 transitions,\n * and throttled URL writes. Bridges timber's SearchParamCodec protocol to\n * nuqs-compatible parsers.\n *\n * Design doc: design/23-search-params.md §\"Codec Bridge\"\n */\n\n'use client';\n\nimport { useQueryStates as nuqsUseQueryStates } from 'nuqs';\nimport type { MultiParser } from 'nuqs';\nimport type {\n SearchParamCodec,\n SearchParamsDefinition,\n SetParams,\n QueryStatesOptions,\n} from '../search-params/define.ts';\nimport { parseTotal } from '../search-params/parse-total.ts';\nimport { serializedEqual } from '../search-params/serialize-equal.ts';\n\n// ─── Codec Bridge ─────────────────────────────────────────────────\n\n// nuqs's parser contract conflates values timber codecs distinguish:\n// parse() returning null means \"unparseable, substitute defaultValue\",\n// and undefined entries are skipped entirely. Timber codecs can\n// legitimately produce both — bare z.string() yields undefined for absent\n// params (implicit optionality), and a codec may map a present value to\n// null. Wrap those two values in sentinels across the nuqs boundary and\n// unwrap them before handing values back to the caller, so the client\n// hook returns exactly what server-side parse() returns.\n// Unique object references compared by identity — a codec can never\n// produce these from URL input, so user-controlled strings cannot collide\n// with them (unlike string sentinels), and unlike Symbols they survive\n// nuqs's internal string coercion without throwing.\nconst NULL_SENTINEL: object = { timberSentinel: 'null' };\nconst UNDEFINED_SENTINEL: object = { timberSentinel: 'undefined' };\n\nfunction wrapNuqsValue(value: unknown): unknown {\n if (value === null) return NULL_SENTINEL;\n if (value === undefined) return UNDEFINED_SENTINEL;\n return value;\n}\n\nfunction unwrapNuqsValue(value: unknown): unknown {\n if (value === NULL_SENTINEL) return null;\n if (value === UNDEFINED_SENTINEL) return undefined;\n return value;\n}\n\n/**\n * Bridge a timber SearchParamCodec to a nuqs-compatible MultiParser.\n *\n * nuqs parsers: { parse(string) → T|null, serialize?(T) → string, eq?, defaultValue? }\n * timber codecs: { parse(string|string[]|undefined) → T, serialize(T) → string|null }\n *\n * The defaultValue is computed eagerly, through `parseTotal` — the same\n * entry point server-side `parse()` uses, so the hook and the server agree\n * on what an absent param means (a bare nuqs parser answers `null`, not\n * `undefined`; TIM-1350). Codecs are documented to return a default rather\n * than throw, but a throwing codec must not crash every component that\n * mounts the hook — treat its default as undefined and let its error\n * surface from server-side parse() instead.\n *\n * A `null` absent-value is NOT registered as the nuqs default. nuqs\n * already represents an absent key as `null`, so the hook reads the same\n * value either way — but registering it makes `clearOnDefault` fire on\n * `setParams({ q: null })` and delete the key before the bridged\n * `serialize` runs. For a codec that encodes `null` as a real query value\n * (`serialize(null) === 'none'`), that silently disagrees with\n * `buildSearchParams({ q: null })`, which writes it. Same reasoning as\n * `getDefaultSerialized` on the server: a codec with no value for an\n * absent param has no default to register.\n */\nfunction bridgeCodec<T>(codec: SearchParamCodec<T>): MultiParser<T> & { defaultValue: T } {\n let absent: unknown;\n try {\n absent = parseTotal(codec, undefined);\n } catch {\n absent = undefined;\n }\n\n const parser = {\n // `multi`, so nuqs reads the key with `searchParams.getAll()` and hands\n // us EVERY value. A single parser reads `.get()` — the first value only\n // — which is not the domain a timber codec is defined over. The server\n // parses `?tags=a&tags=b` as `['a','b']`; a single parser made the hook\n // answer `['a']` for the same URL, under a declared `string[]` that\n // admitted no such disagreement (TIM-1352). Scalar codecs are unaffected:\n // they receive the array and take `value[0]`, exactly as they do on the\n // server, so first-value-wins is preserved through the same code path\n // rather than through nuqs's reader.\n //\n // nuqs never calls this with an empty array — `isAbsentFromUrl` treats\n // `[]` as absent and answers `defaultValue` directly — which is what\n // keeps the absent case agreeing with the server's `undefined`.\n //\n // Reading every value is only half of it: the values must arrive in the\n // SAME SHAPE the server would have produced, or the divergence just\n // moves. `normalizeRaw` (search-params/define.ts) collapses a\n // single-valued key to a bare string and keeps an array only for a\n // repeated one, so this mirrors that rule exactly. Handing a codec\n // `['3']` where the server hands it `'3'` breaks every codec whose\n // `parse` is written for the scalar case — which is most hand-written\n // ones, contract or no contract.\n type: 'multi' as const,\n // Through parseTotal, not codec.parse. nuqs's own `.withDefault(d)`\n // overrides ONLY `parseServerSide`, so `parseAsInteger.withDefault(1)`\n // on `?page=abc` returned 1 from the server and null from the hook —\n // a divergence the declared non-nullable `number` did not admit.\n parse: (v: readonly string[]) =>\n wrapNuqsValue(parseTotal(codec, v.length === 1 ? v[0] : [...v])),\n serialize: (v: unknown) => {\n const value = unwrapNuqsValue(v);\n if (value === undefined) return [''];\n // TIM-1354: catch TypeError from codecs whose serialize is not total\n // over null (e.g. parseAsIsoDate.serialize(null) → null.toISOString()).\n // Only TypeError — deliberate signals must not be swallowed.\n let result: string | string[] | null;\n try {\n result = codec.serialize(value as T);\n } catch (error) {\n if (error instanceof TypeError) return [''];\n throw error;\n }\n // TIM-1353: pass through string[] from codecs that emit repeated\n // keys. nuqs multi parsers append one key=value per array element.\n if (Array.isArray(result)) return result;\n return [result ?? ''];\n },\n eq: (a: unknown, b: unknown) => {\n if (a === b) return true;\n try {\n return serializedEqual(\n codec.serialize(unwrapNuqsValue(a) as T),\n codec.serialize(unwrapNuqsValue(b) as T)\n );\n } catch {\n return false;\n }\n },\n } as MultiParser<T> & { defaultValue: T };\n\n if (absent !== null) parser.defaultValue = wrapNuqsValue(absent) as T;\n return parser;\n}\n\n/**\n * Collect `withUrlKey` aliases off a codec map.\n *\n * `withUrlKey(codec, 'q')` returns a codec carrying `urlKey: 'q'`, so the map\n * alone is enough to reconstruct the aliases — `defineSearchParams` builds its\n * own `urlKeys` from exactly this property.\n */\nfunction deriveUrlKeys(codecs: Record<string, SearchParamCodec<unknown>>): Record<string, string> {\n const result: Record<string, string> = {};\n for (const key of Object.keys(codecs)) {\n const alias = codecs[key]?.urlKey;\n if (alias) result[key] = alias;\n }\n return result;\n}\n\n/**\n * Bridge an entire codec map to nuqs-compatible parsers.\n */\nfunction bridgeCodecs<T extends Record<string, unknown>>(codecs: {\n [K in keyof T]: SearchParamCodec<T[K]>;\n}) {\n const result: Record<string, MultiParser<unknown> & { defaultValue: unknown }> = {};\n for (const key of Object.keys(codecs)) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n result[key] = bridgeCodec(codecs[key as keyof T]) as any;\n }\n return result as { [K in keyof T]: MultiParser<T[K]> & { defaultValue: T[K] } };\n}\n\n// ─── Hook ─────────────────────────────────────────────────────────\n\n/**\n * Read and write typed search params from/to the URL.\n *\n * Delegates to nuqs internally. The timber nuqs adapter (auto-injected in\n * browser-entry.ts) handles RSC navigation on non-shallow updates.\n *\n * Usage:\n * ```ts\n * // Via a SearchParamsDefinition imported from the route's params.ts\n * const [params, setParams] = definition.useQueryStates()\n *\n * // Standalone with inline codecs\n * const [params, setParams] = useQueryStates({\n * page: fromSchema(z.coerce.number().int().min(1).default(1)),\n * })\n * ```\n *\n * There is deliberately no route-string form (`useQueryStates('/products')`).\n * Importing the definition from `params.ts` is the documented way to reach\n * another route's codecs — it needs no runtime registry lookup and so has no\n * \"not registered yet\" failure mode. See design/23-search-params.md\n * §\"Client Access\".\n */\nexport function useQueryStates<T extends Record<string, unknown>>(\n codecs: { [K in keyof T]: SearchParamCodec<T[K]> },\n _options?: QueryStatesOptions,\n urlKeys?: Readonly<Record<string, string>>\n): [T, SetParams<T>] {\n const bridged = bridgeCodecs(codecs);\n\n // Forward hook-level options (shallow, scroll, history) to nuqs.\n // These become the default for all setter calls from this hook instance.\n // Per-call options in setParams(values, opts) override these defaults.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const nuqsOptions: any = {};\n if (_options?.shallow !== undefined) nuqsOptions.shallow = _options.shallow;\n if (_options?.scroll !== undefined) nuqsOptions.scroll = _options.scroll;\n if (_options?.history !== undefined) nuqsOptions.history = _options.history;\n // `withUrlKey` attaches the alias to the codec itself — that is the design's\n // \"URL keys travel with codecs\" principle — so the aliases are derivable\n // here and must be, for the inline codec-map form: nobody passes `urlKeys`\n // on that path, and without this an aliased bundle silently read and wrote\n // the property name instead of the alias. `bindUseQueryStates` still passes\n // the definition's precomputed map, which wins on conflict; it is built from\n // these same codecs, so the two agree by construction rather than by luck.\n const resolvedUrlKeys = { ...deriveUrlKeys(codecs), ...urlKeys };\n if (Object.keys(resolvedUrlKeys).length > 0) {\n nuqsOptions.urlKeys = resolvedUrlKeys;\n }\n\n let values: Record<string, unknown>;\n let setValues: Function;\n try {\n [values, setValues] = nuqsUseQueryStates(bridged, nuqsOptions);\n } catch (err) {\n if (\n err instanceof Error &&\n /Invalid hook call|cannot be called|Cannot read properties of null/i.test(err.message)\n ) {\n throw new Error(\n 'useQueryStates is a client component hook and cannot be called outside a React component. ' +\n 'Use definition.parse(searchParams) in server components instead.'\n );\n }\n throw err;\n }\n\n // Unwrap the null/undefined sentinels the bridge injected (see Codec\n // Bridge above) so callers see exactly what server-side parse() returns.\n // Copy-on-write preserves the identity of nuqs's memoized values object\n // when nothing needs unwrapping.\n let normalized = values;\n for (const key of Object.keys(bridged)) {\n const value = normalized[key];\n if (value === NULL_SENTINEL || value === UNDEFINED_SENTINEL) {\n if (normalized === values) normalized = { ...values };\n normalized[key] = unwrapNuqsValue(value);\n }\n }\n\n // Wrap the nuqs setter to match timber's SetParams<T> signature.\n // nuqs's setter accepts Partial<Nullable<Values>> | UpdaterFn | null.\n // timber's setter accepts Partial<T> with optional SetParamsOptions.\n const setParams: SetParams<T> = (partial, setOptions?) => {\n const nuqsSetOptions: Record<string, unknown> = {};\n if (setOptions?.shallow !== undefined) nuqsSetOptions.shallow = setOptions.shallow;\n if (setOptions?.scroll !== undefined) nuqsSetOptions.scroll = setOptions.scroll;\n if (setOptions?.history !== undefined) nuqsSetOptions.history = setOptions.history;\n // nuqs's update loop skips undefined entries and treats null as a\n // key deletion before serialize runs. Timber semantics:\n // - setParams({ q: undefined }) must clear ?q= (absent = undefined),\n // so explicit undefined maps to a null deletion.\n // - setParams({ q: null }) clears the key only when the codec encodes\n // null as \"omit\" (serialize(null) === null). If the codec encodes\n // null as a real query value, forward the sentinel so the bridged\n // serialize writes it — matching definition.serialize({ q: null }).\n let forwarded: Record<string, unknown> = partial;\n for (const key of Object.keys(partial)) {\n const value = partial[key as keyof T];\n if (value === undefined) {\n if (forwarded === partial) forwarded = { ...partial };\n forwarded[key] = null;\n } else if (value === null) {\n let encoded: string | string[] | null = null;\n try {\n const raw = codecs[key as keyof T]?.serialize(null as T[keyof T]);\n // string[] is non-null, but an empty array carries no value\n encoded = raw === null ? null : Array.isArray(raw) ? (raw.length > 0 ? raw : null) : raw;\n } catch {\n // Codec can't serialize null — treat as a deletion.\n }\n if (encoded !== null) {\n if (forwarded === partial) forwarded = { ...partial };\n forwarded[key] = NULL_SENTINEL;\n }\n }\n }\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n void setValues(forwarded as any, nuqsSetOptions);\n };\n\n return [normalized as T, setParams];\n}\n\n// ─── Definition binding ───────────────────────────────────────────\n\n/**\n * Create a useQueryStates binding for a SearchParamsDefinition.\n * This is used internally by SearchParamsDefinition.useQueryStates().\n */\nexport function bindUseQueryStates<T extends Record<string, unknown>>(\n definition: SearchParamsDefinition<T>\n): (options?: QueryStatesOptions) => [T, SetParams<T>] {\n return (options?: QueryStatesOptions) => {\n return useQueryStates<T>(definition.codecs, options, definition.urlKeys);\n };\n}\n"],"mappings":"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"}