@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/src/cli.ts CHANGED
@@ -11,28 +11,49 @@
11
11
  // timber preview — Serve the production build
12
12
  // timber check — Validate types + routes without building
13
13
 
14
+ // Type-only — the implementation is imported dynamically inside runCheck so the
15
+ // CLI's other commands never load the route scanner.
16
+ import type { CheckDeps } from './cli-check.js';
17
+
18
+ export type { CheckDeps };
19
+
14
20
  const COMMANDS = ['dev', 'build', 'preview', 'check', 'schema', 'init'] as const;
15
21
  type Command = (typeof COMMANDS)[number];
16
22
 
17
23
  export interface ParsedArgs {
18
24
  command: Command;
19
25
  config: string | undefined;
26
+ /** `--project`: the tsconfig `timber check` passes to tsgo. */
27
+ project: string | undefined;
20
28
  /** Positional arguments after the command (used by `timber schema sync`). */
21
29
  rest: string[];
22
30
  }
23
31
 
24
32
  export interface CommandOptions {
33
+ /** Path to a Vite config file. Means the same thing for every command. */
25
34
  config?: string;
35
+ /** `timber check` only: the tsconfig to type-check. */
36
+ project?: string;
26
37
  }
27
38
 
28
39
  /**
29
40
  * Parse CLI arguments into a structured command + options.
30
- * Accepts: timber <command> [--config|-c <path>]
41
+ * Accepts: timber <command> [--config|-c <path>] [--project <path>]
42
+ *
43
+ * `--config` names a **Vite** config file for every command, `check` included
44
+ * (TIM-1332): `check` resolves the project through Vite, so a project that
45
+ * builds with `--config ./custom.vite.config.ts` has to be checkable the same
46
+ * way. `--project` names the tsconfig, spelled as tsgo and tsc spell it, and
47
+ * is meaningful only for `check`.
48
+ *
49
+ * Before TIM-1332 `--config` meant the tsconfig for `check` alone — one flag
50
+ * with two meanings, which the docs had to carry a note to explain and which
51
+ * left custom Vite configs unreachable from `check` entirely.
31
52
  *
32
53
  * Unknown flags are an error, not silently ignored — `timber dev --port 4000`
33
- * starting on the default port would contradict fail-loudly. The CLI surface
34
- * is intentionally --config-only (design/11-platform.md §CLI); port selection
35
- * goes through the PORT env var or server.port in vite.config.ts.
54
+ * starting on the default port would contradict fail-loudly. The surface stays
55
+ * deliberately small (design/11-platform.md §CLI); port selection goes through
56
+ * the PORT env var or server.port in vite.config.ts.
36
57
  */
