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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (314) hide show
  1. package/dist/_chunks/{actions-C-Rw9vPc.js → actions-35jnMdeJ.js} +4 -3
  2. package/dist/_chunks/{actions-C-Rw9vPc.js.map → actions-35jnMdeJ.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js +41 -0
  4. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -0
  5. package/dist/_chunks/{als-slots-mFweg276.js → als-slots-BEEIPKYm.js} +3 -4
  6. package/dist/_chunks/{als-slots-mFweg276.js.map → als-slots-BEEIPKYm.js.map} +1 -1
  7. package/dist/_chunks/{cache-api-Cd0VZ_Pd.js → cache-api-DjNrIWRR.js} +7 -13
  8. package/dist/_chunks/cache-api-DjNrIWRR.js.map +1 -0
  9. package/dist/_chunks/cli-check-CpmN7Nh-.js +256 -0
  10. package/dist/_chunks/cli-check-CpmN7Nh-.js.map +1 -0
  11. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js +298 -0
  12. package/dist/_chunks/cli-schema-sync-CGMp_Psg.js.map +1 -0
  13. package/dist/_chunks/{cloudflare-Cs0uZXea.js → cloudflare-CGP6BZKO.js} +4 -3
  14. package/dist/_chunks/{cloudflare-Cs0uZXea.js.map → cloudflare-CGP6BZKO.js.map} +1 -1
  15. package/dist/_chunks/convention-lint-kXsgc_-7.js +784 -0
  16. package/dist/_chunks/convention-lint-kXsgc_-7.js.map +1 -0
  17. package/dist/_chunks/{error-boundary-DpYRI_I1.js → error-boundary-D-ODYX41.js} +25 -2
  18. package/dist/_chunks/error-boundary-D-ODYX41.js.map +1 -0
  19. package/dist/_chunks/file-cache-DmX7OqZP.js +454 -0
  20. package/dist/_chunks/file-cache-DmX7OqZP.js.map +1 -0
  21. package/dist/_chunks/{logger-N7e5auP0.js → logger-D8xJZXIN.js} +27 -40
  22. package/dist/_chunks/logger-D8xJZXIN.js.map +1 -0
  23. package/dist/_chunks/mdx-file-C005ay-P.js +25 -0
  24. package/dist/_chunks/mdx-file-C005ay-P.js.map +1 -0
  25. package/dist/_chunks/{plugin-context-DEGLSJs3.js → plugin-context-rCinWLiE.js} +7 -2
  26. package/dist/_chunks/{plugin-context-DEGLSJs3.js.map → plugin-context-rCinWLiE.js.map} +1 -1
  27. package/dist/_chunks/purge-store-Byr8XOjU.js +14 -0
  28. package/dist/_chunks/purge-store-Byr8XOjU.js.map +1 -0
  29. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js → resolve-schema-5ma5pp1b.js} +2 -2
  30. package/dist/_chunks/{resolve-schema-Dz3fcFUo.js.map → resolve-schema-5ma5pp1b.js.map} +1 -1
  31. package/dist/_chunks/rsc-media-type-DRqE_lD_.js +46 -0
  32. package/dist/_chunks/rsc-media-type-DRqE_lD_.js.map +1 -0
  33. package/dist/_chunks/{cli-schema-sync-CKgHC2MB.js → scanner-C8b0Gcw3.js} +5 -298
  34. package/dist/_chunks/scanner-C8b0Gcw3.js.map +1 -0
  35. package/dist/_chunks/schema-bridge-Cc2Gngu1.js +199 -0
  36. package/dist/_chunks/schema-bridge-Cc2Gngu1.js.map +1 -0
  37. package/dist/_chunks/segment-classify-C539Pa2O.js.map +1 -1
  38. package/dist/_chunks/{param-value-C8TNYchQ.js → segment-context-CjOlyB8Y.js} +33 -2
  39. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +1 -0
  40. package/dist/_chunks/{use-query-states-DFvWd-EA.js → use-query-states-BbU5Ge1V.js} +74 -26
  41. package/dist/_chunks/use-query-states-BbU5Ge1V.js.map +1 -0
  42. package/dist/_chunks/{navigation-root-B29qg0_T.js → use-segment-params-C4r4BD9T.js} +129 -5
  43. package/dist/_chunks/use-segment-params-C4r4BD9T.js.map +1 -0
  44. package/dist/_chunks/walkers-RzN6AFjr.js +141 -0
  45. package/dist/_chunks/walkers-RzN6AFjr.js.map +1 -0
  46. package/dist/adapters/cloudflare-dev.js +1 -1
  47. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  48. package/dist/adapters/cloudflare.d.ts.map +1 -1
  49. package/dist/adapters/cloudflare.js +1 -1
  50. package/dist/adapters/compress-module.d.ts +12 -0
  51. package/dist/adapters/compress-module.d.ts.map +1 -1
  52. package/dist/adapters/nitro.js +54 -2
  53. package/dist/adapters/nitro.js.map +1 -1
  54. package/dist/cache/index.js +1 -1
  55. package/dist/cdn/cloudflare-purge.js +30 -0
  56. package/dist/cdn/cloudflare-purge.js.map +1 -0
  57. package/dist/cdn/fastly-purge.js +33 -0
  58. package/dist/cdn/fastly-purge.js.map +1 -0
  59. package/dist/cdn/index.js +94 -0
  60. package/dist/cdn/index.js.map +1 -0
  61. package/dist/cdn/workers-cache-purge.js +35 -0
  62. package/dist/cdn/workers-cache-purge.js.map +1 -0
  63. package/dist/cli-check.d.ts +153 -0
  64. package/dist/cli-check.d.ts.map +1 -0
  65. package/dist/cli.d.ts +34 -8
  66. package/dist/cli.d.ts.map +1 -1
  67. package/dist/cli.js +46 -21
  68. package/dist/cli.js.map +1 -1
  69. package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
  70. package/dist/client/browser-entry/index.d.ts +1 -1
  71. package/dist/client/browser-entry/index.d.ts.map +1 -1
  72. package/dist/client/error-boundary.d.ts +6 -0
  73. package/dist/client/error-boundary.d.ts.map +1 -1
  74. package/dist/client/error-boundary.js +1 -1
  75. package/dist/client/index.d.ts +1 -0
  76. package/dist/client/index.d.ts.map +1 -1
  77. package/dist/client/index.js +27 -33
  78. package/dist/client/index.js.map +1 -1
  79. package/dist/client/internal.js +21 -14
  80. package/dist/client/internal.js.map +1 -1
  81. package/dist/client/link.d.ts +1 -7
  82. package/dist/client/link.d.ts.map +1 -1
  83. package/dist/client/navigation-commit.d.ts +16 -0
  84. package/dist/client/navigation-commit.d.ts.map +1 -1
  85. package/dist/client/router-pipeline.d.ts.map +1 -1
  86. package/dist/client/router.d.ts.map +1 -1
  87. package/dist/client/rsc-fetch.d.ts +9 -1
  88. package/dist/client/rsc-fetch.d.ts.map +1 -1
  89. package/dist/client/segment-cache.d.ts +8 -0
  90. package/dist/client/segment-cache.d.ts.map +1 -1
  91. package/dist/client/use-query-states.d.ts +9 -3
  92. package/dist/client/use-query-states.d.ts.map +1 -1
  93. package/dist/codec.js +1 -1
  94. package/dist/cookies/define-cookie.d.ts.map +1 -1
  95. package/dist/cookies/index.js +2 -2
  96. package/dist/cookies/index.js.map +1 -1
  97. package/dist/index.d.ts +4 -1
  98. package/dist/index.d.ts.map +1 -1
  99. package/dist/index.js +142 -477
  100. package/dist/index.js.map +1 -1
  101. package/dist/params/index.js +1 -1
  102. package/dist/plugin-context.d.ts +15 -0
  103. package/dist/plugin-context.d.ts.map +1 -1
  104. package/dist/plugins/routing.d.ts +0 -9
  105. package/dist/plugins/routing.d.ts.map +1 -1
  106. package/dist/plugins/shims.d.ts.map +1 -1
  107. package/dist/plugins/static-build.d.ts +24 -0
  108. package/dist/plugins/static-build.d.ts.map +1 -1
  109. package/dist/routing/codegen-shared.d.ts +3 -44
  110. package/dist/routing/codegen-shared.d.ts.map +1 -1
  111. package/dist/routing/codegen-types.d.ts +10 -31
  112. package/dist/routing/codegen-types.d.ts.map +1 -1
  113. package/dist/routing/codegen-write.d.ts +51 -0
  114. package/dist/routing/codegen-write.d.ts.map +1 -0
  115. package/dist/routing/codegen.d.ts.map +1 -1
  116. package/dist/routing/convention-lint.d.ts +18 -4
  117. package/dist/routing/convention-lint.d.ts.map +1 -1
  118. package/dist/routing/export-detect.d.ts +16 -0
  119. package/dist/routing/export-detect.d.ts.map +1 -1
  120. package/dist/routing/index.js +3 -2
  121. package/dist/routing/link-codegen.d.ts +19 -4
  122. package/dist/routing/link-codegen.d.ts.map +1 -1
  123. package/dist/routing/manifest-codegen.d.ts +1 -7
  124. package/dist/routing/manifest-codegen.d.ts.map +1 -1
  125. package/dist/routing/types.d.ts +0 -6
  126. package/dist/routing/types.d.ts.map +1 -1
  127. package/dist/schema-bridge.d.ts +60 -9
  128. package/dist/schema-bridge.d.ts.map +1 -1
  129. package/dist/search-params/define.d.ts +62 -8
  130. package/dist/search-params/define.d.ts.map +1 -1
  131. package/dist/search-params/index.d.ts +0 -1
  132. package/dist/search-params/index.d.ts.map +1 -1
  133. package/dist/search-params/index.js +66 -29
  134. package/dist/search-params/index.js.map +1 -1
  135. package/dist/search-params/parse-total.d.ts +70 -0
  136. package/dist/search-params/parse-total.d.ts.map +1 -0
  137. package/dist/search-params/wrappers.d.ts +26 -3
  138. package/dist/search-params/wrappers.d.ts.map +1 -1
  139. package/dist/server/access-gate.d.ts +19 -8
  140. package/dist/server/access-gate.d.ts.map +1 -1
  141. package/dist/server/action-handler.d.ts.map +1 -1
  142. package/dist/server/als-registry.d.ts +16 -0
  143. package/dist/server/als-registry.d.ts.map +1 -1
  144. package/dist/server/compress.d.ts.map +1 -1
  145. package/dist/server/deny-boundary.d.ts +148 -15
  146. package/dist/server/deny-boundary.d.ts.map +1 -1
  147. package/dist/server/deny-renderer.d.ts +2 -2
  148. package/dist/server/deny-renderer.d.ts.map +1 -1
  149. package/dist/server/error-boundary-wrapper.d.ts +85 -15
  150. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  151. package/dist/server/index.d.ts +0 -1
  152. package/dist/server/index.d.ts.map +1 -1
  153. package/dist/server/index.js +3 -2
  154. package/dist/server/index.js.map +1 -1
  155. package/dist/server/internal.d.ts +3 -1
  156. package/dist/server/internal.d.ts.map +1 -1
  157. package/dist/server/internal.js +343 -230
  158. package/dist/server/internal.js.map +1 -1
  159. package/dist/server/metadata-collector.d.ts +52 -0
  160. package/dist/server/metadata-collector.d.ts.map +1 -0
  161. package/dist/server/param-coercion.d.ts +12 -5
  162. package/dist/server/param-coercion.d.ts.map +1 -1
  163. package/dist/server/pipeline-helpers.d.ts.map +1 -1
  164. package/dist/server/pipeline-outcome.d.ts.map +1 -1
  165. package/dist/server/pipeline-phases.d.ts.map +1 -1
  166. package/dist/server/primitives.d.ts +23 -0
  167. package/dist/server/primitives.d.ts.map +1 -1
  168. package/dist/server/route-element-builder.d.ts +1 -12
  169. package/dist/server/route-element-builder.d.ts.map +1 -1
  170. package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
  171. package/dist/server/rsc-entry/deny-fallback.d.ts.map +1 -1
  172. package/dist/server/rsc-entry/error-renderer.d.ts +1 -1
  173. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  174. package/dist/server/rsc-entry/helpers.d.ts +0 -7
  175. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  176. package/dist/server/rsc-entry/index.d.ts +0 -1
  177. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  178. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  179. package/dist/server/rsc-entry/revalidate-renderer.d.ts.map +1 -1
  180. package/dist/server/rsc-entry/rsc-payload.d.ts +1 -3
  181. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  182. package/dist/server/rsc-entry/rsc-stream.d.ts +12 -0
  183. package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
  184. package/dist/server/rsc-entry/ssr-renderer.d.ts +0 -2
  185. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  186. package/dist/server/slot-resolver.d.ts.map +1 -1
  187. package/dist/server/ssr-bridge-types.d.ts +11 -0
  188. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  189. package/dist/server/ssr-entry.d.ts +0 -1
  190. package/dist/server/ssr-entry.d.ts.map +1 -1
  191. package/dist/server/static-generator.d.ts.map +1 -1
  192. package/dist/server/status-code-resolver.d.ts +8 -1
  193. package/dist/server/status-code-resolver.d.ts.map +1 -1
  194. package/dist/server/tree-builder.d.ts +28 -36
  195. package/dist/server/tree-builder.d.ts.map +1 -1
  196. package/dist/server/types.d.ts +12 -7
  197. package/dist/server/types.d.ts.map +1 -1
  198. package/dist/server/utils/element-type.d.ts +40 -0
  199. package/dist/server/utils/element-type.d.ts.map +1 -0
  200. package/dist/shared/rsc-media-type.d.ts +40 -0
  201. package/dist/shared/rsc-media-type.d.ts.map +1 -0
  202. package/docs/api/30-api-server.mdx +1 -1
  203. package/docs/api/33-api-search-params.mdx +38 -16
  204. package/docs/api/35-api-typescript.mdx +3 -3
  205. package/docs/api/36-cli.mdx +34 -7
  206. package/docs/learn/00-introduction.mdx +1 -1
  207. package/docs/learn/02-pages-and-layouts.mdx +1 -1
  208. package/docs/learn/05-typed-params.mdx +8 -8
  209. package/docs/learn/07-typed-routes.mdx +12 -7
  210. package/docs/learn/11-error-handling.mdx +16 -0
  211. package/docs/more/01-advanced-routing.mdx +1 -1
  212. package/docs/more/03-coming-from-nextjs.mdx +2 -2
  213. package/docs/more/50-ai-agent-instructions.mdx +6 -4
  214. package/package.json +8 -5
  215. package/src/adapters/cloudflare.ts +4 -1
  216. package/src/adapters/compress-module.ts +79 -1
  217. package/src/cli-check.ts +458 -0
  218. package/src/cli.ts +59 -24
  219. package/src/client/browser-entry/action-dispatch.ts +2 -1
  220. package/src/client/browser-entry/index.ts +0 -5
  221. package/src/client/error-boundary.tsx +65 -1
  222. package/src/client/index.ts +14 -3
  223. package/src/client/link.tsx +65 -64
  224. package/src/client/navigation-commit.ts +27 -4
  225. package/src/client/params-context.ts +4 -4
  226. package/src/client/router-pipeline.ts +4 -0
  227. package/src/client/router.ts +1 -0
  228. package/src/client/rsc-fetch.ts +14 -4
  229. package/src/client/segment-cache.ts +8 -0
  230. package/src/client/use-query-states.ts +102 -39
  231. package/src/cookies/define-cookie.ts +6 -1
  232. package/src/index.ts +20 -3
  233. package/src/plugin-context.ts +26 -0
  234. package/src/plugins/routing.ts +84 -146
  235. package/src/plugins/shims.ts +0 -1
  236. package/src/plugins/static-build.ts +78 -24
  237. package/src/routing/codegen-shared.ts +3 -79
  238. package/src/routing/codegen-types.ts +10 -31
  239. package/src/routing/codegen-write.ts +139 -0
  240. package/src/routing/codegen.ts +56 -182
  241. package/src/routing/convention-lint.ts +139 -40
  242. package/src/routing/export-detect.ts +151 -7
  243. package/src/routing/link-codegen.ts +32 -65
  244. package/src/routing/manifest-codegen.ts +1 -59
  245. package/src/routing/scanner.ts +3 -3
  246. package/src/routing/types.ts +0 -6
  247. package/src/schema-bridge.ts +180 -58
  248. package/src/search-params/define.ts +102 -37
  249. package/src/search-params/index.ts +0 -1
  250. package/src/search-params/parse-total.ts +78 -0
  251. package/src/search-params/wrappers.ts +60 -11
  252. package/src/server/access-gate.tsx +60 -40
  253. package/src/server/action-handler.ts +1 -4
  254. package/src/server/als-registry.ts +16 -0
  255. package/src/server/compress.ts +9 -1
  256. package/src/server/deny-boundary.ts +269 -41
  257. package/src/server/deny-renderer.ts +32 -21
  258. package/src/server/error-boundary-wrapper.ts +166 -79
  259. package/src/server/index.ts +1 -3
  260. package/src/server/internal.ts +2 -2
  261. package/src/server/metadata-collector.ts +115 -0
  262. package/src/server/param-coercion.ts +13 -61
  263. package/src/server/pipeline-helpers.ts +2 -2
  264. package/src/server/pipeline-outcome.ts +2 -1
  265. package/src/server/pipeline-phases.ts +9 -9
  266. package/src/server/primitives.ts +25 -0
  267. package/src/server/route-element-builder.ts +167 -170
  268. package/src/server/rsc-cache-key-guard.ts +2 -8
  269. package/src/server/rsc-entry/deny-fallback.ts +3 -2
  270. package/src/server/rsc-entry/error-renderer.ts +31 -11
  271. package/src/server/rsc-entry/helpers.ts +2 -12
  272. package/src/server/rsc-entry/index.ts +0 -5
  273. package/src/server/rsc-entry/render-route.ts +14 -11
  274. package/src/server/rsc-entry/revalidate-renderer.ts +2 -1
  275. package/src/server/rsc-entry/rsc-payload.ts +104 -20
  276. package/src/server/rsc-entry/rsc-stream.ts +25 -2
  277. package/src/server/rsc-entry/ssr-renderer.ts +28 -16
  278. package/src/server/slot-resolver.ts +10 -2
  279. package/src/server/ssr-bridge-types.ts +12 -0
  280. package/src/server/ssr-entry.ts +8 -6
  281. package/src/server/static-generator.ts +3 -2
  282. package/src/server/status-code-resolver.ts +28 -11
  283. package/src/server/tree-builder.ts +35 -218
  284. package/src/server/types.ts +12 -7
  285. package/src/server/utils/element-type.ts +72 -0
  286. package/src/shared/rsc-media-type.ts +43 -0
  287. package/dist/_chunks/cache-api-Cd0VZ_Pd.js.map +0 -1
  288. package/dist/_chunks/cli-schema-sync-CKgHC2MB.js.map +0 -1
  289. package/dist/_chunks/error-boundary-DpYRI_I1.js.map +0 -1
  290. package/dist/_chunks/logger-N7e5auP0.js.map +0 -1
  291. package/dist/_chunks/navigation-root-B29qg0_T.js.map +0 -1
  292. package/dist/_chunks/param-value-C8TNYchQ.js.map +0 -1
  293. package/dist/_chunks/registry-DbJPKoBp.js +0 -20
  294. package/dist/_chunks/registry-DbJPKoBp.js.map +0 -1
  295. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js +0 -119
  296. package/dist/_chunks/schema-bridge-DT_Tn0Xf.js.map +0 -1
  297. package/dist/_chunks/segment-context-ZDnXDkbz.js +0 -34
  298. package/dist/_chunks/segment-context-ZDnXDkbz.js.map +0 -1
  299. package/dist/_chunks/use-query-states-DFvWd-EA.js.map +0 -1
  300. package/dist/_chunks/use-segment-params-ClyUNq4d.js +0 -128
  301. package/dist/_chunks/use-segment-params-ClyUNq4d.js.map +0 -1
  302. package/dist/_chunks/walkers-BhhwI9TD.js +0 -936
  303. package/dist/_chunks/walkers-BhhwI9TD.js.map +0 -1
  304. package/dist/search-params/registry.d.ts +0 -20
  305. package/dist/search-params/registry.d.ts.map +0 -1
  306. package/dist/segment-params/define.d.ts +0 -83
  307. package/dist/segment-params/define.d.ts.map +0 -1
  308. package/dist/segment-params/index.d.ts +0 -3
  309. package/dist/segment-params/index.d.ts.map +0 -1
  310. package/dist/segment-params/index.js +0 -70
  311. package/dist/segment-params/index.js.map +0 -1
  312. package/src/search-params/registry.ts +0 -31
  313. package/src/segment-params/define.ts +0 -226
  314. package/src/segment-params/index.ts +0 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.186",
