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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/dist/_chunks/{actions-C-Rw9vPc.js → actions-35jnMdeJ.js} +4 -3
  2. package/dist/_chunks/{actions-C-Rw9vPc.js.map → actions-35jnMdeJ.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
  5. package/dist/_chunks/{als-slots-mFweg276.js → als-slots-BEEIPKYm.js} +3 -4
  6. package/dist/_chunks/{als-slots-mFweg276.js.map → als-slots-BEEIPKYm.js.map} +1 -1
  7. package/dist/_chunks/{cache-api-Cd0VZ_Pd.js → cache-api-DjNrIWRR.js} +7 -13
  8. package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
  9. package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
  10. package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
  11. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
  12. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
  13. package/dist/_chunks/{cloudflare-Cs0uZXea.js → cloudflare-CGP6BZKO.js} +4 -3
  14. package/dist/_chunks/{cloudflare-Cs0uZXea.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
  15. package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
  16. package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
  17. package/dist/_chunks/{error-boundary-DpYRI_I1.js → error-boundary-D-ODYX41.js} +25 -2
  18. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
  19. package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
  20. package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
  21. package/dist/_chunks/{logger-N7e5auP0.js → logger-D8xJZXIN.js} +27 -40
  22. package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
  23. package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
  24. package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
  25. package/dist/_chunks/{plugin-context-DEGLSJs3.js → plugin-context-rCinWLiE.js} +7 -2
  26. package/dist/_chunks/{plugin-context-DEGLSJs3.js.map → plugin-context-rCinWLiE.js.map} +1 -1
  27. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  28. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  29. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
  30. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
  31. package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
  32. package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
  33. package/dist/_chunks/{cli-schema-sync-CKgHC2MB.js → scanner-C8b0Gcw3.js} +5 -298
  34. package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
  35. package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
  36. package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
  37. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
  38. package/dist/_chunks/{param-value-C8TNYchQ.js → segment-context-CjOlyB8Y.js} +33 -2
  39. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
  40. package/dist/_chunks/segment-keys-BawYuNFO.js.map +1 -1
  41. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  42. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  43. package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
  44. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  45. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  46. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  47. package/dist/adapters/cloudflare-dev.js +1 -1
  48. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  49. package/dist/adapters/cloudflare.d.ts.map +1 -1
  50. package/dist/adapters/cloudflare.js +1 -1
  51. package/dist/adapters/compress-module.d.ts +12 -0
  52. package/dist/adapters/compress-module.d.ts.map +1 -1
  53. package/dist/adapters/nitro.js +54 -2
  54. package/dist/adapters/nitro.js.map +1 -1
  55. package/dist/cache/index.js +1 -1
  56. package/dist/cdn/cloudflare-purge.js +30 -0
  57. package/dist/cdn/cloudflare-purge.js.map +1 -0
  58. package/dist/cdn/fastly-purge.js +33 -0
  59. package/dist/cdn/fastly-purge.js.map +1 -0
  60. package/dist/cdn/index.js +94 -0
  61. package/dist/cdn/index.js.map +1 -0
  62. package/dist/cdn/workers-cache-purge.js +35 -0
  63. package/dist/cdn/workers-cache-purge.js.map +1 -0
  64. package/dist/cli-check.d.ts +153 -0
  65. package/dist/cli-check.d.ts.map +1 -0
  66. package/dist/cli.d.ts +34 -8
  67. package/dist/cli.d.ts.map +1 -1
  68. package/dist/cli.js +46 -21
  69. package/dist/cli.js.map +1 -1
  70. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  71. package/dist/client/browser-entry/index.d.ts +1 -1
  72. package/dist/client/browser-entry/index.d.ts.map +1 -1
  73. package/dist/client/error-boundary.d.ts +6 -0
  74. package/dist/client/error-boundary.d.ts.map +1 -1
  75. package/dist/client/error-boundary.js +1 -1
  76. package/dist/client/index.d.ts +1 -0
  77. package/dist/client/index.d.ts.map +1 -1
  78. package/dist/client/index.js +27 -33
  79. package/dist/client/index.js.map +1 -1
  80. package/dist/client/internal.js +33 -17
  81. package/dist/client/internal.js.map +1 -1
  82. package/dist/client/link.d.ts +1 -7
  83. package/dist/client/link.d.ts.map +1 -1
  84. package/dist/client/navigation-commit.d.ts +16 -0
  85. package/dist/client/navigation-commit.d.ts.map +1 -1
  86. package/dist/client/router-pipeline.d.ts.map +1 -1
  87. package/dist/client/router.d.ts.map +1 -1
  88. package/dist/client/rsc-fetch.d.ts +9 -1
  89. package/dist/client/rsc-fetch.d.ts.map +1 -1
  90. package/dist/client/segment-cache.d.ts +14 -0
  91. package/dist/client/segment-cache.d.ts.map +1 -1
  92. package/dist/client/use-query-states.d.ts +9 -3
  93. package/dist/client/use-query-states.d.ts.map +1 -1
  94. package/dist/codec.js +1 -1
  95. package/dist/cookies/define-cookie.d.ts.map +1 -1
  96. package/dist/cookies/index.js +2 -2
  97. package/dist/cookies/index.js.map +1 -1
  98. package/dist/index.d.ts +4 -1
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +142 -477
  101. package/dist/index.js.map +1 -1
  102. package/dist/params/index.js +1 -1
  103. package/dist/plugin-context.d.ts +15 -0
  104. package/dist/plugin-context.d.ts.map +1 -1
  105. package/dist/plugins/routing.d.ts +0 -9
  106. package/dist/plugins/routing.d.ts.map +1 -1
  107. package/dist/plugins/shims.d.ts.map +1 -1
  108. package/dist/plugins/static-build.d.ts +24 -0
  109. package/dist/plugins/static-build.d.ts.map +1 -1
  110. package/dist/routing/codegen-shared.d.ts +3 -44
  111. package/dist/routing/codegen-shared.d.ts.map +1 -1
  112. package/dist/routing/codegen-types.d.ts +10 -31
  113. package/dist/routing/codegen-types.d.ts.map +1 -1
  114. package/dist/routing/codegen-write.d.ts +51 -0
  115. package/dist/routing/codegen-write.d.ts.map +1 -0
  116. package/dist/routing/codegen.d.ts.map +1 -1
  117. package/dist/routing/convention-lint.d.ts +18 -4
  118. package/dist/routing/convention-lint.d.ts.map +1 -1
  119. package/dist/routing/export-detect.d.ts +16 -0
  120. package/dist/routing/export-detect.d.ts.map +1 -1
  121. package/dist/routing/index.js +3 -2
  122. package/dist/routing/link-codegen.d.ts +19 -4
  123. package/dist/routing/link-codegen.d.ts.map +1 -1
  124. package/dist/routing/manifest-codegen.d.ts +1 -7
  125. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  126. package/dist/routing/segment-keys.d.ts +22 -0
  127. package/dist/routing/segment-keys.d.ts.map +1 -1
  128. package/dist/routing/types.d.ts +0 -6
  129. package/dist/routing/types.d.ts.map +1 -1
  130. package/dist/schema-bridge.d.ts +60 -9
  131. package/dist/schema-bridge.d.ts.map +1 -1
  132. package/dist/search-params/define.d.ts +62 -8
  133. package/dist/search-params/define.d.ts.map +1 -1
  134. package/dist/search-params/index.d.ts +0 -1
  135. package/dist/search-params/index.d.ts.map +1 -1
  136. package/dist/search-params/index.js +66 -29
  137. package/dist/search-params/index.js.map +1 -1
  138. package/dist/search-params/parse-total.d.ts +70 -0
  139. package/dist/search-params/parse-total.d.ts.map +1 -0
  140. package/dist/search-params/wrappers.d.ts +26 -3
  141. package/dist/search-params/wrappers.d.ts.map +1 -1
  142. package/dist/server/access-gate.d.ts +19 -8
  143. package/dist/server/access-gate.d.ts.map +1 -1
  144. package/dist/server/action-handler.d.ts.map +1 -1
  145. package/dist/server/als-registry.d.ts +16 -0
  146. package/dist/server/als-registry.d.ts.map +1 -1
  147. package/dist/server/chain-url-parts.d.ts +2 -3
  148. package/dist/server/chain-url-parts.d.ts.map +1 -1
  149. package/dist/server/compress.d.ts.map +1 -1
  150. package/dist/server/deny-boundary.d.ts +148 -15
  151. package/dist/server/deny-boundary.d.ts.map +1 -1
  152. package/dist/server/deny-renderer.d.ts +2 -2
  153. package/dist/server/deny-renderer.d.ts.map +1 -1
  154. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  155. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  156. package/dist/server/index.d.ts +0 -1
  157. package/dist/server/index.d.ts.map +1 -1
  158. package/dist/server/index.js +3 -2
  159. package/dist/server/index.js.map +1 -1
  160. package/dist/server/internal.d.ts +3 -1
  161. package/dist/server/internal.d.ts.map +1 -1
  162. package/dist/server/internal.js +344 -237
  163. package/dist/server/internal.js.map +1 -1
  164. package/dist/server/metadata-collector.d.ts +52 -0
  165. package/dist/server/metadata-collector.d.ts.map +1 -0
  166. package/dist/server/param-coercion.d.ts +12 -5
  167. package/dist/server/param-coercion.d.ts.map +1 -1
  168. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  169. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  170. package/dist/server/pipeline-phases.d.ts.map +1 -1
  171. package/dist/server/primitives.d.ts +23 -0
  172. package/dist/server/primitives.d.ts.map +1 -1
  173. package/dist/server/route-element-builder.d.ts +1 -12
  174. package/dist/server/route-element-builder.d.ts.map +1 -1
  175. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  177. package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
  178. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  179. package/dist/server/rsc-entry/helpers.d.ts +0 -7
  180. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  181. package/dist/server/rsc-entry/index.d.ts +0 -1
  182. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  183. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  184. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  185. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
  186. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  187. package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
  188. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  189. package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
  190. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  191. package/dist/server/skippable-prefix.d.ts +3 -0
  192. package/dist/server/skippable-prefix.d.ts.map +1 -1
  193. package/dist/server/slot-resolver.d.ts +18 -19
  194. package/dist/server/slot-resolver.d.ts.map +1 -1
  195. package/dist/server/ssr-bridge-types.d.ts +11 -0
  196. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  197. package/dist/server/ssr-entry.d.ts +0 -1
  198. package/dist/server/ssr-entry.d.ts.map +1 -1
  199. package/dist/server/state-tree-diff.d.ts +9 -16
  200. package/dist/server/state-tree-diff.d.ts.map +1 -1
  201. package/dist/server/static-generator.d.ts.map +1 -1
  202. package/dist/server/status-code-resolver.d.ts +8 -1
  203. package/dist/server/status-code-resolver.d.ts.map +1 -1
  204. package/dist/server/tree-builder.d.ts +28 -43
  205. package/dist/server/tree-builder.d.ts.map +1 -1
  206. package/dist/server/types.d.ts +12 -7
  207. package/dist/server/types.d.ts.map +1 -1
  208. package/dist/server/utils/element-type.d.ts +40 -0
  209. package/dist/server/utils/element-type.d.ts.map +1 -0
  210. package/dist/shared/rsc-media-type.d.ts +40 -0
  211. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  212. package/dist/shared/segment-info.d.ts +7 -0
  213. package/dist/shared/segment-info.d.ts.map +1 -1
  214. package/docs/api/30-api-server.mdx +1 -1
  215. package/docs/api/33-api-search-params.mdx +38 -16
  216. package/docs/api/35-api-typescript.mdx +3 -3
  217. package/docs/api/36-cli.mdx +34 -7
  218. package/docs/learn/00-introduction.mdx +1 -1
  219. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  220. package/docs/learn/05-typed-params.mdx +8 -8
  221. package/docs/learn/07-typed-routes.mdx +12 -7
  222. package/docs/learn/11-error-handling.mdx +16 -0
  223. package/docs/more/01-advanced-routing.mdx +1 -1
  224. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  225. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  226. package/package.json +8 -5
  227. package/src/adapters/cloudflare.ts +4 -1
  228. package/src/adapters/compress-module.ts +79 -1
  229. package/src/cli-check.ts +458 -0
  230. package/src/cli.ts +59 -24
  231. package/src/client/browser-entry/action-dispatch.ts +2 -1
  232. package/src/client/browser-entry/index.ts +0 -5
  233. package/src/client/error-boundary.tsx +65 -1
  234. package/src/client/index.ts +14 -3
  235. package/src/client/link.tsx +65 -64
  236. package/src/client/navigation-commit.ts +27 -4
  237. package/src/client/params-context.ts +4 -4
  238. package/src/client/router-pipeline.ts +4 -0
  239. package/src/client/router.ts +1 -0
  240. package/src/client/rsc-fetch.ts +14 -4
  241. package/src/client/segment-cache.ts +26 -5
  242. package/src/client/use-query-states.ts +102 -39
  243. package/src/cookies/define-cookie.ts +6 -1
  244. package/src/index.ts +20 -3
  245. package/src/plugin-context.ts +26 -0
  246. package/src/plugins/routing.ts +84 -146
  247. package/src/plugins/shims.ts +0 -1
  248. package/src/plugins/static-build.ts +78 -24
  249. package/src/routing/codegen-shared.ts +3 -79
  250. package/src/routing/codegen-types.ts +10 -31
  251. package/src/routing/codegen-write.ts +139 -0
  252. package/src/routing/codegen.ts +56 -182
  253. package/src/routing/convention-lint.ts +139 -40
  254. package/src/routing/export-detect.ts +151 -7
  255. package/src/routing/link-codegen.ts +32 -65
  256. package/src/routing/manifest-codegen.ts +1 -59
  257. package/src/routing/scanner.ts +3 -3
  258. package/src/routing/segment-keys.ts +37 -0
  259. package/src/routing/types.ts +0 -6
  260. package/src/schema-bridge.ts +180 -58
  261. package/src/search-params/define.ts +102 -37
  262. package/src/search-params/index.ts +0 -1
  263. package/src/search-params/parse-total.ts +78 -0
  264. package/src/search-params/wrappers.ts +60 -11
  265. package/src/server/access-gate.tsx +61 -68
  266. package/src/server/action-handler.ts +1 -4
  267. package/src/server/als-registry.ts +16 -0
  268. package/src/server/chain-url-parts.ts +2 -3
  269. package/src/server/compress.ts +9 -1
  270. package/src/server/deny-boundary.ts +269 -41
  271. package/src/server/deny-renderer.ts +32 -21
  272. package/src/server/error-boundary-wrapper.ts +166 -79
  273. package/src/server/index.ts +1 -3
  274. package/src/server/internal.ts +2 -2
  275. package/src/server/metadata-collector.ts +115 -0
  276. package/src/server/param-coercion.ts +13 -61
  277. package/src/server/pipeline-helpers.ts +2 -2
  278. package/src/server/pipeline-outcome.ts +2 -1
  279. package/src/server/pipeline-phases.ts +9 -9
  280. package/src/server/primitives.ts +25 -0
  281. package/src/server/route-element-builder.ts +169 -194
  282. package/src/server/rsc-cache-key-guard.ts +2 -8
  283. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  284. package/src/server/rsc-entry/error-renderer.ts +31 -11
  285. package/src/server/rsc-entry/helpers.ts +3 -12
  286. package/src/server/rsc-entry/index.ts +0 -5
  287. package/src/server/rsc-entry/render-route.ts +14 -11
  288. package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
  289. package/src/server/rsc-entry/rsc-payload.ts +104 -20
  290. package/src/server/rsc-entry/rsc-stream.ts +25 -2
  291. package/src/server/rsc-entry/ssr-renderer.ts +28 -16
  292. package/src/server/skippable-prefix.ts +21 -39
  293. package/src/server/slot-resolver.ts +69 -189
  294. package/src/server/ssr-bridge-types.ts +12 -0
  295. package/src/server/ssr-entry.ts +8 -6
  296. package/src/server/state-tree-diff.ts +11 -62
  297. package/src/server/static-generator.ts +3 -2
  298. package/src/server/status-code-resolver.ts +28 -11
  299. package/src/server/tree-builder.ts +35 -228
  300. package/src/server/types.ts +12 -7
  301. package/src/server/utils/element-type.ts +72 -0
  302. package/src/shared/rsc-media-type.ts +43 -0
  303. package/src/shared/segment-info.ts +7 -0
  304. package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
  305. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
  306. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
  307. package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
  308. package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
  309. package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
  310. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  311. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  312. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  313. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  314. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  315. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  316. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  317. package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
  318. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
  319. package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
  320. package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
  321. package/dist/search-params/registry.d.ts +0 -20
  322. package/dist/search-params/registry.d.ts.map +0 -1
  323. package/dist/segment-params/define.d.ts +0 -83
  324. package/dist/segment-params/define.d.ts.map +0 -1
  325. package/dist/segment-params/index.d.ts +0 -3
  326. package/dist/segment-params/index.d.ts.map +0 -1
  327. package/dist/segment-params/index.js +0 -70
  328. package/dist/segment-params/index.js.map +0 -1
  329. package/src/search-params/registry.ts +0 -31
  330. package/src/segment-params/define.ts +0 -226
  331. package/src/segment-params/index.ts +0 -9
@@ -1,936 +0,0 @@
1
- import { n as findSchemaFile, r as parseExistingSchemaKeys } from "./cli-schema-sync-CKgHC2MB.js";
2
- import { n as classifyUrlSegment, r as effectiveUrlSegment } from "./segment-classify-C539Pa2O.js";
3
- import { parseAst } from "vite";
4
- import { join, posix, relative } from "node:path";
5
- import { existsSync, readFileSync, statSync } from "node:fs";
6
- //#region src/routing/codegen-shared.ts
7
- /**
8
- * Shared codegen helpers — import-path computation, codec chain type
9
- * builder, and searchParams type formatter.
10
- *
11
- * Extracted from `codegen.ts` so `link-codegen.ts` can use the same
12
- * helpers without a cyclic import.
13
- */
14
- /**
15
- * Compute a relative import specifier for a codec/page file, stripping
16
- * the .ts/.tsx extension and resolving against the codegen output dir.
17
- */
18
- function codecImportPath(codecFilePath, importBase) {
19
- const absPath = codecFilePath.replace(/\.(ts|tsx)$/, "");
20
- if (importBase) return "./" + relative(importBase, absPath).replace(/\\/g, "/");
21
- return "./" + posix.basename(absPath);
22
- }
23
- /** Name of the shared helper type emitted at the top of the .d.ts. */
24
- var RESOLVE_SEGMENT_FIELD_TYPE_NAME = "_TimberResolveSegmentField";
25
- /**
26
- * Helper type emitted once at the top of the generated `.d.ts` and
27
- * referenced by every codec-chain conditional. Without this shared
28
- * helper, the inline expansion would duplicate the fallback branch on
29
- * every step and grow O(2^N) in chain depth (a single deep nested route
30
- * could blow up the file size and TS performance). With the helper,
31
- * each step reuses the named type and growth is O(N).
32
- *
33
- * The helper is a 2-arg conditional: given a typeof import expression
34
- * (`Def`), a key (`K`), and a fallback (`F`), it returns `T[K]` if
35
- * `Def extends ParamsDefinition<T>` and `K extends keyof T`, otherwise
36
- * `F`. The codec chain composes calls to this helper.
37
- */
38
- function emitResolveSegmentFieldHelper() {
39
- return [
40
- `type ${RESOLVE_SEGMENT_FIELD_TYPE_NAME}<Def, K extends string, F> =`,
41
- ` Def extends import('@timber-js/app/segment-params').ParamsDefinition<infer T>`,
42
- ` ? K extends keyof T ? T[K] : F`,
43
- ` : F;`
44
- ].join("\n");
45
- }
46
- /**
47
- * Build a TypeScript type expression that resolves a single param's
48
- * codec by walking a chain of params.ts files in priority order.
49
- *
50
- * Each entry in the chain emits one application of the shared
51
- * `_TimberResolveSegmentField` helper type. Composing N applications
52
- * grows linearly with chain depth (O(N) characters), unlike an inline
53
- * conditional that would duplicate the fallback in each branch and
54
- * grow O(2^N).
55
- *
56
- * The closest match (position 0 in the chain) is checked first; if its
57
- * `segmentParams` definition declares the key, its inferred type wins.
58
- * Otherwise we fall through to the next entry, and finally to the
59
- * provided fallback. See TIM-834.
60
- */
61
- function buildCodecChainType(p, importBase, fallback) {
62
- const files = p.codecFilePaths;
63
- if (!files || files.length === 0) return fallback;
64
- const key = JSON.stringify(p.name);
65
- let inner = fallback;
66
- for (let i = files.length - 1; i >= 0; i--) inner = `${RESOLVE_SEGMENT_FIELD_TYPE_NAME}<(typeof import('${codecImportPath(files[i], importBase)}'))['segmentParams'], ${key}, ${inner}>`;
67
- return inner;
68
- }
69
- /**
70
- * Build a TypeScript type expression for a single param using the global
71
- * schema's InferCodecType. Falls back to the param's base type when
72
- * schema doesn't declare this param (schemas are opt-in per key).
73
- *
74
- * Uses `infer` to extract the codec value rather than direct indexing
75
- * because TypeScript doesn't narrow index validity inside a conditional
76
- * type's true branch — `typeof schema.segmentParams[K]` produces TS2538
77
- * even when the condition proves K is a valid key. In .d.ts files that
78
- * error is suppressed and the type silently becomes `any`.
79
- */
80
- function buildSchemaParamType(p, fallback) {
81
- return `typeof schema['segmentParams'] extends Record<${JSON.stringify(p.bracketName)}, infer V> ? InferCodecType<V> : ${fallback ?? p.type}`;
82
- }
83
- /**
84
- * Format the searchParams type for a route entry.
85
- *
86
- * When a page.tsx (or params.ts) exports searchParams, we reference its
87
- * inferred type via an import type. The import path is relative to
88
- * `importBase` (the directory where the .d.ts will be written). When
89
- * importBase is undefined, falls back to a bare relative path.
90
- */
91
- function formatSearchParamsType(route, importBase) {
92
- if (route.hasSearchParams && route.searchParamsPagePath) return `(typeof import('${codecImportPath(route.searchParamsPagePath, importBase)}'))['searchParams'] extends import('@timber-js/app/search-params').SearchParamsDefinition<infer T> ? T : never`;
93
- return "{}";
94
- }
95
- //#endregion
96
- //#region src/routing/file-cache.ts
97
- var cache = /* @__PURE__ */ new Map();
98
- /**
99
- * Read a file's text content, returning a cached copy when the file
100
- * hasn't been modified since the last read (checked via mtime + size).
101
- *
102
- * Falls back to a fresh read on any stat/read error.
103
- */
104
- function readFileCached(filePath) {
105
- try {
106
- const stat = statSync(filePath);
107
- const entry = cache.get(filePath);
108
- if (entry && entry.mtimeMs === stat.mtimeMs && entry.size === stat.size) return entry.content;
109
- const content = readFileSync(filePath, "utf-8");
110
- cache.set(filePath, {
111
- mtimeMs: stat.mtimeMs,
112
- size: stat.size,
113
- content
114
- });
115
- return content;
116
- } catch {
117
- cache.delete(filePath);
118
- return readFileSync(filePath, "utf-8");
119
- }
120
- }
121
- //#endregion
122
- //#region src/routing/export-detect.ts
123
- /**
124
- * AST-based export detection for route files.
125
- *
126
- * Uses Vite's `parseAst` (backed by oxc) to precisely detect named and
127
- * default exports, replacing the regex-based approach that was fragile
128
- * with TypeScript syntax (type exports, `as` aliases, comments, etc.).
129
- */
130
- function tryParse(source) {
131
- try {
132
- return parseAst(source, { lang: "tsx" });
133
- } catch {
134
- return null;
135
- }
136
- }
137
- /**
138
- * Collect all named export identifiers from a parsed program.
139
- * Handles: `export function X`, `export const X`, `export { X }`,
140
- * `export { X } from '...'`, `export { Y as X }`.
141
- */
142
- function collectNamedExports(program) {
143
- const names = /* @__PURE__ */ new Set();
144
- for (const stmt of program.body) {
145
- if (stmt.type !== "ExportNamedDeclaration") continue;
146
- if (stmt.exportKind === "type") continue;
147
- if (stmt.declaration) {
148
- if (stmt.declaration.id?.name) names.add(stmt.declaration.id.name);
149
- if (stmt.declaration.declarations) {
150
- for (const decl of stmt.declaration.declarations) if (decl.id?.name) names.add(decl.id.name);
151
- }
152
- }
153
- if (stmt.specifiers) for (const spec of stmt.specifiers) {
154
- if (spec.exportKind === "type") continue;
155
- if (spec.exported?.name && spec.exported.name !== "default") names.add(spec.exported.name);
156
- }
157
- }
158
- return names;
159
- }
160
- /**
161
- * Check whether a program has a default export.
162
- * Handles: `export default ...`, `export { X as default }`,
163
- * `export { default } from '...'`.
164
- */
165
- function hasDefaultExport(program) {
166
- for (const stmt of program.body) {
167
- if (stmt.type === "ExportDefaultDeclaration") return true;
168
- if (stmt.type === "ExportNamedDeclaration" && stmt.specifiers) {
169
- for (const spec of stmt.specifiers) if (spec.exported?.name === "default") return true;
170
- }
171
- }
172
- return false;
173
- }
174
- /**
175
- * Check whether a program has any bare `export * from '...'` declarations.
176
- * Excludes `export * as X from '...'` (namespace re-exports) since those
177
- * create a namespace object, not individual top-level exports.
178
- */
179
- function hasStarExport(program) {
180
- return program.body.some((stmt) => stmt.type === "ExportAllDeclaration" && !stmt.exported && stmt.exportKind !== "type");
181
- }
182
- /**
183
- * Check if a file exports a specific named export.
184
- * Returns false if the file doesn't exist or can't be parsed.
185
- */
186
- function fileHasExport(filePath, exportName) {
187
- if (!existsSync(filePath)) return false;
188
- try {
189
- const program = tryParse(readFileCached(filePath));
190
- if (!program) return false;
191
- return collectNamedExports(program).has(exportName);
192
- } catch {
193
- return false;
194
- }
195
- }
196
- /**
197
- * Check if a file has any of the given named exports.
198
- * Returns the set of matching export names.
199
- */
200
- function fileHasAnyExport(filePath, exportNames) {
201
- const matches = /* @__PURE__ */ new Set();
202
- try {
203
- const program = tryParse(readFileCached(filePath));
204
- if (!program) return matches;
205
- const exports = collectNamedExports(program);
206
- for (const name of exportNames) if (exports.has(name)) matches.add(name);
207
- } catch {}
208
- return matches;
209
- }
210
- /**
211
- * Check if a file has a default export.
212
- * Returns false if the file doesn't exist or can't be parsed.
213
- */
214
- function fileHasDefaultExport(filePath) {
215
- if (!existsSync(filePath)) return false;
216
- try {
217
- const program = tryParse(readFileCached(filePath));
218
- if (!program) return false;
219
- return hasDefaultExport(program);
220
- } catch {
221
- return false;
222
- }
223
- }
224
- /**
225
- * Check if a file has any `export * from '...'` declarations.
226
- * Returns false if the file doesn't exist or can't be parsed.
227
- */
228
- function fileHasStarExport(filePath) {
229
- if (!existsSync(filePath)) return false;
230
- try {
231
- const program = tryParse(readFileCached(filePath));
232
- if (!program) return false;
233
- return hasStarExport(program);
234
- } catch {
235
- return false;
236
- }
237
- }
238
- function extractLiteralValue(node) {
239
- if (!node) return void 0;
240
- const n = node;
241
- if (n.type === "Literal") return n.value;
242
- if (n.type === "UnaryExpression" && n.operator === "-" && n.argument?.type === "Literal") return -n.argument.value;
243
- if (n.type === "ArrayExpression") {
244
- const elements = n.elements;
245
- if (!elements) return void 0;
246
- const result = [];
247
- for (const el of elements) {
248
- const v = extractLiteralValue(el);
249
- if (v === void 0) return void 0;
250
- result.push(v);
251
- }
252
- return result;
253
- }
254
- }
255
- function extractObjectLiteral(node) {
256
- if (node.type !== "ObjectExpression") return void 0;
257
- const props = node.properties;
258
- if (!props) return void 0;
259
- const result = {};
260
- for (const prop of props) {
261
- const p = prop;
262
- if (p.type === "SpreadElement") return void 0;
263
- if (p.type !== "Property" || p.computed) return void 0;
264
- const key = p.key?.type === "Identifier" ? p.key.name : p.key?.type === "Literal" ? String(p.key.value) : void 0;
265
- if (!key) return void 0;
266
- const value = extractLiteralValue(p.value);
267
- if (value === void 0) return void 0;
268
- result[key] = value;
269
- }
270
- return result;
271
- }
272
- /**
273
- * Extract the value of `export const prerender` from a file.
274
- * Returns undefined if the file doesn't export `prerender` or can't be parsed.
275
- *
276
- * Supports:
277
- * export const prerender = true;
278
- * export const prerender = false;
279
- * export const prerender = { ttl: 3600, tags: ['docs'] };
280
- */
281
- function getPrerenderExport(filePath) {
282
- if (!existsSync(filePath)) return void 0;
283
- try {
284
- const program = tryParse(readFileCached(filePath));
285
- if (!program) return void 0;
286
- for (const stmt of program.body) {
287
- if (stmt.type !== "ExportNamedDeclaration") continue;
288
- if (stmt.exportKind === "type") continue;
289
- if (!stmt.declaration) continue;
290
- const decl = stmt.declaration;
291
- if (decl.type !== "VariableDeclaration") continue;
292
- const declarations = decl.declarations;
293
- if (!declarations) continue;
294
- for (const d of declarations) {
295
- if (d.id?.name !== "prerender" || !d.init) continue;
296
- const init = d.init;
297
- if (init.type === "Literal" && typeof init.value === "boolean") return init.value;
298
- if (init.type === "ObjectExpression") {
299
- const obj = extractObjectLiteral(init);
300
- if (!obj) return void 0;
301
- const result = {};
302
- if ("ttl" in obj && typeof obj.ttl === "number") result.ttl = obj.ttl;
303
- if ("tags" in obj && Array.isArray(obj.tags) && obj.tags.every((t) => typeof t === "string")) result.tags = obj.tags;
304
- return result;
305
- }
306
- return;
307
- }
308
- }
309
- return;
310
- } catch {
311
- return;
312
- }
313
- }
314
- /**
315
- * Check if a file starts with a specific directive (e.g. "use client").
316
- * Directives are string literal expression statements at the top of the file.
317
- */
318
- function fileHasDirective(filePath, directive) {
319
- if (!existsSync(filePath)) return false;
320
- try {
321
- const program = tryParse(readFileCached(filePath));
322
- if (!program) return false;
323
- for (const stmt of program.body) {
324
- if (stmt.type !== "ExpressionStatement") break;
325
- const expr = stmt.expression;
326
- if (expr?.type !== "Literal" || typeof expr.value !== "string") break;
327
- if (expr.value === directive) return true;
328
- }
329
- return false;
330
- } catch {
331
- return false;
332
- }
333
- }
334
- //#endregion
335
- //#region src/routing/link-codegen.ts
336
- /** Shared Link base-props type literal used in every emitted call signature. */
337
- var LINK_BASE_PROPS_TYPE = "Omit<import('react').AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> & { prefetch?: boolean; scroll?: boolean; preserveSearchParams?: true | string[]; onNavigate?: import('./client/link.js').OnNavigateHandler; children?: import('react').ReactNode }";
338
- /**
339
- * Build a TypeScript template literal pattern for a dynamic route.
340
- * e.g. '/products/[id]' → '/products/${string}'
341
- * '/blog/[...slug]' → '/blog/${string}'
342
- * '/docs/[[...path]]' → '/docs/${string}' (also matches /docs)
343
- * '/[org]/[repo]' → '/${string}/${string}'
344
- */
345
- function buildResolvedPattern(route) {
346
- return route.urlPath.split("/").map((part) => {
347
- const seg = classifyUrlSegment(part);
348
- switch (seg.kind) {
349
- case "catch-all":
350
- case "optional-catch-all": return "${string}";
351
- case "dynamic": return `${seg.prefix ?? ""}\${string}${seg.suffix ?? ""}`;
352
- default: return part;
353
- }
354
- }).join("/");
355
- }
356
- /**
357
- * Format the segmentParams type for Link overloads.
358
- *
359
- * Link params accept `string | number` for single dynamic segments
360
- * (convenience — values are stringified at runtime). Catch-all and
361
- * optional catch-all remain `string[]` / `string[] | undefined`.
362
- *
363
- * When the segment's params chain (TIM-834) declares a typed codec, the
364
- * inferred type from the codec wins via a nested conditional.
365
- */
366
- function formatLinkParamsType(params, importBase, hasSchema) {
367
- if (params.length === 0) return "{}";
368
- return `{ ${params.map((p) => {
369
- const fallback = p.type === "string" ? "string | number" : p.type;
370
- const codecType = hasSchema ? buildSchemaParamType(p, fallback) : buildCodecChainType(p, importBase, fallback);
371
- return `${p.name}: ${codecType}`;
372
- }).join("; ")} }`;
373
- }
374
- /**
375
- * Catch-all call signatures for `<Link>` — external hrefs and computed
376
- * `string` variables. Emitted from codegen in a SEPARATE
377
- * `declare module` block (declared AFTER the per-route block) so the TS
378
- * "later overload set ordered first" rule places these catch-all
379
- * signatures ahead of per-route in resolution order, leaving per-route
380
- * as the final overload whose error message is reported on failure.
381
- *
382
- * The conditional `string extends H ? ... : never` protection preserves
383
- * TIM-624's guarantee that unknown internal path literals don't match
384
- * the catch-all — typos like `<Link href="/typo" />` still error.
385
- */
386
- function formatLinkCatchAllOverloads() {
387
- const lines = [];
388
- const baseProps = LINK_BASE_PROPS_TYPE;
389
- const catchAllSearchParams = "{ definition: SearchParamsDefinition<Record<string, unknown>>; values: Record<string, unknown> } | Record<string, unknown>";
390
- const externalHref = "`http://${string}` | `https://${string}` | `mailto:${string}` | `tel:${string}` | `ftp://${string}` | `//${string}` | `#${string}` | `?${string}`";
391
- lines.push(" // Typed Link overloads — catch-all (block 2 / emitted second)");
392
- lines.push(" interface LinkFunction {");
393
- lines.push(` (props: ${baseProps} & {`);
394
- lines.push(` href: ${externalHref}`);
395
- lines.push(` segmentParams?: Record<string, unknown>`);
396
- lines.push(` searchParams?: ${catchAllSearchParams}`);
397
- lines.push(` }): import('react').JSX.Element`);
398
- lines.push(` <H extends string>(`);
399
- lines.push(` props: ${baseProps} & {`);
400
- lines.push(` href: string extends H ? H : never`);
401
- lines.push(` segmentParams?: Record<string, string | number | string[]>`);
402
- lines.push(` searchParams?: ${catchAllSearchParams}`);
403
- lines.push(` }`);
404
- lines.push(` ): import('react').JSX.Element`);
405
- lines.push(" }");
406
- return lines;
407
- }
408
- /**
409
- * Generate typed per-route Link call signatures via LinkFunction
410
- * interface merging.
411
- *
412
- * TIM-835: This emits a SINGLE call signature whose props is a
413
- * discriminated union keyed on `href`. TypeScript narrows the union by
414
- * the literal `href` at the call site, then checks the rest of the
415
- * props against the matched variant. When `segmentParams` or
416
- * `searchParams` is wrong, TS reports the error against the matched
417
- * variant — naming the user's actual `href` and the offending field.
418
- *
419
- * Before TIM-835, this function emitted N separate per-route overloads
420
- * (one per route, sometimes two for dynamic routes). When ALL overloads
421
- * failed, TS would pick an arbitrary failed overload (heuristically the
422
- * "last tried") to render the diagnostic, often pointing at a route
423
- * completely unrelated to the one the user wrote. The discriminated
424
- * union sidesteps overload-resolution heuristics entirely.
425
- *
426
- * Open property shapes (`Record<string, unknown>` instead of `never`)
427
- * for `segmentParams` on routes that don't declare them keep generic
428
- * `LinkProps`-spreading wrappers compiling. (Aligned with TIM-833.)
429
- *
430
- * TIM-832: this function still emits the per-route block FIRST and the
431
- * catch-all block follows, so per-route remains the LAST overload set in
432
- * resolution order — the one TS reports against on failure. With a
433
- * discriminated union there is only one call signature in this block,
434
- * so the resolved-template-vs-pattern ordering reduces to placing the
435
- * pattern variant before the resolved-template variant inside the union.
436
- */
437
- function formatTypedLinkOverloads(routes, importBase, hasSchema) {
438
- const lines = [];
439
- const baseProps = LINK_BASE_PROPS_TYPE;
440
- const variants = [];
441
- for (const route of routes) {
442
- const hasDynamicParams = route.params.length > 0;
443
- const paramsProp = hasDynamicParams ? `segmentParams: ${formatLinkParamsType(route.params, importBase, hasSchema)}` : "segmentParams?: Record<string, unknown>";
444
- const searchParamsType = route.hasSearchParams ? formatSearchParamsType(route, importBase) : null;
445
- const patternSearchParamsProp = searchParamsType ? `searchParams?: Partial<${searchParamsType}>` : "searchParams?: Record<string, unknown>";
446
- variants.push(`${baseProps} & { href: '${route.urlPath}'; ${paramsProp}; ${patternSearchParamsProp} }`);
447
- if (hasDynamicParams) {
448
- const templatePattern = buildResolvedPattern(route);
449
- if (templatePattern) {
450
- const resolvedSearchParamsProp = searchParamsType ? `searchParams?: { definition: SearchParamsDefinition<${searchParamsType}>; values: Partial<${searchParamsType}> }` : "searchParams?: Record<string, unknown>";
451
- variants.push(`${baseProps} & { href: \`${templatePattern}\`; segmentParams?: Record<string, never>; ${resolvedSearchParamsProp} }`);
452
- }
453
- }
454
- }
455
- lines.push(" interface LinkFunction {");
456
- if (variants.length === 0) {
457
- lines.push(" }");
458
- return lines;
459
- }
460
- lines.push(" (");
461
- lines.push(" props:");
462
- for (const variant of variants) lines.push(` | (${variant})`);
463
- lines.push(` ): import('react').JSX.Element`);
464
- lines.push(" }");
465
- return lines;
466
- }
467
- //#endregion
468
- //#region src/routing/codegen.ts
469
- /**
470
- * Route map codegen.
471
- *
472
- * Walks the scanned RouteTree and generates a TypeScript declaration file
473
- * mapping every route to its params and searchParams shapes.
474
- *
475
- * This runs at build time and in dev (regenerated on file changes).
476
- * No runtime overhead — purely static type generation.
477
- */
478
- /**
479
- * Generate a TypeScript declaration file string from a scanned route tree.
480
- *
481
- * The output is a `declare module '@timber-js/app'` block containing the Routes
482
- * interface that maps every route path to its params and searchParams shape.
483
- */
484
- function generateRouteMap(tree, options = {}) {
485
- const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
486
- const routes = [];
487
- collectRoutes(tree.root, [], [], "", routes, hasSchema, false);
488
- routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
489
- return formatDeclarationFile(routes, options.outputDir ?? options.appDir, hasSchema, options.appDir);
490
- }
491
- /** Schema file extensions to check, in priority order. */
492
- var SCHEMA_EXTENSIONS = [
493
- ".ts",
494
- ".tsx",
495
- ".js",
496
- ".jsx"
497
- ];
498
- /**
499
- * Detect whether app/schema.{ts,tsx,js,jsx} exists.
500
- */
501
- function detectSchema(appDir) {
502
- if (!appDir) return false;
503
- return SCHEMA_EXTENSIONS.some((ext) => existsSync(join(appDir, `schema${ext}`)));
504
- }
505
- /**
506
- * Recursively walk the segment tree and collect route entries.
507
- *
508
- * A route entry is created for any segment that has a `page` or `route` file.
509
- * Params accumulate from ancestor dynamic segments.
510
- */
511
- function collectRoutes(node, ancestorParams, ancestorParamsFiles, parentTreePath, routes, hasSchema, insideSlot, insideIntercepting = false) {
512
- const treePath = node.segmentName ? `${parentTreePath}/${node.segmentName}` : parentTreePath;
513
- const ownParamsFile = node.params && fileHasExport(node.params.filePath, "segmentParams") ? node.params.filePath : void 0;
514
- const urlSegment = effectiveUrlSegment(node);
515
- const params = [...ancestorParams];
516
- if (urlSegment.paramName) {
517
- const legacyFallback = ownParamsFile ? void 0 : findLegacyParamsExport(node);
518
- params.push({
519
- name: urlSegment.paramName,
520
- bracketName: bracketNameForSegment(urlSegment),
521
- type: paramTypeForSegment(urlSegment.segmentType),
522
- legacyCodecFilePath: legacyFallback
523
- });
524
- }
525
- const nextAncestorFiles = ownParamsFile ? [...ancestorParamsFiles, ownParamsFile] : ancestorParamsFiles;
526
- const isPage = !!node.page;
527
- const isApiRoute = !!node.route;
528
- if (isPage || isApiRoute) {
529
- const leafFirstChain = nextAncestorFiles.length > 0 ? [...nextAncestorFiles].reverse() : [];
530
- const resolvedParams = params.map((p) => {
531
- const codecFilePaths = leafFirstChain.length > 0 ? leafFirstChain : p.legacyCodecFilePath ? [p.legacyCodecFilePath] : void 0;
532
- return {
533
- name: p.name,
534
- bracketName: p.bracketName,
535
- type: p.type,
536
- codecFilePaths
537
- };
538
- });
539
- const entry = {
540
- urlPath: node.urlPath,
541
- segmentPath: treePath || "/",
542
- params: resolvedParams,
543
- hasSearchParams: false,
544
- isApiRoute,
545
- isSlotRoute: insideSlot,
546
- isInterceptingRoute: insideIntercepting,
547
- hasSchema
548
- };
549
- if (isPage) {
550
- if (node.params && fileHasExport(node.params.filePath, "searchParams")) {
551
- entry.hasSearchParams = true;
552
- entry.searchParamsPagePath = node.params.filePath;
553
- } else if (node.page && fileHasExport(node.page.filePath, "searchParams")) {
554
- entry.hasSearchParams = true;
555
- entry.searchParamsPagePath = node.page.filePath;
556
- }
557
- }
558
- routes.push(entry);
559
- }
560
- for (const child of node.children) collectRoutes(child, params, nextAncestorFiles, treePath, routes, hasSchema, insideSlot, insideIntercepting || child.segmentType === "intercepting");
561
- for (const slot of Object.values(node.slots)) collectRoutes(slot, params, nextAncestorFiles, treePath, routes, hasSchema, true, insideIntercepting);
562
- }
563
- /**
564
- * Dedupe route entries by urlPath for urlPath-keyed emissions (the Routes
565
- * interface, useQueryStates overloads, and typed Link variants).
566
- *
567
- * Parallel-slot pages share their parent's urlPath; emitting one entry per
568
- * slot page produced duplicate interface keys (TS2300/TS2717 under
569
- * skipLibCheck:false) and duplicate overloads. The non-slot entry is the
570
- * canonical one; when only slot entries exist (parent has no page), the
571
- * lexicographically smallest segmentPath wins so output is independent of
572
- * slot scan order. A slot's searchParams definition is carried onto the
573
- * merged entry when the canonical entry has none — never silently dropped.
574
- *
575
- * segmentPath-keyed emissions (getSegmentParams, useSegmentParams,
576
- * TimberSegmentParams) must NOT use this — segmentPaths are already unique
577
- * and slot entries feed those APIs directly.
578
- */
579
- function dedupeRoutesByUrlPath(routes) {
580
- const byPath = /* @__PURE__ */ new Map();
581
- for (const route of routes) {
582
- const existing = byPath.get(route.urlPath);
583
- if (!existing) {
584
- byPath.set(route.urlPath, route);
585
- continue;
586
- }
587
- let winner = existing;
588
- let loser = route;
589
- if (existing.isSlotRoute && !route.isSlotRoute || !!existing.isSlotRoute === !!route.isSlotRoute && route.segmentPath.localeCompare(existing.segmentPath) < 0) {
590
- winner = route;
591
- loser = existing;
592
- }
593
- if (!winner.hasSearchParams && loser.hasSearchParams) winner = {
594
- ...winner,
595
- hasSearchParams: true,
596
- searchParamsPagePath: loser.searchParamsPagePath
597
- };
598
- byPath.set(route.urlPath, winner);
599
- }
600
- return [...byPath.values()];
601
- }
602
- /**
603
- * Determine the TypeScript type for a segment's param.
604
- */
605
- /**
606
- * Derive the bracket key for schema/codegen lookup.
607
- * For bare segments like `[id]` this is the segmentName itself.
608
- * For affixed segments like `img-[id].png` this is `[id]` — the bare bracket form.
609
- */
610
- function bracketNameForSegment(node) {
611
- if (node.paramPrefix || node.paramSuffix) return `[${node.paramName}]`;
612
- return node.segmentName;
613
- }
614
- function paramTypeForSegment(segmentType) {
615
- switch (segmentType) {
616
- case "catch-all": return "string[]";
617
- case "optional-catch-all": return "string[] | undefined";
618
- default: return "string";
619
- }
620
- }
621
- /**
622
- * Find a legacy `segmentParams` export on layout.tsx or page.tsx.
623
- *
624
- * Backward-compat shim: TIM-508 made params.ts the canonical location
625
- * for `segmentParams`. Layout/page exports are still accepted for the
626
- * OWN segment only (not inherited by descendants — see TIM-834).
627
- */
628
- function findLegacyParamsExport(node) {
629
- if (node.layout && fileHasExport(node.layout.filePath, "segmentParams")) return node.layout.filePath;
630
- if (node.page && fileHasExport(node.page.filePath, "segmentParams")) return node.page.filePath;
631
- }
632
- /**
633
- * Emit the schema-based type exports: TimberSegmentParams, TimberRoute,
634
- * AllSegmentParams, RoutesWithParams.
635
- *
636
- * These types use InferCodecType references so they stay in sync with
637
- * the schema without hardcoding resolved types.
638
- *
639
- * Design doc: design/41-global-params.md §Codegen Output
640
- */
641
- function formatSchemaTypes(routes) {
642
- const lines = [];
643
- lines.push("/** Union of all valid route patterns. */");
644
- lines.push("export type TimberRoute = keyof TimberSegmentParams;");
645
- lines.push("");
646
- lines.push("/** Maps each segment path to its accumulated segment params. */");
647
- lines.push("export interface TimberSegmentParams {");
648
- for (const route of routes) if (route.params.length === 0) lines.push(` '${route.segmentPath}': {};`);
649
- else {
650
- lines.push(` '${route.segmentPath}': {`);
651
- for (const p of route.params) lines.push(` ${p.name}: ${buildSchemaParamType(p)};`);
652
- lines.push(" };");
653
- }
654
- lines.push("}");
655
- lines.push("");
656
- lines.push("/** All segment params (union of all route params). */");
657
- lines.push("export type AllSegmentParams = {");
658
- lines.push(" [K in keyof TimberSegmentParams as TimberSegmentParams[K] extends Record<string, never>");
659
- lines.push(" ? never");
660
- lines.push(" : K]: TimberSegmentParams[K];");
661
- lines.push("} extends infer U");
662
- lines.push(" ? { [K in keyof U[keyof U]]: U[keyof U][K] }");
663
- lines.push(" : never;");
664
- lines.push("");
665
- lines.push("/** Filters TimberRoute to routes whose accumulated params include all listed keys. */");
666
- lines.push("export type RoutesWithParams<K extends keyof AllSegmentParams> = {");
667
- lines.push(" [R in TimberRoute]: K extends keyof TimberSegmentParams[R] ? R : never;");
668
- lines.push("}[TimberRoute];");
669
- return lines;
670
- }
671
- /**
672
- * Format the collected routes into a TypeScript declaration file.
673
- */
674
- function formatDeclarationFile(routes, importBase, hasSchema, appDir) {
675
- const lines = [];
676
- lines.push("// This file is auto-generated by timber.js route map codegen.");
677
- lines.push("// Do not edit manually. Regenerated on build and in dev mode.");
678
- lines.push("");
679
- lines.push("export {};");
680
- lines.push("");
681
- if (hasSchema) {
682
- const schemaImport = appDir && importBase ? "./" + relative(importBase, join(appDir, "schema")).replace(/\\/g, "/") : "../app/schema";
683
- lines.push(`import type schema from '${schemaImport}';`);
684
- lines.push("import type { InferCodecType, SegmentParamCodec } from '@timber-js/app/params';");
685
- lines.push("");
686
- const allBracketKeys = /* @__PURE__ */ new Set();
687
- for (const route of routes) for (const p of route.params) allBracketKeys.add(p.bracketName);
688
- if (allBracketKeys.size > 0) {
689
- lines.push("declare module '@timber-js/app/params' {");
690
- lines.push(" interface ValidDynamicSegmentKeysRegistry {");
691
- for (const key of [...allBracketKeys].sort()) lines.push(` ${JSON.stringify(key)}: true;`);
692
- lines.push(" }");
693
- lines.push("}");
694
- lines.push("");
695
- }
696
- lines.push("type _AssertSchemaValues = {");
697
- lines.push(" [K in keyof typeof schema['segmentParams']]: typeof schema['segmentParams'][K] extends SegmentParamCodec ? true : ['Error: value for', K, 'is not a valid Codec or Standard Schema'];");
698
- lines.push("};");
699
- lines.push("type _EnforceSchemaValues<_T extends Record<string, true> = _AssertSchemaValues> = true;");
700
- lines.push("");
701
- }
702
- if (!hasSchema) {
703
- lines.push(emitResolveSegmentFieldHelper());
704
- lines.push("");
705
- }
706
- const uniqueByUrlPath = dedupeRoutesByUrlPath(routes.filter((r) => !r.isInterceptingRoute));
707
- lines.push("declare module '@timber-js/app' {");
708
- lines.push(" interface Routes {");
709
- for (const route of uniqueByUrlPath) {
710
- const paramsType = formatParamsType(route.params, importBase, hasSchema);
711
- const searchParamsType = formatSearchParamsType(route, importBase);
712
- lines.push(` '${route.urlPath}': {`);
713
- lines.push(` segmentParams: ${paramsType}`);
714
- lines.push(` searchParams: ${searchParamsType}`);
715
- lines.push(` }`);
716
- }
717
- lines.push(" }");
718
- if (hasSchema) {
719
- lines.push("");
720
- lines.push(...formatSchemaTypes(routes).map((l) => ` ${l}`));
721
- }
722
- lines.push("}");
723
- lines.push("");
724
- const pageRoutes = uniqueByUrlPath.filter((r) => !r.isApiRoute);
725
- const dynamicRoutes = routes.filter((r) => r.params.length > 0);
726
- if (dynamicRoutes.length > 0) {
727
- lines.push("declare module '@timber-js/app/server' {");
728
- if (hasSchema) lines.push(" import type { AllSegmentParams } from '@timber-js/app'");
729
- for (const route of dynamicRoutes) {
730
- const paramsType = formatParamsType(route.params, importBase, hasSchema);
731
- lines.push(` export function getSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`);
732
- }
733
- lines.push(hasSchema ? " export function getSegmentParams(): Partial<AllSegmentParams>" : " export function getSegmentParams(): Record<string, string | string[]>");
734
- lines.push("}");
735
- lines.push("");
736
- }
737
- if (dynamicRoutes.length > 0 || pageRoutes.length > 0) {
738
- lines.push("declare module '@timber-js/app/client' {");
739
- lines.push(" import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber-js/app/search-params'");
740
- if (hasSchema) lines.push(" import type { AllSegmentParams } from '@timber-js/app'");
741
- lines.push("");
742
- if (dynamicRoutes.length > 0) {
743
- for (const route of dynamicRoutes) {
744
- const paramsType = formatParamsType(route.params, importBase, hasSchema);
745
- lines.push(` export function useSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`);
746
- }
747
- lines.push(hasSchema ? " export function useSegmentParams(): Partial<AllSegmentParams>" : " export function useSegmentParams(): Record<string, string | string[]>");
748
- lines.push("");
749
- }
750
- if (pageRoutes.length > 0) {
751
- lines.push(...formatUseQueryStatesOverloads(pageRoutes, importBase));
752
- lines.push("");
753
- }
754
- if (pageRoutes.length > 0) {
755
- lines.push(" // Typed Link overloads — per-route (block 1 / emitted first)");
756
- lines.push(...formatTypedLinkOverloads(pageRoutes, importBase, hasSchema));
757
- lines.push("");
758
- }
759
- lines.push("}");
760
- lines.push("");
761
- }
762
- lines.push("declare module '@timber-js/app/client' {");
763
- lines.push(" import type { SearchParamsDefinition } from '@timber-js/app/search-params'");
764
- lines.push("");
765
- lines.push(...formatLinkCatchAllOverloads());
766
- lines.push("}");
767
- lines.push("");
768
- return lines.join("\n");
769
- }
770
- /**
771
- * Format the params type for a route entry.
772
- */
773
- function formatParamsType(params, importBase, hasSchema) {
774
- if (params.length === 0) return "{}";
775
- return `{ ${params.map((p) => {
776
- const codecType = hasSchema ? buildSchemaParamType(p) : buildCodecChainType(p, importBase, p.type);
777
- return `${p.name}: ${codecType}`;
778
- }).join("; ")} }`;
779
- }
780
- /**
781
- * Generate useQueryStates overloads.
782
- *
783
- * For each page route:
784
- * - Routes with search-params.ts get a typed overload returning the inferred T
785
- * - Routes without search-params.ts get an overload returning [{}, SetParams<{}>]
786
- *
787
- * A fallback overload for standalone codecs (existing API) is emitted last.
788
- */
789
- function formatUseQueryStatesOverloads(routes, importBase) {
790
- const lines = [];
791
- for (const route of routes) {
792
- const searchParamsType = route.hasSearchParams ? formatSearchParamsType(route, importBase) : "{}";
793
- lines.push(` export function useQueryStates<R extends '${route.urlPath}'>(route: R, options?: QueryStatesOptions): [${searchParamsType}, SetParams<${searchParamsType}>]`);
794
- }
795
- lines.push(" export function useQueryStates<T extends Record<string, unknown>>(codecs: { [K in keyof T]: SearchParamCodec<T[K]> }, options?: QueryStatesOptions): [T, SetParams<T>]");
796
- return lines;
797
- }
798
- //#endregion
799
- //#region src/routing/schema-validation.ts
800
- /**
801
- * Schema key/value validation against the filesystem route tree.
802
- *
803
- * Runs at codegen time (build + dev) to catch mismatches between
804
- * app/schema.ts keys and the actual dynamic segments on disk.
805
- *
806
- * Design doc: design/41-global-params.md
807
- */
808
- /**
809
- * Collect all unique bracket-keyed dynamic segment names from a route tree.
810
- * Returns entries like '[artistSlug]', '[...slug]', '[[...topic]]'.
811
- */
812
- function collectDynamicSegmentsFromTree(tree) {
813
- const segments = /* @__PURE__ */ new Set();
814
- walkTree(tree.root, segments);
815
- return segments;
816
- }
817
- function walkTree(node, segments) {
818
- if (node.paramName && node.segmentName) {
819
- const classified = classifyUrlSegment(node.segmentName);
820
- if (classified.kind !== "static") if (classified.kind === "dynamic" && (classified.prefix || classified.suffix)) segments.add(`[${classified.name}]`);
821
- else segments.add(node.segmentName);
822
- }
823
- for (const child of node.children) walkTree(child, segments);
824
- for (const slot of Object.values(node.slots)) walkTree(slot, segments);
825
- }
826
- /**
827
- * Extract the param name from a bracket key.
828
- * '[id]' → 'id', '[...slug]' → 'slug', '[[...topic]]' → 'topic'
829
- */
830
- function extractParamName(bracketKey) {
831
- const seg = classifyUrlSegment(bracketKey);
832
- if (seg.kind === "static") return null;
833
- return seg.name;
834
- }
835
- /**
836
- * Validate schema keys against the filesystem route tree.
837
- *
838
- * Returns warnings for:
839
- * - Stale keys: schema keys that don't match any filesystem segment
840
- * - Bracket form mismatches: same param name, different bracket form
841
- * - Missing keys: filesystem segments not registered in schema (informational)
842
- */
843
- function validateSchemaAgainstRoutes(tree, appDir) {
844
- const schemaPath = findSchemaFile(appDir);
845
- if (!schemaPath) return [];
846
- const schemaKeys = parseExistingSchemaKeys(readFileCached(schemaPath));
847
- if (schemaKeys.length === 0) return [];
848
- return validateKeys(schemaKeys, collectDynamicSegmentsFromTree(tree));
849
- }
850
- /**
851
- * Core validation: compare schema keys against filesystem segments.
852
- * Exported separately for testing without filesystem dependencies.
853
- */
854
- function validateKeys(schemaKeys, filesystemSegments) {
855
- const warnings = [];
856
- const fsParamToKey = /* @__PURE__ */ new Map();
857
- for (const fsKey of filesystemSegments) {
858
- const name = extractParamName(fsKey);
859
- if (name) fsParamToKey.set(name, fsKey);
860
- }
861
- const schemaParamNames = /* @__PURE__ */ new Set();
862
- for (const schemaKey of schemaKeys) {
863
- const schemaParamName = extractParamName(schemaKey);
864
- if (!schemaParamName) continue;
865
- schemaParamNames.add(schemaParamName);
866
- if (filesystemSegments.has(schemaKey)) continue;
867
- const fsKey = fsParamToKey.get(schemaParamName);
868
- if (fsKey) warnings.push({
869
- type: "bracket-mismatch",
870
- key: schemaKey,
871
- filesystemKey: fsKey,
872
- message: `Schema key '${schemaKey}' does not match filesystem bracket form '${fsKey}'. Update the schema key to '${fsKey}'.`
873
- });
874
- else warnings.push({
875
- type: "stale-key",
876
- key: schemaKey,
877
- message: `Schema key '${schemaKey}' does not match any dynamic segment in the filesystem. Remove it from app/schema.ts or create the corresponding route segment.`
878
- });
879
- }
880
- for (const fsKey of filesystemSegments) {
881
- const name = extractParamName(fsKey);
882
- if (name && !schemaParamNames.has(name)) warnings.push({
883
- type: "missing-key",
884
- key: fsKey,
885
- message: `Dynamic segment '${fsKey}' has no codec in app/schema.ts (defaults to string). Run: timber schema sync`
886
- });
887
- }
888
- return warnings;
889
- }
890
- //#endregion
891
- //#region src/routing/walkers.ts
892
- /**
893
- * Walk a segment tree and collect every leaf with a `page` or `route`
894
- * handler. Generic over `TFile` so it works on both the build-time
895
- * scanner output and the runtime manifest tree.
896
- *
897
- * - Pages and route handlers at the same URL produce two distinct
898
- * entries (the build report deduplicates by URL afterward).
899
- * - Parallel slots are skipped unless `includeSlots: true` (slots
900
- * share their parent's URL and are not addressable on their own).
901
- * - Intercepting subtrees are skipped unconditionally. They render only
902
- * on soft navigation and add no URL depth, so their computed `urlPath`s
903
- * are not addresses — listing them would advertise routes (`/[year]`)
904
- * that no request can reach. This matches every other consumer of the
905
- * tree that produces URLs: the sitemap generator, the static generator,
906
- * prerendering and the prebuilt-payload builder all skip them too.
907
- * - Result is sorted by `urlPath` for deterministic output.
908
- */
909
- function collectLeafRoutes(root, options = {}) {
910
- const { includeSlots = false } = options;
911
- const result = [];
912
- walk(root, [], result, includeSlots);
913
- result.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
914
- return result;
915
- }
916
- function walk(node, chain, result, includeSlots) {
917
- if (node.segmentType === "intercepting") return;
918
- const currentChain = [...chain, node];
919
- const path = node.urlPath || "/";
920
- if (node.page) result.push({
921
- urlPath: path,
922
- segments: currentChain,
923
- page: node.page
924
- });
925
- if (node.route) result.push({
926
- urlPath: path,
927
- segments: currentChain,
928
- route: node.route
929
- });
930
- for (const child of node.children) walk(child, currentChain, result, includeSlots);
931
- if (includeSlots) for (const slotNode of Object.values(node.slots)) walk(slotNode, currentChain, result, includeSlots);
932
- }
933
- //#endregion
934
- export { fileHasAnyExport as a, fileHasExport as c, generateRouteMap as i, fileHasStarExport as l, collectDynamicSegmentsFromTree as n, fileHasDefaultExport as o, validateSchemaAgainstRoutes as r, fileHasDirective as s, collectLeafRoutes as t, getPrerenderExport as u };
935
-
936
- //# sourceMappingURL=walkers-BhhwI9TD.js.map