@timber-js/app 0.2.0-alpha.196 → 0.2.0-alpha.198

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 (289) hide show
  1. package/dist/_chunks/{actions-CWYtq6ii.js → actions-BS-m5SLv.js} +3 -3
  2. package/dist/_chunks/{actions-CWYtq6ii.js.map → actions-BS-m5SLv.js.map} +1 -1
  3. package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
  4. package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
  5. package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
  6. package/dist/_chunks/{cache-api-CQeYzA5g.js → cache-api-DqzgTEqk.js} +4 -49
  7. package/dist/_chunks/cache-api-DqzgTEqk.js.map +1 -0
  8. package/dist/_chunks/{chains-h7EO-u3n.js → chains-CZG7E5zg.js} +2 -2
  9. package/dist/_chunks/{chains-h7EO-u3n.js.map → chains-CZG7E5zg.js.map} +1 -1
  10. package/dist/_chunks/{cli-check-BVthpfLS.js → cli-check-dVDi1GQz.js} +3 -3
  11. package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-dVDi1GQz.js.map} +1 -1
  12. package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js → cli-schema-sync-DTy_-Msq.js} +2 -2
  13. package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js.map → cli-schema-sync-DTy_-Msq.js.map} +1 -1
  14. package/dist/_chunks/{cloudflare-BKJC3SC_.js → cloudflare-BFb__LYG.js} +2 -2
  15. package/dist/_chunks/{cloudflare-BKJC3SC_.js.map → cloudflare-BFb__LYG.js.map} +1 -1
  16. package/dist/_chunks/{convention-lint-DO10_pVl.js → convention-lint-Ph6luW4c.js} +4 -2
  17. package/dist/_chunks/convention-lint-Ph6luW4c.js.map +1 -0
  18. package/dist/_chunks/{error-boundary-D-lkwyaD.js → error-boundary-BvRCCmbN.js} +3 -3
  19. package/dist/_chunks/{error-boundary-D-lkwyaD.js.map → error-boundary-BvRCCmbN.js.map} +1 -1
  20. package/dist/_chunks/{href-validation-CMc5JRls.js → href-validation-BIrxavIy.js} +74 -2
  21. package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
  22. package/dist/_chunks/{live-graph-Bx4HodF1.js → live-graph-BXDsdzBv.js} +3 -3
  23. package/dist/_chunks/{live-graph-Bx4HodF1.js.map → live-graph-BXDsdzBv.js.map} +1 -1
  24. package/dist/_chunks/{logger-pumCm3Il.js → logger-DDirEsn7.js} +3 -4
  25. package/dist/_chunks/{logger-pumCm3Il.js.map → logger-DDirEsn7.js.map} +1 -1
  26. package/dist/_chunks/navigation-root-B00jjGd5.js +233 -0
  27. package/dist/_chunks/navigation-root-B00jjGd5.js.map +1 -0
  28. package/dist/_chunks/{segment-context-CjOlyB8Y.js → param-value-C8TNYchQ.js} +2 -33
  29. package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
  30. package/dist/_chunks/{poison-scan-BAxfTT5L.js → poison-scan-BoDLgbix.js} +2 -2
  31. package/dist/_chunks/{poison-scan-BAxfTT5L.js.map → poison-scan-BoDLgbix.js.map} +1 -1
  32. package/dist/_chunks/{router-ref-BzqbPwYC.js → router-ref-8gr8qsxN.js} +2 -2
  33. package/dist/_chunks/{router-ref-BzqbPwYC.js.map → router-ref-8gr8qsxN.js.map} +1 -1
  34. package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js → rsc-cache-key-ClUiXQnK.js} +2 -2
  35. package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js.map → rsc-cache-key-ClUiXQnK.js.map} +1 -1
  36. package/dist/_chunks/{scanner-BRIOmHE2.js → scanner-tdFPvDYi.js} +174 -7
  37. package/dist/_chunks/scanner-tdFPvDYi.js.map +1 -0
  38. package/dist/_chunks/segment-context-D9_89u34.js +34 -0
  39. package/dist/_chunks/segment-context-D9_89u34.js.map +1 -0
  40. package/dist/_chunks/singleflight-2lUWfcAk.js +54 -0
  41. package/dist/_chunks/singleflight-2lUWfcAk.js.map +1 -0
  42. package/dist/_chunks/{ssr-data-Ya2HJPFp.js → ssr-data-BQGhTPAK.js} +2 -17
  43. package/dist/_chunks/ssr-data-BQGhTPAK.js.map +1 -0
  44. package/dist/_chunks/{walkers-BU6z9xRV.js → walkers-DNX05dC0.js} +2 -2
  45. package/dist/_chunks/{walkers-BU6z9xRV.js.map → walkers-DNX05dC0.js.map} +1 -1
  46. package/dist/adapters/cloudflare-dev.js +1 -1
  47. package/dist/adapters/cloudflare-kv-cache.js +1 -1
  48. package/dist/adapters/cloudflare.js +1 -1
  49. package/dist/adapters/nitro.d.ts +1 -1
  50. package/dist/adapters/nitro.d.ts.map +1 -1
  51. package/dist/adapters/nitro.js.map +1 -1
  52. package/dist/analyze/crawl-entry.js +2 -2
  53. package/dist/analyze/graph-command.js +2 -2
  54. package/dist/cache/index.js +1 -1
  55. package/dist/cache/singleflight.d.ts +2 -0
  56. package/dist/cache/singleflight.d.ts.map +1 -1
  57. package/dist/cli.js +2 -2
  58. package/dist/client/browser-entry/hydrate.d.ts +21 -15
  59. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  60. package/dist/client/browser-entry/index.d.ts +4 -3
  61. package/dist/client/browser-entry/index.d.ts.map +1 -1
  62. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  63. package/dist/client/browser-entry/router-init.d.ts +17 -1
  64. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  65. package/dist/client/error-boundary.js +1 -1
  66. package/dist/client/global-context.d.ts +15 -0
  67. package/dist/client/global-context.d.ts.map +1 -0
  68. package/dist/client/index.js +138 -35
  69. package/dist/client/index.js.map +1 -1
  70. package/dist/client/internal.d.ts +0 -1
  71. package/dist/client/internal.d.ts.map +1 -1
  72. package/dist/client/internal.js +206 -55
  73. package/dist/client/internal.js.map +1 -1
  74. package/dist/client/link.d.ts.map +1 -1
  75. package/dist/client/location-search.d.ts +12 -0
  76. package/dist/client/location-search.d.ts.map +1 -0
  77. package/dist/client/navigation-api.d.ts.map +1 -1
  78. package/dist/client/navigation-commit.d.ts +18 -0
  79. package/dist/client/navigation-commit.d.ts.map +1 -1
  80. package/dist/client/navigation-context.d.ts +13 -11
  81. package/dist/client/navigation-context.d.ts.map +1 -1
  82. package/dist/client/navigation-root.d.ts +47 -108
  83. package/dist/client/navigation-root.d.ts.map +1 -1
  84. package/dist/client/navigation-transition.d.ts +136 -0
  85. package/dist/client/navigation-transition.d.ts.map +1 -0
  86. package/dist/client/nuqs-adapter.d.ts.map +1 -1
  87. package/dist/client/params-context.d.ts +4 -5
  88. package/dist/client/params-context.d.ts.map +1 -1
  89. package/dist/client/react-root.d.ts +44 -0
  90. package/dist/client/react-root.d.ts.map +1 -0
  91. package/dist/client/router-pipeline.d.ts +2 -2
  92. package/dist/client/router-pipeline.d.ts.map +1 -1
  93. package/dist/client/router-types.d.ts +12 -2
  94. package/dist/client/router-types.d.ts.map +1 -1
  95. package/dist/client/router.d.ts.map +1 -1
  96. package/dist/client/segment-cache.d.ts +39 -0
  97. package/dist/client/segment-cache.d.ts.map +1 -1
  98. package/dist/client/segment-context.d.ts.map +1 -1
  99. package/dist/client/segment-outlet.d.ts +25 -14
  100. package/dist/client/segment-outlet.d.ts.map +1 -1
  101. package/dist/client/segment-update-context.d.ts +3 -9
  102. package/dist/client/segment-update-context.d.ts.map +1 -1
  103. package/dist/client/slot-content-cache-context.d.ts +35 -0
  104. package/dist/client/slot-content-cache-context.d.ts.map +1 -0
  105. package/dist/client/ssr-data.d.ts +8 -2
  106. package/dist/client/ssr-data.d.ts.map +1 -1
  107. package/dist/client/state.d.ts +0 -15
  108. package/dist/client/state.d.ts.map +1 -1
  109. package/dist/client/use-pathname.d.ts +13 -11
  110. package/dist/client/use-pathname.d.ts.map +1 -1
  111. package/dist/client/use-search-params.d.ts +13 -13
  112. package/dist/client/use-search-params.d.ts.map +1 -1
  113. package/dist/client/use-segment-params.d.ts +18 -68
  114. package/dist/client/use-segment-params.d.ts.map +1 -1
  115. package/dist/config-types.d.ts +17 -0
  116. package/dist/config-types.d.ts.map +1 -1
  117. package/dist/config-validation.d.ts.map +1 -1
  118. package/dist/cookies/index.js +1 -1
  119. package/dist/dev-tools/holding-server.d.ts +15 -10
  120. package/dist/dev-tools/holding-server.d.ts.map +1 -1
  121. package/dist/index.d.ts.map +1 -1
  122. package/dist/index.js +44 -43
  123. package/dist/index.js.map +1 -1
  124. package/dist/plugins/dev-server.d.ts.map +1 -1
  125. package/dist/plugins/entries.d.ts.map +1 -1
  126. package/dist/plugins/shims.d.ts.map +1 -1
  127. package/dist/plugins/static-build.d.ts +2 -2
  128. package/dist/plugins/static-build.d.ts.map +1 -1
  129. package/dist/routing/codegen-write.d.ts.map +1 -1
  130. package/dist/routing/index.js +2 -2
  131. package/dist/routing/interception-overlap.d.ts +35 -0
  132. package/dist/routing/interception-overlap.d.ts.map +1 -0
  133. package/dist/routing/interception.d.ts.map +1 -1
  134. package/dist/rsc-runtime/ssr.d.ts +3 -1
  135. package/dist/rsc-runtime/ssr.d.ts.map +1 -1
  136. package/dist/server/als-registry.d.ts +6 -0
  137. package/dist/server/als-registry.d.ts.map +1 -1
  138. package/dist/server/csp-nonce.d.ts +45 -0
  139. package/dist/server/csp-nonce.d.ts.map +1 -0
  140. package/dist/server/default-status-page.d.ts.map +1 -1
  141. package/dist/server/deny-renderer.d.ts.map +1 -1
  142. package/dist/server/flight-scripts.d.ts +5 -2
  143. package/dist/server/flight-scripts.d.ts.map +1 -1
  144. package/dist/server/html-injector-core.d.ts +17 -2
  145. package/dist/server/html-injector-core.d.ts.map +1 -1
  146. package/dist/server/html-injectors.d.ts +3 -2
  147. package/dist/server/html-injectors.d.ts.map +1 -1
  148. package/dist/server/index.js +2 -2
  149. package/dist/server/internal.js +86 -37
  150. package/dist/server/internal.js.map +1 -1
  151. package/dist/server/metadata-render.d.ts.map +1 -1
  152. package/dist/server/node-stream-transforms.d.ts +3 -17
  153. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  154. package/dist/server/nuqs-ssr-provider.d.ts +7 -3
  155. package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
  156. package/dist/server/pipeline-phases.d.ts.map +1 -1
  157. package/dist/server/prebuilt/key-discipline.d.ts +32 -3
  158. package/dist/server/prebuilt/key-discipline.d.ts.map +1 -1
  159. package/dist/server/primitives.d.ts.map +1 -1
  160. package/dist/server/render-utils.d.ts +4 -3
  161. package/dist/server/render-utils.d.ts.map +1 -1
  162. package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
  163. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  164. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  165. package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
  166. package/dist/server/ssr-bridge-types.d.ts +22 -2
  167. package/dist/server/ssr-bridge-types.d.ts.map +1 -1
  168. package/dist/server/ssr-entry.d.ts.map +1 -1
  169. package/dist/server/ssr-render.d.ts +5 -1
  170. package/dist/server/ssr-render.d.ts.map +1 -1
  171. package/dist/server/ssr-wrappers.d.ts +59 -27
  172. package/dist/server/ssr-wrappers.d.ts.map +1 -1
  173. package/dist/server/types.d.ts +10 -0
  174. package/dist/server/types.d.ts.map +1 -1
  175. package/dist/shims/navigation-rsc.d.ts +21 -0
  176. package/dist/shims/navigation-rsc.d.ts.map +1 -0
  177. package/docs/api/30-api-server.mdx +1 -0
  178. package/docs/api/35-api-typescript.mdx +4 -83
  179. package/docs/learn/03-fetching-data.mdx +1 -1
  180. package/docs/learn/{03b-access-control.mdx → 04-access-control.mdx} +2 -17
  181. package/docs/learn/05-the-flush-point.mdx +175 -0
  182. package/docs/learn/{05-typed-params.mdx → 06-typed-params.mdx} +1 -1
  183. package/docs/learn/07-typed-routes.mdx +25 -49
  184. package/docs/learn/{08-streaming.mdx → 09-streaming.mdx} +1 -7
  185. package/docs/learn/{10-middleware.mdx → 11-middleware.mdx} +1 -0
  186. package/package.json +3 -3
  187. package/src/adapters/nitro.ts +7 -7
  188. package/src/cache/singleflight.ts +5 -0
  189. package/src/client/browser-entry/hydrate.ts +54 -104
  190. package/src/client/browser-entry/index.ts +16 -6
  191. package/src/client/browser-entry/post-hydration.ts +3 -2
  192. package/src/client/browser-entry/router-init.ts +84 -33
  193. package/src/client/global-context.ts +31 -0
  194. package/src/client/internal.ts +1 -2
  195. package/src/client/link.tsx +18 -18
  196. package/src/client/location-search.ts +15 -0
  197. package/src/client/navigation-api.ts +4 -2
  198. package/src/client/navigation-commit.ts +48 -2
  199. package/src/client/navigation-context.ts +25 -37
  200. package/src/client/navigation-root.tsx +55 -411
  201. package/src/client/navigation-transition.ts +278 -0
  202. package/src/client/nuqs-adapter.tsx +4 -5
  203. package/src/client/params-context.ts +13 -18
  204. package/src/client/react-root.ts +72 -0
  205. package/src/client/router-lifecycle.ts +1 -1
  206. package/src/client/router-pipeline.ts +96 -22
  207. package/src/client/router-types.ts +12 -2
  208. package/src/client/router.ts +48 -36
  209. package/src/client/segment-cache.ts +70 -2
  210. package/src/client/segment-context.ts +7 -4
  211. package/src/client/segment-outlet.tsx +41 -86
  212. package/src/client/segment-update-context.ts +7 -26
  213. package/src/client/slot-content-cache-context.ts +43 -0
  214. package/src/client/ssr-data.ts +8 -2
  215. package/src/client/state.ts +0 -26
  216. package/src/client/use-pathname.ts +21 -31
  217. package/src/client/use-search-params.ts +31 -29
  218. package/src/client/use-segment-params.ts +27 -126
  219. package/src/config-types.ts +17 -0
  220. package/src/config-validation.ts +17 -0
  221. package/src/dev-tools/holding-server.ts +23 -12
  222. package/src/index.ts +26 -11
  223. package/src/plugins/dev-server.ts +9 -12
  224. package/src/plugins/entries.ts +3 -0
  225. package/src/plugins/shims.ts +8 -7
  226. package/src/plugins/static-build.ts +9 -5
  227. package/src/react-canary.d.ts +2 -0
  228. package/src/routing/codegen-write.ts +2 -0
  229. package/src/routing/interception-overlap.ts +141 -0
  230. package/src/routing/interception.ts +118 -5
  231. package/src/rsc-runtime/ssr.ts +3 -2
  232. package/src/server/als-registry.ts +6 -0
  233. package/src/server/csp-nonce.ts +70 -0
  234. package/src/server/default-status-page.ts +1 -0
  235. package/src/server/deny-renderer.ts +7 -3
  236. package/src/server/flight-scripts.ts +9 -4
  237. package/src/server/html-injector-core.ts +26 -9
  238. package/src/server/html-injectors.ts +8 -8
  239. package/src/server/metadata-render.ts +26 -4
  240. package/src/server/node-stream-transforms.ts +7 -20
  241. package/src/server/nuqs-ssr-provider.tsx +8 -7
  242. package/src/server/pipeline-phases.ts +5 -0
  243. package/src/server/prebuilt/key-discipline.ts +82 -13
  244. package/src/server/prebuilt-runtime.ts +2 -2
  245. package/src/server/primitives.ts +4 -4
  246. package/src/server/render-utils.ts +8 -4
  247. package/src/server/rsc-entry/action-middleware-runner.ts +7 -0
  248. package/src/server/rsc-entry/error-renderer.ts +5 -2
  249. package/src/server/rsc-entry/index.ts +8 -0
  250. package/src/server/rsc-entry/ssr-renderer.ts +11 -4
  251. package/src/server/ssr-bridge-types.ts +22 -2
  252. package/src/server/ssr-entry.ts +35 -28
  253. package/src/server/ssr-render.ts +13 -4
  254. package/src/server/ssr-wrappers.tsx +81 -61
  255. package/src/server/types.ts +10 -0
  256. package/src/shared/slot-params.ts +3 -4
  257. package/src/shims/navigation-rsc.ts +47 -0
  258. package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
  259. package/dist/_chunks/cache-api-CQeYzA5g.js.map +0 -1
  260. package/dist/_chunks/convention-lint-DO10_pVl.js.map +0 -1
  261. package/dist/_chunks/href-validation-CMc5JRls.js.map +0 -1
  262. package/dist/_chunks/scanner-BRIOmHE2.js.map +0 -1
  263. package/dist/_chunks/segment-context-CjOlyB8Y.js.map +0 -1
  264. package/dist/_chunks/slot-params-BCTmZkQB.js +0 -76
  265. package/dist/_chunks/slot-params-BCTmZkQB.js.map +0 -1
  266. package/dist/_chunks/ssr-data-Ya2HJPFp.js.map +0 -1
  267. package/dist/_chunks/use-segment-params-DzTBpkvj.js +0 -398
  268. package/dist/_chunks/use-segment-params-DzTBpkvj.js.map +0 -1
  269. package/docs/learn/04-loading-states.mdx +0 -67
  270. package/docs/learn/04b-the-flush-point.mdx +0 -115
  271. package/docs/learn/12-client-navigation.mdx +0 -176
  272. package/docs/learn/13-configuration.mdx +0 -166
  273. package/docs/more/01-advanced-routing.mdx +0 -344
  274. package/docs/more/02-advanced-forms.mdx +0 -137
  275. package/docs/more/03-coming-from-nextjs.mdx +0 -186
  276. package/docs/more/04-metadata-and-fonts.mdx +0 -193
  277. package/docs/more/04b-mdx.mdx +0 -229
  278. package/docs/more/05-content-collections.mdx +0 -90
  279. package/docs/more/06-instrumentation.mdx +0 -214
  280. package/docs/more/07-security.mdx +0 -129
  281. package/docs/more/08-developer-experience.mdx +0 -134
  282. package/docs/more/40-why-timber.mdx +0 -50
  283. package/docs/more/41-timber-vs-nextjs.mdx +0 -81
  284. package/docs/more/42-timber-vs-others.mdx +0 -68
  285. package/docs/more/50-ai-agent-instructions.mdx +0 -171
  286. /package/docs/learn/{06-forms-and-actions.mdx → 08-forms-and-actions.mdx} +0 -0
  287. /package/docs/learn/{09-caching.mdx → 10-caching.mdx} +0 -0
  288. /package/docs/learn/{11-error-handling.mdx → 12-error-handling.mdx} +0 -0
  289. /package/docs/learn/{14-deploying.mdx → 13-deploying.mdx} +0 -0