3
+ "version": "0.2.0-alpha.187",
4
4
  "description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -113,10 +113,6 @@
113
113
  "types": "./dist/search-params/index.d.ts",
114
114
  "import": "./dist/search-params/index.js"
115
115
  },
116
- "./segment-params": {
117
- "types": "./dist/segment-params/index.d.ts",
118
- "import": "./dist/segment-params/index.js"
119
- },
120
116
  "./params": {
121
117
  "types": "./dist/params/index.d.ts",
122
118
  "import": "./dist/params/index.js"
@@ -165,6 +161,7 @@
165
161
  "@content-collections/mdx": "^0.2.0",
166
162
  "@content-collections/vite": "^0.2.0 || ^0.3.0",
167
163
  "@mdx-js/rollup": "^3.0.0",
164
+ "@typescript/native-preview": "^7.0.0-dev.0",
168
165
  "@vitejs/plugin-react": "^6.0.0",
169
166
  "@vitejs/plugin-rsc": ">=0.5.28",
170
167
  "nuqs": "^2.0.0",
@@ -187,6 +184,9 @@
187
184
  "@mdx-js/rollup": {
188
185
  "optional": true
189
186
  },
187
+ "@typescript/native-preview": {
188
+ "optional": true
189
+ },
190
190
  "wrangler": {
191
191
  "optional": true
192
192
  },
@@ -194,6 +194,9 @@
194
194
  "optional": true
195
195
  }
