@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
@@ -12,14 +12,8 @@ import { existsSync } from 'node:fs';
12
12
  import { join, relative } from 'node:path';
13
13
  import type { RouteTree, SegmentNode } from './types.js';
14
14
  import type { ParamEntry, RouteEntry } from './codegen-types.js';
15
- import {
16
- buildCodecChainType,
17
- buildSchemaParamType,
18
- emitResolveSegmentFieldHelper,
19
- formatSearchParamsType,
20
- } from './codegen-shared.js';
15
+ import { buildSchemaParamType } from './codegen-shared.js';
21
16
  import { effectiveUrlSegment, type UrlSegmentIdentity } from './segment-classify.js';
22
- import { fileHasExport } from './export-detect.js';
23
17
  import { formatLinkCatchAllOverloads, formatTypedLinkOverloads } from './link-codegen.js';
24
18
 
25
19
  /** Options for route map generation. */
@@ -50,7 +44,7 @@ export function generateRouteMap(tree: RouteTree, options: CodegenOptions = {}):
50
44
  const hasSchema = options.hasSchema ?? detectSchema(options.appDir);
51
45
 
52
46
  const routes: RouteEntry[] = [];
53
- collectRoutes(tree.root, [], [], '', routes, hasSchema, false);
47
+ collectRoutes(tree.root, [], '', routes, hasSchema, false);
54
48
 
55
49
  // Sort routes alphabetically for deterministic output
56
50
  routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