37
58
  export function parseArgs(args: string[]): ParsedArgs {
38
59
  if (args.length === 0) {
@@ -47,6 +68,7 @@ export function parseArgs(args: string[]): ParsedArgs {
47
68
  }
48
69
 
49
70
  let config: string | undefined;
71
+ let project: string | undefined;
50
72
  const rest: string[] = [];
51
73
  for (let i = 1; i < args.length; i++) {
52
74
  const arg = args[i];
@@ -55,6 +77,17 @@ export function parseArgs(args: string[]): ParsedArgs {
55
77
  if (!config) {
56
78
  throw new Error('--config requires a path argument');
57
79
  }
80
+ } else if (arg === '--project') {
81
+ if (command !== 'check') {
82
+ throw new Error(
83
+ `--project is only valid for \`timber check\` (it names the tsconfig to type-check). ` +
84
+ `To point \`timber ${command}\` at a different Vite config, use --config <path>.`
85
+ );
86
+ }
87
+ project = args[++i];
88
+ if (!project) {
89
+ throw new Error('--project requires a path argument');
90
+ }
58
91
  } else if (arg.startsWith('-')) {
59
92
  const portHint =
60
93
  arg === '--port' || arg === '-p'
@@ -62,7 +95,8 @@ export function parseArgs(args: string[]): ParsedArgs {
62
95
  'or server.port in vite.config.ts.'
63
96
  : '';
64
97
  throw new Error(
65
- `Unknown option: ${arg}. The timber CLI only accepts --config <path>.${portHint}`
98
+ `Unknown option: ${arg}. The timber CLI accepts --config <path>` +
99
+ ` (and --project <path> for \`check\`).${portHint}`
66
100
  );
67
101
  } else if (command === 'schema' || command === 'init') {
68
102
  rest.push(arg);
@@ -71,7 +105,7 @@ export function parseArgs(args: string[]): ParsedArgs {
71
105
  }
72
106
  }
73
107
 
74
- return { command: command as Command, config, rest };
108
+ return { command: command as Command, config, project, rest };
75
109
  }
76
110
 
77
111
  // ─── Command Implementations ─────────────────────────────────────────────────
@@ -160,24 +194,25 @@ export async function runPreview(options: CommandOptions, _deps?: ViteDeps): Pro
160
194
  }
161
195
 
162
196
  /**
163
- * Validate types and routes without producing build output.
164
- * Runs tsgo --noEmit for type checking.
197
+ * Validate routes and types without producing build output.
198
+ *
199
+ * Runs the route pass first — resolve the project through Vite, scan it, fail
200
+ * on proven-fatal convention errors, print warnings, and regenerate the route
201
+ * types — then `tsgo --noEmit`. Codegen has to precede the type pass: the
202
+ * generated `.timber/*.d.ts` files are what type `<Link href>` and
203
+ * `useSegmentParams()`, and a fresh clone has never produced them. See
204
+ * `cli-check.ts` for the full reasoning.
205
+ *
206
+ * `--config` names the **Vite** config, as it does for every other command, so
207
+ * an app that builds with a custom one is checkable the same way.
208
+ * `--project` names the tsconfig handed to tsgo.
165
209
  */
166
- export async function runCheck(options: CommandOptions): Promise<void> {
167
- const { execFile } = await import('node:child_process');
168
-
169
- await new Promise<void>((resolve, reject) => {
170
- const configArgs = options.config ? ['--project', options.config] : [];
171
- execFile('tsgo', ['--noEmit', ...configArgs], (err, stdout, stderr) => {
172
- if (stdout) process.stdout.write(stdout);
173
- if (stderr) process.stderr.write(stderr);
174
- if (err) {
175
- reject(new Error(`Type check failed with exit code ${err.code}`));
176
- } else {
177
- resolve();
178
- }
179
- });
180
- });
210
+ export async function runCheck(options: CommandOptions, _deps?: CheckDeps): Promise<void> {
211
+ const { checkRoutes, runTypeCheck } = await import('./cli-check.js');
212
+ const { root } = await checkRoutes({ viteConfigFile: options.config }, _deps);
213
+ // tsgo runs from the root the route pass resolved, not from cwd — that is
214
+ // where it just wrote the declarations tsgo has to read.
215
+ await runTypeCheck({ root, config: options.project }, _deps);
181
216
  }
182
217
 
183
218
  // ─── Schema Subcommand ───────────────────────────────────────────────────────
@@ -305,7 +340,7 @@ export async function runInit(): Promise<void> {
305
340
 
306
341
  async function main(): Promise<void> {
307
342
  const parsed = parseArgs(process.argv.slice(2));
308
- const options: CommandOptions = { config: parsed.config };
343
+ const options: CommandOptions = { config: parsed.config, project: parsed.project };
309
344
 
310
345
  switch (parsed.command) {
311
346
  case 'dev':
@@ -19,6 +19,7 @@ import { getRouter, getRouterOrNull } from '#client-internal';
19
19
  import { setHardNavigating } from '../navigation-root.js';
20
20
  import { getClientDeploymentId, DEPLOYMENT_ID_HEADER, RELOAD_HEADER } from '../rsc-fetch.js';
21
21
  import { markClientStale } from '../stale-client.js';
22
+ import { RSC_CONTENT_TYPE } from '../../shared/rsc-media-type.js';
22
23
  export function setupServerActions(): void {
23
24
  setServerCallback(async (id: string, args: unknown[]) => {
24
25
  const body = await encodeReply(args);
@@ -32,7 +33,7 @@ export function setupServerActions(): void {
32
33
  // Build action request headers. Include deployment ID for version
33
34
  // skew detection (TIM-446) — the server rejects stale actions gracefully.
34
35
  const actionHeaders: Record<string, string> = {
35
- 'Accept': 'text/x-component',
36
+ 'Accept': RSC_CONTENT_TYPE,
36
37
  'x-rsc-action': id,
37
38
  };
38
39
  const actionDeploymentId = getClientDeploymentId();
@@ -28,11 +28,6 @@
28
28
 
29
29
  // @ts-expect-error — virtual module provided by timber-entries plugin
30
30
  import config from 'virtual:timber-config';
31
- // TIM-830: Populate the search-params registry eagerly so <Link> on the
32
- // client can serialize flat `Partial<T>` values synchronously during
33
- // render and click handlers. Side-effect-only import.
34
- // @ts-expect-error — virtual module provided by timber-routing plugin
35
- import 'virtual:timber-search-params-registry';
36
31
  // TIM-932: Populate the codec registry so Link's interpolateParams can
37
32
  // use codec.serialize() for URL construction. Side-effect-only import.
38
33
  // @ts-expect-error — virtual module provided by timber-routing plugin
@@ -35,6 +35,12 @@ interface DenyDigest {
35
35
  type: 'deny';
36
36
  status: number;
37
37
  data: unknown;
38
+ /**
39
+ * Segment key of the segment that owns the matched deny page, present when
40
+ * an in-tree catch site matched a file it may not render at its own
41
+ * position and re-threw to reach the owner. See TIM-1356.
42
+ */
43
+ ownerKey?: string;
38
44
  }
39
45
 
40
46
  /** Structured digest returned by RSC onError for RenderError. */
@@ -78,6 +84,12 @@ export interface TimberErrorBoundaryProps {
78
84
  * graceful degradation — they must NOT change the HTTP status code.
79
85
  */
80
86
  isSlotBoundary?: boolean;
87
+ /**
88
+ * Segment key of the segment that declared this boundary. Only meaningful
89
+ * against a deny digest carrying an `ownerKey`. See the decline rule in
90
+ * `render()` and TIM-1356.
91
+ */
92
+ segmentKey?: string;
81
93
  children: ReactNode;
82
94
  }
83
95
 
@@ -209,6 +221,34 @@ export class TimberErrorBoundary extends Component<
209
221
  return null;
210
222
  }
211
223
 
224
+ // Owner-addressed denies: an in-tree catch site already resolved the
225
+ // fallback chain and matched a file it may not render at its own
226
+ // position, so it re-threw to reach the segment that owns that file.
227
+ // Every boundary but that one declines — otherwise the nearest boundary
228
+ // whose status filter happens to match catches first and renders the
229
+ // deny page at its own depth, wrapped in the layouts the hoist existed
230
+ // to skip, and from a different file than the chain chose. (Boundary
231
+ // nesting is per-segment `{status} → category → error`; the 4xx chain
232
+ // runs status files across ALL segments before any error.tsx, so a
233
+ // leaf error.tsx would otherwise beat a root 404.tsx.)
234
+ //
235
+ // Declining is the default: an unkeyed boundary — a slot boundary, or
236
+ // one from a construction site that builds no deny chains — never
237
+ // claims a deny addressed to a segment. See TIM-1356.
238
+ // `typeof === 'string'`, not a null check: the digest is untrusted JSON —
239
+ // `parseDigest` validates only `type`, and any component can set
240
+ // `error.digest` itself. A non-string `ownerKey` is unequal to every
241
+ // boundary's key, so a truthiness test would let one error decline its
242
+ // way past the entire tree and blank the page, where it would otherwise
243
+ // have hit the nearest matching boundary.
244
+ if (
245
+ parsed?.type === 'deny' &&
246
+ typeof parsed.ownerKey === 'string' &&
247
+ parsed.ownerKey !== this.props.segmentKey
248
+ ) {
249
+ throw error;
250
+ }
251
+
212
252
  // If this boundary has a status filter, check whether the error matches.
213
253
  // Non-matching errors re-throw so an outer boundary can catch them.
214
254
  if (this.props.status != null) {
@@ -272,7 +312,7 @@ export class TimberErrorBoundary extends Component<
272
312
  });
273
313
  }
274
314
  return createElement(this.props.fallbackComponent as never, {
275
- error,
315
+ error: denyError(parsed.status, error),
276
316
  digest: null,
277
317
  reset: this.reset,
278
318
  status: parsed.status,
@@ -312,6 +352,30 @@ function parseDigest(error: Error): ParsedDigest | null {
312
352
  return null;
313
353
  }
314
354
 
355
+ /**
356
+ * The Error an `error.tsx` serving a deny should receive.
357
+ *
358
+ * The error that actually arrives here is React's, not the framework's: a
359
+ * server-thrown error crossing Flight has its message and name replaced with
360
+ * a generic redacted string in production, by design. An `error.tsx` written
361
+ * per the docs branches on `error.message` — so handing it the redacted error
362
+ * gives it nothing to branch on, while the in-tree path hands the same file a
363
+ * `DenySignal` named error reading `Access denied with status <N>` (see
364
+ * `renderDenyEntry`). This reconstructs that shape from the digest, which is
365
+ * where the status already is.
366
+ *
367
+ * Derived from the status alone — no stack, no server data — so it is safe on
368
+ * the client for the same reason the digest is. See design/13-security.md
369
+ * §"Errors don't leak", TIM-1081.
370
+ */
371
+ function denyError(status: number, original: Error): Error {
372
+ // A dev build may have preserved something more useful than the redaction.
373
+ if (original.name === 'DenySignal') return original;
374
+ const error = new Error(`Access denied with status ${status}`);
375
+ error.name = 'DenySignal';
376
+ return error;
377
+ }
378
+
315
379
  /**
316
380
  * Extract the HTTP status code from a parsed digest or error message.
317
381
  * Falls back to message pattern matching for errors without a digest.
@@ -75,11 +75,22 @@ export { useSelectedLayoutSegment, useSelectedLayoutSegments } from './use-selec
75
75
  export { useActionState, useFormAction } from './form';
76
76
  export type { UseActionStateFn, UseActionStateReturn, FormErrorsResult } from './form';
77
77
 
78
- // Params — raw hook for untyped access. For typed params, use defineSegmentParams().useSegmentParams().
78
+ // Params. Called with no argument this returns the untyped accumulated params;
79
+ // pass the route's SEGMENT_PATH (from its generated `./$segment` module) to get
80
+ // the exact types `app/schema.ts` declares — `useSegmentParams(SEGMENT_PATH)`.
79
81
  export { useSegmentParams } from './use-segment-params';
80
82
 
81
- // Query states — useQueryStates lives on defineSearchParams().useQueryStates()
82
- // and is available via direct import for advanced cases.
83
+ // Query states. `definition.useQueryStates()` is the usual form; this standalone
84
+ // export is the inline codec-map form, for shared param bundles reused across
85
+ // routes (design/23-search-params.md §"Client Access").
86
+ //
87
+ // Until TIM-1341 this export did not exist, yet four doc sites told users to
88
+ // import it from here. It type-checked only because the generated `.d.ts`
89
+ // augmented this module with a `useQueryStates` overload — so TypeScript
90
+ // believed in a binding that was never emitted, and the documented import
91
+ // failed at build time. Deleting that overload removed the lie; exporting the
92
+ // hook makes the docs true.
93
+ export { useQueryStates } from './use-query-states';
83
94
 
84
95
  // Cookies — useCookie lives on defineCookie().useCookie().
85
96
  // Types still exported for advanced use.
@@ -14,7 +14,8 @@
14
14
  // Typed Link: design/09-typescript.md §"Typed Link"
15
15
  // - href validated against known routes (via codegen overloads, not runtime)
16
16
  // - params prop typed per-route, URL interpolated at runtime
17
- // - searchParams prop serialized via SearchParamsDefinition
17
+ // - searchParams prop is a query string (from `definition.buildSearchParams()`)
18
+ // or a plain object whose values are String()-coerced
18
19
  // - params and fully-resolved string href are mutually exclusive
19
20
  // - searchParams and inline query string are mutually exclusive
20
21
 
@@ -25,8 +26,6 @@ import {
25
26
  type ReactNode,
26
27
  type MouseEvent as ReactMouseEvent,
27
28
  } from 'react';
28
- import type { SearchParamsDefinition } from '../search-params/define.js';
29
- import { getSearchParamsDefinition } from '../search-params/registry.js';
30
29
  import type { LinkFunction } from './index.js';
31
30
  import { classifyUrlSegment, type UrlSegment } from '../routing/segment-classify.js';
32
31
  import {
@@ -131,26 +130,37 @@ export type LinkSegmentParams<T> = {
131
130
 
132
131
  // ─── searchParams prop shapes ────────────────────────────────────
133
132
  //
134
- // Per-route Link overloads (emitted by codegen) pass the flat values shape:
135
- // searchParams={{ page: 2, q: 'boots' }}
136
- // The framework looks up the route's SearchParamsDefinition from the
137
- // search-params registry at runtime (see TIM-830).
133
+ // Two shapes, discriminated at runtime by `typeof === 'string'`:
138
134
  //
139
- // The catch-all overload in client/index.ts (external/computed hrefs)
140
- // additionally accepts the legacy wrapped shape:
141
- // searchParams={{ definition: def, values: { page: 2 } }}
142
- // because there is no way to look up a definition from a computed string.
135
+ // 1. Codec-aware (preferred) — a query string built from a definition:
136
+ // searchParams={productParams.buildSearchParams({ page: 2, search: 'boots' })}
137
+ // `buildSearchParams` applies each codec, honours `withUrlKey` aliases,
138
+ // and omits values equal to their default. It is typed `Partial<T>` at
139
+ // the call site, so the definition supplies the type checking.
143
140
  //
144
- // `resolveHref` discriminates at runtime by presence of a `definition` key.
141
+ // 2. Plain object (escape hatch) — every value is String()-coerced:
142
+ // searchParams={{ ref: 'email' }}
143
+ // No codecs, no aliases. This is for one-off params that have no
144
+ // definition. Passing a *definition's* keys this way is a mistake the
145
+ // types cannot catch: `{ search: 'x' }` emits `?search=x`, where the
146
+ // definition would emit `?q=x`. Prefer (1) whenever a definition exists.
147
+ //
148
+ // Why a STRING and not a URLSearchParams: a Link is routinely rendered by a
149
+ // Server Component, so this prop crosses the RSC Flight boundary before the
150
+ // client Link runs. `URLSearchParams` is iterable, and React serializes any
151
+ // iterable as an array — so the prop arrived as `[['pg','2'], …]`, fell into
152
+ // the plain-object branch, and rendered `?0=pg&0=2`. Strings survive Flight
153
+ // unchanged. (Codex P1 on PR #1021; reproduced against a live dev server.)
154
+ //
155
+ // TIM-1343 removed the third shape — a flat values object resolved against a
156
+ // runtime registry keyed by href. It required scanning a `params.ts`
157
+ // convention file, a generated registry module, and eager imports of every
158
+ // route's definition into all three entries, to save one `.buildSearchParams`
159
+ // call. See design/23-search-params.md §"Link Integration".
145
160
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
146
161
  export type ParamValue = string | number | string[] | { toString(): string; [key: string]: any };
147
162
 
148
- type WrappedSearchParamsProp = {
149
- definition: SearchParamsDefinition<Record<string, unknown>>;
150
- values: Record<string, unknown>;
151
- };
152
- type FlatSearchParamsProp = Record<string, unknown>;
153
- type LinkSearchParamsProp = WrappedSearchParamsProp | FlatSearchParamsProp;
163
+ type LinkSearchParamsProp = string | Record<string, unknown>;
154
164
 
155
165
  /**
156
166
  * Runtime-only loose props used internally by the Link implementation.
@@ -303,16 +313,35 @@ export function interpolateParams(pattern: string, params: Record<string, ParamV
303
313
  *
304
314
  * Handles:
305
315
  * - params interpolation into route patterns
306
- * - searchParams serialization via SearchParamsDefinition
316
+ * - searchParams serialization (see the two shapes documented above)
307
317
  * - Validation that searchParams and inline query strings are exclusive
308
318
  */
309
319
  /**
310
- * Runtime discriminator: treat `searchParams` as the legacy wrapped shape
311
- * only when it literally has a `definition` key. Everything else is the
312
- * flat `Partial<T>` values shape (TIM-830).
320
+ * Tolerate a leading '?'. `buildSearchParams()` never emits one, but callers
321
+ * hand-rolling a query string reasonably might, and silently producing
322
+ * `?%3Fa=b` for it would be a worse failure than accepting both.
323
+ */
324
+ function stripLeadingQuestionMark(qs: string): string {
325
+ return qs.startsWith('?') ? qs.slice(1) : qs;
326
+ }
327
+
328
+ /**
329
+ * Escape-hatch serialization for a plain object: String()-coerce every
330
+ * value, append arrays as repeated keys, skip null/undefined.
331
+ *
332
+ * Deliberately codec-free and alias-free — see the shape docs above.
313
333
  */
314
- function isWrappedSearchParamsProp(sp: LinkSearchParamsProp): sp is WrappedSearchParamsProp {
315
- return 'definition' in sp;
334
+ function coerceToQueryString(values: Record<string, unknown>): string {
335
+ const usp = new URLSearchParams();
336
+ for (const [key, val] of Object.entries(values)) {
337
+ if (val === undefined || val === null) continue;
338
+ if (Array.isArray(val)) {
339
+ for (const item of val) usp.append(key, String(item));
340
+ } else {
341
+ usp.set(key, String(val));
342
+ }
343
+ }
344
+ return usp.toString();
316
345
  }
317
346
 
318
347
  export function resolveHref(
@@ -337,46 +366,18 @@ export function resolveHref(
337
366
  );
338
367
  }
339
368
 
340
- let definition: SearchParamsDefinition<Record<string, unknown>> | undefined;
341
- let values: Record<string, unknown>;
342
-
343
- if (isWrappedSearchParamsProp(searchParams)) {
344
- // Legacy wrapped shape — used by the catch-all overload for
345
- // computed/external hrefs where no route lookup is possible.
346
- definition = searchParams.definition;
347
- values = searchParams.values;
348
- } else {
349
- // Flat shape (TIM-830): look up the definition from the runtime
350
- // registry using the un-interpolated href pattern (e.g. '/products/[id]').
351
- // The search-params registry is populated eagerly at startup by the
352
- // virtual:timber-search-params-registry module generated by the
353
- // timber-routing Vite plugin.
354
- definition = getSearchParamsDefinition(href) as
355
- | SearchParamsDefinition<Record<string, unknown>>
356
- | undefined;
357
- values = searchParams;
358
- }
359
-
360
- if (definition) {
361
- const qs = definition.serialize(values);
362
- if (qs) {
363
- resolvedPath = `${resolvedPath}?${qs}`;
364
- }
365
- } else {
366
- // No registered definition — serialize flat object as plain query params.
367
- const usp = new URLSearchParams();
368
- for (const [key, val] of Object.entries(values)) {
369
- if (val === undefined || val === null) continue;
370
- if (Array.isArray(val)) {
371
- for (const item of val) usp.append(key, String(item));
372
- } else {
373
- usp.set(key, String(val));
374
- }
375
- }
376
- const qs = usp.toString();
377
- if (qs) {
378
- resolvedPath = `${resolvedPath}?${qs}`;
379
- }
369
+ // A string is already a serialized query (from
370
+ // `definition.buildSearchParams()`), so it passes through untouched.
371
+ // `typeof` is the only discriminator that survives the RSC Flight
372
+ // boundary — see the shape docs above for what happened when this was
373
+ // an object test.
374
+ const qs =
375
+ typeof searchParams === 'string'
376
+ ? stripLeadingQuestionMark(searchParams)
377
+ : coerceToQueryString(searchParams);
378
+
379
+ if (qs) {
380
+ resolvedPath = `${resolvedPath}?${qs}`;
380
381
  }
381
382
  }
382
383
 
@@ -38,6 +38,22 @@ export interface NavigationCommitInput {
38
38
  /** When true, clear the segment cache if segmentInfo is absent
39
39
  * (popstate replay for entries without layout metadata). */
40
40
  clearSegmentCacheOnEmpty?: boolean;
41
+ /**
42
+ * HTTP status of the response this navigation rendered, when it came from a
43
+ * fetch. Omitted by replays, which re-publish a tree already judged.
44
+ *
45
+ * An error response rendered a status page, and a status page replaces a
46
+ * subtree: the deny page lands at the segment owning the matched file and
47
+ * nothing below it mounts. `segmentInfo` nonetheless describes the whole
48
+ * chain the payload was built against — it must, because the merge point is
49
+ * chosen out of it — so the cache has to be told not to learn from this one.
50
+ *
51
+ * Enforced here rather than at the call sites because there are three of
52
+ * them (`performNavigationFetch`, `refresh()`, and uncached traversal, the
53
+ * last two through `fetchCommitAndRender`) and only one had the check.
54
+ * See TIM-1356.
55
+ */
56
+ status?: number;
41
57
  }
42
58
 
43
59
  /**
@@ -151,12 +167,19 @@ export function createNavigationCommitter(deps: {
151
167
  prepareNavigation(url: string, opts: NavigationCommitInput): PreparedNavigation {
152
168
  const navState = deriveNavigationState(url);
153
169
 
170
+ // A tree rendered by an error response is not one to cache — see
171
+ // `status` on NavigationCommitInput. Normalized here, once, so no fetch
172
+ // caller can forget it; the history entry records the same thing, or a
173
+ // traversal back to this URL would replay the metadata this drops.
174
+ const cacheable = opts.status === undefined || opts.status < 400;
175
+ const segmentInfo = cacheable ? opts.segmentInfo : [];
176
+
154
177
  return {
155
178
  navState,
156
179
  commit() {
157
- if (opts.segmentInfo && opts.segmentInfo.length > 0) {
158
- updateSegmentCache(opts.segmentInfo);
159
- } else if (opts.segmentInfo?.length === 0 || opts.clearSegmentCacheOnEmpty) {
180
+ if (segmentInfo && segmentInfo.length > 0) {
181
+ updateSegmentCache(segmentInfo);
182
+ } else if (segmentInfo?.length === 0 || opts.clearSegmentCacheOnEmpty) {
160
183
  segmentCache.clear();
161
184
  }
162
185
 
@@ -165,7 +188,7 @@ export function createNavigationCommitter(deps: {
165
188
  historyStack.push(url, {
166
189
  payload: opts.payload,
167
190
  params: opts.params,
168
- segmentInfo: opts.segmentInfo,
191
+ segmentInfo,
169
192
  });
170
193
  },
171
194
  };
@@ -64,10 +64,10 @@ export type ParamsContextValue = PublishedParams;
64
64
  * do it: React's `react-server` export provides neither `createContext` nor
65
65
  * `useContext`, and a *named* ESM import of a missing export fails at module
66
66
  * instantiation — before any feature check could run. This module is reachable
67
- * from `@timber-js/app/segment-params`, which a Server Component imports for
68
- * `defineSegmentParams`, so the named form crashed that entry outright
69
- * (codex, PR #992; reproduced with
70
- * `node --conditions react-server -e "import('./dist/segment-params/index.js')"`).
67
+ * from every entry a Server Component imports, so the named form crashed
68
+ * those entries outright (codex, PR #992; originally reproduced against
69
+ * `@timber-js/app/segment-params`, an entry point since deleted by TIM-1342 —
70
+ * the hazard is unchanged for the entries that remain).
71
71
  *
72
72
  * See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
73
73
  */
@@ -314,6 +314,9 @@ export function createNavigationPipeline({
314
314
  decodePromise: prefetched.decodePromise ?? null,
315
315
  segmentInfo: prefetched.segmentInfo ?? null,
316
316
  skippedSegments: prefetched.skippedSegments ?? null,
317
+ // Entries written before this field carried it are treated as
318
+ // cacheable, which is what they were before a deny could be one.
319
+ status: prefetched.status ?? 200,
317
320
  }
318
321
  : undefined;
319
322
 
@@ -364,6 +367,7 @@ export function createNavigationPipeline({
364
367
  payload: isPartial || hasSkippedSlots ? null : payload,
365
368
  params,
366
369
  segmentInfo: result.segmentInfo,
370
+ status: result.status,
367
371
  });
368
372
 
369
373
  return {
@@ -218,6 +218,7 @@ export function createRouter(deps: RouterDeps): RouterInstance {
218
218
  payload,
219
219
  params,
220
220
  segmentInfo: result.segmentInfo,
221
+ status: result.status,
221
222
  });
222
223
  return { ...result, payload, params, navState, commit };
223
224
  });
@@ -27,6 +27,7 @@ import { normalizeBasePath, stripBasePath, withBasePath } from '../shared/base-p
27
27
  import { rscPayloadPath } from '../shared/rsc-payload-path.js';
28
28
  import type { SegmentInfo, StateTree } from '../shared/segment-info.js';
29
29
  import { TIMBER_ERROR_HEADER } from '../shared/rsc-error-envelope.js';
30
+ import { RSC_CONTENT_TYPE } from '../shared/rsc-media-type.js';
30
31
  import type { RouterDeps } from './router-types.js';
31
32
 
32
33
  // ─── Types ───────────────────────────────────────────────────────
@@ -57,12 +58,17 @@ export interface FetchResult {
57
58
  segmentInfo: SegmentInfo[] | null;
58
59
  /** Segment paths that were skipped by the server (for client-side merging). */
59
60
  skippedSegments: string[] | null;
61
+ /**
62
+ * HTTP status of the navigation response.
63
+ *
64
+ * Read at the commit site to decide whether the tree this payload produced
65
+ * is one worth caching. A deny page replaced a subtree, so `segmentInfo`
66
+ * describes more than actually mounted — but it cannot simply be emptied,
67
+ * because the merge point is chosen out of it. See TIM-1356.
68
+ */
69
+ status: number;
60
70
  }
61
71
 
62
- // ─── Constants ───────────────────────────────────────────────────
63
-
64
- export const RSC_CONTENT_TYPE = 'text/x-component';
65
-
66
72
  // ─── URL Helpers ─────────────────────────────────────────────────
67
73
 
68
74
  /**
@@ -531,6 +537,7 @@ export async function fetchRscPayload(
531
537
  const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });
532
538
  let segmentInfo: SegmentInfo[] | null = null;
533
539
  let skippedSegments: string[] | null = null;
540
+ let status = 200;
534
541
  // Track when the full RSC body stream is consumed (not just shell).
535
542
  // Initialized to resolved for bodyless responses; overwritten when
536
543
  // the response has a body.
@@ -586,6 +593,7 @@ export async function fetchRscPayload(
586
593
  // as React elements — React 19 Float handles them. See TIM-1151.
587
594
  segmentInfo = extractSegmentInfo(response);
588
595
  skippedSegments = extractSkippedSegments(response);
596
+ status = response.status;
589
597
 
590
598
  // Wrap the body to track full stream consumption. createFromFetch's
591
599
  // thenable resolves when the root model (shell) arrives, but we need
@@ -634,6 +642,7 @@ export async function fetchRscPayload(
634
642
  decodePromise,
635
643
  segmentInfo,
636
644
  skippedSegments,
645
+ status,
637
646
  };
638
647
  }
639
648
  // Test/fallback path: return raw text
@@ -677,5 +686,6 @@ export async function fetchRscPayload(
677
686
  decodePromise: null,
678
687
  segmentInfo: extractSegmentInfo(response),
679
688
  skippedSegments: extractSkippedSegments(response),
689
+ status: response.status,
680
690
  };
681
691
  }
@@ -26,6 +26,14 @@ export interface PrefetchResult {
26
26
  params?: PublishedParams | Promise<PublishedParams>;
27
27
  /** Segment paths skipped by the server (for client-side merging). */
28
28
  skippedSegments?: string[] | null;
29
+ /**
30
+ * HTTP status of the prefetched response. A deny payload is an ordinary
31
+ * Flight response carrying a 4xx, so it prefetches and caches like any
32
+ * other — and the tree it renders is one the segment cache must not learn
33
+ * from. Absent on entries that predate the field, and on the negative
34
+ * sentinel. See TIM-1356.
35
+ */
36
+ status?: number;
29
37
  }
30
38
 
31
39
  /**