196
196
  },
197
+ "devDependencies": {
198
+ "@typescript/native-preview": "7.0.0-dev.20260707.2"
199
+ },
197
200
  "engines": {
198
201
  "node": ">=22.18.0"
199
202
  },
@@ -20,6 +20,9 @@ import { adapterBase, generateHeadersFile } from './shared.js';
20
20
  // native ESM loader evaluates before Vite's resolver exists. Same precedent as
21
21
  // `adapters/shared.ts` importing `../shared/base-path.js`.
22
22
  import { RSC_KEY_HEADERS } from '../shared/rsc-cache-key.js';
23
+ // Same reasoning: the media type the origin's own RSC gate matches on, so the
24
+ // two predicates cannot drift apart. See `shared/rsc-media-type.ts`.
25
+ import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
23
26
 
24
27
  // ─── Bindings passthrough ─────────────────────────────────────────────────
25
28
  // ALS stores the env object per-request so server components and middleware
@@ -486,7 +489,7 @@ ${handlersSpread} async fetch(request, env, ctx) {
486
489
  // This exclusion is load-bearing in the other direction too: the store
487
490
  // predicate below treats "Vary: Accept" as honoured *because* of it
488
491
  // (see VARY_HONORED). Removing it would make that allowlist unsound.
489
- const isRsc = request.headers.get('Accept')?.includes('text/x-component')
492
+ const isRsc = request.headers.get('Accept')?.includes(${JSON.stringify(RSC_CONTENT_TYPE)})
490
493
  const reqCc = request.headers.get('Cache-Control') || ''
491
494
  const bypassCache = reqCc.includes('no-cache') || reqCc.includes('max-age=0')
492
495
  || request.headers.has('Pragma')
@@ -7,12 +7,90 @@
7
7
  // hand-written string template, any fix to server/compress.ts automatically
8
8
  // flows to the production module. See design/18-build-system.md ("entry
9
9
  // modules are real TypeScript files, not codegen strings").
10
+ //
11
+ // The transform strips types; it does NOT bundle. The emitted file sits
12
+ // alone in the build output, so any import specifier that survives into it
13
+ // is a module resolution failure at runtime on the deployed server. Shared
14
+ // constants are therefore interpolated here — the same treatment the
15
+ // Cloudflare worker template gives `RSC_KEY_HEADERS` — and anything else
16
+ // that reaches the output as an import is rejected with a build error
17
+ // rather than shipped.
10
18
 
11
19
  import { transformWithOxc } from 'vite';
12
20
  // Vite's ?raw import embeds the file as a string at build time — works
13
21
  // in both dev (Vite resolves the TS source) and the published package
14
22
  // (the string is inlined into the compiled adapter entry).
15
23
  import compressSource from '../server/compress.ts?raw';
24
+ import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
25
+
26
+ /**
27
+ * The one import `server/compress.ts` is allowed to carry, and its inlined
28
+ * form.
29
+ *
30
+ * The binding list is matched exactly rather than as `{…}`. A second binding
31
+ * added to that import would otherwise be erased along with the statement and
32
+ * become a `ReferenceError` in the deployed `_compress.mjs` — the failure this
33
+ * whole module exists to make impossible. Matching exactly means such an
34
+ * import falls through to the rejection below instead.
35
+ */
36
+ const INLINED_IMPORTS: ReadonlyArray<{ pattern: RegExp; replacement: string }> = [
37
+ {
38
+ pattern:
39
+ /^import\s*\{\s*RSC_CONTENT_TYPE\s*,?\s*\}\s*from\s*["'][^"']*shared\/rsc-media-type\.js["'];?[ \t]*$/m,
40
+ replacement: `const RSC_CONTENT_TYPE = ${JSON.stringify(RSC_CONTENT_TYPE)};`,
41
+ },
42
+ ];
43
+
44
+ /**
45
+ * Every way a module specifier can reach the emitted file, minus Node
46
+ * builtins.
47
+ *
48
+ * `node:*` is exempt because it resolves on the deployed server exactly as it
49
+ * does here — `_compress.mjs` already imports `node:zlib` and `node:stream`,
50
+ * and self-hosted (Node/Bun) is the only place this file runs. Everything
51
+ * else — relative paths, bare packages — has no resolution target next to a
52
+ * lone `.mjs` in the build output.
53
+ *
54
+ * All three forms are listed rather than the one `server/compress.ts` happens
55
+ * to use, because a re-export or a lazy `await import()` fails in exactly the
56
+ * same place and is exactly as invisible until deploy.
57
+ */
58
+ const MODULE_SPECIFIERS: readonly RegExp[] = [
59
+ // import … from '…' / export … from '…'
60
+ /^[ \t]*(?:import|export)\b[^\n]*\bfrom\s*["'](?!node:)[^"']+["']/m,
61
+ // import '…' (side effect only)
62
+ /^[ \t]*import\s*["'](?!node:)[^"']+["']/m,
63
+ // import('…') (dynamic, literal specifier)
64
+ /\bimport\s*\(\s*["'](?!node:)[^"']+["']/,
65
+ ];
66
+
67
+ /**
68
+ * Replace the shared-module imports with their values and reject anything
69
+ * else that would leave a dangling specifier in `_compress.mjs`.
70
+ *
71
+ * Exported separately from `generateCompressModule` so both branches can be
72
+ * driven with fixtures: the source it normally compiles is fixed, so a test
73
+ * of the generator alone can only ever exercise whichever branch that source
74
+ * happens to take today.
75
+ *
76
+ * @internal Exported for testing.
77
+ */
78
+ export function inlineCompressImports(code: string): string {
79
+ let out = code;
80
+ for (const { pattern, replacement } of INLINED_IMPORTS) {
81
+ out = out.replace(pattern, replacement);
82
+ }
83
+ const surviving = MODULE_SPECIFIERS.map((pattern) => pattern.exec(out)).find(Boolean);
84
+ if (surviving) {
85
+ throw new Error(
86
+ `[timber] server/compress.ts is emitted as a standalone _compress.mjs (type-stripped, ` +
87
+ `not bundled), so it cannot import anything at runtime. Found: ${surviving[0].trim()}\n` +
88
+ ` Either inline the value in adapters/compress-module.ts (see INLINED_IMPORTS) or ` +
89
+ `keep the dependency out of server/compress.ts.`
90
+ );
91
+ }
92
+ return out;
93
+ }
16
94
 
17
95
  /**
18
96
  * Compile server/compress.ts into a standalone ESM module.
@@ -31,6 +109,6 @@ export async function generateCompressModule(): Promise<string> {
31
109
  return (
32
110
  '// Generated by @timber-js/app — compiled from server/compress.ts.\n' +
33
111
  '// Do not edit — this file is regenerated on each build.\n' +
34
- result.code
112
+ inlineCompressImports(result.code)
35
113
  );
36
114
  }
@@ -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
+ }