@timber-js/app 0.2.0-alpha.147 → 0.2.0-alpha.148

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 (274) hide show
  1. package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
  2. package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
  3. package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
  4. package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
  5. package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
  6. package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
  7. package/dist/_chunks/{cli-schema-sync-CQaxupHv.js → cli-schema-sync-DJDdyEae.js} +111 -260
  8. package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
  9. package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
  10. package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
  11. package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
  12. package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
  13. package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
  14. package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
  15. package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
  16. package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
  17. package/dist/_chunks/logger-CM0clugc.js.map +1 -0
  18. package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
  19. package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
  20. package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
  21. package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
  22. package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
  23. package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
  24. package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
  25. package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
  26. package/dist/_chunks/tree-match-D2l830j2.js +102 -0
  27. package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
  28. package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
  29. package/dist/_chunks/{walkers-Ck4Y45XN.js → walkers-DvWDj453.js} +107 -82
  30. package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
  31. package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
  32. package/dist/adapters/cloudflare-kv-cache.js +36 -14
  33. package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
  34. package/dist/adapters/compress-module.d.ts +5 -4
  35. package/dist/adapters/compress-module.d.ts.map +1 -1
  36. package/dist/adapters/nitro.d.ts +1 -3
  37. package/dist/adapters/nitro.d.ts.map +1 -1
  38. package/dist/adapters/nitro.js +14 -134
  39. package/dist/adapters/nitro.js.map +1 -1
  40. package/dist/cache/cache-api.d.ts.map +1 -1
  41. package/dist/cache/index.d.ts +3 -1
  42. package/dist/cache/index.d.ts.map +1 -1
  43. package/dist/cache/index.js +20 -235
  44. package/dist/cache/index.js.map +1 -1
  45. package/dist/cache/invalidation-epoch.d.ts +43 -0
  46. package/dist/cache/invalidation-epoch.d.ts.map +1 -0
  47. package/dist/cache/redis-handler.d.ts +13 -0
  48. package/dist/cache/redis-handler.d.ts.map +1 -1
  49. package/dist/cache/timber-cache.d.ts +17 -9
  50. package/dist/cache/timber-cache.d.ts.map +1 -1
  51. package/dist/cli-schema-sync.d.ts +21 -1
  52. package/dist/cli-schema-sync.d.ts.map +1 -1
  53. package/dist/cli.d.ts +12 -0
  54. package/dist/cli.d.ts.map +1 -1
  55. package/dist/cli.js +26 -31
  56. package/dist/cli.js.map +1 -1
  57. package/dist/client/browser-entry/hmr.d.ts.map +1 -1
  58. package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
  59. package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
  60. package/dist/client/browser-entry/router-init.d.ts.map +1 -1
  61. package/dist/client/history.d.ts +10 -17
  62. package/dist/client/history.d.ts.map +1 -1
  63. package/dist/client/index.js +1 -1
  64. package/dist/client/index.js.map +1 -1
  65. package/dist/client/internal.js +30 -33
  66. package/dist/client/internal.js.map +1 -1
  67. package/dist/client/navigation-api.d.ts +9 -2
  68. package/dist/client/navigation-api.d.ts.map +1 -1
  69. package/dist/client/navigation-context.d.ts +0 -11
  70. package/dist/client/navigation-context.d.ts.map +1 -1
  71. package/dist/client/navigation-root.d.ts +0 -5
  72. package/dist/client/navigation-root.d.ts.map +1 -1
  73. package/dist/client/router.d.ts.map +1 -1
  74. package/dist/client/rsc-fetch.d.ts.map +1 -1
  75. package/dist/client/use-search-params.d.ts +10 -5
  76. package/dist/client/use-search-params.d.ts.map +1 -1
  77. package/dist/codec.js +1 -1
  78. package/dist/config-validation.d.ts.map +1 -1
  79. package/dist/cookies/define-cookie.d.ts.map +1 -1
  80. package/dist/cookies/index.js +2 -4
  81. package/dist/cookies/index.js.map +1 -1
  82. package/dist/dev-tools/dev-page-shell.d.ts +1 -0
  83. package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
  84. package/dist/dev-tools/hydration-overlay.d.ts +11 -0
  85. package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
  86. package/dist/dev-tools/index.d.ts +1 -2
  87. package/dist/dev-tools/index.d.ts.map +1 -1
  88. package/dist/fonts/ast-walk.d.ts +68 -0
  89. package/dist/fonts/ast-walk.d.ts.map +1 -0
  90. package/dist/fonts/ast.d.ts +23 -11
  91. package/dist/fonts/ast.d.ts.map +1 -1
  92. package/dist/fonts/bundle.d.ts +3 -1
  93. package/dist/fonts/bundle.d.ts.map +1 -1
  94. package/dist/fonts/dev-middleware.d.ts +1 -1
  95. package/dist/fonts/dev-middleware.d.ts.map +1 -1
  96. package/dist/fonts/fallbacks.d.ts +0 -7
  97. package/dist/fonts/fallbacks.d.ts.map +1 -1
  98. package/dist/fonts/google.d.ts +4 -2
  99. package/dist/fonts/google.d.ts.map +1 -1
  100. package/dist/fonts/local.d.ts +3 -3
  101. package/dist/fonts/local.d.ts.map +1 -1
  102. package/dist/fonts/paths.d.ts +43 -0
  103. package/dist/fonts/paths.d.ts.map +1 -0
  104. package/dist/fonts/pipeline.d.ts +7 -21
  105. package/dist/fonts/pipeline.d.ts.map +1 -1
  106. package/dist/fonts/transform.d.ts.map +1 -1
  107. package/dist/fonts/virtual-modules.d.ts +1 -18
  108. package/dist/fonts/virtual-modules.d.ts.map +1 -1
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +609 -406
  111. package/dist/index.js.map +1 -1
  112. package/dist/params/index.js +1 -1
  113. package/dist/params/resolve-schema.d.ts.map +1 -1
  114. package/dist/plugin-context.d.ts +28 -5
  115. package/dist/plugin-context.d.ts.map +1 -1
  116. package/dist/plugins/cache.d.ts +13 -5
  117. package/dist/plugins/cache.d.ts.map +1 -1
  118. package/dist/plugins/dev-server.d.ts.map +1 -1
  119. package/dist/plugins/fonts.d.ts +1 -2
  120. package/dist/plugins/fonts.d.ts.map +1 -1
  121. package/dist/plugins/routing.d.ts.map +1 -1
  122. package/dist/routing/codegen.d.ts.map +1 -1
  123. package/dist/routing/convention-lint.d.ts.map +1 -1
  124. package/dist/routing/file-cache.d.ts +8 -0
  125. package/dist/routing/file-cache.d.ts.map +1 -0
  126. package/dist/routing/index.js +2 -2
  127. package/dist/routing/interception.d.ts +0 -5
  128. package/dist/routing/interception.d.ts.map +1 -1
  129. package/dist/routing/manifest-codegen.d.ts +32 -0
  130. package/dist/routing/manifest-codegen.d.ts.map +1 -0
  131. package/dist/routing/scanner.d.ts.map +1 -1
  132. package/dist/schema-bridge.d.ts +15 -0
  133. package/dist/schema-bridge.d.ts.map +1 -1
  134. package/dist/search-params/define.d.ts.map +1 -1
  135. package/dist/search-params/index.js +7 -3
  136. package/dist/search-params/index.js.map +1 -1
  137. package/dist/search-params/wrappers.d.ts.map +1 -1
  138. package/dist/segment-params/define.d.ts.map +1 -1
  139. package/dist/segment-params/index.js +1 -1
  140. package/dist/server/access-gate.d.ts.map +1 -1
  141. package/dist/server/actions.d.ts +0 -14
  142. package/dist/server/actions.d.ts.map +1 -1
  143. package/dist/server/build-manifest.d.ts +0 -29
  144. package/dist/server/build-manifest.d.ts.map +1 -1
  145. package/dist/server/canonicalize.d.ts +10 -0
  146. package/dist/server/canonicalize.d.ts.map +1 -1
  147. package/dist/server/deny-renderer.d.ts +1 -1
  148. package/dist/server/deny-renderer.d.ts.map +1 -1
  149. package/dist/server/html-injector-core.d.ts +8 -0
  150. package/dist/server/html-injector-core.d.ts.map +1 -1
  151. package/dist/server/html-injectors.d.ts.map +1 -1
  152. package/dist/server/index.js +2 -2
  153. package/dist/server/internal.d.ts +1 -5
  154. package/dist/server/internal.d.ts.map +1 -1
  155. package/dist/server/internal.js +12 -171
  156. package/dist/server/internal.js.map +1 -1
  157. package/dist/server/node-stream-transforms.d.ts +0 -11
  158. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  159. package/dist/server/render-utils.d.ts +25 -0
  160. package/dist/server/render-utils.d.ts.map +1 -0
  161. package/dist/server/route-element-builder.d.ts +11 -25
  162. package/dist/server/route-element-builder.d.ts.map +1 -1
  163. package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
  164. package/dist/server/rsc-entry/helpers.d.ts +2 -18
  165. package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
  166. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  167. package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
  168. package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
  169. package/dist/server/ssr-render.d.ts.map +1 -1
  170. package/dist/shared/href-validation.d.ts.map +1 -1
  171. package/dist/shims/image.d.ts +15 -15
  172. package/package.json +2 -2
  173. package/src/adapters/cloudflare-kv-cache.ts +56 -19
  174. package/src/adapters/compress-module.ts +27 -118
  175. package/src/adapters/nitro.ts +3 -41
  176. package/src/adapters/raw-imports.d.ts +5 -0
  177. package/src/cache/cache-api.ts +9 -1
  178. package/src/cache/index.ts +5 -2
  179. package/src/cache/invalidation-epoch.ts +93 -0
  180. package/src/cache/redis-handler.ts +33 -2
  181. package/src/cache/timber-cache.ts +109 -53
  182. package/src/cli-schema-sync.ts +123 -30
  183. package/src/cli.ts +33 -37
  184. package/src/client/browser-entry/hmr.ts +0 -11
  185. package/src/client/browser-entry/hydrate.ts +39 -5
  186. package/src/client/browser-entry/post-hydration.ts +6 -15
  187. package/src/client/browser-entry/router-init.ts +98 -3
  188. package/src/client/history.ts +10 -24
  189. package/src/client/navigation-api.ts +14 -9
  190. package/src/client/navigation-context.ts +1 -21
  191. package/src/client/navigation-root.tsx +0 -8
  192. package/src/client/router.ts +18 -8
  193. package/src/client/rsc-fetch.ts +7 -2
  194. package/src/client/use-search-params.ts +10 -5
  195. package/src/config-validation.ts +33 -25
  196. package/src/cookies/define-cookie.ts +2 -18
  197. package/src/dev-tools/dev-page-shell.ts +6 -2
  198. package/src/dev-tools/hydration-overlay.ts +158 -0
  199. package/src/dev-tools/index.ts +3 -16
  200. package/src/fonts/ast-walk.ts +235 -0
  201. package/src/fonts/ast.ts +91 -249
  202. package/src/fonts/bundle.ts +16 -9
  203. package/src/fonts/dev-middleware.ts +16 -5
  204. package/src/fonts/fallbacks.ts +0 -23
  205. package/src/fonts/google.ts +7 -3
  206. package/src/fonts/local.ts +7 -6
  207. package/src/fonts/paths.ts +58 -0
  208. package/src/fonts/pipeline.ts +27 -48
  209. package/src/fonts/transform.ts +27 -0
  210. package/src/fonts/virtual-modules.ts +1 -57
  211. package/src/index.ts +5 -11
  212. package/src/params/resolve-schema.ts +2 -34
  213. package/src/plugin-context.ts +95 -17
  214. package/src/plugins/cache.ts +64 -17
  215. package/src/plugins/dev-server.ts +61 -9
  216. package/src/plugins/fonts.ts +21 -6
  217. package/src/plugins/routing.ts +5 -348
  218. package/src/routing/codegen.ts +3 -2
  219. package/src/routing/convention-lint.ts +4 -3
  220. package/src/routing/file-cache.ts +23 -0
  221. package/src/routing/interception.ts +2 -21
  222. package/src/routing/manifest-codegen.ts +290 -0
  223. package/src/routing/scanner.ts +5 -17
  224. package/src/routing/schema-validation.ts +2 -2
  225. package/src/schema-bridge.ts +63 -2
  226. package/src/search-params/define.ts +34 -2
  227. package/src/search-params/wrappers.ts +21 -3
  228. package/src/segment-params/define.ts +2 -62
  229. package/src/server/access-gate.tsx +8 -0
  230. package/src/server/actions.ts +8 -31
  231. package/src/server/build-manifest.ts +0 -56
  232. package/src/server/canonicalize.ts +6 -3
  233. package/src/server/deny-renderer.ts +6 -0
  234. package/src/server/html-injector-core.ts +19 -0
  235. package/src/server/html-injectors.ts +14 -4
  236. package/src/server/internal.ts +1 -18
  237. package/src/server/node-stream-transforms.ts +5 -72
  238. package/src/server/render-utils.ts +31 -0
  239. package/src/server/route-element-builder.ts +77 -47
  240. package/src/server/rsc-entry/error-renderer.ts +1 -0
  241. package/src/server/rsc-entry/helpers.ts +6 -34
  242. package/src/server/rsc-entry/index.ts +11 -0
  243. package/src/server/rsc-entry/render-route.ts +7 -9
  244. package/src/server/rsc-entry/rsc-payload.ts +2 -1
  245. package/src/server/rsc-entry/ssr-renderer.ts +2 -2
  246. package/src/server/ssr-render.ts +1 -11
  247. package/src/shared/href-validation.ts +8 -0
  248. package/dist/_chunks/cli-schema-sync-CBiMEtqa.js +0 -2
  249. package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +0 -1
  250. package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
  251. package/dist/_chunks/define-DMVhsipD.js.map +0 -1
  252. package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
  253. package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
  254. package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
  255. package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
  256. package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
  257. package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
  258. package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
  259. package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
  260. package/dist/_chunks/walkers-Ck4Y45XN.js.map +0 -1
  261. package/dist/_chunks/warnings-CNPXaypM.js +0 -125
  262. package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
  263. package/dist/client/link-pending-store.d.ts +0 -65
  264. package/dist/client/link-pending-store.d.ts.map +0 -1
  265. package/dist/client/nav-link-store.d.ts +0 -27
  266. package/dist/client/nav-link-store.d.ts.map +0 -1
  267. package/dist/dev-tools/warnings.d.ts +0 -109
  268. package/dist/dev-tools/warnings.d.ts.map +0 -1
  269. package/dist/server/flush.d.ts +0 -74
  270. package/dist/server/flush.d.ts.map +0 -1
  271. package/src/client/link-pending-store.ts +0 -116
  272. package/src/client/nav-link-store.ts +0 -37
  273. package/src/dev-tools/warnings.ts +0 -238
  274. package/src/server/flush.ts +0 -183
