@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
@@ -0,0 +1,458 @@
1
+ /**
2
+ * `timber check` — the implementation behind the CLI command.
3
+ *
4
+ * Two passes, in order:
5
+ *
6
+ * 1. **Routes.** Resolve the project through Vite — so root, `appDir`, and
7
+ * `pageExtensions` are the ones `timber build` uses, including config
8
+ * passed inline to `timber()` — then scan, run the convention linter, and
9
+ * write the route codegen (`.timber/timber-routes.d.ts`,
10
+ * `.timber/segment-types.d.ts`, `timber-env.d.ts`).
11
+ *
12
+ * Codegen runs *before* the type pass on purpose: those
13
+ * files are what give `<Link href>`, `useSegmentParams()` and
14
+ * `MiddlewareContext` their per-route types (design/09-typescript.md
15
+ * §"Typed Routes"), and they are build artifacts — a fresh clone that has
16
+ * never run `timber dev` or `timber build` does not have them, so a type
17
+ * pass on its own would report errors about correct code. A clone whose
18
+ * routes changed since the last build has the same problem with stale ones.
19
+ * 2. **Types.** `tsgo --noEmit` over the project.
20
+ *
21
+ * tsgo is spawned as `node <@typescript/native-preview>/lib/tsgo.js`, never as
22
+ * a bare `tsgo` off PATH — the package is an *optional peer dependency*, so
23
+ * there is no reason for its shim to be on a user's PATH, and the shim is a
24
+ * `.cmd` on Windows which cannot be spawned without a shell. `shell: true`
25
+ * would then concatenate the argv into an unescaped command string, so a
26
+ * project path containing a space would break on exactly the platform the
27
+ * shell was there to support. This mirrors `scripts/build-types.js`, whose
28
+ * header comment is the specification for the pattern.
29
+ *
30
+ * Design docs: 11-platform.md §CLI, 09-typescript.md, 28-npm-packaging.md
31
+ */
32
+
33
+ import { createRequire } from 'node:module';
34
+ import { dirname, join, resolve } from 'node:path';
35
+ import { scanRoutes } from './routing/scanner.js';
36
+ import { lintConventions, formatConventionWarnings } from './routing/convention-lint.js';
37
+ import { writeRouteCodegen, CODEGEN_OUTPUT, ENV_DTS_FILENAME } from './routing/codegen-write.js';
38
+ import { isTimberPluginApi } from './plugin-context.js';
39
+
40
+ /** The plugin that publishes timber's resolved context on its Vite `api`. */
41
+ const TIMBER_ROOT_SYNC_PLUGIN = 'timber-root-sync';
42
+
43
+ /** The npm package that provides tsgo. Named in the missing-tool message. */
44
+ const TSGO_PACKAGE = '@typescript/native-preview';
45
+
46
+ /**
47
+ * The two generated declaration paths, relative to the project root, as they
48
+ * appear in a resolved tsconfig file list. Derived from the writer's own
49
+ * constants — a rename there cannot leave this check hunting for a file nothing
50
+ * writes.
51
+ */
52
+ const GENERATED_TYPE_PATHS = [ENV_DTS_FILENAME, CODEGEN_OUTPUT];
53
+
54
+ /**
55
+ * Run the compiler to completion.
56
+ *
57
+ * Resolves with its exit status (`null` when a signal killed it) and rejects
58
+ * only when the process could not be started — the two outcomes `runTypeCheck`
59
+ * has to keep apart. The seam is this narrow on purpose: a test double is a
60
+ * plain async function, with no `ChildProcess` shape to imitate.
61
+ */
62
+ export type RunTsgo = (command: string, args: string[], cwd: string) => Promise<number | null>;
63
+
64
+ /**
65
+ * Run the compiler and capture its stdout — used for `--showConfig`, whose
66
+ * output is data we read rather than diagnostics the user reads.
67
+ */
68
+ export type ReadTsgoConfig = (command: string, args: string[], cwd: string) => Promise<string>;
69
+
70
+ /**
71
+ * Vite's own `resolveConfig`, narrowed to what this module passes and reads.
72
+ *
73
+ * The seam sits at `resolveConfig` rather than at "give me the plugins" so the
74
+ * *arguments* are the thing under test: which config file, which command, and
75
+ * which mode defaults. Those arguments have been wrong three separate times
76
+ * (TIM-1332 rounds 4-6), and a double that answered "here are the plugins"
77
+ * could not see any of it. The double returns what Vite returns — plugins with
78
+ * an opaque `api` — so the lookup and narrowing under test stay the real ones.
79
+ */
80
+ export type ResolveViteConfig = (
81
+ inlineConfig: { configFile: string | undefined },
82
+ command: 'build',
83
+ defaultMode: string,
84
+ defaultNodeEnv: string
85
+ ) => Promise<{ plugins: readonly PluginWithApi[] }>;
86
+
87
+ /** @internal Dependency injection for testing. */
88
+ export interface CheckDeps {
89
+ /**
90
+ * Resolve tsgo's own JS entry point. Throws when the package is not
91
+ * installed — the caller turns that into an install instruction.
92
+ */
93
+ resolveTsgoEntry?: () => string;
94
+ runTsgo?: RunTsgo;
95
+ readTsgoConfig?: ReadTsgoConfig;
96
+ resolveViteConfig?: ResolveViteConfig;
97
+ }
98
+
99
+ export interface CheckOptions {
100
+ /** Project root, for resolving tsgo. Defaults to `process.cwd()`. */
101
+ root?: string;
102
+ /**
103
+ * Path to a **tsconfig**, forwarded to tsgo as `--project` and sourced from
104
+ * the CLI's own `--project`. The route pass takes no part in it: it resolves
105
+ * the app's configuration through Vite, as `timber build` does.
106
+ */
107
+ config?: string;
108
+ }
109
+
110
+ /**
111
+ * Locate `@typescript/native-preview`'s JS entry.
112
+ *
113
+ * Resolution is attempted from this package first (it is declared an optional
114
+ * peer dependency, so a package manager links it here when the user installs
115
+ * it) and then from the project root (covering a hoisted install, or a user who
116
+ * added it without the peer link). Throws if neither finds it.
117
+ */
118
+ export function resolveTsgoEntry(
119
+ root: string,
120
+ resolveFrom: (from: string, specifier: string) => string = (from, specifier) =>
121
+ createRequire(from).resolve(specifier)
122
+ ): string {
123
+ // `createRequire` takes a filename or a file URL; a plain absolute path is
124
+ // the portable spelling for the project root (a `file://` prefix on a Windows
125
+ // path is not a valid URL). Same form as `checkPeerDependencies`.
126
+ const candidates = [import.meta.url, join(root, 'package.json')];
127
+ let lastError: unknown;
128
+
129
+ for (const from of candidates) {
130
+ try {
131
+ // The package exports no subpath for its entry, so resolve the manifest
132
+ // and walk to the module its `bin/tsgo` shim imports.
133
+ const manifest = resolveFrom(from, `${TSGO_PACKAGE}/package.json`);
134
+ return join(dirname(manifest), 'lib', 'tsgo.js');
135
+ } catch (err) {
136
+ lastError = err;
137
+ }
138
+ }
139
+
140
+ throw new Error(
141
+ `[timber] Cannot find ${TSGO_PACKAGE}, which \`timber check\` uses to type-check.\n\n` +
142
+ ` Install it as a dev dependency:\n\n` +
143
+ ` pnpm add -D ${TSGO_PACKAGE}\n\n` +
144
+ ` (resolution failed: ${lastError instanceof Error ? lastError.message : String(lastError)})`
145
+ );
146
+ }
147
+
148
+ /** What the route pass needs from the command line. */
149
+ export interface RouteOptions {
150
+ /**
151
+ * Path to a **Vite** config file, from `--config`. Undefined means Vite's own
152
+ * discovery, the same as `timber dev`/`build` without the flag. A project
153
+ * that builds with a custom Vite config has to be checkable with it too, or
154
+ * `check` resolves a different app than `build` does (TIM-1332, round 4).
155
+ */
156
+ viteConfigFile?: string;
157
+ }
158
+
159
+ /** Everything the route scan needs, as the build itself resolves it. */
160
+ export interface RouteInputs {
161
+ root: string;
162
+ appDir: string;
163
+ pageExtensions: string[] | undefined;
164
+ }
165
+
166
+ /** The subset of a resolved Vite plugin this module reads. */
167
+ export interface PluginWithApi {
168
+ name: string;
169
+ api?: unknown;
170
+ }
171
+
172
+ /**
173
+ * Ask Vite for the resolved config, which is what carries timber's context.
174
+ *
175
+ * `resolveConfig` runs the whole config pipeline — config file discovery,
176
+ * `config` and `configResolved` hooks — so the context it produces is the one
177
+ * `timber build` would use. Measured at ~300-400ms on a real app; the type pass
178
+ * it precedes costs far more.
179
+ */
180
+ const defaultResolveViteConfig: ResolveViteConfig = async (
181
+ inlineConfig,
182
+ command,
183
+ defaultMode,
184
+ defaultNodeEnv
185
+ ) => {
186
+ const { resolveConfig } = await import('vite');
187
+ return resolveConfig(inlineConfig, command, defaultMode, defaultNodeEnv);
188
+ };
189
+
190
+ /**
191
+ * The mode defaults `timber build` resolves under.
192
+ *
193
+ * `resolveConfig`'s own defaults are `'development'` for both, but nothing
194
+ * reaches it with those on a build: `createBuilder` goes through Vite's
195
+ * internal `resolveConfigToBuild`, which calls
196
+ * `resolveConfig(inlineConfig, 'build', 'production', 'production')`. A config
197
+ * written as `defineConfig(({ mode }) => …)`, or one that loads `.env.production`,
198
+ * therefore resolves to a *different* app under the two. Passing the defaults
199
+ * explicitly is what makes "check validates what build builds" true rather than
200
+ * true-by-coincidence for configs that ignore mode (TIM-1332, round 6).
201
+ */
202
+ const BUILD_MODE_DEFAULTS = ['production', 'production'] as const;
203
+
204
+ /**
205
+ * Resolve root, appDir and pageExtensions the way `timber build` resolves them.
206
+ *
207
+ * Reading `timber.config.ts` directly is **not** equivalent, which is the whole
208
+ * reason this goes through Vite (TIM-1332, review round 3):
209
+ *
210
+ * - Config passed inline — `timber({ pageExtensions: [...] })` in
211
+ * `vite.config.ts` — lives only in the plugin's closure. Nothing that
212
+ * re-reads the config file can see it, so a scan built that way silently uses
213
+ * default page extensions and writes a route map missing the app's pages.
214
+ * `examples/next-playground-migration` is exactly this shape.
215
+ * - `root` is Vite's resolved root, not the caller's cwd, and the two differ in
216
+ * the monorepo layouts TIM-498 describes. `appDir` resolves against it.
217
+ *
218
+ * There is no fallback to a second, hand-rolled resolution. A project whose
219
+ * Vite config does not produce the timber plugin cannot be built either, so a
220
+ * check that quietly proceeded on a guessed tree would report a verdict about
221
+ * an app that does not exist.
222
+ */
223
+ export async function resolveRouteInputs(
224
+ options: RouteOptions = {},
225
+ deps: CheckDeps = {}
226
+ ): Promise<RouteInputs> {
227
+ const resolveViteConfig = deps.resolveViteConfig ?? defaultResolveViteConfig;
228
+ // `configFile: undefined` is Vite's own "discover it" value, so the default
229
+ // path stays exactly what Vite does for `timber build` without `--config`.
230
+ const { plugins } = await resolveViteConfig(
231
+ { configFile: options.viteConfigFile },
232
+ 'build',
233
+ ...BUILD_MODE_DEFAULTS
234
+ );
235
+
236
+ const rootSync = plugins.find((plugin) => plugin.name === TIMBER_ROOT_SYNC_PLUGIN);
237
+ if (!rootSync || !isTimberPluginApi(rootSync.api)) {
238
+ throw new Error(
239
+ `[timber] Could not read the resolved timber configuration from your Vite config.\n\n` +
240
+ ` \`timber check\` resolves your project through Vite so it validates the same\n` +
241
+ ` routes \`timber build\` builds — including config passed inline to timber().\n\n` +
242
+ ` Check that vite.config.ts exists and lists the timber plugin:\n\n` +
243
+ ` import { timber } from '@timber-js/app';\n` +
244
+ ` export default { plugins: [timber()] };\n`
245
+ );
246
+ }
247
+
248
+ const ctx = rootSync.api.getContext();
249
+ return { root: ctx.root, appDir: ctx.appDir, pageExtensions: ctx.config.pageExtensions };
250
+ }
251
+
252
+ /**
253
+ * Resolve the app directory, scan it, fail on proven-fatal route
254
+ * misconfigurations, print warnings, and write the route codegen the type pass
255
+ * depends on.
256
+ *
257
+ * Error-level lint findings throw rather than print, matching what
258
+ * `plugins/routing.ts` does at build time: the tree is not fit to publish, so
259
+ * it must not reach codegen either. Warnings are printed and do not fail the
260
+ * command — same severity contract as dev and build, so `timber check` cannot
261
+ * disagree with them about what a valid app is.
262
+ */
263
+ export async function checkRoutes(
264
+ options: RouteOptions = {},
265
+ deps: CheckDeps = {}
266
+ ): Promise<RouteInputs> {
267
+ const inputs = await resolveRouteInputs(options, deps);
268
+ const { root, appDir, pageExtensions } = inputs;
269
+
270
+ const tree = scanRoutes(appDir, { pageExtensions });
271
+
272
+ const warnings = lintConventions(tree, appDir);
273
+ const fatal = warnings.filter((w) => w.level === 'error');
274
+ if (fatal.length > 0) {
275
+ throw new Error(formatConventionWarnings(fatal, { color: false }));
276
+ }
277
+ if (warnings.length > 0) {
278
+ process.stderr.write(`${formatConventionWarnings(warnings)}\n`);
279
+ }
280
+
281
+ // Awaited, unlike the dev/build path: the type pass reads these files, so a
282
+ // failed write would be reported as type errors in the user's own code.
283
+ // Reported as a codegen failure rather than surfaced as a bare `EROFS`/
284
+ // `EACCES` from somewhere unnamed — the raw error says which path could not
285
+ // be written but not which pass wanted it.
286
+ try {
287
+ await writeRouteCodegen(tree, appDir, root);
288
+ } catch (err) {
289
+ throw new Error(
290
+ `[timber] Failed to write the generated route types, which the type check reads: ` +
291
+ `${err instanceof Error ? err.message : String(err)}`
292
+ );
293
+ }
294
+
295
+ // Returned so the type pass runs against the same root these files landed in.
296
+ return inputs;
297
+ }
298
+
299
+ /**
300
+ * The production `RunTsgo`: spawn the compiler with its stdio inherited, so its
301
+ * diagnostics stream to the terminal as they are produced (and are not capped
302
+ * by a buffer, as `execFile` output would be).
303
+ */
304
+ const spawnTsgo: RunTsgo = async (command, args, cwd) => {
305
+ const { spawn } = await import('node:child_process');
306
+ return new Promise<number | null>((resolve, reject) => {
307
+ // No `shell: true`: Node would concatenate the argv into an unescaped
308
+ // command string, so a project path containing a space would break.
309
+ const child = spawn(command, args, { stdio: 'inherit', cwd });
310
+ child.on('error', reject);
311
+ child.on('close', (code) => resolve(code));
312
+ });
313
+ };
314
+
315
+ /**
316
+ * The production `ReadTsgoConfig`: `--showConfig`, with stdout captured.
317
+ *
318
+ * `maxBuffer` is raised well past `execFile`'s 1 MB default because the output
319
+ * is one line per file in the program — a large app can exceed that, and
320
+ * overflowing it would make this advisory go quiet on exactly the projects
321
+ * where a misconfigured `include` costs the most. A size-dependent silence is
322
+ * indistinguishable from "everything is fine".
323
+ */
324
+ const execTsgoConfig: ReadTsgoConfig = async (command, args, cwd) => {
325
+ const { execFile } = await import('node:child_process');
326
+ return new Promise<string>((resolve, reject) => {
327
+ execFile(command, args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (err, stdout) =>
328
+ err ? reject(err) : resolve(stdout)
329
+ );
330
+ });
331
+ };
332
+
333
+ /**
334
+ * Warn when the generated route declarations will not reach the type checker.
335
+ *
336
+ * Writing `.timber/timber-routes.d.ts` and `timber-env.d.ts` is only half the
337
+ * job: TypeScript loads them only if the project's `include`/`files` actually
338
+ * pull `timber-env.d.ts` in (it is what `/// <reference>`s the route map). A
339
+ * tsconfig with an explicit `include` that lists only source directories —
340
+ * `["app", "*.config.ts"]`, which is what `create-timber-app` shipped before
341
+ * TIM-1332 — silently omits it, and every `<Link href>` then type-checks
342
+ * against the empty placeholder instead of the generated route union. The
343
+ * check would pass while answering the wrong question.
344
+ *
345
+ * The resolved file list comes from `tsgo --showConfig` rather than from our
346
+ * own reading of the tsconfig: `include`, `exclude`, `files`, and `extends`
347
+ * compose in ways that are the compiler's to decide, and a second
348
+ * implementation of them would be a duplicate that drifts. `--showConfig` only
349
+ * resolves globs, so it costs no type checking.
350
+ *
351
+ * This warns rather than fails. An app that never uses typed routes is
352
+ * correct without those declarations, and a hard error would refuse it — the
353
+ * one outcome a check must not produce for a working project. When the answer
354
+ * cannot be determined (tsgo errored, output was not the expected JSON), the
355
+ * warning is skipped: an advisory that fires on an unknown is noise.
356
+ */
357
+ async function warnIfRouteTypesUnreachable(
358
+ tsgoEntry: string,
359
+ configArgs: string[],
360
+ cwd: string,
361
+ readConfig: ReadTsgoConfig
362
+ ): Promise<void> {
363
+ let files: unknown;
364
+ try {
365
+ const raw = await readConfig(process.execPath, [tsgoEntry, '--showConfig', ...configArgs], cwd);
366
+ files = (JSON.parse(raw) as { files?: unknown }).files;
367
+ } catch {
368
+ return;
369
+ }
370
+ if (!Array.isArray(files)) return;
371
+
372
+ // Suffix match, not path resolution: `--showConfig` prints paths relative to
373
+ // whichever tsconfig the compiler *found*, which for a bare `timber check`
374
+ // is a directory we never learn. Over-matching only suppresses an advisory.
375
+ //
376
+ // The bare form is matched as well as the `/`-prefixed one. tsgo emits
377
+ // `./timber-env.d.ts` today, but a leading `./` is cosmetic and an emitter
378
+ // that dropped it would turn this into a warning printed at every correctly
379
+ // configured project — the failure direction that trains people to ignore it.
380
+ const reaches = files.some((file) => {
381
+ if (typeof file !== 'string') return false;
382
+ const normalized = file.replace(/\\/g, '/');
383
+ return GENERATED_TYPE_PATHS.some(
384
+ (path) => normalized === path || normalized.endsWith(`/${path}`)
385
+ );
386
+ });
387
+ if (reaches) return;
388
+
389
+ process.stderr.write(
390
+ `[timber] The generated route types are not part of this TypeScript project, so\n` +
391
+ ` \`<Link href>\` and \`useSegmentParams()\` are being checked against empty\n` +
392
+ ` placeholders. Your tsconfig's "include" does not pull in timber-env.d.ts.\n\n` +
393
+ ` To fix: add it to "include":\n\n` +
394
+ ` "include": ["app", "*.config.ts", "timber-env.d.ts"]\n\n`
395
+ );
396
+ }
397
+
398
+ /**
399
+ * Run `tsgo --noEmit`, streaming its output.
400
+ *
401
+ * Rejects with the real numeric exit status on type errors. A failure to *start*
402
+ * the compiler is reported separately — the historical bug this replaces
403
+ * surfaced ENOENT as "Type check failed with exit code undefined", which read
404
+ * as a type error and sent users looking at their own code.
405
+ */
406
+ export async function runTypeCheck(
407
+ options: CheckOptions = {},
408
+ deps: CheckDeps = {}
409
+ ): Promise<void> {
410
+ // The *resolved* root, handed over by the route pass — not `process.cwd()`.
411
+ // The two differ whenever `--config` names a Vite config elsewhere, and the
412
+ // route pass wrote `.timber/*` and `timber-env.d.ts` under the resolved one.
413
+ // Running tsgo from cwd would then discover a different tsconfig entirely and
414
+ // type-check a different project than the one just validated (TIM-1332, round
415
+ // 5) — routes checked for the app, types checked for the monorepo around it.
416
+ const root = options.root ?? process.cwd();
417
+ const tsgoEntry = (deps.resolveTsgoEntry ?? (() => resolveTsgoEntry(root)))();
418
+ const runTsgo = deps.runTsgo ?? spawnTsgo;
419
+
420
+ // An explicit `--project` is a path the user typed at their own prompt, so it
421
+ // is resolved against *their* cwd before the compiler is moved to the project
422
+ // root. Without `--project`, tsgo discovers `tsconfig.json` from `root`,
423
+ // which is where the declarations it needs were just written.
424
+ const configArgs = options.config ? ['--project', resolve(process.cwd(), options.config)] : [];
425
+
426
+ // Before checking: say so if the declarations the route pass just wrote will
427
+ // not be part of the program. Otherwise the check passes while `<Link href>`
428
+ // is measured against a placeholder. Same root and same argv as the check
429
+ // itself, or it would inspect a different program than the one tsgo reads.
430
+ await warnIfRouteTypesUnreachable(
431
+ tsgoEntry,
432
+ configArgs,
433
+ root,
434
+ deps.readTsgoConfig ?? execTsgoConfig
435
+ );
436
+
437
+ let code: number | null;
438
+ try {
439
+ code = await runTsgo(process.execPath, [tsgoEntry, '--noEmit', ...configArgs], root);
440
+ } catch (err) {
441
+ // Could not start the compiler at all. Kept distinct from a nonzero exit:
442
+ // the version this replaced reported ENOENT as "Type check failed with
443
+ // exit code undefined", which reads as a verdict on the user's types.
444
+ throw new Error(
445
+ `[timber] Failed to start the type checker: ${err instanceof Error ? err.message : String(err)}`
446
+ );
447
+ }
448
+
449
+ if (code === 0) return;
450
+
451
+ // A null code means a signal killed the process rather than it exiting.
452
+ // Say so instead of printing "exit code null".
453
+ throw new Error(
454
+ code === null
455
+ ? 'Type check terminated before it finished'
456
+ : `Type check failed with exit code ${code}`
457
+ );
458
+ }
package/src/cli.ts CHANGED
@@ -11,28 +11,49 @@
11
11
  // timber preview — Serve the production build