@@ -1,21 +1,26 @@
1
1
  /**
2
- * NavigationRoot — Wrapper component for transition-based rendering.
3
- *
4
- * Solves the "new boundary has no old content" problem for client-side
5
- * navigation. When React renders a completely new Suspense boundary via
6
- * root.render(), it shows the fallback immediately — root.render() is
7
- * always an urgent update regardless of startTransition.
8
- *
9
- * NavigationRoot holds the current element in React state. Navigation
10
- * updates call startTransition(() => setState(newElement)), which IS
11
- * a transition update. React keeps the old committed tree visible while
12
- * the new tree resolves, instead of hiding it behind a Suspense fallback.
13
- *
14
- * The navigation's async work runs OUTSIDE the transition scope and every
15
- * state update it schedules gets its own synchronous `startTransition` — a
16
- * transition scope does not survive an `await`, and an async callback that
17
- * returns a thenable defers the commit past the promise callers await. See
18
- * the long comment on `_navigateTransition` (TIM-1306).
2
+ * NavigationRoot — the component the router renders the page through.
3
+ *
4
+ * It is stateless. The router owns the displayed tree and drives React with
5
+ * `root.render(<NavigationRoot rendered={{ element, publish }} />)` inside a
6
+ * synchronous `startTransition` (see `client/react-root.ts`). A transition
7
+ * update keeps the committed tree on screen while the incoming one resolves,
8
+ * instead of replacing it with a Suspense fallback — the reason this
9
+ * component exists (TIM-1306).
10
+ *
11
+ * It used to hold the tree in `useState` and register `setState` closures
12
+ * into module globals during render so the router could reach into it, with a
13
+ * second set of stand-in closures for the window before a root existed
14
+ * (TIM-600). Inverting that — router calls React, not the reverse — deleted
15
+ * all of it (TIM-1431). Next.js's `use-action-queue.ts` says it wants this
16
+ * shape and cannot have it because it must decode Flight during render;
17
+ * timber decodes in `router-pipeline.ts`, so nothing stands in the way.
18
+ *
19
+ * What this component does own is **the commit-time publish**: a navigation's
20
+ * state (segment cache, address bar, history stack, pathname) is published
21
+ * from a layout effect keyed on the tree object, so the event that puts a
22
+ * route on screen is the event that makes it current (TIM-1301). Props work
23
+ * for that exactly as state did.
19
24
  *
20
25
  * This component holds no pending state. The `TopLoader` it renders and the
21
26
  * public `usePendingNavigation()` both subscribe to the router's external
@@ -35,166 +40,39 @@
35
40
  * See design/19-client-navigation.md §"NavigationContext"
36
41
  */
