@timber-js/app 0.2.0-alpha.147 → 0.2.0-alpha.149
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.
- package/LICENSE +8 -0
- package/dist/_chunks/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
- package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
- package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
- package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
- package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-CQaxupHv.js → cli-schema-sync-DJDdyEae.js} +111 -260
- package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
- package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
- package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
- package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
- package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
- package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
- package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
- package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
- package/dist/_chunks/logger-CM0clugc.js.map +1 -0
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
- package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
- package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
- package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
- package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
- package/dist/_chunks/tree-match-D2l830j2.js +102 -0
- package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
- package/dist/_chunks/{walkers-Ck4Y45XN.js → walkers-DvWDj453.js} +107 -82
- package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +36 -14
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/compress-module.d.ts +5 -4
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +1 -3
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -134
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +3 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +20 -235
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/invalidation-epoch.d.ts +43 -0
- package/dist/cache/invalidation-epoch.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +13 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +17 -9
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli-schema-sync.d.ts +21 -1
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -31
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hmr.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -17
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +30 -33
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +9 -2
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +0 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +0 -5
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +10 -5
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -4
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +1 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
- package/dist/dev-tools/hydration-overlay.d.ts +11 -0
- package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +1 -2
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/fonts/ast-walk.d.ts +68 -0
- package/dist/fonts/ast-walk.d.ts.map +1 -0
- package/dist/fonts/ast.d.ts +23 -11
- package/dist/fonts/ast.d.ts.map +1 -1
- package/dist/fonts/bundle.d.ts +3 -1
- package/dist/fonts/bundle.d.ts.map +1 -1
- package/dist/fonts/dev-middleware.d.ts +1 -1
- package/dist/fonts/dev-middleware.d.ts.map +1 -1
- package/dist/fonts/fallbacks.d.ts +0 -7
- package/dist/fonts/fallbacks.d.ts.map +1 -1
- package/dist/fonts/google.d.ts +4 -2
- package/dist/fonts/google.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +3 -3
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/fonts/paths.d.ts +43 -0
- package/dist/fonts/paths.d.ts.map +1 -0
- package/dist/fonts/pipeline.d.ts +7 -21
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts +1 -18
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +610 -406
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/params/resolve-schema.d.ts.map +1 -1
- package/dist/plugin-context.d.ts +28 -5
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +13 -5
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +1 -2
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/plugins/server-bundle.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/file-cache.d.ts +8 -0
- package/dist/routing/file-cache.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +0 -5
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +32 -0
- package/dist/routing/manifest-codegen.d.ts.map +1 -0
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +15 -0
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +7 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/actions.d.ts +0 -14
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +0 -29
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/canonicalize.d.ts +10 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +8 -0
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +1 -5
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +12 -171
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +0 -11
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +25 -0
- package/dist/server/render-utils.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -25
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -18
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +7 -8
- package/src/adapters/cloudflare-kv-cache.ts +56 -19
- package/src/adapters/compress-module.ts +27 -118
- package/src/adapters/nitro.ts +3 -41
- package/src/adapters/raw-imports.d.ts +5 -0
- package/src/cache/cache-api.ts +9 -1
- package/src/cache/index.ts +5 -2
- package/src/cache/invalidation-epoch.ts +93 -0
- package/src/cache/redis-handler.ts +33 -2
- package/src/cache/timber-cache.ts +109 -53
- package/src/cli-schema-sync.ts +123 -30
- package/src/cli.ts +33 -37
- package/src/client/browser-entry/hmr.ts +0 -11
- package/src/client/browser-entry/hydrate.ts +39 -5
- package/src/client/browser-entry/post-hydration.ts +6 -15
- package/src/client/browser-entry/router-init.ts +98 -3
- package/src/client/history.ts +10 -24
- package/src/client/navigation-api.ts +14 -9
- package/src/client/navigation-context.ts +1 -21
- package/src/client/navigation-root.tsx +0 -8
- package/src/client/router.ts +18 -8
- package/src/client/rsc-fetch.ts +7 -2
- package/src/client/use-search-params.ts +10 -5
- package/src/config-validation.ts +33 -25
- package/src/cookies/define-cookie.ts +2 -18
- package/src/dev-tools/dev-page-shell.ts +6 -2
- package/src/dev-tools/hydration-overlay.ts +158 -0
- package/src/dev-tools/index.ts +3 -16
- package/src/fonts/ast-walk.ts +235 -0
- package/src/fonts/ast.ts +91 -249
- package/src/fonts/bundle.ts +16 -9
- package/src/fonts/dev-middleware.ts +16 -5
- package/src/fonts/fallbacks.ts +0 -23
- package/src/fonts/google.ts +7 -3
- package/src/fonts/local.ts +7 -6
- package/src/fonts/paths.ts +58 -0
- package/src/fonts/pipeline.ts +27 -48
- package/src/fonts/transform.ts +27 -0
- package/src/fonts/virtual-modules.ts +1 -57
- package/src/index.ts +5 -11
- package/src/params/resolve-schema.ts +2 -34
- package/src/plugin-context.ts +95 -17
- package/src/plugins/cache.ts +64 -17
- package/src/plugins/dev-server.ts +61 -9
- package/src/plugins/fonts.ts +21 -6
- package/src/plugins/routing.ts +5 -348
- package/src/plugins/server-bundle.ts +1 -0
- package/src/routing/codegen.ts +3 -2
- package/src/routing/convention-lint.ts +4 -3
- package/src/routing/file-cache.ts +23 -0
- package/src/routing/interception.ts +2 -21
- package/src/routing/manifest-codegen.ts +290 -0
- package/src/routing/scanner.ts +5 -17
- package/src/routing/schema-validation.ts +2 -2
- package/src/schema-bridge.ts +63 -2
- package/src/search-params/define.ts +34 -2
- package/src/search-params/wrappers.ts +21 -3
- package/src/segment-params/define.ts +2 -62
- package/src/server/access-gate.tsx +8 -0
- package/src/server/actions.ts +8 -31
- package/src/server/build-manifest.ts +0 -56
- package/src/server/canonicalize.ts +6 -3
- package/src/server/deny-renderer.ts +6 -0
- package/src/server/html-injector-core.ts +19 -0
- package/src/server/html-injectors.ts +14 -4
- package/src/server/internal.ts +1 -18
- package/src/server/node-stream-transforms.ts +5 -72
- package/src/server/render-utils.ts +31 -0
- package/src/server/route-element-builder.ts +77 -47
- package/src/server/rsc-entry/error-renderer.ts +1 -0
- package/src/server/rsc-entry/helpers.ts +6 -34
- package/src/server/rsc-entry/index.ts +11 -0
- package/src/server/rsc-entry/render-route.ts +7 -9
- package/src/server/rsc-entry/rsc-payload.ts +2 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/ssr-render.ts +1 -11
- package/src/shared/href-validation.ts +8 -0
- package/dist/_chunks/cli-schema-sync-CBiMEtqa.js +0 -2
- package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +0 -1
- package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
- package/dist/_chunks/define-DMVhsipD.js.map +0 -1
- package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
- package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
- package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
- package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
- package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
- package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
- package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
- package/dist/_chunks/walkers-Ck4Y45XN.js.map +0 -1
- package/dist/_chunks/warnings-CNPXaypM.js +0 -125
- package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
- package/dist/client/link-pending-store.d.ts +0 -65
- package/dist/client/link-pending-store.d.ts.map +0 -1
- package/dist/client/nav-link-store.d.ts +0 -27
- package/dist/client/nav-link-store.d.ts.map +0 -1
- package/dist/dev-tools/warnings.d.ts +0 -109
- package/dist/dev-tools/warnings.d.ts.map +0 -1
- package/dist/server/flush.d.ts +0 -74
- package/dist/server/flush.d.ts.map +0 -1
- package/src/client/link-pending-store.ts +0 -116
- package/src/client/nav-link-store.ts +0 -37
- package/src/dev-tools/warnings.ts +0 -238
- 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
|
|
59
|
-
*
|
|
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 —
|
|
243
|
-
//
|
|
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:
|
|
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
|
|
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
|
*
|
package/src/client/router.ts
CHANGED
|
@@ -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(
|
|
783
|
-
if (historyStack.has(
|
|
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(
|
|
792
|
+
void fetchRscPayload(fetchUrl, deps, stateTree).then(
|
|
788
793
|
(result) => {
|
|
789
794
|
result.decodePromise?.catch(() => {});
|
|
790
|
-
prefetchCache.set(
|
|
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
|
},
|
package/src/client/rsc-fetch.ts
CHANGED
|
@@ -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
|
|
67
|
-
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
package/src/config-validation.ts
CHANGED
|
@@ -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
|
-
|
|
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?: {
|
|
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, '&')
|
|
67
|
+
.replace(/</g, '<')
|
|
68
|
+
.replace(/>/g, '>')
|
|
69
|
+
.replace(/"/g, '"');
|
|
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
|
+
}
|
package/src/dev-tools/index.ts
CHANGED
|
@@ -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';
|