@timber-js/app 0.2.0-alpha.146 → 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.
- 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-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
- 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-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
- 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/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +13 -4
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.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 +609 -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/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.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-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.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/error-boundary-wrapper.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 +238 -349
- 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/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +3 -3
- 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/error-boundary.tsx +14 -0
- package/src/client/error-reconstituter.tsx +15 -1
- 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/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +66 -8
- 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 +85 -80
- 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-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +46 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- 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/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +2 -12
- 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/server/status-code-resolver.ts +3 -2
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/href-validation.ts +8 -0
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
- 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-7FBPM-hX.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
|
@@ -11,9 +11,8 @@
|
|
|
11
11
|
* See design/19-client-navigation.md §"History Stack"
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { setCurrentParams } from '#client-internal';
|
|
15
14
|
import type { RouterInstance } from '#client-internal';
|
|
16
|
-
import {
|
|
15
|
+
import { getNavigationState } from '../navigation-context.js';
|
|
17
16
|
import type { NavigationApiController } from '../navigation-api.js';
|
|
18
17
|
import { getScrollY } from './scroll.js';
|
|
19
18
|
|
|
@@ -35,9 +34,14 @@ export function setupPostHydration({
|
|
|
35
34
|
// Store the initial page in the history stack so back-button works
|
|
36
35
|
// after the first navigation. We store the decoded RSC element so
|
|
37
36
|
// back navigation can replay it instantly without a server fetch.
|
|
37
|
+
// Params were captured from the server-embedded __timber_params by
|
|
38
|
+
// runPreHydration() — without them on the entry, the cached replay on
|
|
39
|
+
// back-navigation would normalize params to {} and wipe every
|
|
40
|
+
// useSegmentParams() consumer on the initial page (TIM-1037).
|
|
38
41
|
router.historyStack.push(window.location.pathname + window.location.search, {
|
|
39
42
|
payload: initialElement,
|
|
40
43
|
headElements: null, // SSR already set the correct head
|
|
44
|
+
params: getNavigationState().params,
|
|
41
45
|
});
|
|
42
46
|
|
|
43
47
|
// Initialize scroll state for the initial entry.
|
|
@@ -58,19 +62,6 @@ export function setupPostHydration({
|
|
|
58
62
|
delete (self as unknown as Record<string, unknown>).__timber_segments;
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
// If the hydration path was skipped (no RSC payload), populate the
|
|
62
|
-
// fallback params from server embed here.
|
|
63
|
-
const lateTimberParams = (self as unknown as Record<string, unknown>).__timber_params;
|
|
64
|
-
if (lateTimberParams && typeof lateTimberParams === 'object') {
|
|
65
|
-
setCurrentParams(lateTimberParams as Record<string, string | string[]>);
|
|
66
|
-
setNavigationState({
|
|
67
|
-
params: lateTimberParams as Record<string, string | string[]>,
|
|
68
|
-
pathname: window.location.pathname,
|
|
69
|
-
search: window.location.search,
|
|
70
|
-
});
|
|
71
|
-
delete (self as unknown as Record<string, unknown>).__timber_params;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
65
|
// Register popstate handler for back/forward navigation.
|
|
75
66
|
// When Navigation API is active, the navigate event covers traversals —
|
|
76
67
|
// popstate is a no-op. When unavailable, popstate handles back/forward.
|
|
@@ -14,7 +14,11 @@ import type { RouterDeps, RouterInstance } from '#client-internal';
|
|
|
14
14
|
import type { NavigationState } from '#client-internal';
|
|
15
15
|
import { applyHeadElements } from '../head.js';
|
|
16
16
|
import { TimberNuqsAdapter } from '../nuqs-adapter.js';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
NavigationProvider,
|
|
19
|
+
getNavigationState,
|
|
20
|
+
setNavigationState,
|
|
21
|
+
} from '../navigation-context.js';
|
|
18
22
|
import { transitionRender, navigateTransition } from '../navigation-root.js';
|
|
19
23
|
import { SegmentUpdateContext, EMPTY_SEGMENT_UPDATES } from '../segment-update-context.js';
|
|
20
24
|
import {
|
|
@@ -54,10 +58,96 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
54
58
|
// very first client navigation (before any wrapPayload call).
|
|
55
59
|
let currentPayload: unknown = options?.initialElement ?? null;
|
|
56
60
|
|
|
61
|
+
// ─── History API Patch ──────────────────────────────────────────
|
|
62
|
+
// Patch pushState/replaceState to detect external URL changes (nuqs
|
|
63
|
+
// shallow updates, replaceUrl, third-party libraries). When the router
|
|
64
|
+
// itself calls pushState/replaceState, it sets a flag so the patch
|
|
65
|
+
// skips the sync — the router already updates NavigationContext.
|
|
66
|
+
//
|
|
67
|
+
// On Navigation API browsers, the navigate event catches most URL
|
|
68
|
+
// changes. The patch is defense-in-depth and the primary mechanism
|
|
69
|
+
// for History API-only browsers (older Safari/Firefox).
|
|
70
|
+
const ROUTER_HISTORY_FLAG = Symbol.for('__timber_router_history_update');
|
|
71
|
+
const gFlags = globalThis as Record<symbol, boolean>;
|
|
72
|
+
|
|
73
|
+
const originalPushState = window.history.pushState.bind(window.history);
|
|
74
|
+
const originalReplaceState = window.history.replaceState.bind(window.history);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sync NavigationContext.search after a shallow URL update. Called by
|
|
78
|
+
* the Navigation API shallow handler and the History API patch.
|
|
79
|
+
*
|
|
80
|
+
* @param search — the new search string. The Navigation API path passes
|
|
81
|
+
* this from event.destination.url (window.location hasn't committed
|
|
82
|
+
* yet when the navigate listener fires). The History API patch omits
|
|
83
|
+
* it, reading window.location.search (already committed by the time
|
|
84
|
+
* the patched pushState/replaceState returns).
|
|
85
|
+
*
|
|
86
|
+
* Bails when search is unchanged (scroll saves, no-ops, dual-fire).
|
|
87
|
+
* Re-wraps currentPayload with updated NavigationProvider and commits
|
|
88
|
+
* via transitionRender (startTransition — non-blocking).
|
|
89
|
+
*/
|
|
90
|
+
function syncShallowSearch(search?: string): void {
|
|
91
|
+
const currentSearch = search ?? window.location.search;
|
|
92
|
+
const navState = getNavigationState();
|
|
93
|
+
if (currentSearch === navState.search) return;
|
|
94
|
+
if (!currentPayload) return;
|
|
95
|
+
|
|
96
|
+
const newNavState: NavigationState = {
|
|
97
|
+
params: navState.params,
|
|
98
|
+
pathname: navState.pathname,
|
|
99
|
+
search: currentSearch,
|
|
100
|
+
};
|
|
101
|
+
setNavigationState(newNavState);
|
|
102
|
+
|
|
103
|
+
const withNav = createElement(
|
|
104
|
+
NavigationProvider,
|
|
105
|
+
{ value: newNavState },
|
|
106
|
+
currentPayload as React.ReactNode
|
|
107
|
+
);
|
|
108
|
+
const withUpdates = createElement(
|
|
109
|
+
SegmentUpdateContext.Provider,
|
|
110
|
+
{ value: EMPTY_SEGMENT_UPDATES },
|
|
111
|
+
withNav
|
|
112
|
+
);
|
|
113
|
+
const wrapped = createElement(TimberNuqsAdapter, null, withUpdates);
|
|
114
|
+
transitionRender(wrapped);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
window.history.pushState = function (
|
|
118
|
+
data: unknown,
|
|
119
|
+
unused: string,
|
|
120
|
+
url?: string | URL | null
|
|
121
|
+
): void {
|
|
122
|
+
originalPushState(data, unused, url);
|
|
123
|
+
if (!gFlags[ROUTER_HISTORY_FLAG]) {
|
|
124
|
+
syncShallowSearch();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
window.history.replaceState = function (
|
|
129
|
+
data: unknown,
|
|
130
|
+
unused: string,
|
|
131
|
+
url?: string | URL | null
|
|
132
|
+
): void {
|
|
133
|
+
originalReplaceState(data, unused, url);
|
|
134
|
+
if (!gFlags[ROUTER_HISTORY_FLAG]) {
|
|
135
|
+
syncShallowSearch();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
57
139
|
const deps: RouterDeps = {
|
|
58
140
|
fetch: (url, init) => window.fetch(url, init),
|
|
59
|
-
pushState: (data, unused, url) =>
|
|
60
|
-
|
|
141
|
+
pushState: (data, unused, url) => {
|
|
142
|
+
gFlags[ROUTER_HISTORY_FLAG] = true;
|
|
143
|
+
window.history.pushState(data, unused, url);
|
|
144
|
+
gFlags[ROUTER_HISTORY_FLAG] = false;
|
|
145
|
+
},
|
|
146
|
+
replaceState: (data, unused, url) => {
|
|
147
|
+
gFlags[ROUTER_HISTORY_FLAG] = true;
|
|
148
|
+
window.history.replaceState(data, unused, url);
|
|
149
|
+
gFlags[ROUTER_HISTORY_FLAG] = false;
|
|
150
|
+
},
|
|
61
151
|
navigationApiActive: useNavApi,
|
|
62
152
|
scrollTo: (x, y) => {
|
|
63
153
|
// Scroll the document viewport.
|
|
@@ -94,6 +184,7 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
94
184
|
// navState is passed explicitly by the router — no temporal coupling
|
|
95
185
|
// with getNavigationState().
|
|
96
186
|
renderRoot: (element: unknown, navState: NavigationState) => {
|
|
187
|
+
currentPayload = element;
|
|
97
188
|
const withNav = createElement(
|
|
98
189
|
NavigationProvider,
|
|
99
190
|
{ value: navState },
|
|
@@ -183,6 +274,10 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
183
274
|
// Back/forward — delegate to the router's popstate handler.
|
|
184
275
|
await router.handlePopState(url, scrollY, signal);
|
|
185
276
|
},
|
|
277
|
+
onShallowNavigate: (destinationUrl: string) => {
|
|
278
|
+
const search = new URL(destinationUrl).search;
|
|
279
|
+
syncShallowSearch(search);
|
|
280
|
+
},
|
|
186
281
|
});
|
|
187
282
|
|
|
188
283
|
// Wire the router-navigating flag into RouterDeps.
|
|
@@ -255,7 +255,21 @@ export class TimberErrorBoundary extends Component<
|
|
|
255
255
|
|
|
256
256
|
// Render the fallback component with the right props shape.
|
|
257
257
|
if (parsed?.type === 'deny') {
|
|
258
|
+
// Status-code file boundaries (404.tsx, 4xx.tsx, …) get the documented
|
|
259
|
+
// 4xx props. error.tsx boundaries (no status filter) get their
|
|
260
|
+
// documented { error, digest, reset } contract — plus the deny extras
|
|
261
|
+
// for dual-shape implementations. Without the error prop, an error.tsx
|
|
262
|
+
// reading error.message crashes on deny fall-through. See TIM-1081.
|
|
263
|
+
if (this.props.status != null) {
|
|
264
|
+
return createElement(this.props.fallbackComponent as never, {
|
|
265
|
+
status: parsed.status,
|
|
266
|
+
dangerouslyPassData: parsed.data,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
258
269
|
return createElement(this.props.fallbackComponent as never, {
|
|
270
|
+
error,
|
|
271
|
+
digest: null,
|
|
272
|
+
reset: this.reset,
|
|
259
273
|
status: parsed.status,
|
|
260
274
|
dangerouslyPassData: parsed.data,
|
|
261
275
|
});
|
|
@@ -33,6 +33,9 @@ export interface SerializableError {
|
|
|
33
33
|
* - digest: plain JSON or null
|
|
34
34
|
* - reset: undefined (only meaningful on client after boundary catch)
|
|
35
35
|
* - component: client module reference (RSC Flight serializes as opaque ref)
|
|
36
|
+
* - status / dangerouslyPassData: set only when error.tsx serves a deny()
|
|
37
|
+
* as the last entry in the 4xx fallback chain — forwarded so dual-shape
|
|
38
|
+
* error.tsx implementations can branch on the deny status (TIM-1081)
|
|
36
39
|
*/
|
|
37
40
|
interface ErrorReconstituterProps {
|
|
38
41
|
error: SerializableError;
|
|
@@ -42,7 +45,11 @@ interface ErrorReconstituterProps {
|
|
|
42
45
|
error: Error;
|
|
43
46
|
digest: { code: string; data: unknown } | null;
|
|
44
47
|
reset: (() => void) | undefined;
|
|
48
|
+
status?: number;
|
|
49
|
+
dangerouslyPassData?: unknown;
|
|
45
50
|
}>;
|
|
51
|
+
status?: number;
|
|
52
|
+
dangerouslyPassData?: unknown;
|
|
46
53
|
}
|
|
47
54
|
|
|
48
55
|
/**
|
|
@@ -54,6 +61,8 @@ export function ErrorReconstituter({
|
|
|
54
61
|
digest,
|
|
55
62
|
reset,
|
|
56
63
|
component,
|
|
64
|
+
status,
|
|
65
|
+
dangerouslyPassData,
|
|
57
66
|
}: ErrorReconstituterProps): ReactNode {
|
|
58
67
|
// Reconstitute a real Error so instanceof checks work in user code
|
|
59
68
|
const error = Object.assign(new Error(serialized.message), {
|
|
@@ -61,5 +70,10 @@ export function ErrorReconstituter({
|
|
|
61
70
|
...(serialized.stack != null ? { stack: serialized.stack } : {}),
|
|
62
71
|
});
|
|
63
72
|
|
|
64
|
-
return createElement(component, {
|
|
73
|
+
return createElement(component, {
|
|
74
|
+
error,
|
|
75
|
+
digest,
|
|
76
|
+
reset,
|
|
77
|
+
...(status != null ? { status, dangerouslyPassData } : {}),
|
|
78
|
+
});
|
|
65
79
|
}
|
package/src/client/history.ts
CHANGED
|
@@ -10,7 +10,12 @@ export interface HistoryEntry {
|
|
|
10
10
|
payload: unknown;
|
|
11
11
|
/** Resolved head elements for this page (title, meta tags). Null for SSR'd initial page. */
|
|
12
12
|
headElements?: HeadElement[] | null;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Route params for this page (for useSegmentParams). Every entry that can
|
|
15
|
+
* be replayed must carry them — the initial SSR entry gets them from the
|
|
16
|
+
* server-embedded __timber_params, navigation entries from X-Timber-Params,
|
|
17
|
+
* and revalidation overwrites preserve the current state (TIM-1037).
|
|
18
|
+
*/
|
|
14
19
|
params?: Record<string, string | string[]> | null;
|
|
15
20
|
}
|
|
16
21
|
|
|
@@ -23,13 +28,8 @@ export interface HistoryEntry {
|
|
|
23
28
|
* On forward navigation, the new page's payload is pushed onto the stack.
|
|
24
29
|
* On popstate, the cached payload is replayed instantly.
|
|
25
30
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* Used with the History API fallback.
|
|
29
|
-
* - **Entry-key + URL**: when the Navigation API is available,
|
|
30
|
-
* entries can also be stored by Navigation entry key for
|
|
31
|
-
* disambiguation of duplicate URLs in the history stack.
|
|
32
|
-
* Falls back to URL lookup when entry key is not found.
|
|
31
|
+
* Entries are keyed by pathname + search. Used with the History API
|
|
32
|
+
* fallback and the Navigation API.
|
|
33
33
|
*
|
|
34
34
|
* Scroll positions are stored in history.state or Navigation API entry
|
|
35
35
|
* state, not in this stack — see design/19-client-navigation.md §Scroll Restoration.
|
|
@@ -39,26 +39,12 @@ export interface HistoryEntry {
|
|
|
39
39
|
*/
|
|
40
40
|
export class HistoryStack {
|
|
41
41
|
private entries = new Map<string, HistoryEntry>();
|
|
42
|
-
/** Entries keyed by Navigation API entry key for duplicate URL disambiguation. */
|
|
43
|
-
private entryKeyMap = new Map<string, HistoryEntry>();
|
|
44
42
|
|
|
45
|
-
push(url: string, entry: HistoryEntry
|
|
43
|
+
push(url: string, entry: HistoryEntry): void {
|
|
46
44
|
this.entries.set(url, entry);
|
|
47
|
-
if (entryKey) {
|
|
48
|
-
this.entryKeyMap.set(entryKey, entry);
|
|
49
|
-
}
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
|
|
53
|
-
* Get an entry. When an entry key is provided (Navigation API),
|
|
54
|
-
* tries the entry-key map first for accurate disambiguation of
|
|
55
|
-
* duplicate URLs, then falls back to URL lookup.
|
|
56
|
-
*/
|
|
57
|
-
get(url: string, entryKey?: string): HistoryEntry | undefined {
|
|
58
|
-
if (entryKey) {
|
|
59
|
-
const byKey = this.entryKeyMap.get(entryKey);
|
|
60
|
-
if (byKey) return byKey;
|
|
61
|
-
}
|
|
47
|
+
get(url: string): HistoryEntry | undefined {
|
|
62
48
|
return this.entries.get(url);
|
|
63
49
|
}
|
|
64
50
|
|
|
@@ -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
|
};
|