@timber-js/app 0.2.0-alpha.33 → 0.2.0-alpha.35

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 (236) hide show
  1. package/dist/_chunks/{als-registry-B7DbZ2hS.js → als-registry-Ba7URUIn.js} +1 -1
  2. package/dist/_chunks/als-registry-Ba7URUIn.js.map +1 -0
  3. package/dist/_chunks/chunk-DYhsFzuS.js +33 -0
  4. package/dist/_chunks/{debug-B3Gypr3D.js → debug-ECi_61pb.js} +1 -1
  5. package/dist/_chunks/{debug-B3Gypr3D.js.map → debug-ECi_61pb.js.map} +1 -1
  6. package/dist/_chunks/define-cookie-w5GWm_bL.js +93 -0
  7. package/dist/_chunks/define-cookie-w5GWm_bL.js.map +1 -0
  8. package/dist/_chunks/error-boundary-TYEQJZ1-.js +211 -0
  9. package/dist/_chunks/error-boundary-TYEQJZ1-.js.map +1 -0
  10. package/dist/_chunks/{format-RyoGQL74.js → format-cX7wzEp2.js} +2 -2
  11. package/dist/_chunks/{format-RyoGQL74.js.map → format-cX7wzEp2.js.map} +1 -1
  12. package/dist/_chunks/{interception-BOoWmLUA.js → interception-D2djYaIm.js} +112 -77
  13. package/dist/_chunks/interception-D2djYaIm.js.map +1 -0
  14. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js → metadata-routes-BU684ls2.js} +1 -1
  15. package/dist/_chunks/{metadata-routes-Cjmvi3rQ.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  16. package/dist/_chunks/{request-context-BQUC8PHn.js → request-context-CZz_T0Bc.js} +40 -71
  17. package/dist/_chunks/request-context-CZz_T0Bc.js.map +1 -0
  18. package/dist/_chunks/segment-context-Dpq2XOKg.js +34 -0
  19. package/dist/_chunks/segment-context-Dpq2XOKg.js.map +1 -0
  20. package/dist/_chunks/stale-reload-C0ValzG7.js +47 -0
  21. package/dist/_chunks/stale-reload-C0ValzG7.js.map +1 -0
  22. package/dist/_chunks/{tracing-CemImE6h.js → tracing-BPyIzIdu.js} +2 -2
  23. package/dist/_chunks/{tracing-CemImE6h.js.map → tracing-BPyIzIdu.js.map} +1 -1
  24. package/dist/_chunks/{use-query-states-D5KaffOK.js → use-query-states-BvW0TKDn.js} +1 -1
  25. package/dist/_chunks/{use-query-states-D5KaffOK.js.map → use-query-states-BvW0TKDn.js.map} +1 -1
  26. package/dist/_chunks/wrappers-C1SN725w.js +331 -0
  27. package/dist/_chunks/wrappers-C1SN725w.js.map +1 -0
  28. package/dist/adapters/compress-module.d.ts.map +1 -1
  29. package/dist/adapters/nitro.js +5 -2
  30. package/dist/adapters/nitro.js.map +1 -1
  31. package/dist/cache/index.js +1 -2
  32. package/dist/cache/index.js.map +1 -1
  33. package/dist/client/error-boundary.d.ts +10 -1
  34. package/dist/client/error-boundary.d.ts.map +1 -1
  35. package/dist/client/error-boundary.js +1 -125
  36. package/dist/client/index.d.ts +2 -2
  37. package/dist/client/index.d.ts.map +1 -1
  38. package/dist/client/index.js +193 -90
  39. package/dist/client/index.js.map +1 -1
  40. package/dist/client/link.d.ts +8 -8
  41. package/dist/client/link.d.ts.map +1 -1
  42. package/dist/client/navigation-context.d.ts +2 -2
  43. package/dist/client/router.d.ts +25 -3
  44. package/dist/client/router.d.ts.map +1 -1
  45. package/dist/client/rsc-fetch.d.ts +23 -2
  46. package/dist/client/rsc-fetch.d.ts.map +1 -1
  47. package/dist/client/segment-cache.d.ts +1 -1
  48. package/dist/client/segment-cache.d.ts.map +1 -1
  49. package/dist/client/stale-reload.d.ts +15 -0
  50. package/dist/client/stale-reload.d.ts.map +1 -1
  51. package/dist/client/top-loader.d.ts +1 -1
  52. package/dist/client/top-loader.d.ts.map +1 -1
  53. package/dist/client/use-params.d.ts +2 -2
  54. package/dist/client/use-params.d.ts.map +1 -1
  55. package/dist/client/use-query-states.d.ts +1 -1
  56. package/dist/codec.d.ts +21 -0
  57. package/dist/codec.d.ts.map +1 -0
  58. package/dist/cookies/define-cookie.d.ts +33 -12
  59. package/dist/cookies/define-cookie.d.ts.map +1 -1
  60. package/dist/cookies/index.js +1 -83
  61. package/dist/index.d.ts +87 -12
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +356 -215
  64. package/dist/index.js.map +1 -1
  65. package/dist/params/define.d.ts +76 -0
  66. package/dist/params/define.d.ts.map +1 -0
  67. package/dist/params/index.d.ts +8 -0
  68. package/dist/params/index.d.ts.map +1 -0
  69. package/dist/params/index.js +104 -0
  70. package/dist/params/index.js.map +1 -0
  71. package/dist/plugins/adapter-build.d.ts.map +1 -1
  72. package/dist/plugins/build-manifest.d.ts.map +1 -1
  73. package/dist/plugins/client-chunks.d.ts +32 -0
  74. package/dist/plugins/client-chunks.d.ts.map +1 -0
  75. package/dist/plugins/entries.d.ts.map +1 -1
  76. package/dist/plugins/routing.d.ts.map +1 -1
  77. package/dist/plugins/server-bundle.d.ts.map +1 -1
  78. package/dist/plugins/static-build.d.ts.map +1 -1
  79. package/dist/routing/codegen.d.ts +2 -2
  80. package/dist/routing/codegen.d.ts.map +1 -1
  81. package/dist/routing/index.js +1 -1
  82. package/dist/routing/scanner.d.ts.map +1 -1
  83. package/dist/routing/status-file-lint.d.ts +2 -1
  84. package/dist/routing/status-file-lint.d.ts.map +1 -1
  85. package/dist/routing/types.d.ts +6 -4
  86. package/dist/routing/types.d.ts.map +1 -1
  87. package/dist/rsc-runtime/rsc.d.ts +1 -1
  88. package/dist/rsc-runtime/rsc.d.ts.map +1 -1
  89. package/dist/search-params/codecs.d.ts +1 -1
  90. package/dist/search-params/define.d.ts +153 -0
  91. package/dist/search-params/define.d.ts.map +1 -0
  92. package/dist/search-params/index.d.ts +4 -5
  93. package/dist/search-params/index.d.ts.map +1 -1
  94. package/dist/search-params/index.js +3 -474
  95. package/dist/search-params/registry.d.ts +1 -1
  96. package/dist/search-params/wrappers.d.ts +53 -0
  97. package/dist/search-params/wrappers.d.ts.map +1 -0
  98. package/dist/server/access-gate.d.ts +4 -0
  99. package/dist/server/access-gate.d.ts.map +1 -1
  100. package/dist/server/action-encryption.d.ts +76 -0
  101. package/dist/server/action-encryption.d.ts.map +1 -0
  102. package/dist/server/action-handler.d.ts.map +1 -1
  103. package/dist/server/als-registry.d.ts +4 -4
  104. package/dist/server/als-registry.d.ts.map +1 -1
  105. package/dist/server/build-manifest.d.ts +2 -2
  106. package/dist/server/early-hints.d.ts +13 -5
  107. package/dist/server/early-hints.d.ts.map +1 -1
  108. package/dist/server/error-boundary-wrapper.d.ts +4 -0
  109. package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
  110. package/dist/server/flight-injection-state.d.ts +78 -0
  111. package/dist/server/flight-injection-state.d.ts.map +1 -0
  112. package/dist/server/form-data.d.ts +29 -0
  113. package/dist/server/form-data.d.ts.map +1 -1
  114. package/dist/server/html-injectors.d.ts.map +1 -1
  115. package/dist/server/index.d.ts +1 -1
  116. package/dist/server/index.d.ts.map +1 -1
  117. package/dist/server/index.js +1819 -1629
  118. package/dist/server/index.js.map +1 -1
  119. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  120. package/dist/server/pipeline.d.ts.map +1 -1
  121. package/dist/server/request-context.d.ts +28 -40
  122. package/dist/server/request-context.d.ts.map +1 -1
  123. package/dist/server/route-element-builder.d.ts +7 -0
  124. package/dist/server/route-element-builder.d.ts.map +1 -1
  125. package/dist/server/route-matcher.d.ts +2 -2
  126. package/dist/server/route-matcher.d.ts.map +1 -1
  127. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  128. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  129. package/dist/server/slot-resolver.d.ts.map +1 -1
  130. package/dist/server/ssr-entry.d.ts.map +1 -1
  131. package/dist/server/ssr-render.d.ts +3 -0
  132. package/dist/server/ssr-render.d.ts.map +1 -1
  133. package/dist/server/tree-builder.d.ts +12 -8
  134. package/dist/server/tree-builder.d.ts.map +1 -1
  135. package/dist/server/types.d.ts +1 -3
  136. package/dist/server/types.d.ts.map +1 -1
  137. package/dist/server/version-skew.d.ts +61 -0
  138. package/dist/server/version-skew.d.ts.map +1 -0
  139. package/dist/shims/navigation-client.d.ts +1 -1
  140. package/dist/shims/navigation-client.d.ts.map +1 -1
  141. package/dist/shims/navigation.d.ts +1 -1
  142. package/dist/shims/navigation.d.ts.map +1 -1
  143. package/dist/utils/state-machine.d.ts +80 -0
  144. package/dist/utils/state-machine.d.ts.map +1 -0
  145. package/package.json +12 -8
  146. package/src/adapters/compress-module.ts +5 -2
  147. package/src/client/browser-entry.ts +94 -85
  148. package/src/client/error-boundary.tsx +18 -1
  149. package/src/client/index.ts +9 -1
  150. package/src/client/link.tsx +9 -9
  151. package/src/client/navigation-context.ts +2 -2
  152. package/src/client/router.ts +102 -55
  153. package/src/client/rsc-fetch.ts +63 -2
  154. package/src/client/segment-cache.ts +1 -1
  155. package/src/client/stale-reload.ts +28 -0
  156. package/src/client/top-loader.tsx +2 -2
  157. package/src/client/use-params.ts +3 -3
  158. package/src/client/use-query-states.ts +1 -1
  159. package/src/codec.ts +21 -0
  160. package/src/cookies/define-cookie.ts +69 -18
  161. package/src/index.ts +255 -65
  162. package/src/params/define.ts +260 -0
  163. package/src/params/index.ts +28 -0
  164. package/src/plugins/adapter-build.ts +6 -0
  165. package/src/plugins/build-manifest.ts +11 -0
  166. package/src/plugins/client-chunks.ts +65 -0
  167. package/src/plugins/entries.ts +3 -6
  168. package/src/plugins/routing.ts +40 -14
  169. package/src/plugins/server-bundle.ts +32 -1
  170. package/src/plugins/shims.ts +1 -1
  171. package/src/plugins/static-build.ts +8 -4
  172. package/src/routing/codegen.ts +109 -88
  173. package/src/routing/scanner.ts +55 -6
  174. package/src/routing/status-file-lint.ts +2 -1
  175. package/src/routing/types.ts +7 -4
  176. package/src/rsc-runtime/rsc.ts +2 -0
  177. package/src/search-params/codecs.ts +1 -1
  178. package/src/search-params/define.ts +504 -0
  179. package/src/search-params/index.ts +12 -18
  180. package/src/search-params/registry.ts +1 -1
  181. package/src/search-params/wrappers.ts +85 -0
  182. package/src/server/access-gate.tsx +38 -8
  183. package/src/server/action-encryption.ts +144 -0
  184. package/src/server/action-handler.ts +16 -0
  185. package/src/server/als-registry.ts +4 -4
  186. package/src/server/build-manifest.ts +4 -4
  187. package/src/server/compress.ts +25 -7
  188. package/src/server/early-hints.ts +36 -15
  189. package/src/server/error-boundary-wrapper.ts +57 -14
  190. package/src/server/flight-injection-state.ts +152 -0
  191. package/src/server/form-data.ts +76 -0
  192. package/src/server/html-injectors.ts +42 -26
  193. package/src/server/index.ts +2 -4
  194. package/src/server/node-stream-transforms.ts +91 -46
  195. package/src/server/pipeline.ts +98 -26
  196. package/src/server/request-context.ts +49 -124
  197. package/src/server/route-element-builder.ts +102 -99
  198. package/src/server/route-matcher.ts +2 -2
  199. package/src/server/rsc-entry/error-renderer.ts +3 -2
  200. package/src/server/rsc-entry/index.ts +26 -11
  201. package/src/server/rsc-entry/rsc-payload.ts +2 -2
  202. package/src/server/rsc-entry/ssr-renderer.ts +4 -4
  203. package/src/server/slot-resolver.ts +204 -206
  204. package/src/server/ssr-entry.ts +3 -1
  205. package/src/server/ssr-render.ts +3 -0
  206. package/src/server/tree-builder.ts +84 -48
  207. package/src/server/types.ts +1 -3
  208. package/src/server/version-skew.ts +104 -0
  209. package/src/shims/navigation-client.ts +1 -1
  210. package/src/shims/navigation.ts +1 -1
  211. package/src/utils/state-machine.ts +111 -0
  212. package/dist/_chunks/als-registry-B7DbZ2hS.js.map +0 -1
  213. package/dist/_chunks/interception-BOoWmLUA.js.map +0 -1
  214. package/dist/_chunks/request-context-BQUC8PHn.js.map +0 -1
  215. package/dist/_chunks/ssr-data-MjmprTmO.js +0 -88
  216. package/dist/_chunks/ssr-data-MjmprTmO.js.map +0 -1
  217. package/dist/_chunks/use-cookie-DX-l1_5E.js +0 -91
  218. package/dist/_chunks/use-cookie-DX-l1_5E.js.map +0 -1
  219. package/dist/client/error-boundary.js.map +0 -1
  220. package/dist/cookies/index.js.map +0 -1
  221. package/dist/plugins/dynamic-transform.d.ts +0 -72
  222. package/dist/plugins/dynamic-transform.d.ts.map +0 -1
  223. package/dist/search-params/analyze.d.ts +0 -54
  224. package/dist/search-params/analyze.d.ts.map +0 -1
  225. package/dist/search-params/builtin-codecs.d.ts +0 -105
  226. package/dist/search-params/builtin-codecs.d.ts.map +0 -1
  227. package/dist/search-params/create.d.ts +0 -106
  228. package/dist/search-params/create.d.ts.map +0 -1
  229. package/dist/search-params/index.js.map +0 -1
  230. package/dist/server/prerender.d.ts +0 -77
  231. package/dist/server/prerender.d.ts.map +0 -1
  232. package/src/plugins/dynamic-transform.ts +0 -161
  233. package/src/search-params/analyze.ts +0 -192
  234. package/src/search-params/builtin-codecs.ts +0 -228
  235. package/src/search-params/create.ts +0 -321
  236. package/src/server/prerender.ts +0 -139
@@ -47,6 +47,7 @@ import {
47
47
  NavigationProvider,
48
48
  getNavigationState,
49
49
  setNavigationState,
50
+ type NavigationState,
50
51
  } from './navigation-context.js';
51
52
  import { setupServerLogReplay, setupClientErrorForwarding } from './browser-dev.js';
52
53
  // browser-links.ts removed — Link components own their click/hover handlers directly.
@@ -54,9 +55,16 @@ import { setupServerLogReplay, setupClientErrorForwarding } from './browser-dev.
54
55
  import { TransitionRoot, transitionRender, navigateTransition } from './transition-root.js';
55
56
  import {
56
57
  isStaleClientReference,
58
+ isChunkLoadError,
57
59
  triggerStaleReload,
58
60
  clearStaleReloadFlag,
59
61
  } from './stale-reload.js';
62
+ import {
63
+ setClientDeploymentId,
64
+ getClientDeploymentId,
65
+ DEPLOYMENT_ID_HEADER,
66
+ RELOAD_HEADER,
67
+ } from './rsc-fetch.js';
60
68
 
61
69
  // ─── Server Action Dispatch ──────────────────────────────────────
62
70
 
@@ -85,14 +93,28 @@ setServerCallback(async (id: string, args: unknown[]) => {
85
93
  let hasRedirect = false;
86
94
  let headElementsJson: string | null = null;
87
95
 
96
+ // Build action request headers. Include deployment ID for version
97
+ // skew detection (TIM-446) — the server rejects stale actions gracefully.
98
+ const actionHeaders: Record<string, string> = {
99
+ 'Accept': 'text/x-component',
100
+ 'x-rsc-action': id,
101
+ };
102
+ const actionDeploymentId = getClientDeploymentId();
103
+ if (actionDeploymentId) {
104
+ actionHeaders[DEPLOYMENT_ID_HEADER] = actionDeploymentId;
105
+ }
106
+
88
107
  const response = fetch(window.location.href, {
89
108
  method: 'POST',
90
- headers: {
91
- 'Accept': 'text/x-component',
92
- 'x-rsc-action': id,
93
- },
109
+ headers: actionHeaders,
94
110
  body,
95
111
  }).then((res) => {
112
+ // Version skew detection (TIM-446): if the server signals a reload,
113
+ // trigger a full page load to pick up the new deployment.
114
+ if (res.headers.get(RELOAD_HEADER) === '1') {
115
+ window.location.reload();
116
+ throw new Error('Version skew detected — reloading page');
117
+ }
96
118
  hasRevalidation = res.headers.get('X-Timber-Revalidation') === '1';
97
119
  hasRedirect = res.headers.get('X-Timber-Redirect') != null;
98
120
  headElementsJson = res.headers.get('X-Timber-Head');
@@ -159,7 +181,17 @@ setServerCallback(async (id: string, args: unknown[]) => {
159
181
  * Hydrates the server-rendered HTML with React, then initializes
160
182
  * client-side navigation for SPA transitions.
161
183
  */
162
- /** Read scroll position from window or scroll containers. */
184
+ /**
185
+ * Read the current scroll position.
186
+ *
187
+ * Checks window scroll first, then explicit `data-timber-scroll-restoration`
188
+ * containers. With segment tree merging, shared layouts are reconciled in
189
+ * place via `cloneElement` — React preserves their DOM and scroll state
190
+ * naturally. We don't need to auto-detect overflow containers; only
191
+ * explicitly marked containers are tracked.
192
+ *
193
+ * See design/19-client-navigation.md §"Overflow Scroll Containers".
194
+ */
163
195
  function getScrollY(): number {
164
196
  if (window.scrollY || document.documentElement.scrollTop || document.body.scrollTop) {
165
197
  return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop;
@@ -167,71 +199,24 @@ function getScrollY(): number {
167
199
  for (const el of document.querySelectorAll('[data-timber-scroll-restoration]')) {
168
200
  if ((el as HTMLElement).scrollTop > 0) return (el as HTMLElement).scrollTop;
169
201
  }
170
- // Auto-detect: if window isn't scrolled, check for overflow containers.
171
- // Common pattern: layouts use a scrollable div (overflow-y: auto/scroll)
172
- // inside a fixed-height parent (h-screen). In this case window.scrollY is
173
- // always 0 and the real scroll position lives on the overflow container.
174
- const container = findOverflowContainer();
175
- if (container && container.scrollTop > 0) return container.scrollTop;
176
202
  return 0;
177
203
  }
178
204
 
179
- /**
180
- * Find the primary overflow scroll container in the document.
181
- *
182
- * Walks direct children of body and their immediate children looking for
183
- * an element with overflow-y: auto|scroll that is actually scrollable
184
- * (scrollHeight > clientHeight). Returns the first match, or null.
185
- *
186
- * This heuristic covers the common layout patterns:
187
- * <body> → <root-layout> → <div class="overflow-y-auto">
188
- * <body> → <root-layout> → <main> → <nested-layout overflow-y-auto>
189
- *
190
- * We limit depth to 3 to avoid expensive full-tree traversals while still
191
- * reaching nested layout scroll containers (e.g., parallel route layouts
192
- * inside a root layout's <main> element).
193
- *
194
- * DIVERGENCE FROM NEXT.JS: Next.js's ScrollAndFocusHandler scrolls only
195
- * document.documentElement.scrollTop — it does NOT handle overflow containers.
196
- * Layouts using h-screen + overflow-y-auto have the same scroll bug in Next.js.
197
- * This heuristic is a deliberate improvement. The tradeoff is fragility: depth-3
198
- * traversal may miss deeply nested containers or match the wrong element.
199
- * See design/19-client-navigation.md §"Overflow Scroll Containers".
200
- */
201
- function findOverflowContainer(): HTMLElement | null {
202
- const candidates: HTMLElement[] = [];
203
- // Check body's descendants up to depth 3. Depth 3 covers the common case:
204
- // <body> → <root-layout-div> → <main> → <overflow-container>
205
- // React context providers (SegmentProvider, NavigationProvider) don't add
206
- // DOM elements, so depth 3 from body reaches nested layout scroll containers.
207
- for (const child of document.body.children) {
208
- candidates.push(child as HTMLElement);
209
- for (const grandchild of child.children) {
210
- candidates.push(grandchild as HTMLElement);
211
- for (const greatGrandchild of grandchild.children) {
212
- candidates.push(greatGrandchild as HTMLElement);
213
- }
214
- }
215
- }
216
- for (const el of candidates) {
217
- if (!(el instanceof HTMLElement)) continue;
218
- const style = getComputedStyle(el);
219
- const overflowY = style.overflowY;
220
- if ((overflowY === 'auto' || overflowY === 'scroll') && el.scrollHeight > el.clientHeight) {
221
- return el;
222
- }
223
- }
224
- return null;
225
- }
226
-
227
205
  function bootstrap(runtimeConfig: typeof config): void {
228
206
  const _config = runtimeConfig;
229
207
 
230
- // Take manual control of scroll restoration. React's render() on the
231
- // document root resets scroll during DOM reconciliation, so the browser's
232
- // native scroll restoration (scrollRestoration = 'auto') doesn't work
233
- // the browser restores scroll, then React's commit resets it to 0.
234
- // We save/restore scroll positions explicitly in the history stack.
208
+ // Initialize deployment ID for version skew detection (TIM-446).
209
+ // In dev mode this is null skew checks are skipped.
210
+ const deploymentId = (_config as Record<string, unknown>).deploymentId as string | null;
211
+ if (deploymentId) {
212
+ setClientDeploymentId(deploymentId);
213
+ }
214
+
215
+ // Take manual control of scroll restoration. Even though segment tree
216
+ // merging preserves shared layout DOM via cloneElement (so React doesn't
217
+ // reset scroll on those elements), the root-level reactRoot.render() with
218
+ // a new element tree can still cause scroll resets on the document during
219
+ // reconciliation. Manual control ensures consistent behavior.
235
220
  window.history.scrollRestoration = 'manual';
236
221
 
237
222
  // Hydrate the React tree from the RSC payload.
@@ -247,6 +232,21 @@ function bootstrap(runtimeConfig: typeof config): void {
247
232
  // For subsequent navigations, it's fetched from the server.
248
233
  type FlightSegment = [isBootstrap: 0] | [isData: 1, data: string];
249
234
 
235
+ // On streaming pages, the browser entry module may load before the RSC
236
+ // payload scripts arrive. The <script id="_R_"> tag is in the shell (flushed
237
+ // on onShellReady), but the RSC bootstrap script `(self.__timber_f=...).push([0])`
238
+ // is injected by the flight injector AFTER Suspense resolution scripts.
239
+ // If the module import resolves before those scripts execute, __timber_f
240
+ // will be undefined.
241
+ //
242
+ // Fix: if __timber_f isn't available yet, pre-initialize it so the RSC
243
+ // bootstrap script's `(self.__timber_f=self.__timber_f||[]).push([0])` finds
244
+ // our array and pushes into it. This avoids a race condition that causes
245
+ // the browser entry to fall through to createRoot() (no hydration) on
246
+ // streaming pages.
247
+ if (!(self as unknown as Record<string, unknown>).__timber_f) {
248
+ (self as unknown as Record<string, FlightSegment[]>).__timber_f = [];
249
+ }
250
250
  const timberChunks = (self as unknown as Record<string, FlightSegment[]>).__timber_f;
251
251
 
252
252
  let _reactRoot: Root | null = null;
@@ -348,7 +348,7 @@ function bootstrap(runtimeConfig: typeof config): void {
348
348
 
349
349
  // ── Initialize navigation state BEFORE hydration ───────────────────
350
350
  // Read server-embedded params and set navigation state so that
351
- // useParams() and usePathname() return correct values during hydration.
351
+ // useSegmentParams() and usePathname() return correct values during hydration.
352
352
  // This must happen before hydrateRoot so the NavigationProvider
353
353
  // wrapping the element has the right values on the initial render.
354
354
  const earlyParams = (self as unknown as Record<string, unknown>).__timber_params;
@@ -424,23 +424,20 @@ function bootstrap(runtimeConfig: typeof config): void {
424
424
  pushState: (data, unused, url) => window.history.pushState(data, unused, url),
425
425
  replaceState: (data, unused, url) => window.history.replaceState(data, unused, url),
426
426
  scrollTo: (x, y) => {
427
+ // Scroll the document viewport.
427
428
  window.scrollTo(x, y);
428
429
  document.documentElement.scrollTop = y;
429
430
  document.body.scrollTop = y;
430
- // Also scroll any element explicitly marked as a scroll container.
431
+ // Scroll any element explicitly marked as a scroll container.
432
+ // With segment tree merging, shared layouts (sidebars, nav bars)
433
+ // are reconciled in place via cloneElement — React preserves their
434
+ // DOM and scroll state naturally. We no longer auto-detect overflow
435
+ // containers, which previously found the wrong element (e.g.,
436
+ // scrolling a sidebar instead of the main content area).
437
+ // Use `data-timber-scroll-restoration` to opt in specific containers.
431
438
  for (const el of document.querySelectorAll('[data-timber-scroll-restoration]')) {
432
439
  (el as HTMLElement).scrollTop = y;
433
440
  }
434
- // Auto-detect overflow containers for layouts that scroll inside
435
- // a fixed-height wrapper (e.g., h-screen + overflow-y-auto).
436
- // In these layouts, window.scrollY is always 0 and the real scroll
437
- // lives on the overflow container. Without this, forward navigation
438
- // between pages that share a layout with parallel route slots won't
439
- // scroll to top — the router's window.scrollTo(0,0) is a no-op.
440
- const container = findOverflowContainer();
441
- if (container) {
442
- container.scrollTop = y;
443
- }
444
441
  },
445
442
  getCurrentUrl: () => window.location.pathname + window.location.search,
446
443
  getScrollY,
@@ -475,8 +472,10 @@ function bootstrap(runtimeConfig: typeof config): void {
475
472
  // For navigation renders (navigate, refresh, popstate-with-fetch),
476
473
  // navigateTransition is used instead — it wraps the entire navigation
477
474
  // in a React transition with useOptimistic for the pending URL.
478
- renderRoot: (element: unknown) => {
479
- const navState = getNavigationState();
475
+ //
476
+ // navState is passed explicitly by the router — no temporal coupling
477
+ // with getNavigationState().
478
+ renderRoot: (element: unknown, navState: NavigationState) => {
480
479
  const withNav = createElement(
481
480
  NavigationProvider,
482
481
  { value: navState },
@@ -492,13 +491,11 @@ function bootstrap(runtimeConfig: typeof config): void {
492
491
  // commits (atomic with the new tree + params).
493
492
  //
494
493
  // The perform callback receives a wrapPayload function that wraps the
495
- // decoded RSC payload with NavigationProvider + NuqsAdapter — this must
496
- // happen inside the transition so the NavigationProvider reads the
497
- // UPDATED navigation state (set by the router inside perform).
494
+ // decoded RSC payload with NavigationProvider + NuqsAdapter. navState
495
+ // is passed explicitly by the router no getNavigationState() needed.
498
496
  navigateTransition: (pendingUrl: string, perform) => {
499
497
  return navigateTransition(pendingUrl, async () => {
500
- const payload = await perform((rawPayload: unknown) => {
501
- const navState = getNavigationState();
498
+ const payload = await perform((rawPayload: unknown, navState: NavigationState) => {
502
499
  const withNav = createElement(
503
500
  NavigationProvider,
504
501
  { value: navState },
@@ -590,7 +587,6 @@ function bootstrap(runtimeConfig: typeof config): void {
590
587
  scrollTimer = setTimeout(() => {
591
588
  const state = window.history.state;
592
589
  if (state && typeof state === 'object') {
593
- // Use getScrollY to capture scroll from overflow containers too.
594
590
  window.history.replaceState({ ...state, scrollY: getScrollY() }, '');
595
591
  }
596
592
  }, 100);
@@ -659,8 +655,21 @@ clearStaleReloadFlag();
659
655
  // If the payload references a module ID from a newer deployment, the error
660
656
  // surfaces as an unhandled rejection during React's render/hydration cycle.
661
657
  // This handler catches those errors and triggers a full page reload.
658
+ //
659
+ // Also catches chunk load failures (dynamic import of missing assets after
660
+ // a deployment) — these surface as "Failed to fetch dynamically imported module"
661
+ // or "Loading chunk <name> failed" errors. See TIM-446.
662
662
  window.addEventListener('unhandledrejection', (event) => {
663
- if (isStaleClientReference(event.reason)) {
663
+ if (isStaleClientReference(event.reason) || isChunkLoadError(event.reason)) {
664
+ event.preventDefault();
665
+ triggerStaleReload();
666
+ }
667
+ });
668
+
669
+ // Also catch synchronous errors from chunk loads (some browsers throw
670
+ // TypeError synchronously instead of via unhandled rejection).
671
+ window.addEventListener('error', (event) => {
672
+ if (isChunkLoadError(event.error)) {
664
673
  event.preventDefault();
665
674
  triggerStaleReload();
666
675
  }
@@ -65,7 +65,16 @@ type ParsedDigest = DenyDigest | RenderErrorDigest | RedirectDigest;
65
65
 
66
66
  export interface TimberErrorBoundaryProps {
67
67
  /** The component to render when an error is caught. */
68
- fallbackComponent: (...args: unknown[]) => ReactNode;
68
+ fallbackComponent?: (...args: unknown[]) => ReactNode;
69
+ /**
70
+ * Pre-rendered fallback element. Used for MDX status files which are server
71
+ * components and cannot be passed as function props across the RSC→client
72
+ * boundary. When set, rendered directly instead of calling fallbackComponent.
73
+ *
74
+ * See design/10-error-handling.md §"Status-Code File Variants" — MDX status
75
+ * files are server components by default (zero client JS).
76
+ */
77
+ fallbackElement?: ReactNode;
69
78
  /**
70
79
  * Status code filter. If set, only catches errors matching this status.
71
80
  * 400 = any 4xx, 500 = any 5xx, specific number = exact match.
@@ -162,6 +171,14 @@ export class TimberErrorBoundary extends Component<
162
171
  }
163
172
  }
164
173
 
174
+ // Pre-rendered fallback element (MDX status files) — render directly.
175
+ // MDX components are server components that cannot be passed as function
176
+ // props across the RSC→client boundary. Instead, they are pre-rendered
177
+ // as elements in the RSC environment and passed here as fallbackElement.
178
+ if (this.props.fallbackElement != null) {
179
+ return this.props.fallbackElement;
180
+ }
181
+
165
182
  // Render the fallback component with the right props shape.
166
183
  if (parsed?.type === 'deny') {
167
184
  return createElement(this.props.fallbackComponent as never, {
@@ -12,6 +12,7 @@ export type {
12
12
  RouterInstance,
13
13
  NavigationOptions,
14
14
  RouterDeps,
15
+ RouterPhase,
15
16
  RscDecoder,
16
17
  RootRenderer,
17
18
  } from './router';
@@ -42,7 +43,7 @@ export { useActionState, useFormAction, useFormErrors } from './form';
42
43
  export type { UseActionStateFn, UseActionStateReturn, FormErrorsResult } from './form';
43
44
 
44
45
  // Params
45
- export { useParams, setCurrentParams } from './use-params';
46
+ export { useSegmentParams, setCurrentParams } from './use-params';
46
47
 
47
48
  // Navigation context (framework-internal, used by browser-entry for atomic updates)
48
49
  export { NavigationProvider, getNavigationState, setNavigationState } from './navigation-context';
@@ -55,6 +56,13 @@ export { useQueryStates, bindUseQueryStates } from './use-query-states';
55
56
  export { useCookie } from './use-cookie';
56
57
  export type { ClientCookieOptions, CookieSetter } from './use-cookie';
57
58
 
59
+ // Register the client cookie module with defineCookie's lazy reference.
60
+ // This runs at module load time in the client/SSR environment, wiring up
61
+ // the useCookie hook without a top-level import in define-cookie.ts.
62
+ import * as _useCookieMod from './use-cookie.js';
63
+ import { _registerUseCookieModule } from '#/cookies/define-cookie.js';
64
+ _registerUseCookieModule(_useCookieMod);
65
+
58
66
  // SSR data (framework-internal, used by ssr-entry to provide request data to hooks)
59
67
  export { setSsrData, clearSsrData, getSsrData } from './ssr-data';
60
68
  export type { SsrData } from './ssr-data';
@@ -19,7 +19,7 @@
19
19
  // - searchParams and inline query string are mutually exclusive
20
20
 
21
21
  import type { AnchorHTMLAttributes, ReactNode, MouseEvent as ReactMouseEvent } from 'react';
22
- import type { SearchParamsDefinition } from '#/search-params/create.js';
22
+ import type { SearchParamsDefinition } from '#/search-params/define.js';
23
23
  import { LinkStatusProvider } from './link-status-provider.js';
24
24
  import { getRouterOrNull } from './router-ref.js';
25
25
 
@@ -61,7 +61,7 @@ interface LinkBaseProps extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, 'h
61
61
  */
62
62
  export interface LinkPropsWithHref extends LinkBaseProps {
63
63
  href: string;
64
- params?: never;
64
+ segmentParams?: never;
65
65
  /**
66
66
  * Typed search params — serialized via the route's SearchParamsDefinition.
67
67
  * Mutually exclusive with an inline query string in href.
@@ -73,9 +73,9 @@ export interface LinkPropsWithHref extends LinkBaseProps {
73
73
  }
74
74
 
75
75
  /**
76
- * Link with a route pattern + params for interpolation.
77
- * e.g. <Link href="/products/[id]" params={{ id: "123" }}>
78
- * <Link href="/products/[id]" params={{ id: 123 }}>
76
+ * Link with a route pattern + segmentParams for interpolation.
77
+ * e.g. <Link href="/products/[id]" segmentParams={{ id: "123" }}>
78
+ * <Link href="/products/[id]" segmentParams={{ id: 123 }}>
79
79
  */
80
80
  export interface LinkPropsWithParams extends LinkBaseProps {
81
81
  /** Route pattern with dynamic segments (e.g. "/products/[id]") */
@@ -85,7 +85,7 @@ export interface LinkPropsWithParams extends LinkBaseProps {
85
85
  * Single dynamic segments accept string | number (numbers are stringified).
86
86
  * Catch-all segments accept string[].
87
87
  */
88
- params: Record<string, string | number | string[]>;
88
+ segmentParams: Record<string, string | number | string[]>;
89
89
  /**
90
90
  * Typed search params — serialized via the route's SearchParamsDefinition.
91
91
  */
@@ -303,14 +303,14 @@ function shouldInterceptClick(
303
303
  * its own click handling.
304
304
  *
305
305
  * Supports typed routes via codegen overloads. At runtime:
306
- * - `params` prop interpolates dynamic segments in the href pattern
306
+ * - `segmentParams` prop interpolates dynamic segments in the href pattern
307
307
  * - `searchParams` prop serializes query parameters via a SearchParamsDefinition
308
308
  */
309
309
  export function Link({
310
310
  href,
311
311
  prefetch,
312
312
  scroll,
313
- params,
313
+ segmentParams,
314
314
  searchParams,
315
315
  onNavigate,
316
316
  onClick: userOnClick,
@@ -318,7 +318,7 @@ export function Link({
318
318
  children,
319
319
  ...rest
320
320
  }: LinkProps) {
321
- const { href: resolvedHref } = buildLinkProps({ href, params, searchParams });
321
+ const { href: resolvedHref } = buildLinkProps({ href, params: segmentParams, searchParams });
322
322
  const internal = isInternalHref(resolvedHref);
323
323
 
324
324
  // ─── Click handler ───────────────────────────────────────────
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Holds the current route params and pathname, updated atomically
7
7
  * with the RSC tree on each navigation. This replaces the previous
8
- * useSyncExternalStore approach for useParams() and usePathname(),
8
+ * useSyncExternalStore approach for useSegmentParams() and usePathname(),
9
9
  * which suffered from a timing gap: the new tree could commit before
10
10
  * the external store re-renders fired, causing a frame where both
11
11
  * old and new active states were visible simultaneously.
@@ -90,7 +90,7 @@ function getOrCreateContext(): React.Context<NavigationState | null> | undefined
90
90
  /**
91
91
  * Read the navigation context. Returns null during SSR (no provider)
92
92
  * or in the RSC environment (no context available).
93
- * Internal — used by useParams() and usePathname().
93
+ * Internal — used by useSegmentParams() and usePathname().
94
94
  */
95
95
  export function useNavigationContext(): NavigationState | null {
96
96
  const ctx = getOrCreateContext();