@@ -81,7 +75,6 @@ function detectSchema(appDir: string | undefined): boolean {
81
75
  function collectRoutes(
82
76
  node: SegmentNode,
83
77
  ancestorParams: ParamEntry[],
84
- ancestorParamsFiles: string[],
85
78
  parentTreePath: string,
86
79
  routes: RouteEntry[],
87
80
  hasSchema: boolean,
@@ -91,23 +84,7 @@ function collectRoutes(
91
84
  // Build the tree path (includes groups, slots — uniquely identifies this segment).
92
85
  // Root node has empty segmentName, producing treePath = ''.
93
86
  const treePath = node.segmentName ? `${parentTreePath}/${node.segmentName}` : parentTreePath;
94
- // TIM-834: Identify this segment's own params.ts (if it has a
95
- // segmentParams export). The full chain of params.ts files in the
96
- // route ancestry is threaded down via `ancestorParamsFiles`; codec
97
- // resolution for each ParamEntry is deferred until leaf time so that
98
- // descendant params.ts files can override ancestor codecs (closest-
99
- // to-leaf wins, matching the runtime semantics of
100
- // coerceSegmentParams which walks segments top-down and overwrites
101
- // earlier coercions).
102
- const ownParamsFile =
103
- node.params && fileHasExport(node.params.filePath, 'segmentParams')
104
- ? node.params.filePath
105
- : undefined;
106
-
107
- // Accumulate params from this segment. We attach `codecFilePaths`
108
- // later (at leaf time) using the FULL chain so descendant overrides
109
- // are visible. The legacy layout/page fallback is recorded now
110
- // because it is per-segment (and does not participate in inheritance).
87
+ // Accumulate params from this segment.
111
88
  //
112
89
  // The segment is read through `effectiveUrlSegment` for the same reason the
113
90
  // runtime coercer is: an intercepting node's own `segmentType` is
@@ -120,72 +97,44 @@ function collectRoutes(
120
97
  const urlSegment = effectiveUrlSegment(node);
121
98
  const params = [...ancestorParams];
122
99
  if (urlSegment.paramName) {
123
- const legacyFallback = ownParamsFile ? undefined : findLegacyParamsExport(node);
124
100
  params.push({
125
101
  name: urlSegment.paramName,
126
102
  bracketName: bracketNameForSegment(urlSegment),
127
103
  type: paramTypeForSegment(urlSegment.segmentType),
128
- // Codec chain populated at leaf time. We carry the per-segment
129
- // legacy fallback (if any) so leaf-time resolution can fall back
130
- // to it when no params.ts in the chain declares this key.
131
- legacyCodecFilePath: legacyFallback,
132
104
  });
133
105
  }
134
106
 
135
- // Extend the chain for descendants of this segment.
136
- const nextAncestorFiles = ownParamsFile
137
- ? [...ancestorParamsFiles, ownParamsFile]
138
- : ancestorParamsFiles;
139
-
140
107
  // Check if this segment is a leaf route (has page or route file)
141
108
  const isPage = !!node.page;
142
109
  const isApiRoute = !!node.route;
143
110
 
144
- if (isPage || isApiRoute) {
145
- // TIM-834 P1 fix: at LEAF time, the full chain of params.ts files
146
- // (root-to-leaf) is known. Resolve every ParamEntry's
147
- // `codecFilePaths` to the chain in LEAF-FIRST order so the
148
- // closest-to-leaf entry is checked first — matching runtime
149
- // closest-wins semantics. The chain is shared by all params in the
150
- // route, so we compute it once.
151
- const leafFirstChain = nextAncestorFiles.length > 0 ? [...nextAncestorFiles].reverse() : [];
152
- const resolvedParams: ParamEntry[] = params.map((p) => {
153
- const codecFilePaths =
154
- leafFirstChain.length > 0
155
- ? leafFirstChain
156
- : p.legacyCodecFilePath
157
- ? [p.legacyCodecFilePath]
158
- : undefined;
159
- return {
160
- name: p.name,
161
- bracketName: p.bracketName,
162
- type: p.type,
163
- codecFilePaths,
164
- };
165
- });
111
+ // A segment that hosts code but is not itself addressable — a layout,
112
+ // middleware, or access check above the leaf — still gets a `$segment`
113
+ // module, so `getSegmentParams(SEGMENT_PATH)` is callable from it and needs
114
+ // a matching typed overload. Emitting only leaves left those call sites on
115
+ // the broad `Record<string, string | string[]>` signature, which since
116
+ // TIM-1342 (which deleted `defineSegmentParams`) is their ONLY typed path.
117
+ // design/41-global-params.md's Type Summary promises layouts and middleware
118
+ // exact accumulated params.
119
+ //
120
+ // `isRoutable: false` keeps them out of the urlPath-keyed emissions — the
121
+ // `Routes` interface and `<Link>` variants — where they would publish a URL
122
+ // that no request can reach. They belong only to the segmentPath-keyed
123
+ // APIs: `getSegmentParams`, `useSegmentParams`, `TimberSegmentParams`.
124
+ const hostsCode = !!node.layout || !!node.middleware || !!node.access;
166
125
 
126
+ if (isPage || isApiRoute || hostsCode) {
167
127
  const entry: RouteEntry = {
168
128
  urlPath: node.urlPath,
169
129
  segmentPath: treePath || '/',
170
- params: resolvedParams,
171
- hasSearchParams: false,
130
+ params,
172
131
  isApiRoute,
132
+ isRoutable: isPage || isApiRoute,
173
133
  isSlotRoute: insideSlot,
174
134
  isInterceptingRoute: insideIntercepting,
175
135
  hasSchema,
176
136
  };
177
137
 
178
- // Detect searchParams export from params.ts (primary) or page.tsx (fallback)
179
- if (isPage) {
180
- if (node.params && fileHasExport(node.params.filePath, 'searchParams')) {
181
- entry.hasSearchParams = true;
182
- entry.searchParamsPagePath = node.params.filePath;
183
- } else if (node.page && fileHasExport(node.page.filePath, 'searchParams')) {
184
- entry.hasSearchParams = true;
185
- entry.searchParamsPagePath = node.page.filePath;
186
- }
187
- }
188
-
189
138
  routes.push(entry);
190
139
  }
191
140
 
@@ -195,44 +144,30 @@ function collectRoutes(
195
144
  // alone was not enough.
196
145
  for (const child of node.children) {
197
146
  const childIntercepting = insideIntercepting || child.segmentType === 'intercepting';
198
- collectRoutes(
199
- child,
200
- params,
201
- nextAncestorFiles,
202
- treePath,
203
- routes,
204
- hasSchema,
205
- insideSlot,
206
- childIntercepting
207
- );
147
+ collectRoutes(child, params, treePath, routes, hasSchema, insideSlot, childIntercepting);
208
148
  }
209
149
 
210
150
  // Recurse into slots (they share the parent's URL path, but may have their own pages)
211
151
  for (const slot of Object.values(node.slots)) {
212
- collectRoutes(
213
- slot,
214
- params,
215
- nextAncestorFiles,
216
- treePath,
217
- routes,
218
- hasSchema,
219
- true,
220
- insideIntercepting
221
- );
152
+ collectRoutes(slot, params, treePath, routes, hasSchema, true, insideIntercepting);
222
153
  }
223
154
  }
224
155
 
225
156
  /**
226
157
  * Dedupe route entries by urlPath for urlPath-keyed emissions (the Routes
227
- * interface, useQueryStates overloads, and typed Link variants).
158
+ * interface and typed Link variants).
228
159
  *
229
160
  * Parallel-slot pages share their parent's urlPath; emitting one entry per
230
161
  * slot page produced duplicate interface keys (TS2300/TS2717 under
231
162
  * skipLibCheck:false) and duplicate overloads. The non-slot entry is the
232
163
  * canonical one; when only slot entries exist (parent has no page), the
233
164
  * lexicographically smallest segmentPath wins so output is independent of
234
- * slot scan order. A slot's searchParams definition is carried onto the
235
- * merged entry when the canonical entry has none — never silently dropped.
165
+ * slot scan order.
166
+ *
167
+ * The loser is discarded outright. It used to donate its `searchParams`
168
+ * definition to the winner, because a slot could carry one the parent page
169
+ * lacked; since TIM-1343 no route entry carries a searchParams definition at
170
+ * all, there is nothing left to merge.
236
171
  *
237
172
  * segmentPath-keyed emissions (getSegmentParams, useSegmentParams,
238
173
  * TimberSegmentParams) must NOT use this — segmentPaths are already unique
@@ -247,25 +182,11 @@ function dedupeRoutesByUrlPath(routes: RouteEntry[]): RouteEntry[] {
247
182
  continue;
248
183
  }
249
184
 
250
- let winner = existing;
251
- let loser = route;
252
185
  const routeWins =
253
186
  (existing.isSlotRoute && !route.isSlotRoute) ||
254
187
  (!!existing.isSlotRoute === !!route.isSlotRoute &&
255
188
  route.segmentPath.localeCompare(existing.segmentPath) < 0);
256
- if (routeWins) {
257
- winner = route;
258
- loser = existing;
259
- }
260
-
261
- if (!winner.hasSearchParams && loser.hasSearchParams) {
262
- winner = {
263
- ...winner,
264
- hasSearchParams: true,
265
- searchParamsPagePath: loser.searchParamsPagePath,
266
- };
267
- }
268
- byPath.set(route.urlPath, winner);
189
+ byPath.set(route.urlPath, routeWins ? route : existing);
269
190
  }
270
191
  return [...byPath.values()];
271
192
  }
@@ -296,23 +217,6 @@ function paramTypeForSegment(segmentType: string): ParamEntry['type'] {
296
217
  }
297
218
  }
298
219
 
299
- /**
300
- * Find a legacy `segmentParams` export on layout.tsx or page.tsx.
301
- *
302
- * Backward-compat shim: TIM-508 made params.ts the canonical location
303
- * for `segmentParams`. Layout/page exports are still accepted for the
304
- * OWN segment only (not inherited by descendants — see TIM-834).
305
- */
306
- function findLegacyParamsExport(node: SegmentNode): string | undefined {
307
- if (node.layout && fileHasExport(node.layout.filePath, 'segmentParams')) {
308
- return node.layout.filePath;
309
- }
310
- if (node.page && fileHasExport(node.page.filePath, 'segmentParams')) {
311
- return node.page.filePath;
312
- }
313
- return undefined;
314
- }
315
-
316
220
  /**
317
221
  * Emit the schema-based type exports: TimberSegmentParams, TimberRoute,
318
222
  * AllSegmentParams, RoutesWithParams.
@@ -436,14 +340,6 @@ function formatDeclarationFile(
436
340
  lines.push('');
437
341
  }
438
342
 
439
- if (!hasSchema) {
440
- // TIM-834 P2: emit the shared codec-resolution helper type ONCE so the
441
- // per-param chain conditionals reference it instead of inlining the
442
- // fallback in both branches (which grows O(2^N) in chain depth).
443
- lines.push(emitResolveSegmentFieldHelper());
444
- lines.push('');
445
- }
446
-
447
343
  // urlPath-keyed emissions use the deduped list (slot pages share their
448
344
  // parent's urlPath); segmentPath-keyed emissions below keep all entries.
449
345
  //
@@ -451,18 +347,23 @@ function formatDeclarationFile(
451
347
  // computed urlPath is not a route any request can reach, so a path that
452
348
  // happens NOT to collide with a real one would otherwise be published as a
453
349
  // real route. See `RouteEntry.isInterceptingRoute`.
454
- const uniqueByUrlPath = dedupeRoutesByUrlPath(routes.filter((r) => !r.isInterceptingRoute));
350
+ const uniqueByUrlPath = dedupeRoutesByUrlPath(
351
+ routes.filter((r) => r.isRoutable && !r.isInterceptingRoute)
352
+ );
455
353
 
456
354
  lines.push("declare module '@timber-js/app' {");
457
355
  lines.push(' interface Routes {');
458
356
 
357
+ // TIM-1343: no `searchParams` member. With `params.ts` gone there is no
358
+ // convention file to read a definition from, so the framework has nothing
359
+ // to type it with — and an always-`{}` member is a claim that every route
360
+ // has no search params, which is false. Search-param types now come from
361
+ // the definition object itself, wherever the app chooses to keep it.
459
362
  for (const route of uniqueByUrlPath) {
460
- const paramsType = formatParamsType(route.params, importBase, hasSchema);
461
- const searchParamsType = formatSearchParamsType(route, importBase);
363
+ const paramsType = formatParamsType(route.params, hasSchema);
462
364
 
463
365
  lines.push(` '${route.urlPath}': {`);
464
366
  lines.push(` segmentParams: ${paramsType}`);
465
- lines.push(` searchParams: ${searchParamsType}`);
466
367
  lines.push(` }`);
467
368
  }
468
369
 
@@ -493,7 +394,7 @@ function formatDeclarationFile(
493
394
  lines.push(" import type { AllSegmentParams } from '@timber-js/app'");
494
395
  }
495
396
  for (const route of dynamicRoutes) {
496
- const paramsType = formatParamsType(route.params, importBase, hasSchema);
397
+ const paramsType = formatParamsType(route.params, hasSchema);
497
398
  lines.push(
498
399
  ` export function getSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`
499
400
  );
@@ -511,9 +412,7 @@ function formatDeclarationFile(
511
412
 
512
413
  if (dynamicRoutes.length > 0 || pageRoutes.length > 0) {
513
414
  lines.push("declare module '@timber-js/app/client' {");
514
- lines.push(
515
- " import type { SearchParamsDefinition, SetParams, QueryStatesOptions, SearchParamCodec } from '@timber-js/app/search-params'"
516
- );
415
+ lines.push(" import type { SearchParamsDefinition } from '@timber-js/app/search-params'");
517
416
  if (hasSchema) {
518
417
  lines.push(" import type { AllSegmentParams } from '@timber-js/app'");
519
418
  }
@@ -522,7 +421,7 @@ function formatDeclarationFile(
522
421
  // useSegmentParams overloads
523
422
  if (dynamicRoutes.length > 0) {
524
423
  for (const route of dynamicRoutes) {
525
- const paramsType = formatParamsType(route.params, importBase, hasSchema);
424
+ const paramsType = formatParamsType(route.params, hasSchema);
526
425
  lines.push(
527
426
  ` export function useSegmentParams(segmentPath: '${route.segmentPath}'): ${paramsType}`
528
427
  );
@@ -535,12 +434,6 @@ function formatDeclarationFile(
535
434
  lines.push('');
536
435
  }
537
436
 
538
- // useQueryStates overloads
539
- if (pageRoutes.length > 0) {
540
- lines.push(...formatUseQueryStatesOverloads(pageRoutes, importBase));
541
- lines.push('');
542
- }
543
-
544
437
  // Typed Link overloads — per-route with DIRECT types (no conditionals).
545
438
  // Direct types preserve TypeScript's excess property checking.
546
439
  //
@@ -555,7 +448,7 @@ function formatDeclarationFile(
555
448
  // confusing "Type 'string' is not assignable to type 'never'" cascade.
556
449
  if (pageRoutes.length > 0) {
557
450
  lines.push(' // Typed Link overloads — per-route (block 1 / emitted first)');
558
- lines.push(...formatTypedLinkOverloads(pageRoutes, importBase, hasSchema));
451
+ lines.push(...formatTypedLinkOverloads(pageRoutes, hasSchema));
559
452
  lines.push('');
560
453
  }
561
454
 
@@ -580,48 +473,29 @@ function formatDeclarationFile(
580
473
  /**
581
474
  * Format the params type for a route entry.
582
475
  */
583
- function formatParamsType(params: ParamEntry[], importBase?: string, hasSchema?: boolean): string {
476
+ function formatParamsType(params: ParamEntry[], hasSchema?: boolean): string {
584
477
  if (params.length === 0) {
585
478
  return '{}';
586
479
  }
587
480
 
481
+ // Without a schema a URL segment is an uninterpreted string — that IS
482
+ // its type. Per-segment codec chains used to narrow it here; TIM-1342
483
+ // deleted them along with params.ts.
588
484
  const fields = params.map((p) => {
589
- const codecType = hasSchema
590
- ? buildSchemaParamType(p)
591
- : buildCodecChainType(p, importBase, p.type);
485
+ const codecType = hasSchema ? buildSchemaParamType(p) : p.type;
592
486
  return `${p.name}: ${codecType}`;
593
487
  });
594
488
  return `{ ${fields.join('; ')} }`;
595
489
  }
596
490
 
597
- /**
598
- * Generate useQueryStates overloads.
599
- *
600
- * For each page route:
601
- * - Routes with search-params.ts get a typed overload returning the inferred T
602
- * - Routes without search-params.ts get an overload returning [{}, SetParams<{}>]
603
- *
604
- * A fallback overload for standalone codecs (existing API) is emitted last.
605
- */
606
- function formatUseQueryStatesOverloads(routes: RouteEntry[], importBase?: string): string[] {
607
- const lines: string[] = [];
608
-
609
- for (const route of routes) {
610
- const searchParamsType = route.hasSearchParams
611
- ? formatSearchParamsType(route, importBase)
612
- : '{}';
613
- lines.push(
614
- ` export function useQueryStates<R extends '${route.urlPath}'>(route: R, options?: QueryStatesOptions): [${searchParamsType}, SetParams<${searchParamsType}>]`
615
- );
616
- }
617
-
618
- // Fallback: standalone codecs (existing API)
619
- lines.push(
620
- ' export function useQueryStates<T extends Record<string, unknown>>(codecs: { [K in keyof T]: SearchParamCodec<T[K]> }, options?: QueryStatesOptions): [T, SetParams<T>]'
621
- );
622
-
623
- return lines;
624
- }
491
+ // TIM-1341: `useQueryStates` no longer has a route-string form, so the
492
+ // generated `.d.ts` emits no overloads for it. Its source signature —
493
+ // `useQueryStates(codecs, options?)` — is fully generic over the codec map
494
+ // and needs no per-route augmentation. Reaching another route's codecs is
495
+ // done by importing that route's `searchParams` definition from its
496
+ // `params.ts` and calling `definition.useQueryStates()`, which is typed by
497
+ // inference and cannot fail at runtime on an unpopulated registry.
498
+ // See design/23-search-params.md §"Client Access".
625
499
 
626
500
  // Link overload formatters and helpers (`formatTypedLinkOverloads`,
627
501
  // `formatLinkCatchAllOverloads`, `formatLinkParamsType`,
@@ -4,17 +4,25 @@
4
4
  * Runs at scan time (build and dev startup). Each check produces a warning
5
5
  * with the file path, what's wrong, and what to do about it.
6
6
  *
7
- * These are warnings, not errors — they don't block the build. The goal is
8
- * to catch issues that would otherwise produce cryptic runtime behavior
9
- * (silent 404s, empty pages, confusing React errors).
7
+ * Most checks are warnings — they don't block the build. The goal is to catch
8
+ * issues that would otherwise produce cryptic runtime behavior (silent 404s,
9
+ * empty pages, confusing React errors).
10
+ *
11
+ * A check emits `level: 'error'` only when the misconfiguration is *proven*
12
+ * (never inferred from an absence the analysis cannot see through) and fatal
13
+ * to rendering. `plugins/routing.ts` turns those into a thrown build error,
14
+ * so a false positive there refuses a working app — see
15
+ * `classifyClientBoundaryFile` for how that bar is met.
10
16
  *
11
17
  * Design doc: 07-routing.md, 10-error-handling.md
12
18
  */
13
19
 
14
20
  import { existsSync } from 'node:fs';
15
- import type { RouteTree, SegmentNode } from './types.js';
21
+ import type { RouteFile, RouteTree, SegmentNode } from './types.js';
16
22
  import { swallow } from '../server/logger.js';
23
+ import { isMdxFilePath } from '../server/utils/mdx-file.js';
17
24
  import {
25
+ fileDefaultExportIsLocalValue,
18
26
  fileHasAnyExport,
19
27
  fileHasDefaultExport,
20
28
  fileHasDirective,
@@ -61,10 +69,10 @@ export function lintConventions(tree: RouteTree, appDir: string): ConventionWarn
61
69
  // Check 5: page.tsx / layout.tsx without default export
62
70
  checkDefaultExports(tree.root, warnings);
63
71
 
64
- // Check 6: error.tsx / global-error.tsx without 'use client' directive
65
- checkErrorDirectives(tree.root, warnings);
66
- if (tree.globalError && isScriptExtension(tree.globalError.extension)) {
67
- checkFileUseClientDirective(tree.globalError.filePath, 'global-error', warnings);
72
+ // Check 6: error.tsx / 404.tsx / 4xx.tsx / … without 'use client' directive
73
+ checkClientBoundaryFiles(tree.root, warnings);
74
+ if (tree.globalError) {
75
+ checkFileUseClientDirective(tree.globalError, 'global-error', warnings);
68
76
  }
69
77
 
70
78
  return warnings;
@@ -272,54 +280,132 @@ function checkFileDefaultExport(
272
280
  }
273
281
  }
274
282
 
275
- // ─── Check: Error Directive ─────────────────────────────────────────────────
283
+ // ─── Check: Client Boundary Directive ───────────────────────────────────────
276
284
 
277
285
  /**
278
- * Warn when error.tsx or global-error.tsx is missing 'use client'.
286
+ * Check every file the renderer hands to `TimberErrorBoundary` as a component
287
+ * prop: `error.tsx`, `global-error.tsx`, and the status-code family
288
+ * (`404.tsx`, `4xx.tsx`, `503.tsx`, …).
289
+ *
290
+ * `TimberErrorBoundary` is a client component, so a server component passed as
291
+ * `fallbackComponent` is a function crossing the RSC → client boundary. React
292
+ * Flight refuses to serialize it, and because the boundary wraps the segment's
293
+ * *children*, the failure hits every page under that segment — not just the
294
+ * error path. The raw message ("Functions cannot be passed directly to Client
295
+ * Components") names neither the file nor the fix (TIM-1329).
279
296
  *
280
- * React's error boundary API requires client components. Without the directive,
281
- * the error boundary silently fails — the component renders as a server component
282
- * with no error catching. Users discover this only via cryptic runtime errors.
297
+ * MDX status files are exempt: they are server components by design and the
298
+ * renderer pre-renders them into `fallbackElement` instead. See TIM-503 and
299
+ * `server/error-boundary-wrapper.ts`.
300
+ *
301
+ * `.json` status files never reach React at all, and are not in `statusFiles`.
283
302
  */
284
- function checkErrorDirectives(node: SegmentNode, warnings: ConventionWarning[]): void {
285
- if (node.error && isScriptExtension(node.error.extension)) {
286
- checkFileUseClientDirective(node.error.filePath, 'error', warnings);
303
+ function checkClientBoundaryFiles(node: SegmentNode, warnings: ConventionWarning[]): void {
304
+ if (node.error) {
305
+ checkFileUseClientDirective(node.error, 'error', warnings);
306
+ }
307
+
308
+ for (const [key, file] of Object.entries(node.statusFiles ?? {})) {
309
+ checkFileUseClientDirective(file, key, warnings);
287
310
  }
288
311
 
289
312
  for (const child of node.children) {
290
- checkErrorDirectives(child, warnings);
313
+ checkClientBoundaryFiles(child, warnings);
291
314
  }
292
315
  for (const slot of Object.values(node.slots)) {
293
- checkErrorDirectives(slot, warnings);
316
+ checkClientBoundaryFiles(slot, warnings);
294
317
  }
295
318
  }
296
319
 
320
+ /**
321
+ * `'client'` — carries the directive.
322
+ * `'server'` — proven server component: no directive, and the default export is
323
+ * a function or class built by this very module, so no other file's directive
324
+ * can be governing it.
325
+ * `'unknown'` — no directive, but the exported value may come from elsewhere
326
+ * (`export { default } from './client-thing'`), or the file could not be
327
+ * read or parsed. Never escalated to a build error.
328
+ */
329
+ type ClientBoundaryVerdict = 'client' | 'server' | 'unknown';
330
+
331
+ function classifyClientBoundaryFile(filePath: string): ClientBoundaryVerdict {
332
+ if (fileHasDirective(filePath, 'use client')) return 'client';
333
+ if (fileDefaultExportIsLocalValue(filePath)) return 'server';
334
+ return 'unknown';
335
+ }
336
+
337
+ /** Example component name for the fix snippet, keyed by convention name. */
338
+ function exampleComponentName(convention: string): string {
339
+ if (convention === 'global-error') return 'GlobalError';
340
+ if (convention === 'error') return 'ErrorBoundary';
341
+ if (convention === '404') return 'NotFound';
342
+ return 'StatusPage';
343
+ }
344
+
345
+ /**
346
+ * @param file the route file (its extension decides whether the check applies)
347
+ * @param convention the convention name — `error`, `global-error`, `404`, `4xx`, …
348
+ */
297
349
  function checkFileUseClientDirective(
298
- filePath: string,
299
- fileType: string,
350
+ file: RouteFile,
351
+ convention: string,
300
352
  warnings: ConventionWarning[]
301
353
  ): void {
354
+ // MDX/markdown status and error files are server components by design — the
355
+ // renderer pre-renders them into `fallbackElement`, so they never cross the
356
+ // boundary as a function.
357
+ //
358
+ // The exemption is the *complement* of the renderer's own test, using the
359
+ // renderer's own helper: everything `error-boundary-wrapper.ts` and
360
+ // `error-renderer.ts` do not treat as MDX, they pass as a component. An
361
+ // independent "is this a script extension?" list here would be a second
362
+ // expression of that fact, free to drift into a gap where a file is a
363
+ // component to the renderer and exempt to the linter.
364
+ if (isMdxFilePath(file.filePath)) return;
365
+
366
+ const filePath = file.filePath;
367
+ const label = `${convention}.${file.extension}`;
368
+ let verdict: ClientBoundaryVerdict;
302
369
  try {
303
- if (!fileHasDirective(filePath, 'use client')) {
304
- warnings.push({
305
- id: `ERROR_NO_USE_CLIENT:${filePath}`,
306
- summary: `${fileType}.tsx is missing 'use client': ${filePath}`,
307
- details:
308
- ` File: ${filePath}\n\n` +
309
- ` ${fileType}.tsx must start with 'use client' because React error\n` +
310
- ' boundaries require client components. Without it, the error boundary\n' +
311
- ' silently fails and errors are uncaught.\n\n' +
312
- " To fix: Add 'use client' as the first line of the file:\n\n" +
313
- " 'use client';\n\n" +
314
- ` export default function ${fileType === 'global-error' ? 'GlobalError' : 'ErrorBoundary'}({ error, reset }) {\n` +
315
- ' return <div><h2>Something went wrong</h2><button onClick={reset}>Try again</button></div>;\n' +
316
- ' }\n',
317
- level: 'warn',
318
- });
319
- }
370
+ verdict = classifyClientBoundaryFile(filePath);
320
371
  } catch (err) {
321
- swallow(err, `convention-lint: unreadable ${fileType} file ${filePath}`);
372
+ swallow(err, `convention-lint: unreadable ${label} file ${filePath}`);
373
+ return;
322
374
  }
375
+ if (verdict === 'client') return;
376
+
377
+ const isError = verdict === 'server';
378
+ const name = exampleComponentName(convention);
379
+ const props =
380
+ convention === 'error' || convention === 'global-error' ? '{ error, reset }' : '{ status }';
381
+
382
+ warnings.push({
383
+ id: `NEEDS_USE_CLIENT:${filePath}`,
384
+ summary: isError
385
+ ? `${label} is a server component and must be a client component: ${filePath}`
386
+ : `${label} may be missing 'use client': ${filePath}`,
387
+ details:
388
+ ` File: ${filePath}\n\n` +
389
+ ` ${label} is handed to timber's error boundary — a client\n` +
390
+ ' component — as a prop. A server component cannot cross that boundary:\n' +
391
+ ' React fails with "Functions cannot be passed directly to Client\n' +
392
+ ' Components", and every page under this segment fails to render.\n\n' +
393
+ (isError
394
+ ? " To fix: Add 'use client' as the first line of the file:\n\n" +
395
+ " 'use client';\n\n" +
396
+ ` export default function ${name}(${props}) {\n` +
397
+ ' return <h1>…</h1>;\n' +
398
+ ' }\n'
399
+ : // The default export comes from somewhere this analysis cannot follow
400
+ // — a re-export, an imported binding, a wrapper call. It may well
401
+ // already be a client component, so say what was checked rather than
402
+ // assert a break.
403
+ " This file has no 'use client' directive, and its default export\n" +
404
+ ' comes from elsewhere, so timber cannot tell which it is. If the\n' +
405
+ " module it re-exports starts with 'use client', nothing is wrong\n" +
406
+ ' and you can ignore this. Otherwise add the directive there.\n'),
407
+ level: isError ? 'error' : 'warn',
408
+ });
323
409
  }
324
410
 
325
411
  // ─── Check: App Directory Exists ────────────────────────────────────────────
@@ -352,14 +438,27 @@ import { styleText } from 'node:util';
352
438
 
353
439
  type Format = InspectColor | readonly InspectColor[];
354
440
  const noValidate = { validateStream: false } as const;
355
- const style = (fmt: Format, text: string) => styleText(fmt, text, noValidate);
441
+
442
+ export interface FormatOptions {
443
+ /**
444
+ * Emit ANSI escapes. Off when the text becomes an `Error` message —
445
+ * whoever prints it decides its formatting, and colored escapes inside
446
+ * an exception leak into logs and stack traces.
447
+ */
448
+ color?: boolean;
449
+ }
356
450
 
357
451
  /**
358
452
  * Format warnings for terminal output.
359
453
  *
360
454
  * Groups by severity, uses colors, and includes fix suggestions.
361
455
  */
362
- export function formatConventionWarnings(warnings: ConventionWarning[]): string {
456
+ export function formatConventionWarnings(
457
+ warnings: ConventionWarning[],
458
+ options: FormatOptions = {}
459
+ ): string {
460
+ const { color = true } = options;
461
+ const style = (fmt: Format, text: string) => (color ? styleText(fmt, text, noValidate) : text);
363
462
  if (warnings.length === 0) return '';
364
463
 
365
464
  const errors = warnings.filter((w) => w.level === 'error');