12
12
  // timber check — Validate types + routes without building
13
13
 
14
+ // Type-only — the implementation is imported dynamically inside runCheck so the
15
+ // CLI's other commands never load the route scanner.
16
+ import type { CheckDeps } from './cli-check.js';
17
+
18
+ export type { CheckDeps };
19
+
14
20
  const COMMANDS = ['dev', 'build', 'preview', 'check', 'schema', 'init'] as const;
15
21
  type Command = (typeof COMMANDS)[number];
16
22
 
17
23
  export interface ParsedArgs {
18
24
  command: Command;
19
25
  config: string | undefined;
26
+ /** `--project`: the tsconfig `timber check` passes to tsgo. */
27
+ project: string | undefined;
20
28
  /** Positional arguments after the command (used by `timber schema sync`). */
21
29
  rest: string[];
22
30
  }
23
31
 
24
32
  export interface CommandOptions {
33
+ /** Path to a Vite config file. Means the same thing for every command. */
25
34
  config?: string;
35
+ /** `timber check` only: the tsconfig to type-check. */
36
+ project?: string;
26
37
  }
27
38
 
28
39
  /**
29
40
  * Parse CLI arguments into a structured command + options.
30
- * Accepts: timber <command> [--config|-c <path>]
41
+ * Accepts: timber <command> [--config|-c <path>] [--project <path>]
42
+ *
43
+ * `--config` names a **Vite** config file for every command, `check` included
44
+ * (TIM-1332): `check` resolves the project through Vite, so a project that
45
+ * builds with `--config ./custom.vite.config.ts` has to be checkable the same
46
+ * way. `--project` names the tsconfig, spelled as tsgo and tsc spell it, and
47
+ * is meaningful only for `check`.
48
+ *
49
+ * Before TIM-1332 `--config` meant the tsconfig for `check` alone — one flag
50
+ * with two meanings, which the docs had to carry a note to explain and which
51
+ * left custom Vite configs unreachable from `check` entirely.
31
52
  *
32
53
  * Unknown flags are an error, not silently ignored — `timber dev --port 4000`
33
- * starting on the default port would contradict fail-loudly. The CLI surface
34
- * is intentionally --config-only (design/11-platform.md §CLI); port selection
35
- * goes through the PORT env var or server.port in vite.config.ts.
54
+ * starting on the default port would contradict fail-loudly. The surface stays
55
+ * deliberately small (design/11-platform.md §CLI); port selection goes through
56
+ * the PORT env var or server.port in vite.config.ts.
36
57
  */
