@timber-js/app 0.2.0-alpha.195 → 0.2.0-alpha.197
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-CWYtq6ii.js → actions-BS-m5SLv.js} +3 -3
- package/dist/_chunks/{actions-CWYtq6ii.js.map → actions-BS-m5SLv.js.map} +1 -1
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{build-manifest-DWppEdLB.js → build-manifest-DTmSGLRz.js} +51 -2
- package/dist/_chunks/build-manifest-DTmSGLRz.js.map +1 -0
- package/dist/_chunks/{cache-api-CQeYzA5g.js → cache-api-DqzgTEqk.js} +4 -49
- package/dist/_chunks/cache-api-DqzgTEqk.js.map +1 -0
- package/dist/_chunks/{chains-h7EO-u3n.js → chains-CZG7E5zg.js} +2 -2
- package/dist/_chunks/{chains-h7EO-u3n.js.map → chains-CZG7E5zg.js.map} +1 -1
- package/dist/_chunks/{cli-check-BVthpfLS.js → cli-check-bE3H5Bjr.js} +3 -3
- package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-bE3H5Bjr.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js → cli-schema-sync-DTy_-Msq.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-3Wutm8pH.js.map → cli-schema-sync-DTy_-Msq.js.map} +1 -1
- package/dist/_chunks/{cloudflare-BKJC3SC_.js → cloudflare-BFb__LYG.js} +2 -2
- package/dist/_chunks/{cloudflare-BKJC3SC_.js.map → cloudflare-BFb__LYG.js.map} +1 -1
- package/dist/_chunks/{convention-lint-DO10_pVl.js → convention-lint-n3RJLgww.js} +2 -2
- package/dist/_chunks/{convention-lint-DO10_pVl.js.map → convention-lint-n3RJLgww.js.map} +1 -1
- package/dist/_chunks/{error-boundary-D-lkwyaD.js → error-boundary-BvRCCmbN.js} +3 -3
- package/dist/_chunks/{error-boundary-D-lkwyaD.js.map → error-boundary-BvRCCmbN.js.map} +1 -1
- package/dist/_chunks/{href-validation-CMc5JRls.js → href-validation-BIrxavIy.js} +74 -2
- package/dist/_chunks/href-validation-BIrxavIy.js.map +1 -0
- package/dist/_chunks/{live-graph-Bx4HodF1.js → live-graph-BXDsdzBv.js} +3 -3
- package/dist/_chunks/{live-graph-Bx4HodF1.js.map → live-graph-BXDsdzBv.js.map} +1 -1
- package/dist/_chunks/{logger-pumCm3Il.js → logger-DDirEsn7.js} +3 -4
- package/dist/_chunks/{logger-pumCm3Il.js.map → logger-DDirEsn7.js.map} +1 -1
- package/dist/_chunks/navigation-root-B00jjGd5.js +233 -0
- package/dist/_chunks/navigation-root-B00jjGd5.js.map +1 -0
- package/dist/_chunks/{segment-context-CjOlyB8Y.js → param-value-C8TNYchQ.js} +2 -33
- package/dist/_chunks/param-value-C8TNYchQ.js.map +1 -0
- package/dist/_chunks/{poison-scan-BAxfTT5L.js → poison-scan-BoDLgbix.js} +2 -2
- package/dist/_chunks/{poison-scan-BAxfTT5L.js.map → poison-scan-BoDLgbix.js.map} +1 -1
- package/dist/_chunks/{router-ref-BzqbPwYC.js → router-ref-8gr8qsxN.js} +2 -2
- package/dist/_chunks/{router-ref-BzqbPwYC.js.map → router-ref-8gr8qsxN.js.map} +1 -1
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js → rsc-cache-key-ClUiXQnK.js} +2 -2
- package/dist/_chunks/{rsc-cache-key-DD0fl_-s.js.map → rsc-cache-key-ClUiXQnK.js.map} +1 -1
- package/dist/_chunks/{scanner-BRIOmHE2.js → scanner-tdFPvDYi.js} +174 -7
- package/dist/_chunks/scanner-tdFPvDYi.js.map +1 -0
- package/dist/_chunks/segment-context-D9_89u34.js +34 -0
- package/dist/_chunks/segment-context-D9_89u34.js.map +1 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js +54 -0
- package/dist/_chunks/singleflight-2lUWfcAk.js.map +1 -0
- package/dist/_chunks/{ssr-data-Ya2HJPFp.js → ssr-data-BQGhTPAK.js} +2 -17
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +1 -0
- package/dist/_chunks/{walkers-BU6z9xRV.js → walkers-DNX05dC0.js} +2 -2
- package/dist/_chunks/{walkers-BU6z9xRV.js.map → walkers-DNX05dC0.js.map} +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.js +1 -1
- package/dist/adapters/nitro.d.ts +1 -1
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cache/singleflight.d.ts +2 -0
- package/dist/cache/singleflight.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/hydrate.d.ts +21 -15
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/index.d.ts +4 -3
- package/dist/client/browser-entry/index.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 +17 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.js +1 -1
- package/dist/client/global-context.d.ts +15 -0
- package/dist/client/global-context.d.ts.map +1 -0
- package/dist/client/index.js +139 -36
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +0 -1
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +206 -55
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/location-search.d.ts +12 -0
- package/dist/client/location-search.d.ts.map +1 -0
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +18 -0
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +13 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +47 -108
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +136 -0
- package/dist/client/navigation-transition.d.ts.map +1 -0
- package/dist/client/nuqs-adapter.d.ts.map +1 -1
- package/dist/client/params-context.d.ts +4 -5
- package/dist/client/params-context.d.ts.map +1 -1
- package/dist/client/react-root.d.ts +44 -0
- package/dist/client/react-root.d.ts.map +1 -0
- package/dist/client/router-pipeline.d.ts +2 -2
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +12 -2
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +39 -0
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/segment-context.d.ts.map +1 -1
- package/dist/client/segment-outlet.d.ts +25 -14
- package/dist/client/segment-outlet.d.ts.map +1 -1
- package/dist/client/segment-update-context.d.ts +3 -9
- package/dist/client/segment-update-context.d.ts.map +1 -1
- package/dist/client/slot-content-cache-context.d.ts +35 -0
- package/dist/client/slot-content-cache-context.d.ts.map +1 -0
- package/dist/client/ssr-data.d.ts +8 -2
- package/dist/client/ssr-data.d.ts.map +1 -1
- package/dist/client/state.d.ts +0 -15
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/use-pathname.d.ts +13 -11
- package/dist/client/use-pathname.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +13 -13
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/client/use-segment-params.d.ts +18 -68
- package/dist/client/use-segment-params.d.ts.map +1 -1
- package/dist/config-types.d.ts +17 -0
- package/dist/config-types.d.ts.map +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +15 -10
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -43
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/shims.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +2 -2
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception-overlap.d.ts +35 -0
- package/dist/routing/interception-overlap.d.ts.map +1 -0
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/rsc-runtime/ssr.d.ts +3 -1
- package/dist/rsc-runtime/ssr.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +6 -0
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/csp-nonce.d.ts +45 -0
- package/dist/server/csp-nonce.d.ts.map +1 -0
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/flight-scripts.d.ts +5 -2
- package/dist/server/flight-scripts.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +17 -2
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts +3 -2
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.js +86 -37
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-render.d.ts.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +3 -17
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/nuqs-ssr-provider.d.ts +7 -3
- package/dist/server/nuqs-ssr-provider.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/prebuilt/key-discipline.d.ts +32 -3
- package/dist/server/prebuilt/key-discipline.d.ts.map +1 -1
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +4 -3
- package/dist/server/render-utils.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/ssr-bridge-types.d.ts +22 -2
- package/dist/server/ssr-bridge-types.d.ts.map +1 -1
- package/dist/server/ssr-entry.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts +5 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/ssr-wrappers.d.ts +59 -27
- package/dist/server/ssr-wrappers.d.ts.map +1 -1
- package/dist/server/types.d.ts +10 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/shims/navigation-rsc.d.ts +21 -0
- package/dist/shims/navigation-rsc.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +1 -0
- package/docs/api/35-api-typescript.mdx +4 -83
- package/docs/learn/03-fetching-data.mdx +1 -1
- package/docs/learn/{03b-access-control.mdx → 04-access-control.mdx} +2 -17
- package/docs/learn/05-the-flush-point.mdx +175 -0
- package/docs/learn/{05-typed-params.mdx → 06-typed-params.mdx} +1 -1
- package/docs/learn/07-typed-routes.mdx +25 -49
- package/docs/learn/{08-streaming.mdx → 09-streaming.mdx} +1 -7
- package/docs/learn/{10-middleware.mdx → 11-middleware.mdx} +1 -0
- package/package.json +3 -3
- package/src/adapters/nitro.ts +7 -7
- package/src/cache/singleflight.ts +5 -0
- package/src/client/browser-entry/hydrate.ts +54 -104
- package/src/client/browser-entry/index.ts +16 -6
- package/src/client/browser-entry/post-hydration.ts +3 -2
- package/src/client/browser-entry/router-init.ts +84 -33
- package/src/client/global-context.ts +31 -0
- package/src/client/internal.ts +1 -2
- package/src/client/link.tsx +25 -19
- package/src/client/location-search.ts +15 -0
- package/src/client/navigation-api.ts +4 -2
- package/src/client/navigation-commit.ts +48 -2
- package/src/client/navigation-context.ts +25 -37
- package/src/client/navigation-root.tsx +55 -411
- package/src/client/navigation-transition.ts +278 -0
- package/src/client/nuqs-adapter.tsx +4 -5
- package/src/client/params-context.ts +13 -18
- package/src/client/react-root.ts +72 -0
- package/src/client/router-lifecycle.ts +1 -1
- package/src/client/router-pipeline.ts +96 -22
- package/src/client/router-types.ts +12 -2
- package/src/client/router.ts +48 -36
- package/src/client/segment-cache.ts +70 -2
- package/src/client/segment-context.ts +7 -4
- package/src/client/segment-outlet.tsx +41 -86
- package/src/client/segment-update-context.ts +7 -26
- package/src/client/slot-content-cache-context.ts +43 -0
- package/src/client/ssr-data.ts +8 -2
- package/src/client/state.ts +0 -26
- package/src/client/use-pathname.ts +21 -31
- package/src/client/use-search-params.ts +31 -29
- package/src/client/use-segment-params.ts +27 -126
- package/src/config-types.ts +17 -0
- package/src/config-validation.ts +17 -0
- package/src/dev-tools/holding-server.ts +23 -12
- package/src/index.ts +26 -11
- package/src/plugins/dev-server.ts +9 -12
- package/src/plugins/entries.ts +3 -0
- package/src/plugins/shims.ts +8 -7
- package/src/plugins/static-build.ts +9 -5
- package/src/routing/interception-overlap.ts +141 -0
- package/src/routing/interception.ts +118 -5
- package/src/rsc-runtime/ssr.ts +3 -2
- package/src/server/als-registry.ts +6 -0
- package/src/server/csp-nonce.ts +70 -0
- package/src/server/default-status-page.ts +1 -0
- package/src/server/deny-renderer.ts +7 -3
- package/src/server/flight-scripts.ts +9 -4
- package/src/server/html-injector-core.ts +26 -9
- package/src/server/html-injectors.ts +8 -8
- package/src/server/metadata-render.ts +26 -4
- package/src/server/node-stream-transforms.ts +7 -20
- package/src/server/nuqs-ssr-provider.tsx +8 -7
- package/src/server/pipeline-phases.ts +5 -0
- package/src/server/prebuilt/key-discipline.ts +82 -13
- package/src/server/prebuilt-runtime.ts +2 -2
- package/src/server/primitives.ts +4 -4
- package/src/server/render-utils.ts +8 -4
- package/src/server/rsc-entry/action-middleware-runner.ts +7 -0
- package/src/server/rsc-entry/error-renderer.ts +5 -2
- package/src/server/rsc-entry/index.ts +8 -0
- package/src/server/rsc-entry/ssr-renderer.ts +11 -4
- package/src/server/ssr-bridge-types.ts +22 -2
- package/src/server/ssr-entry.ts +35 -28
- package/src/server/ssr-render.ts +13 -4
- package/src/server/ssr-wrappers.tsx +81 -61
- package/src/server/types.ts +10 -0
- package/src/shared/slot-params.ts +3 -4
- package/src/shims/navigation-rsc.ts +47 -0
- package/dist/_chunks/build-manifest-DWppEdLB.js.map +0 -1
- package/dist/_chunks/cache-api-CQeYzA5g.js.map +0 -1
- package/dist/_chunks/href-validation-CMc5JRls.js.map +0 -1
- package/dist/_chunks/scanner-BRIOmHE2.js.map +0 -1
- package/dist/_chunks/segment-context-CjOlyB8Y.js.map +0 -1
- package/dist/_chunks/slot-params-BCTmZkQB.js +0 -76
- package/dist/_chunks/slot-params-BCTmZkQB.js.map +0 -1
- package/dist/_chunks/ssr-data-Ya2HJPFp.js.map +0 -1
- package/dist/_chunks/use-segment-params-DzTBpkvj.js +0 -398
- package/dist/_chunks/use-segment-params-DzTBpkvj.js.map +0 -1
- package/docs/learn/04-loading-states.mdx +0 -67
- package/docs/learn/04b-the-flush-point.mdx +0 -115
- package/docs/learn/12-client-navigation.mdx +0 -176
- package/docs/learn/13-configuration.mdx +0 -166
- package/docs/more/01-advanced-routing.mdx +0 -344
- package/docs/more/02-advanced-forms.mdx +0 -137
- package/docs/more/03-coming-from-nextjs.mdx +0 -186
- package/docs/more/04-metadata-and-fonts.mdx +0 -193
- package/docs/more/04b-mdx.mdx +0 -229
- package/docs/more/05-content-collections.mdx +0 -90
- package/docs/more/06-instrumentation.mdx +0 -214
- package/docs/more/07-security.mdx +0 -129
- package/docs/more/08-developer-experience.mdx +0 -134
- package/docs/more/40-why-timber.mdx +0 -50
- package/docs/more/41-timber-vs-nextjs.mdx +0 -81
- package/docs/more/42-timber-vs-others.mdx +0 -68
- package/docs/more/50-ai-agent-instructions.mdx +0 -171
- /package/docs/learn/{06-forms-and-actions.mdx → 08-forms-and-actions.mdx} +0 -0
- /package/docs/learn/{09-caching.mdx → 10-caching.mdx} +0 -0
- /package/docs/learn/{11-error-handling.mdx → 12-error-handling.mdx} +0 -0
- /package/docs/learn/{14-deploying.mdx → 13-deploying.mdx} +0 -0
package/src/client/link.tsx
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
// - searchParams and inline query string are mutually exclusive
|
|
21
21
|
|
|
22
22
|
import {
|
|
23
|
+
startTransition,
|
|
23
24
|
useRef,
|
|
24
25
|
useState,
|
|
25
26
|
type AnchorHTMLAttributes,
|
|
@@ -37,6 +38,8 @@ import { getRouterOrNull } from './router-ref.ts';
|
|
|
37
38
|
import { getSsrData } from './ssr-data.ts';
|
|
38
39
|
import { mergePreservedSearchParams } from '../shared/merge-search-params.ts';
|
|
39
40
|
import { getLinkCodec } from '../params/codec-registry.ts';
|
|
41
|
+
import { locationSearch } from './location-search.ts';
|
|
42
|
+
import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';
|
|
40
43
|
import type { LinkStatus } from './use-link-status.ts';
|
|
41
44
|
|
|
42
45
|
const LINK_PENDING: LinkStatus = { isPending: true };
|
|
@@ -46,16 +49,15 @@ const LINK_IDLE: LinkStatus = { isPending: false };
|
|
|
46
49
|
|
|
47
50
|
/**
|
|
48
51
|
* Read the current URL's search string without requiring a React hook.
|
|
49
|
-
* On the client, reads window.location.search. During SSR, reads
|
|
50
|
-
* the request context (getSsrData)
|
|
52
|
+
* On the client, reads window.location.search. During SSR, reads the raw
|
|
53
|
+
* query string from the request context (getSsrData) — the same `''` or
|
|
54
|
+
* `?…` shape, preserving repeated keys (`?tag=a&tag=b`) so the
|
|
55
|
+
* server-rendered href matches what the hydrated client rebuilds from the
|
|
56
|
+
* address bar (TIM-1428). Returns empty string if unavailable.
|
|
51
57
|
*/
|
|
52
58
|
function getCurrentSearch(): string {
|
|
53
|
-
if (typeof window !== 'undefined') return
|
|
54
|
-
|
|
55
|
-
if (!data) return '';
|
|
56
|
-
const sp = new URLSearchParams(data.searchParams);
|
|
57
|
-
const str = sp.toString();
|
|
58
|
-
return str ? `?${str}` : '';
|
|
59
|
+
if (typeof window !== 'undefined') return locationSearch();
|
|
60
|
+
return getSsrData()?.search ?? '';
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
// ─── Types ───────────────────────────────────────────────────────
|
|
@@ -482,7 +484,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
482
484
|
// a thenable from `startTransition` hands it to `ReactSharedInternals.S`,
|
|
483
485
|
// which calls react-dom's `entangleAsyncAction`. That opens an action scope
|
|
484
486
|
// whose `currentEntangledLane` collects every transition update scheduled
|
|
485
|
-
// while it is open — including
|
|
487
|
+
// while it is open — including the router's `root.render` of `NavigationRoot`, which is a
|
|
486
488
|
// fully synchronous `startTransition` in a different component — and
|
|
487
489
|
// rendering that lane suspends on `currentEntangledActionThenable` until the
|
|
488
490
|
// action settles. `navigateTransition` awaits `decodePromise`, so a Link
|
|
@@ -496,7 +498,12 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
496
498
|
//
|
|
497
499
|
// `isPending` runs from the click until `router.navigate()` settles, which
|
|
498
500
|
// is after `decodePromise` — the same lifecycle the router's pending store
|
|
499
|
-
// and the TopLoader use.
|
|
501
|
+
// and the TopLoader use. The clear wraps in `startTransition` so it lands
|
|
502
|
+
// on the same React lane as the router's `root.render` — without it,
|
|
503
|
+
// the clear is an urgent update that paints before the transition commits,
|
|
504
|
+
// and destinations with pending Suspense boundaries flash idle for one frame
|
|
505
|
+
// while the old page is still showing (TIM-1418). The callback is
|
|
506
|
+
// synchronous, so no action scope is opened (TIM-1306/1307 safe).
|
|
500
507
|
//
|
|
501
508
|
// `clickSeq` exists because a superseded navigation RESOLVES rather than
|
|
502
509
|
// rejecting: `runNavigation` swallows AbortErrors. So clicking the same link
|
|
@@ -509,11 +516,10 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
509
516
|
const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;
|
|
510
517
|
|
|
511
518
|
// Preserve search params from the current URL when requested.
|
|
512
|
-
//
|
|
513
|
-
//
|
|
514
|
-
//
|
|
515
|
-
//
|
|
516
|
-
// getSsrData() provides the request's search params.
|
|
519
|
+
// Read via getCurrentSearch() rather than a hook, to avoid an
|
|
520
|
+
// unconditional hook call for a prop most links don't pass. On the
|
|
521
|
+
// client, window.location.search is always current; during SSR,
|
|
522
|
+
// getSsrData() provides the request's raw query string.
|
|
517
523
|
const internal = isInternalHref(baseHref);
|
|
518
524
|
|
|
519
525
|
// Only preserve search params for internal links — leaking current
|
|
@@ -559,7 +565,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
559
565
|
|
|
560
566
|
if (
|
|
561
567
|
resolved.pathname === window.location.pathname &&
|
|
562
|
-
resolved.search ===
|
|
568
|
+
stripRscCacheKey(resolved.search) === locationSearch() &&
|
|
563
569
|
resolved.hash
|
|
564
570
|
) {
|
|
565
571
|
return;
|
|
@@ -571,11 +577,11 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
571
577
|
// Keep the #fragment — the router commits it to the address bar and
|
|
572
578
|
// scrolls to the matching element after render. The hash is stripped
|
|
573
579
|
// from the RSC fetch URL inside the router (TIM-1035).
|
|
574
|
-
const absoluteHref = resolved.pathname + resolved.search + resolved.hash;
|
|
580
|
+
const absoluteHref = resolved.pathname + stripRscCacheKey(resolved.search) + resolved.hash;
|
|
575
581
|
|
|
576
582
|
const seq = ++clickSeq.current;
|
|
577
583
|
const settle = () => {
|
|
578
|
-
if (clickSeq.current === seq) setIsPending(false);
|
|
584
|
+
if (clickSeq.current === seq) startTransition(() => setIsPending(false));
|
|
579
585
|
};
|
|
580
586
|
setIsPending(true);
|
|
581
587
|
router.navigate(absoluteHref, { scroll: shouldScroll }).then(settle, (error: unknown) => {
|
|
@@ -609,7 +615,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
609
615
|
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
610
616
|
: resolvedHref;
|
|
611
617
|
const resolved = new URL(prefetchHref, window.location.href);
|
|
612
|
-
router.prefetch(resolved.pathname + resolved.search);
|
|
618
|
+
router.prefetch(resolved.pathname + stripRscCacheKey(resolved.search));
|
|
613
619
|
}
|
|
614
620
|
}
|
|
615
621
|
: userOnMouseEnter;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The app-visible search string from the address bar.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors what `appVisibleSearch()` does on the server: strips key-shaped
|
|
7
|
+
* `_rsc` values so the client and server agree on search state regardless
|
|
8
|
+
* of whether the document URL itself carries a payload cache key (a user
|
|
9
|
+
* pasting/sharing an RSC payload URL). Non-key-shaped `_rsc` values — an
|
|
10
|
+
* application legitimately owning that name — survive, using the same
|
|
11
|
+
* `isRscCacheKeyShape` predicate the server uses.
|
|
12
|
+
*/
|
|
13
|
+
export function locationSearch(): string {
|
|
14
|
+
return stripRscCacheKey(window.location.search);
|
|
15
|
+
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { isHardNavigating } from './navigation-root.tsx';
|
|
21
|
+
import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';
|
|
21
22
|
|
|
22
23
|
// ─── Feature Detection ───────────────────────────────────────────
|
|
23
24
|
|
|
@@ -220,7 +221,7 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
|
|
|
220
221
|
// Skip reload navigations — let the browser handle full page reload
|
|
221
222
|
if (event.navigationType === 'reload') return;
|
|
222
223
|
|
|
223
|
-
const url = destUrl.pathname + destUrl.search;
|
|
224
|
+
const url = destUrl.pathname + stripRscCacheKey(destUrl.search);
|
|
224
225
|
|
|
225
226
|
if (event.navigationType === 'traverse') {
|
|
226
227
|
// Back/forward button — intercept and delegate to router.
|
|
@@ -265,7 +266,8 @@ export function setupNavigationApi(callbacks: NavigationApiCallbacks): Navigatio
|
|
|
265
266
|
// destination. Once intercept() is called, currentEntry switches and
|
|
266
267
|
// getCurrentUrl() returns the destination (TIM-1232).
|
|
267
268
|
const departingUrl = nav.currentEntry?.url
|
|
268
|
-
? new URL(nav.currentEntry.url).pathname +
|
|
269
|
+
? new URL(nav.currentEntry.url).pathname +
|
|
270
|
+
stripRscCacheKey(new URL(nav.currentEntry.url).search)
|
|
269
271
|
: undefined;
|
|
270
272
|
|
|
271
273
|
event.intercept({
|
|
@@ -54,6 +54,51 @@ export interface NavigationCommitInput {
|
|
|
54
54
|
* See TIM-1356.
|
|
55
55
|
*/
|
|
56
56
|
status?: number;
|
|
57
|
+
/**
|
|
58
|
+
* Layouts the server skipped on this response (`X-Timber-Skipped-Segments`),
|
|
59
|
+
* when it came from a fetch. Omitted by replays and revalidation.
|
|
60
|
+
*
|
|
61
|
+
* Together with `skipped` slot entries in `segmentInfo`, this decides
|
|
62
|
+
* whether the payload can be *replayed*. A partial payload only renders
|
|
63
|
+
* merged over the tree on screen, and a `skip=true` outlet resolves against
|
|
64
|
+
* the slot content cache at render time — so a stored copy of either,
|
|
65
|
+
* replayed later, shows whatever the client holds *then*, not what was on
|
|
66
|
+
* screen when the entry committed. Such entries store a null payload and
|
|
67
|
+
* traversals to them fetch instead. Decided here, not at the fetch call
|
|
68
|
+
* sites, for the same reason as `status`: `performNavigationFetch` had the
|
|
69
|
+
* check and `fetchCommitAndRender` did not, so a back/forward fetch that
|
|
70
|
+
* was answered with skips stored the holes (TIM-1432).
|
|
71
|
+
*/
|
|
72
|
+
skippedSegments?: string[] | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Whether a response skipped layouts — its payload merges over the tree on screen. */
|
|
76
|
+
export function isPartialNavigation(skippedSegments: string[] | null | undefined): boolean {
|
|
77
|
+
return skippedSegments != null && skippedSegments.length > 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether a payload can stand on its own when replayed from the history
|
|
82
|
+
* stack. See `NavigationCommitInput.skippedSegments`.
|
|
83
|
+
*/
|
|
84
|
+
function isReplayable(opts: NavigationCommitInput): boolean {
|
|
85
|
+
return !isPartialNavigation(opts.skippedSegments) && !hasSkippedSlot(opts.segmentInfo);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasSkippedSlot(segmentInfo: SegmentInfo[] | null | undefined): boolean {
|
|
89
|
+
return segmentInfo?.some((s) => s.slot && s.skipped) ?? false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The metadata a history entry keeps. `skipped` describes the *response*
|
|
94
|
+
* — "this slot's content was omitted" — and the entry stores no such
|
|
95
|
+
* payload (it stores null). Left on the entry, the flag outlives the
|
|
96
|
+
* response: `applyRevalidation()` reuses the entry's metadata beside a full
|
|
97
|
+
* re-render and would have that judged non-replayable too.
|
|
98
|
+
*/
|
|
99
|
+
function storedSegmentInfo(segmentInfo: SegmentInfo[] | null | undefined) {
|
|
100
|
+
if (!hasSkippedSlot(segmentInfo)) return segmentInfo;
|
|
101
|
+
return segmentInfo!.map(({ skipped: _skipped, ...rest }) => rest);
|
|
57
102
|
}
|
|
58
103
|
|
|
59
104
|
/**
|
|
@@ -173,6 +218,7 @@ export function createNavigationCommitter(deps: {
|
|
|
173
218
|
// traversal back to this URL would replay the metadata this drops.
|
|
174
219
|
const cacheable = opts.status === undefined || opts.status < 400;
|
|
175
220
|
const segmentInfo = cacheable ? opts.segmentInfo : [];
|
|
221
|
+
const payload = isReplayable(opts) ? opts.payload : null;
|
|
176
222
|
|
|
177
223
|
return {
|
|
178
224
|
navState,
|
|
@@ -186,9 +232,9 @@ export function createNavigationCommitter(deps: {
|
|
|
186
232
|
setNavigationState(navState);
|
|
187
233
|
|
|
188
234
|
historyStack.push(url, {
|
|
189
|
-
payload
|
|
235
|
+
payload,
|
|
190
236
|
params: opts.params,
|
|
191
|
-
segmentInfo,
|
|
237
|
+
segmentInfo: storedSegmentInfo(segmentInfo),
|
|
192
238
|
});
|
|
193
239
|
},
|
|
194
240
|
};
|
|
@@ -18,14 +18,15 @@
|
|
|
18
18
|
* All consumers (usePathname, useSearchParams) see the new values in the
|
|
19
19
|
* same render pass as the new tree.
|
|
20
20
|
*
|
|
21
|
-
* During SSR,
|
|
22
|
-
*
|
|
21
|
+
* During SSR, the wrapper chain mounts this same provider with the
|
|
22
|
+
* request's pathname and raw search string (TIM-1424), so the hooks
|
|
23
|
+
* resolve through the identical code path on both sides. Per-request
|
|
24
|
+
* isolation is by construction — the value is part of the request's
|
|
25
|
+
* element tree.
|
|
23
26
|
*
|
|
24
|
-
*
|
|
25
|
-
* environment
|
|
26
|
-
*
|
|
27
|
-
* that depend on these APIs are safe to call from any environment —
|
|
28
|
-
* they return null or no-op when the APIs aren't available.
|
|
27
|
+
* This module is `'use client'` and is never evaluated in the RSC
|
|
28
|
+
* environment. The shims plugin resolves next/navigation to
|
|
29
|
+
* navigation-rsc.ts in RSC, which has throwing stubs (TIM-1420).
|
|
29
30
|
*
|
|
30
31
|
* SINGLETON GUARANTEE: All shared mutable state uses globalThis via
|
|
31
32
|
* Symbol.for keys. The RSC client bundler can duplicate this module
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
* See design/19-client-navigation.md §"NavigationContext"
|
|
42
43
|
*/
|
|
43
44
|
|
|
44
|
-
import
|
|
45
|
+
import { createContext, useContext, createElement, type ReactNode } from 'react';
|
|
45
46
|
import type { SlotParamsRecord } from '../shared/slot-params.ts';
|
|
46
47
|
|
|
47
48
|
// ---------------------------------------------------------------------------
|
|
@@ -68,10 +69,6 @@ export interface NavigationState {
|
|
|
68
69
|
// ---------------------------------------------------------------------------
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
|
-
* The context is created lazily to avoid calling createContext at module
|
|
72
|
-
* level. In the RSC environment, React.createContext doesn't exist —
|
|
73
|
-
* calling it at import time would crash the server.
|
|
74
|
-
*
|
|
75
72
|
* Context instances are stored on globalThis (NOT in module-level
|
|
76
73
|
* variables) because the ESM bundler can duplicate this module across
|
|
77
74
|
* chunks. Module-level variables would create separate instances per
|
|
@@ -79,38 +76,34 @@ export interface NavigationState {
|
|
|
79
76
|
* context A while the consumer in usePendingNavigation (shared chunk)
|
|
80
77
|
* reads from context B. globalThis guarantees a single instance.
|
|
81
78
|
*
|
|
79
|
+
* This module is `'use client'` so it is never evaluated in the RSC
|
|
80
|
+
* environment — `createContext` and `useContext` are always available.
|
|
81
|
+
* The shims plugin resolves next/navigation to navigation-rsc.ts in RSC,
|
|
82
|
+
* which has throwing stubs for the hooks (TIM-1420).
|
|
83
|
+
*
|
|
82
84
|
* See design/27-chunking-strategy.md §"Singleton Safety"
|
|
83
85
|
*/
|
|
84
86
|
|
|
85
|
-
// Symbol keys for globalThis storage — prevents collisions with user code
|
|
86
87
|
const NAV_CTX_KEY = Symbol.for('__timber_nav_ctx');
|
|
87
88
|
|
|
88
|
-
function getOrCreateContext():
|
|
89
|
+
function getOrCreateContext(): import('react').Context<NavigationState | null> {
|
|
89
90
|
const existing = (globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] as
|
|
90
|
-
|
|
|
91
|
+
| import('react').Context<NavigationState | null>
|
|
91
92
|
| undefined;
|
|
92
93
|
if (existing !== undefined) return existing;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
(globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;
|
|
97
|
-
return ctx;
|
|
98
|
-
}
|
|
99
|
-
return undefined;
|
|
94
|
+
const ctx = createContext<NavigationState | null>(null);
|
|
95
|
+
(globalThis as Record<symbol, unknown>)[NAV_CTX_KEY] = ctx;
|
|
96
|
+
return ctx;
|
|
100
97
|
}
|
|
101
98
|
|
|
102
99
|
/**
|
|
103
|
-
* Read the navigation context. Returns null
|
|
104
|
-
*
|
|
100
|
+
* Read the navigation context. Returns null only when no provider is above
|
|
101
|
+
* the caller — a component rendered outside the timber app.
|
|
102
|
+
*
|
|
105
103
|
* Internal — used by usePathname() and useSearchParams().
|
|
106
104
|
*/
|
|
107
105
|
export function useNavigationContext(): NavigationState | null {
|
|
108
|
-
|
|
109
|
-
if (!ctx) return null;
|
|
110
|
-
// useContext may not exist in the RSC environment — caller wraps in try/catch
|
|
111
|
-
if (typeof React.useContext !== 'function') return null;
|
|
112
|
-
// eslint-disable-next-line rules-of-hooks -- conditional on environment, not render path
|
|
113
|
-
return React.useContext(ctx);
|
|
106
|
+
return useContext(getOrCreateContext());
|
|
114
107
|
}
|
|
115
108
|
|
|
116
109
|
// ---------------------------------------------------------------------------
|
|
@@ -131,13 +124,8 @@ export interface NavigationProviderProps {
|
|
|
131
124
|
export function NavigationProvider({
|
|
132
125
|
value,
|
|
133
126
|
children,
|
|
134
|
-
}: NavigationProviderProps):
|
|
135
|
-
|
|
136
|
-
if (!ctx) {
|
|
137
|
-
// RSC environment — no context available. Return children as-is.
|
|
138
|
-
return children as React.ReactElement;
|
|
139
|
-
}
|
|
140
|
-
return createElement(ctx.Provider, { value }, children);
|
|
127
|
+
}: NavigationProviderProps): import('react').ReactElement {
|
|
128
|
+
return createElement(getOrCreateContext().Provider, { value }, children);
|
|
141
129
|
}
|
|
142
130
|
|
|
143
131
|
// ---------------------------------------------------------------------------
|