37
42
 
38
- import {
39
- createElement,
40
- Fragment,
41
- startTransition,
42
- useLayoutEffect,
43
- useRef,
44
- useState,
45
- type ReactNode,
46
- } from 'react';
43
+ import { createElement, Fragment, useLayoutEffect, useRef, type ReactNode } from 'react';
47
44
  import { TopLoader, type TopLoaderConfig } from './top-loader.tsx';
48
45
 
49
- // ─── Transition Result ──────────────────────────────────────────
46
+ // ─── Rendered Tree ──────────────────────────────────────────────
50
47
 
51
48
  /**
52
- * What a navigation's `perform()` hands back to the transition.
53
- *
54
- * Declared once and shared by every layer that passes it along — the router's
55
- * `RouterDeps.navigateTransition` imports it too — so a field cannot be added
56
- * to the producer and dropped by the adapter in between. `E` is the element
57
- * type: `ReactNode` here, `unknown` in the router, which never touches it.
49
+ * The tree the router has handed to React, and the state publish that belongs
50
+ * to it. They travel as one object so React's commit of the tree is the event
51
+ * that publishes — see the effect in NavigationRoot. Every `render` builds a
52
+ * fresh one, which is what keys that effect: a tree is published once, however
53
+ * many times React re-runs the effect for it.
58
54
  */