37
58
  export function parseArgs(args: string[]): ParsedArgs {
38
59
  if (args.length === 0) {
@@ -47,6 +68,7 @@ export function parseArgs(args: string[]): ParsedArgs {
47
68
  }
48
69
 
49
70
  let config: string | undefined;
71
+ let project: string | undefined;
50
72
  const rest: string[] = [];
51
73
  for (let i = 1; i < args.length; i++) {
52
74
  const arg = args[i];
@@ -55,6 +77,17 @@ export function parseArgs(args: string[]): ParsedArgs {
55
77
  if (!config) {
56
78
  throw new Error('--config requires a path argument');
57
79
  }
80
+ } else if (arg === '--project') {
81
+ if (command !== 'check') {
82
+ throw new Error(
83
+ `--project is only valid for \`timber check\` (it names the tsconfig to type-check). ` +
84
+ `To point \`timber ${command}\` at a different Vite config, use --config <path>.`
85
+ );
86
+ }
87
+ project = args[++i];
88
+ if (!project) {
89
+ throw new Error('--project requires a path argument');
90
+ }
58
91
  } else if (arg.startsWith('-')) {
59
92
  const portHint =
60
93
  arg === '--port' || arg === '-p'
@@ -62,7 +95,8 @@ export function parseArgs(args: string[]): ParsedArgs {
62
95
  'or server.port in vite.config.ts.'
63
96
  : '';
64
97
  throw new Error(
65
- `Unknown option: ${arg}. The timber CLI only accepts --config <path>.${portHint}`
98
+ `Unknown option: ${arg}. The timber CLI accepts --config <path>` +
99
+ ` (and --project <path> for \`check\`).${portHint}`
66
100
  );
67
101
  } else if (command === 'schema' || command === 'init') {
68
102
  rest.push(arg);
@@ -71,7 +105,7 @@ export function parseArgs(args: string[]): ParsedArgs {
71
105
  }
72
106
  }
73
107
 
74
- return { command: command as Command, config, rest };
108
+ return { command: command as Command, config, project, rest };
75
109
  }
76
110
 
77
111
  // ─── Command Implementations ─────────────────────────────────────────────────
@@ -160,24 +194,25 @@ export async function runPreview(options: CommandOptions, _deps?: ViteDeps): Pro
160
194
  }
161
195
 
162
196
  /**
163
- * Validate types and routes without producing build output.
164
- * Runs tsgo --noEmit for type checking.
197
+ * Validate routes and types without producing build output.
198
+ *
199
+ * Runs the route pass first — resolve the project through Vite, scan it, fail
200
+ * on proven-fatal convention errors, print warnings, and regenerate the route
201
+ * types — then `tsgo --noEmit`. Codegen has to precede the type pass: the
202
+ * generated `.timber/*.d.ts` files are what type `<Link href>` and
203
+ * `useSegmentParams()`, and a fresh clone has never produced them. See
204
+ * `cli-check.ts` for the full reasoning.
205
+ *
206
+ * `--config` names the **Vite** config, as it does for every other command, so
207
+ * an app that builds with a custom one is checkable the same way.
208
+ * `--project` names the tsconfig handed to tsgo.
165
209
  */
166
- export async function runCheck(options: CommandOptions): Promise<void> {
167
- const { execFile } = await import('node:child_process');
168
-
169
- await new Promise<void>((resolve, reject) => {
170
- const configArgs = options.config ? ['--project', options.config] : [];
171
- execFile('tsgo', ['--noEmit', ...configArgs], (err, stdout, stderr) => {
172
- if (stdout) process.stdout.write(stdout);
173
- if (stderr) process.stderr.write(stderr);
174
- if (err) {
175
- reject(new Error(`Type check failed with exit code ${err.code}`));
176
- } else {
177
- resolve();
178
- }
179
- });
180
- });
210
+ export async function runCheck(options: CommandOptions, _deps?: CheckDeps): Promise<void> {
211
+ const { checkRoutes, runTypeCheck } = await import('./cli-check.js');
212
+ const { root } = await checkRoutes({ viteConfigFile: options.config }, _deps);
213
+ // tsgo runs from the root the route pass resolved, not from cwd — that is
214
+ // where it just wrote the declarations tsgo has to read.
215
+ await runTypeCheck({ root, config: options.project }, _deps);
181
216
  }
182
217
 
183
218
  // ─── Schema Subcommand ───────────────────────────────────────────────────────
@@ -305,7 +340,7 @@ export async function runInit(): Promise<void> {
305
340
 
306
341
  async function main(): Promise<void> {
307
342
  const parsed = parseArgs(process.argv.slice(2));
308
- const options: CommandOptions = { config: parsed.config };
343
+ const options: CommandOptions = { config: parsed.config, project: parsed.project };
309
344
 
310
345
  switch (parsed.command) {
311
346
  case 'dev':
@@ -19,6 +19,7 @@ import { getRouter, getRouterOrNull } from '#client-internal';
19
19
  import { setHardNavigating } from '../navigation-root.js';
20
20
  import { getClientDeploymentId, DEPLOYMENT_ID_HEADER, RELOAD_HEADER } from '../rsc-fetch.js';
21
21
  import { markClientStale } from '../stale-client.js';
22
+ import { RSC_CONTENT_TYPE } from '../../shared/rsc-media-type.js';
22
23
  export function setupServerActions(): void {
23
24
  setServerCallback(async (id: string, args: unknown[]) => {
24
25
  const body = await encodeReply(args);
@@ -32,7 +33,7 @@ export function setupServerActions(): void {
32
33
  // Build action request headers. Include deployment ID for version
33
34
  // skew detection (TIM-446) — the server rejects stale actions gracefully.
34
35
  const actionHeaders: Record<string, string> = {
35
- 'Accept': 'text/x-component',
36
+ 'Accept': RSC_CONTENT_TYPE,
36
37
  'x-rsc-action': id,
37
38
  };
38
39
  const actionDeploymentId = getClientDeploymentId();
@@ -28,11 +28,6 @@
28
28
 
29
29
  // @ts-expect-error — virtual module provided by timber-entries plugin
30
30
  import config from 'virtual:timber-config';
31
- // TIM-830: Populate the search-params registry eagerly so <Link> on the
32
- // client can serialize flat `Partial<T>` values synchronously during
33
- // render and click handlers. Side-effect-only import.
34
- // @ts-expect-error — virtual module provided by timber-routing plugin
35
- import 'virtual:timber-search-params-registry';
36
31
  // TIM-932: Populate the codec registry so Link's interpolateParams can
37
32
  // use codec.serialize() for URL construction. Side-effect-only import.
38
33
  // @ts-expect-error — virtual module provided by timber-routing plugin