@@ -18,7 +18,6 @@
18
18
  */
19
19
 
20
20
  import type { NavigationApi, NavigateEvent } from './navigation-api-types.js';
21
- import { consumeNavLinkMetadata } from './nav-link-store.js';
22
21
  import { isHardNavigating } from './navigation-root.js';
23
22
 
24
23
  // ─── Feature Detection ───────────────────────────────────────────
@@ -55,8 +54,8 @@ export function getNavigationApi(): NavigationApi | null {
55
54
  export interface NavigationApiCallbacks {
56
55
  /**
57
56
  * Handle a push/replace navigation intercepted by the Navigation API.
58
- * This covers both Link <a> clicks (user-initiated, with metadata from
59
- * nav-link-store) and external navigations (plain <a> tags, programmatic).
57
+ * This covers both Link <a> clicks (user-initiated) and external
58
+ * navigations (plain <a> tags, programmatic).
60
59
  * The Navigation API handles the URL update via event.intercept().
61
60
  */
62
61
  onExternalNavigate: (
@@ -69,6 +68,14 @@ export interface NavigationApiCallbacks {
69
68
  * the traversal and delegates to us for RSC replay/fetch.
70
69
  */
71
70
  onTraverse: (url: string, scrollY: number, signal: AbortSignal) => Promise<void>;
71
+
72
+ /**
73
+ * Called when a shallow URL update is intercepted (e.g., nuqs with
74
+ * shallow: true, or replaceUrl). The URL has already been committed —
75
+ * this callback syncs NavigationContext.search so useSearchParams()
76
+ * reflects the new value without a full router navigation.
77
+ */
78
+ onShallowNavigate?: (url: string) => void;
72
79
  }
73
80
 
74
81
  /**
@@ -187,6 +194,7 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
187
194
  focusReset: 'manual',
188
195
  scroll: 'manual',
189
196
  });
197
+ callbacks.onShallowNavigate?.(event.destination.url);
190
198
  return;
191
199
  }
192
200
 
@@ -239,11 +247,8 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
239
247
  },
240
248
  });
241
249
  } else if (event.navigationType === 'push' || event.navigationType === 'replace') {
242
- // Push/replace — either a Link <a> click (with metadata in
243
- // nav-link-store) or an external navigation (plain <a>, programmatic).
244
- // Consume link metadata if present — tells us scroll preference
245
- // and which Link component to track pending state for.
246
- const linkMeta = consumeNavLinkMetadata();
250
+ // Push/replace — a Link <a> click or an external navigation
251
+ // (plain <a> tag, programmatic).
247
252
 
248
253
  // Save the departing page's scroll position BEFORE event.intercept()
249
254
  // commits the URL change. Once intercept() is called, currentEntry
@@ -273,7 +278,7 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
273
278
  await callbacks.onExternalNavigate(url + destUrl.hash, {
274
279
  replace: event.navigationType === 'replace',
275
280
  signal: event.signal,
276
- scroll: linkMeta?.scroll,
281
+ scroll: undefined,
277
282
  });
278
283
  },
279
284
  });
@@ -176,7 +176,7 @@ export function getNavigationState(): NavigationState {
176
176
  * Separate context for the in-flight navigation URL. Provided by
177
177
  * NavigationRoot (urgent useState), consumed by usePendingNavigation
178
178
  * and TopLoader. Per-link pending state uses useOptimistic instead
179
- * (see link-pending-store.ts).
179
+ * (see design/19-client-navigation.md §"Per-Link Pending State").
180
180
  *
181
181
  * Uses globalThis via Symbol.for for the same reason as NavigationContext
182
182
  * above — the bundler may duplicate this module across chunks, and module-
@@ -225,23 +225,3 @@ export function PendingNavigationProvider({
225
225
  }
226
226
  return createElement(ctx.Provider, { value }, children);
227
227
  }
228
-
229
- // ---------------------------------------------------------------------------
230
- // Navigation API transition state (optional progressive enhancement)
231
- // ---------------------------------------------------------------------------
232
-
233
- /**
234
- * Check if the browser's Navigation API has an active transition.
235
- *
236
- * When the Navigation API is available and a navigation has been intercepted
237
- * via event.intercept(), `navigation.transition` is non-null until the
238
- * handler resolves. This provides browser-native progress tracking that
239
- * can be used alongside the existing pendingUrl mechanism.
240
- *
241
- * Returns false when Navigation API is unavailable or no transition is active.
242
- */
243
- export function hasNativeNavigationTransition(): boolean {
244
- if (typeof window === 'undefined') return false;
245
- const nav = (window as unknown as { navigation?: { transition?: unknown } }).navigation;
246
- return nav?.transition != null;
247
- }
@@ -324,14 +324,6 @@ export function navigateTransition(
324
324
  return perform().then(() => {});
325
325
  }
326
326
 
327
- /**
328
- * Check if the NavigationRoot is mounted and ready for renders.
329
- * Used by browser-entry.ts to guard against renders before hydration.
330
- */
331
- export function isNavigationRootReady(): boolean {
332
- return _transitionRender !== null;
333
- }
334
-
335
327
  /**
336
328
  * Install one-shot deferred callbacks for the no-RSC bootstrap path (TIM-600).
337
329
  *
@@ -778,16 +778,21 @@ export function createRouter(deps: RouterDeps): RouterInstance {
778
778
  * Called on hover of <Link prefetch> elements.
779
779
  */
780
780
  function prefetch(url: string): void {
781
+ // Strip fragment — it's client-only and would swallow the _rsc cache-bust
782
+ // param into the hash. The hash-less key also matches navigate()'s fetchUrl.
783
+ const hashIndex = url.indexOf('#');
784
+ const fetchUrl = hashIndex === -1 ? url : url.slice(0, hashIndex);
785
+
781
786
  // Don't prefetch if already cached
782
- if (prefetchCache.get(url) !== undefined) return;
783
- if (historyStack.has(url)) return;
787
+ if (prefetchCache.get(fetchUrl) !== undefined) return;
788
+ if (historyStack.has(fetchUrl)) return;
784
789
 
785
790
  // Fire-and-forget fetch
786
791
  const stateTree = segmentCache.serializeStateTree();
787
- void fetchRscPayload(url, deps, stateTree).then(
792
+ void fetchRscPayload(fetchUrl, deps, stateTree).then(
788
793
  (result) => {
789
794
  result.decodePromise?.catch(() => {});
790
- prefetchCache.set(url, result);
795
+ prefetchCache.set(fetchUrl, result);
791
796
  },
792
797
  () => {
793
798
  // Prefetch failure is non-fatal — navigation will fetch fresh
@@ -813,15 +818,20 @@ export function createRouter(deps: RouterDeps): RouterInstance {
813
818
  // Revalidation payloads are already resolved (no Flight thenable),
814
819
  // so no segment merging is needed — just cache and render.
815
820
  const currentUrl = deps.getCurrentUrl();
816
- historyStack.push(currentUrl, {
817
- payload: element,
818
- headElements,
819
- });
820
821
 
821
822
  // Revalidation doesn't change params/pathname — preserve current state.
822
823
  // DO NOT call updateNavigationState(null, ...) here: that normalizes
823
824
  // params to {}, clearing dynamic route params on every action response.
824
825
  const navState = getNavigationState();
826
+
827
+ // Keep params on the overwritten entry — dropping them would make
828
+ // away-and-back navigation replay this page with params normalized
829
+ // to {} (TIM-1037).
830
+ historyStack.push(currentUrl, {
831
+ payload: element,
832
+ headElements,
833
+ params: navState.params,
834
+ });
825
835
  renderPayload(element, navState);
826
836
  applyHead(headElements);
827
837
  },
@@ -61,10 +61,15 @@ function generateCacheBustId(): string {
61
61
  * Append a `_rsc=<id>` query parameter to the URL.
62
62
  * Follows Next.js's pattern — prevents CDN/browser from serving cached HTML
63
63
  * for RSC navigation requests and signals that this is an RSC fetch.
64
+ *
65
+ * Strips any #fragment before appending — fragments are client-only and
66
+ * fetch() discards them, so _rsc would land inside the hash and be lost.
64
67
  */
65
68
  function appendRscParam(url: string): string {
66
- const separator = url.includes('?') ? '&' : '?';
67
- return `${url}${separator}_rsc=${generateCacheBustId()}`;
69
+ const hashIndex = url.indexOf('#');
70
+ const urlWithoutHash = hashIndex === -1 ? url : url.slice(0, hashIndex);
71
+ const separator = urlWithoutHash.includes('?') ? '&' : '?';
72
+ return `${urlWithoutHash}${separator}_rsc=${generateCacheBustId()}`;
68
73
  }
69
74
 
70
75
  // ─── Deployment ID ───────────────────────────────────────────────
@@ -5,11 +5,16 @@
5
5
  * URL's query string. Updates when client-side navigation changes the URL.
6
6
  *
7
7
  * On the client, reads from NavigationContext which is updated atomically
8
- * with the RSC tree render. This replaces the previous useSyncExternalStore
9
- * approach which read window.location.search directly — causing React to
10
- * detect external store tearing during transitions and fall back to
11
- * synchronous rendering (renderRootSync instead of renderRootConcurrent),
12
- * blocking the main thread and freezing animations.
8
+ * with the RSC tree render during full navigations, AND by
9
+ * syncShallowSearch() for shallow URL updates (nuqs shallow: true,
10
+ * replaceUrl, or any external pushState/replaceState that changes the
11
+ * query string). See router-init.ts.
12
+ *
13
+ * This replaces the previous useSyncExternalStore approach which read
14
+ * window.location.search directly — causing React to detect external
15
+ * store tearing during transitions and fall back to synchronous rendering
16
+ * (renderRootSync instead of renderRootConcurrent), blocking the main
17
+ * thread and freezing animations.
13
18
  *
14
19
  * Unlike Next.js's ReadonlyURLSearchParams, this returns a standard
15
20
  * URLSearchParams. Mutation methods (set, delete, append) work on the
@@ -20,6 +20,39 @@ export interface ConfigError {
20
20
  suggestion?: string;
21
21
  }
22
22
 
23
+ // ─── Known config keys ──────────────────────────────────────────────────────
24
+
25
+ // `satisfies Record<keyof TimberUserConfig, true>` makes this drift-proof:
26
+ // adding a key to TimberUserConfig without listing it here is a compile
27
+ // error, so validateConfig can never reject a documented option (TIM-1066).
28
+ const KNOWN_CONFIG_KEYS = {
29
+ output: true,
30
+ debug: true,
31
+ clientJavascript: true,
32
+ adapter: true,
33
+ cacheHandler: true,
34
+ allowedOrigins: true,
35
+ csrf: true,
36
+ limits: true,
37
+ actions: true,
38
+ forms: true,
39
+ pageExtensions: true,
40
+ slowRequestMs: true,
41
+ renderTimeoutMs: true,
42
+ devBrowserLogs: true,
43
+ dev: true,
44
+ serverTiming: true,
45
+ appDir: true,
46
+ mdx: true,
47
+ actionEncryption: true,
48
+ reactCompiler: true,
49
+ sitemap: true,
50
+ buildDir: true,
51
+ topLoader: true,
52
+ } satisfies Record<keyof TimberUserConfig, true>;
53
+
54
+ const knownKeys = new Set<string>(Object.keys(KNOWN_CONFIG_KEYS));
55
+
23
56
  // ─── Validation ─────────────────────────────────────────────────────────────
24
57
 
25
58
  /**
@@ -147,31 +180,6 @@ export function validateConfig(config: TimberUserConfig): ConfigError[] {
147
180
  }
148
181
  }
149
182
 
150
- // Unknown top-level keys
151
- const knownKeys = new Set([
152
- 'output',
153
- 'debug',
154
- 'clientJavascript',
155
- 'adapter',
156
- 'cacheHandler',
157
- 'allowedOrigins',
158
- 'csrf',
159
- 'limits',
160
- 'pageExtensions',
161
- 'slowRequestMs',
162
- 'renderTimeoutMs',
163
- 'devBrowserLogs',
164
- 'dev',
165
- 'serverTiming',
166
- 'appDir',
167
- 'mdx',
168
- 'actionEncryption',
169
- 'reactCompiler',
170
- 'sitemap',
171
- 'buildDir',
172
- 'topLoader',
173
- ]);
174
-
175
183
  for (const key of Object.keys(config)) {
176
184
  if (!knownKeys.has(key)) {
177
185
  errors.push({
@@ -24,6 +24,7 @@ import type { ClientCookieOptions } from '../client/use-cookie.js';
24
24
 
25
25
  import type { Codec } from '../codec.js';
26
26
  import type { StandardSchemaV1 } from '../schema-bridge.js';
27
+ import { resolveCodecOrSchema } from '../schema-bridge.js';
27
28
 
28
29
  /**
29
30
  * A codec that converts between string cookie values and typed values.
@@ -87,7 +88,6 @@ export interface CookieDefinition<T> extends ServerOnlyCookieDefinition<T> {
87
88
  // Schema: fromSchema is a pure function — safe in all environments.
88
89
 
89
90
  import { getCookieJar } from '@timber-js/app/server';
90
- import { fromSchema } from '../schema-bridge.js';
91
91
  import {
92
92
  useCookie as useRawCookie,
93
93
  notifyCookieChange,
@@ -147,23 +147,7 @@ function deleteClientCookie(name: string, options?: ClientCookieOptions): void {
147
147
  // ─── Standard Schema Auto-Detection ───────────────────────────────────────
148
148
 
149
149
  function resolveCodec<T>(codecOrSchema: CookieCodec<T> | StandardSchemaV1<T>): CookieCodec<T> {
150
- // If it has parse + serialize, it's already a Codec
151
- if (
152
- typeof (codecOrSchema as Codec<T>).parse === 'function' &&
153
- typeof (codecOrSchema as Codec<T>).serialize === 'function'
154
- ) {
155
- return codecOrSchema as CookieCodec<T>;
156
- }
157
-
158
- // Auto-detect Standard Schema
159
- if (typeof codecOrSchema === 'object' && codecOrSchema !== null && '~standard' in codecOrSchema) {
160
- return fromSchema(codecOrSchema as StandardSchemaV1<unknown>) as CookieCodec<T>;
161
- }
162
-
163
- throw new Error(
164
- '[timber] defineCookie: codec must be a Codec<T> (with parse/serialize methods) ' +
165
- 'or a Standard Schema object (Zod, Valibot, ArkType).'
166
- );
150
+ return resolveCodecOrSchema('codec', codecOrSchema, 'search') as CookieCodec<T>;
167
151
  }
168
152
 
169
153
  // ─── Factory ──────────────────────────────────────────────────────────────
@@ -37,7 +37,11 @@ export interface DevErrorHmrOptions {
37
37
  * so the auto-reload WebSocket connects to the correct endpoint.
38
38
  */
39
39
  export function extractHmrOptions(config: {
40
- server?: { hmr?: { protocol?: string; host?: string; port?: number; path?: string } | boolean };
40
+ server?: {
41
+ hmr?:
42
+ | { protocol?: string; host?: string; port?: number; clientPort?: number; path?: string }
43
+ | boolean;
44
+ };
41
45
  webSocketToken?: string;
42
46
  }): DevErrorHmrOptions | undefined {
43
47
  const hmr = config.server?.hmr;
@@ -46,7 +50,7 @@ export function extractHmrOptions(config: {
46
50
  const opts: DevErrorHmrOptions = {
47
51
  protocol: hmrOpts.protocol,
48
52
  host: hmrOpts.host,
49
- port: hmrOpts.port,
53
+ port: hmrOpts.clientPort || hmrOpts.port,
50
54
  path: hmrOpts.path,
51
55
  token: config.webSocketToken,
52
56
  };
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Hydration mismatch diff overlay — dev-only.
3
+ *
4
+ * Parses React 19's structured diff from hydration error messages and
5
+ * renders a color-coded + Client / - Server diff in a dismissible overlay.
6
+ *
7
+ * See design/21-dev-server.md §"Error Overlay"
8
+ */
9
+
10
+ const REACT_HYDRATION_LINK = 'https://react.dev/link/hydration-mismatch';
11
+
12
+ const HYDRATION_PATTERNS = [
13
+ /^In HTML, (.+?) cannot be a child of <(.+?)>\./,
14
+ /^In HTML, (.+?) cannot be a descendant of <(.+?)>\./,
15
+ /^In HTML, text nodes cannot be a child of <(.+?)>\./,
16
+ /^In HTML, whitespace text nodes cannot be a child of <(.+?)>\./,
17
+ /Hydration failed because the server rendered (text|HTML) didn't match the client\./,
18
+ /A tree hydrated but some attributes of the server rendered HTML didn't match the client properties/,
19
+ ];
20
+
21
+ export function isHydrationError(error: unknown): boolean {
22
+ if (!error || typeof error !== 'object') return false;
23
+ const msg = (error as { message?: string }).message;
24
+ if (typeof msg !== 'string') return false;
25
+ return HYDRATION_PATTERNS.some((re) => re.test(msg));
26
+ }
27
+
28
+ interface ParsedHydrationError {
29
+ message: string;
30
+ diff: string | null;
31
+ }
32
+
33
+ function parseHydrationError(msg: string): ParsedHydrationError {
34
+ const hasComponentStackDiff = [
35
+ /^In HTML, (.+?) cannot be a child of <(.+?)>\.(.*)\nThis will cause a hydration error\.(.*)/,
36
+ /^In HTML, (.+?) cannot be a descendant of <(.+?)>\.\nThis will cause a hydration error\.(.*)/,
37
+ /^In HTML, text nodes cannot be a child of <(.+?)>\.\nThis will cause a hydration error\./,
38
+ /^In HTML, whitespace text nodes cannot be a child of <(.+?)>\./,
39
+ ].some((re) => re.test(msg));
40
+
41
+ if (hasComponentStackDiff) {
42
+ const [message, diffLog = ''] = msg.split('\n\n');
43
+ const diff = diffLog.trim();
44
+ return { message: message.trim(), diff: diff || null };
45
+ }
46
+
47
+ const [before, after] = msg.split(REACT_HYDRATION_LINK);
48
+ if (after !== undefined && after.length > 1) {
49
+ const lines: string[] = [];
50
+ for (const line of after.split('\n')) {
51
+ const trimmed = line.trim();
52
+ if (trimmed === '' || trimmed.startsWith('at ')) continue;
53
+ lines.push(line);
54
+ }
55
+ return { message: before.trim(), diff: lines.join('\n') || null };
56
+ }
57
+
58
+ return { message: msg.trim(), diff: null };
59
+ }
60
+
61
+ let overlayEl: HTMLDivElement | null = null;
62
+ let errors: ParsedHydrationError[] = [];
63
+
64
+ function escapeHtml(s: string): string {
65
+ return s
66
+ .replace(/&/g, '&amp;')
67
+ .replace(/</g, '&lt;')
68
+ .replace(/>/g, '&gt;')
69
+ .replace(/"/g, '&quot;');
70
+ }
71
+
72
+ function renderDiffHtml(diff: string): string {
73
+ return diff
74
+ .split('\n')
75
+ .map((line) => {
76
+ const trimmed = line.trimStart();
77
+ if (trimmed.startsWith('+')) {
78
+ return `<span style="color:#4ade80;display:block">${escapeHtml(line)}</span>`;
79
+ }
80
+ if (trimmed.startsWith('-')) {
81
+ return `<span style="color:#f87171;display:block">${escapeHtml(line)}</span>`;
82
+ }
83
+ return `<span style="color:#a1a1aa;display:block">${escapeHtml(line)}</span>`;
84
+ })
85
+ .join('');
86
+ }
87
+
88
+ function render(): void {
89
+ if (errors.length === 0) {
90
+ if (overlayEl) {
91
+ overlayEl.remove();
92
+ overlayEl = null;
93
+ }
94
+ return;
95
+ }
96
+
97
+ if (!overlayEl) {
98
+ overlayEl = document.createElement('div');
99
+ overlayEl.setAttribute('data-timber-hydration-overlay', '');
100
+ document.body.appendChild(overlayEl);
101
+ }
102
+
103
+ const count = errors.length;
104
+ const errorsHtml = errors
105
+ .map(
106
+ (err, i) => `
107
+ <div style="margin-bottom:${i < count - 1 ? '16px' : '0'};padding-bottom:${i < count - 1 ? '16px' : '0'};border-bottom:${i < count - 1 ? '1px solid #333' : 'none'}">
108
+ <div style="color:#fbbf24;font-weight:600;margin-bottom:8px">${escapeHtml(err.message)}</div>
109
+ ${
110
+ err.diff
111
+ ? `<div style="margin-top:8px">
112
+ <div style="display:flex;gap:16px;margin-bottom:6px;font-size:11px">
113
+ <span><span style="color:#4ade80">+</span> Client</span>
114
+ <span><span style="color:#f87171">-</span> Server</span>
115
+ </div>
116
+ <pre style="margin:0;padding:10px;background:#1a1a1a;border-radius:4px;overflow-x:auto;font-size:12px;line-height:1.5">${renderDiffHtml(err.diff)}</pre>
117
+ </div>`
118
+ : ''
119
+ }
120
+ </div>`
121
+ )
122
+ .join('');
123
+
124
+ overlayEl.innerHTML = `
125
+ <div style="position:fixed;inset:0;background:rgba(0,0,0,0.6);z-index:2147483646;display:flex;align-items:center;justify-content:center;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif" data-timber-hydration-backdrop>
126
+ <div style="background:#18181b;color:#e4e4e7;border:1px solid #333;border-radius:8px;max-width:720px;width:90vw;max-height:80vh;display:flex;flex-direction:column;box-shadow:0 8px 32px rgba(0,0,0,0.5)">
127
+ <div style="display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid #333">
128
+ <div style="display:flex;align-items:center;gap:8px">
129
+ <span style="background:#dc2626;color:#fff;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700;text-transform:uppercase">Hydration Mismatch</span>
130
+ ${count > 1 ? `<span style="color:#71717a;font-size:12px">${count} errors</span>` : ''}
131
+ </div>
132
+ <button data-timber-hydration-close style="background:none;border:1px solid #333;color:#a1a1aa;cursor:pointer;padding:4px 10px;border-radius:4px;font-size:12px;line-height:1">Dismiss</button>
133
+ </div>
134
+ <div style="padding:16px;overflow-y:auto;font-size:13px;line-height:1.6">
135
+ ${errorsHtml}
136
+ </div>
137
+ </div>
138
+ </div>`;
139
+
140
+ overlayEl.querySelector('[data-timber-hydration-close]')?.addEventListener('click', dismiss);
141
+ overlayEl.querySelector('[data-timber-hydration-backdrop]')?.addEventListener('click', (e) => {
142
+ if ((e.target as HTMLElement).hasAttribute('data-timber-hydration-backdrop')) dismiss();
143
+ });
144
+ }
145
+
146
+ function dismiss(): void {
147
+ errors = [];
148
+ render();
149
+ }
150
+
151
+ export function showHydrationError(error: unknown): void {
152
+ if (!error || typeof error !== 'object') return;
153
+ const msg = (error as { message?: string }).message;
154
+ if (typeof msg !== 'string') return;
155
+ const parsed = parseHydrationError(msg);
156
+ errors.push(parsed);
157
+ render();
158
+ }
@@ -8,7 +8,6 @@
8
8
  * - 404-page: self-contained HTML 404 page
9
9
  * - terminal: boxed terminal error output
10
10
  * - logger: structured request tree logger
11
- * - warnings: dev-mode footgun warnings
12
11
  * - browser-logs: forward browser console to server
13
12
  * - logs: forward server console to browser
14
13
  * - holding-server: startup holding server
@@ -64,21 +63,6 @@ export {
64
63
  type DevLoggerConfig,
65
64
  } from './logger.js';
66
65
 
67
- // Dev-mode warnings
68
- export {
69
- warnSuspenseWrappingChildren,
70
- warnDenyInSuspense,
71
- warnRedirectInSuspense,
72
- warnRedirectInAccess,
73
- warnStaticRequestApi,
74
- warnSlowSlotWithoutSuspense,
75
- setViteServer,
76
- WarningId,
77
- type DevWarningConfig,
78
- _resetWarnings,
79
- _getEmitted,
80
- } from './warnings.js';
81
-
82
66
  // Browser log forwarding
83
67
  export {
84
68
  timberDevBrowserLogs,
@@ -94,3 +78,6 @@ export { createHoldingServer, type HoldingServer } from './holding-server.js';
94
78
 
95
79
  // OTEL instrumentation
96
80
  export { instrumentDevFetch, DevSpanProcessor, type DevFetchCleanup } from './instrumentation.js';
81
+
82
+ // Hydration mismatch overlay (client-side, dev-only)
83
+ export { isHydrationError, showHydrationError } from './hydration-overlay.js';