59
- export interface TransitionResult<E = ReactNode> {
60
- /** The wrapped tree to render. */
61
- element: E;
62
- /** Resolves when the Flight stream finishes decoding, or null. */
63
- decodePromise: Promise<void> | null;
55
+ export interface RenderedTree {
56
+ element: ReactNode;
64
57
  /**
65
58
  * Publishes the navigation's state — segment cache, pathname, address bar,
66
59
  * history stack, and the client's record of the mounted tree — and
67
- * announces it to listeners outside React.
68
- *
69
- * Run when React commits this tree, not when the tree is handed over: a
70
- * tree React is still waiting on, or one a later navigation replaces first,
71
- * has been given to React without being on screen. A superseded navigation
72
- * never runs it and leaves every one of those consumers describing the
73
- * route still on screen (TIM-1301).
60
+ * announces it to listeners outside React. Runs when React commits
61
+ * `element`, never before (TIM-1301). Null for a render that has nothing to
62
+ * publish on commit (hydration, the shallow search re-wrap).
74
63
  */
75
- commit: () => void;
76
- }
77
-
78
- /**
79
- * The tree NavigationRoot holds in state, and the state publish that belongs
80
- * to it. They travel as one object so React's commit of the tree is the event
81
- * that publishes — see the effect in NavigationRoot.
82
- */
83
- interface RenderedTree {
84
- element: ReactNode;
85
64
  publish: (() => void) | null;
86
65
  }
87
66
 
88
- // ─── Navigation Transition Counter ──────────────────────────────
89
- // Monotonically increasing counter that increments each time
90
- // navigateTransition() is called. Used to detect stale transitions:
91
- // if a newer transition started while the current one's perform()
92
- // was in flight, the current transition is stale and should reject.
93
- //
94
- // Separate from the link-pending navId (which only increments on
95
- // link clicks). This counter covers all navigation types: link clicks,
96
- // programmatic navigate(), refresh(), and handlePopState().
97
- //
98
- // Uses globalThis for singleton guarantee across chunks — same pattern
99
- // as NavigationContext and the link pending store.
100
-
101
- const NAV_TRANSITION_KEY = Symbol.for('__timber_nav_transition_counter');
102
-
103
67
  /**
104
- * `waiters` are woken on every bump so an in-flight navigation can stop
105
- * waiting on its own payload the moment it is superseded — see
106
- * `settleOnDecodeOrSupersession`.
107
- */
108
- interface TransitionCounter {
109
- id: number;
110
- waiters: Set<() => void>;
111
- }
112
-
113
- function getTransitionCounter(): TransitionCounter {
114
- const g = globalThis as Record<symbol, unknown>;
115
- const existing = g[NAV_TRANSITION_KEY] as Partial<TransitionCounter> | undefined;
116
- if (!existing) {
117
- const created: TransitionCounter = { id: 0, waiters: new Set() };
118
- g[NAV_TRANSITION_KEY] = created;
119
- return created;
120
- }
121
- // A duplicated copy of this module may have created the singleton before
122
- // `waiters` existed. The object is shared across chunks, so fill it in
123
- // rather than replacing it — replacing would strand the other copy's id.
124
- existing.waiters ??= new Set();
125
- return existing as TransitionCounter;
126
- }
127
-
128
- /** Bump the counter and wake everything waiting on an older transition. */
129
- function bumpTransitionCounter(): number {
130
- const counter = getTransitionCounter();
131
- counter.id += 1;
132
- for (const wake of [...counter.waiters]) wake();
133
- return counter.id;
134
- }
135
-
136
- /**
137
- * Invalidate all in-flight navigation transitions. Any navigateTransition()
138
- * call whose perform() has not yet committed will reject with AbortError
139
- * instead of committing its element.
140
- *
141
- * Called by the router when a render supersedes in-flight navigations
142
- * WITHOUT going through navigateTransition() — the cached popstate replay
143
- * renders via transitionRender(), which doesn't bump the counter, so a
144
- * stale forward navigation's setElement would otherwise pass the
145
- * `counter.id !== transId` guard and commit the forward page over the
146
- * replayed back page (TIM-1022).
147
- */
148
- export function supersedeNavigationTransitions(): void {
149
- bumpTransitionCounter();
150
- }
151
-
152
- /**
153
- * Wait for the payload to finish decoding, OR for this transition to be
154
- * superseded — whichever happens first.
68
+ * Hand a tree to React in a transition. `publish` runs when React commits it.
155
69
  *
156
- * A superseded navigation must stop waiting on its own stream. The stream is
157
- * deliberately NOT aborted once its tree has been handed to React (the tree
158
- * may be on screen with boundaries still feeding from it — see
159
- * `handedOffNavAbort` in `client/router.ts`), so there is nothing left to make
160
- * `decodePromise` settle promptly. Awaiting it bare would keep the loser's
161
- * `router.navigate()` promise pending for the rest of the stream — and
162
- * forever if it stalls — which is what `<Link>`'s `isPending` is timed
163
- * against, so the losing link would sit spinning while the winner loaded
164
- * (codex on #1004).
165
- *
166
- * A decode *failure* still propagates: it is a real error for this
167
- * navigation, and the caller's recovery is timed against it.
70
+ * Every path that puts a page on screen goes through one of these — a
71
+ * navigation, a revalidation, a popstate replay, a shallow search re-wrap —
72
+ * and it is always a synchronous `startTransition` around `root.render`.
73
+ * The production one is `createReactRoot().render`.
168
74
  */
169
- function settleOnDecodeOrSupersession(
170
- decodePromise: Promise<void>,
171
- counter: TransitionCounter,
172
- transId: number
173
- ): Promise<void> {
174
- if (counter.id !== transId) return Promise.resolve();
175
- return new Promise<void>((resolve, reject) => {
176
- const stopWaiting = (): void => {
177
- counter.waiters.delete(wake);
178
- };
179
- const wake = (): void => {
180
- if (counter.id !== transId) {
181
- stopWaiting();
182
- resolve();
183
- }
184
- };
185
- counter.waiters.add(wake);
186
- decodePromise.then(
187
- () => {
188
- stopWaiting();
189
- resolve();
190
- },
191
- (error: unknown) => {
192
- stopWaiting();
193
- reject(error instanceof Error ? error : new Error(String(error)));
194
- }
195
- );
196
- });
197
- }
75
+ export type NavigationRender = (element: ReactNode, publish: (() => void) | null) => void;
198
76
 
199
77
  // ─── Hard Navigation Guard ──────────────────────────────────────
200
78
 
@@ -255,52 +133,25 @@ export function isHardNavigating(): boolean {
255
133
  // eslint-disable-next-line unicorn/no-thenable -- Intentionally a never-resolving thenable
256
134
  const unresolvedThenable = { then() {} } as PromiseLike<never>;
257
135
 
258
- // ─── Module-level functions ──────────────────────────────────────
259
-
260
- /**
261
- * Module-level reference to the state setter wrapped in startTransition.
262
- * Used for non-navigation renders (applyRevalidation, popstate replay).
263
- */
264
- let _transitionRender: ((element: ReactNode) => void) | null = null;
265
-
266
- /**
267
- * Module-level reference to the navigation transition function.
268
- *
269
- * Runs the fetch OUTSIDE any transition scope and wraps only the state update
270
- * that hands the resulting tree to React — describing this as "a full
271
- * navigation in a single startTransition" is the shape TIM-1306 removed.
272
- */
273
- let _navigateTransition:
274
- | ((url: string, perform: () => Promise<TransitionResult>) => Promise<void>)
275
- | null = null;
276
-
277
136
  // ─── Component ───────────────────────────────────────────────────
278
137
 
279
138
  /**
280
- * Root wrapper component that enables transition-based rendering.
281
- *
282
- * Renders the TopLoader alongside the tree it holds in state. Neither adds a
283
- * DOM element on the hydration path, so the tree matches the server HTML.
284
- *
285
- * Usage in browser-entry.ts:
286
- * const rootEl = createElement(NavigationRoot, { initial: wrapped });
287
- * reactRoot = hydrateRoot(document, rootEl);
139
+ * Root component the router renders the page through.
288
140
  *
289
- * Subsequent navigations:
290
- * navigateTransition(url, async () => { fetch; return wrappedElement; });
141
+ * Renders the TopLoader alongside the tree it is given. Neither adds a DOM
142
+ * element on the hydration path, so the tree matches the server HTML.
291
143
  *
292
- * Non-navigation renders:
293
- * transitionRender(newWrappedElement);
144
+ * Rendered only by `createReactRoot` (client/react-root.ts): hydration passes
145
+ * `{ element, publish: null }`, and every later render is a synchronous
146
+ * `startTransition(() => root.render(...))` with a fresh `rendered` object.
294
147
  */
295
148
  export function NavigationRoot({
296
- initial,
149
+ rendered,
297
150
  topLoaderConfig,
298
151
  }: {
299
- initial: ReactNode;
152
+ rendered: RenderedTree;
300
153
  topLoaderConfig?: TopLoaderConfig;
301
154
  }): ReactNode {
302
- const [rendered, setRendered] = useState<RenderedTree>({ element: initial, publish: null });
303
-
304
155
  // Publish the navigation's state when React commits its tree — not when the
305
156
  // tree is handed over. A tree React is still waiting on, or one a later
306
157
  // navigation replaces before React renders it, has been *given* to React
@@ -324,8 +175,8 @@ export function NavigationRoot({
324
175
  //
325
176
  // Keyed on the tree object rather than on the effect run. A publish moves
326
177
  // the address bar, which is not idempotent, so any second invocation for the
327
- // same tree — an effect re-run React is entitled to perform — must be a
328
- // no-op rather than a second history entry.
178
+ // same tree — an effect re-run React is entitled to perform, StrictMode's
179
+ // double-invoke — must be a no-op rather than a second history entry.
329
180
  const publishedRef = useRef<RenderedTree | null>(null);
330
181
  useLayoutEffect(() => {
331
182
  if (publishedRef.current === rendered) return;
@@ -333,141 +184,17 @@ export function NavigationRoot({
333
184
  rendered.publish?.();
334
185
  }, [rendered]);
335
186
 
336
- // NOTE: We use standalone `startTransition` (imported from 'react'),
337
- // NOT `useTransition`. The `useTransition` hook's `startTransition`
338
- // is tied to a single fiber and tracks one async callback at a time.
339
- // When two navigations overlap (click slow-page, then click dashboard),
340
- // calling useTransition's startTransition twice with concurrent async
341
- // callbacks corrupts React's internal hook tracking — causing
342
- // "Rendered more hooks than during the previous render."
343
- //
344
- // Standalone `startTransition` creates independent transition lanes
345
- // for each call, so concurrent navigations don't interfere. We don't
346
- // need useTransition's `isPending` — pending state lives in the router's
347
- // external store, which TopLoader and usePendingNavigation() subscribe to.
348
- //
349
- // This matches the Next.js pattern (TIM-625): "No useTransition in
350
- // the router at all — only standalone startTransition."
351
-
352
- // Non-navigation render (revalidation, popstate cached replay).
353
- // Non-navigation render (revalidation, popstate cached replay). Both publish
354
- // synchronously in the router before calling this — neither has an in-flight
355
- // window in which it could be superseded — so there is nothing to publish
356
- // on commit.
357
- _transitionRender = (newElement: ReactNode) => {
358
- startTransition(() => {
359
- setRendered({ element: newElement, publish: null });
360
- });
361
- };
362
-
363
- // Full navigation transition.
364
- //
365
- // The async work runs OUTSIDE any transition scope, and each state update is
366
- // its own synchronous `startTransition`. Two React behaviours force that
367
- // shape; both were measured on React 19.2.7 (tests/navigation-transition-
368
- // suspense.test.ts pins the observable half).
369
- //
370
- // 1. A transition scope does not survive an `await`. `startTransition`
371
- // restores the previous scope in its `finally`, which for an async
372
- // callback runs when that callback RETURNS — i.e. at its first `await`.
373
- // So an update scheduled past an await inside `startTransition(async …)`
374
- // is an ordinary urgent update:
375
- //
376
- // startTransition(() => setEl(x)) -> 'HOME' held
377
- // startTransition(async () => { await p; setEl(x) }) -> fallback shown
378
- //
379
- // Left uncorrected, every navigation whose new tree suspends replaces the
380
- // visible page with a Suspense fallback — the exact thing this component
381
- // exists to prevent (TIM-1306). React documents the caveat under
382
- // `startTransition`: updates after an await need their own transition.
383
- //
384
- // 2. Re-wrapping *inside* the async callback is not enough. Returning a
385
- // thenable from `startTransition` hands it to `ReactSharedInternals.S`,
386
- // which calls react-dom's `entangleAsyncAction`. That opens an action
387
- // scope: `currentEntangledLane` collects EVERY transition update
388
- // scheduled while the scope is open — including one from a nested,
389
- // fully synchronous `startTransition` — and rendering that lane throws
390
- // `currentEntangledActionThenable`, suspending until the action settles.
391
- // Which is after the promise `navigateTransition` hands back, so a caller
392
- // that awaits a navigation and then reads the DOM sees the departing
393
- // page, and the TIM-1301 publish (a layout effect on the commit) is just
394
- // as late. (Not `ReactSharedInternals.asyncTransitions` — that counter is
395
- // write-only in 19.2.7.)
396
- //
397
- // No async action is created here, so every navigation commits as soon as
398
- // React can render its destination — where TIM-1301 put it. That
399
- // now holds for ALL of them. It used not to hold for `<Link>`, which wrapped
400
- // `router.navigate()` in its own `useTransition`: that action scope
401
- // entangled this component's updates just the same, so a Link navigation
402
- // committed only once the payload had finished decoding, and with it
403
- // `pushState`, the segment cache and `timber:navigation-end`. Link now calls
404
- // `router.navigate()` outside any transition and tracks its own `isPending`
405
- // with a plain `useState` (TIM-1307).
406
- //
407
- // Nothing here may reopen an action scope: no `startTransition` callback in
408
- // this path may return a thenable, and no caller may invoke this from inside
409
- // a React action scope. That is a rule about `startTransition`, not about
410
- // `perform` — `perform` is async by contract and is awaited OUTSIDE any
411
- // transition scope, which is exactly why it is safe.
412
- //
413
- // Standalone `startTransition` rather than `useTransition`'s is still
414
- // deliberate — see the TIM-625 note above; each navigation needs an
415
- // independent lane.
416
- //
417
- // `url` is unused: it named the pending state this component used to hold,
418
- // and the router already publishes the same URL to its own pending store
419
- // before calling here. It stays in the signature because the router's
420
- // `RouterDeps.navigateTransition` adapter passes it and the argument reads
421
- // at the call site.
422
- _navigateTransition = (_url: string, perform: () => Promise<TransitionResult>) => {
423
- // Increment the transition counter SYNCHRONOUSLY (before any await). Each
424
- // call gets a unique transId; the counter is the same globalThis
425
- // singleton, so a newer call always has a higher id.
426
- const counter = getTransitionCounter();
427
- const transId = bumpTransitionCounter();
428
-
429
- const superseded = () => new DOMException('Navigation superseded', 'AbortError');
430
-
431
- return (async () => {
432
- const { element, decodePromise, commit } = await perform();
433
- if (counter.id !== transId) {
434
- decodePromise?.catch(() => {});
435
- throw superseded();
436
- }
437
- // Hand the tree over with its commit attached. The effect above runs
438
- // it if and when React commits this tree, so a navigation that is
439
- // superseded while its payload streams publishes nothing (TIM-1301).
440
- //
441
- // This is THE update that must not hide the departing page (TIM-1306).
442
- startTransition(() => {
443
- setRendered({ element, publish: commit });
444
- });
445
- // React may commit the tree before this settles — that is the point:
446
- // the destination reveals as React is able to render it
447
- // rather than waiting for the whole Flight stream. The await is here so
448
- // the promise this function hands back still means "the payload is
449
- // decoded", which is what the router's scroll restoration, the
450
- // Navigation API deferred and `<Link>`'s `isPending` are timed against.
451
- //
452
- // ...unless this navigation loses first, in which case it stops waiting
453
- // on a stream that is no longer its business. See
454
- // `settleOnDecodeOrSupersession`.
455
- if (decodePromise) await settleOnDecodeOrSupersession(decodePromise, counter, transId);
456
- if (counter.id !== transId) throw superseded();
457
- })();
458
- };
459
-
460
187
  // ─── Hard navigation guard ─────────────────────────────────
461
188
  // When a hard navigation is in progress (500 error, version skew),
462
189
  // suspend forever to prevent React from rendering children during
463
190
  // page teardown. This avoids "Rendered more hooks" crashes in
464
191
  // CHILD components whose hook counts may shift during teardown.
465
192
  //
466
- // CRITICAL: This throw MUST come AFTER all hooks (the useState,
467
- // useRef and useLayoutEffect above). React requires the same hooks
468
- // to run on every render. If we threw before hooks, React would see
469
- // 0 hooks on the re-render vs 3 on the initial render — triggering
470
- // the exact "Rendered more hooks" error we're trying to prevent.
193
+ // CRITICAL: This throw MUST come AFTER all hooks (the useRef and
194
+ // useLayoutEffect above). React requires the same hooks to run on every
195
+ // render. If we threw before hooks, React would see 0 hooks on the
196
+ // re-render vs 2 on the initial render — triggering the exact "Rendered
197
+ // more hooks" error we're trying to prevent.
471
198
  //
472
199
  // By placing it after hooks but before the return, all hooks
473
200
  // satisfy React's rules, but the thrown thenable prevents any
@@ -495,86 +222,3 @@ export function NavigationRoot({
495
222
  rendered.element
496
223
  );
497
224
  }
498
-
499
- // ─── Public API ──────────────────────────────────────────────────
500
-
501
- /**
502
- * Trigger a transition render for non-navigation updates.
503
- * React keeps the old committed tree visible while any new Suspense
504
- * boundaries in the update resolve.
505
- *
506
- * Used for: applyRevalidation, popstate replay with cached payload.
507
- */
508
- export function transitionRender(element: ReactNode): void {
509
- if (_transitionRender) {
510
- _transitionRender(element);
511
- }
512
- }
513
-
514
- /**
515
- * Run a full navigation, handing its tree to React in a transition.
516
- *
517
- * The `perform` callback fetches the RSC payload, updates router state, and
518
- * returns the wrapped React element. `perform` is async by contract and runs
519
- * OUTSIDE any transition scope — only the `setRendered` that hands its result
520
- * over is wrapped, in a synchronous `startTransition`.
521
- *
522
- * Do not call this from inside a React action scope, and never let a
523
- * `startTransition` callback in this path return a thenable: either opens an
524
- * action scope that entangles the update and defers the commit until the
525
- * action settles (TIM-1306, TIM-1307).
526
- *
527
- * Returns a Promise that resolves when the async work completes — the payload
528
- * is fetched, decoded, and handed to React. It does **not** wait for React to
529
- * commit the tree, so the state that publishes on that commit (address bar,
530
- * segment cache, history entry, pathname) may still be a beat behind when it
531
- * resolves. Anything that needs the destination to be current should listen
532
- * for `timber:navigation-end`, which is dispatched by the publish itself.
533
- *
534
- * Awaiting the publish here was tried and rejected: it makes the promise
535
- * depend on a React commit, which never arrives if the root unmounts
536
- * mid-navigation, and deadlocks any caller that awaits a navigation inside
537
- * `act()`.
538
- *
539
- * Used for: navigate(), refresh(), popstate with fetch.
540
- */
541
- export function navigateTransition(
542
- url: string,
543
- perform: () => Promise<TransitionResult>
544
- ): Promise<void> {
545
- if (_navigateTransition) {
546
- return _navigateTransition(url, perform);
547
- }
548
- // Fallback: no NavigationRoot mounted (shouldn't happen in production).
549
- // Nothing can supersede a transition that does not exist, so the commit
550
- // runs unconditionally.
551
- return perform().then((result) => result.commit());
552
- }
553
-
554
- /**
555
- * Install one-shot deferred callbacks for the no-RSC bootstrap path (TIM-600).
556
- *
557
- * When there's no RSC payload, we can't create a React root immediately —
558
- * `createRoot(document).render(...)` would blank the SSR HTML. Instead,
559
- * this sets up `_transitionRender` and `_navigateTransition` so that the
560
- * first client navigation triggers root creation via `createAndMount`.
561
- *
562
- * After `createAndMount` runs, NavigationRoot renders and overwrites these
563
- * callbacks with its real `startTransition`-based implementations.
564
- */
565
- export function installDeferredNavigation(createAndMount: (initial: ReactNode) => void): void {
566
- let mounted = false;
567
- const mountOnce = (element: ReactNode) => {
568
- if (mounted) return;
569
- mounted = true;
570
- createAndMount(element);
571
- };
572
- _transitionRender = (element: ReactNode) => {
573
- mountOnce(element);
574
- };
575
- _navigateTransition = async (_url: string, perform: () => Promise<TransitionResult>) => {
576
- const { element, commit } = await perform();
577
- commit();
578
- mountOnce(element);
579
- };
580
- }