@timber-js/app 0.2.0-alpha.196 → 0.2.0-alpha.198
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-dVDi1GQz.js} +3 -3
- package/dist/_chunks/{cli-check-BVthpfLS.js.map → cli-check-dVDi1GQz.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-Ph6luW4c.js} +4 -2
- package/dist/_chunks/convention-lint-Ph6luW4c.js.map +1 -0
- 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 +138 -35
- 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/codegen-write.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 +18 -18
- 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/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- 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/convention-lint-DO10_pVl.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
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hydration — pre-hydration sequence and React root
|
|
2
|
+
* Hydration — pre-hydration sequence and hydrating the React root.
|
|
3
3
|
*
|
|
4
4
|
* Handles two paths:
|
|
5
|
-
* 1. RSC payload available:
|
|
6
|
-
* 2. No RSC payload:
|
|
5
|
+
* 1. RSC payload available: hydrate the router's React root with the payload
|
|
6
|
+
* 2. No RSC payload: nothing — the root is created by the first render
|
|
7
|
+
* (client/react-root.ts)
|
|
7
8
|
*
|
|
8
9
|
* Pre-hydration ordering contract (MUST execute in this order):
|
|
9
10
|
* 1. initRouter() — creates the global router so useRouter()
|
|
@@ -21,22 +22,14 @@
|
|
|
21
22
|
* See design/19-client-navigation.md §"NavigationContext"
|
|
22
23
|
*/
|
|
23
24
|
|
|
24
|
-
import { createElement } from 'react';
|
|
25
|
-
import { hydrateRoot, createRoot } from 'react-dom/client';
|
|
26
25
|
import { getRouterOrNull } from '#client-internal';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
NavigationProvider,
|
|
30
|
-
getNavigationState,
|
|
31
|
-
setNavigationState,
|
|
32
|
-
} from '../navigation-context.ts';
|
|
33
|
-
import { SegmentUpdateContext, EMPTY_SEGMENT_UPDATES } from '../segment-update-context.ts';
|
|
34
|
-
import { TimberNuqsAdapter } from '../nuqs-adapter.tsx';
|
|
26
|
+
import { getNavigationState, setNavigationState } from '../navigation-context.ts';
|
|
35
27
|
import { isPageUnloading } from '../unload-guard.ts';
|
|
36
|
-
import {
|
|
37
|
-
import type {
|
|
28
|
+
import { locationSearch } from '../location-search.ts';
|
|
29
|
+
import type { ReactRootHost } from '../react-root.ts';
|
|
38
30
|
|
|
39
31
|
import type { RscStreamResult } from './rsc-stream.ts';
|
|
32
|
+
import type { RouterInitResult } from './router-init.ts';
|
|
40
33
|
|
|
41
34
|
let showHydrationError: ((error: unknown) => void) | null = null;
|
|
42
35
|
let isHydrationError: ((error: unknown) => boolean) | null = null;
|
|
@@ -79,8 +72,10 @@ function queueOrShowHydrationError(error: unknown): void {
|
|
|
79
72
|
interface HydrateOptions {
|
|
80
73
|
/** RSC stream result (null if no inlined payload) */
|
|
81
74
|
rscResult: RscStreamResult | null;
|
|
82
|
-
/**
|
|
83
|
-
|
|
75
|
+
/** The router's React root — the one every navigation renders through. */
|
|
76
|
+
reactRoot: ReactRootHost;
|
|
77
|
+
/** The router's wrapper chain — see `RouterInitResult.renderTree`. */
|
|
78
|
+
renderTree: RouterInitResult['renderTree'];
|
|
84
79
|
}
|
|
85
80
|
|
|
86
81
|
/**
|
|
@@ -97,102 +92,57 @@ interface HydrateOptions {
|
|
|
97
92
|
export function runPreHydration(): void {
|
|
98
93
|
setNavigationState({
|
|
99
94
|
pathname: window.location.pathname,
|
|
100
|
-
search:
|
|
95
|
+
search: locationSearch(),
|
|
101
96
|
});
|
|
102
97
|
}
|
|
103
98
|
|
|
104
99
|
/**
|
|
105
|
-
* Hydrate the React tree
|
|
100
|
+
* Hydrate the React tree when an RSC payload is available.
|
|
106
101
|
*
|
|
107
|
-
*
|
|
108
|
-
* TimberNuqsAdapter
|
|
109
|
-
* document.
|
|
102
|
+
* Wraps the payload with the router's chain (NavigationProvider,
|
|
103
|
+
* TimberNuqsAdapter, the slot content cache, ...) and hydrates the router's
|
|
104
|
+
* React root with it, on the document.
|
|
110
105
|
*
|
|
111
|
-
* When no RSC payload is available (JS-only client)
|
|
112
|
-
*
|
|
106
|
+
* When no RSC payload is available (JS-only client) there is nothing to
|
|
107
|
+
* hydrate and nothing to do: the root must not be created at bootstrap —
|
|
108
|
+
* `createRoot(document).render()` would take React ownership of the entire
|
|
109
|
+
* document and blank the SSR HTML — so the router's first render, i.e. the
|
|
110
|
+
* first navigation or revalidation, creates it with the tree it renders
|
|
111
|
+
* (`createReactRoot`, TIM-600 / TIM-580).
|
|
113
112
|
*/
|
|
114
|
-
export function hydrateApp({ rscResult,
|
|
115
|
-
if (rscResult)
|
|
116
|
-
const element = rscResult.element;
|
|
113
|
+
export function hydrateApp({ rscResult, reactRoot, renderTree }: HydrateOptions): void {
|
|
114
|
+
if (!rscResult) return;
|
|
117
115
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// changes the type at that position, and React remounts everything below
|
|
130
|
-
// it — losing the layout state partial navigation exists to preserve.
|
|
131
|
-
//
|
|
132
|
-
// `source` is the payload's params promise, not a resolved record: the
|
|
133
|
-
// inlined Flight stream is still arriving and hydration cannot wait for
|
|
134
|
-
// it. PayloadRoot suspends on it exactly where React was going to suspend
|
|
135
|
-
// on `element` (TIM-1297).
|
|
136
|
-
const withParams = createElement(
|
|
137
|
-
PayloadRoot,
|
|
138
|
-
{ source: rscResult.params },
|
|
139
|
-
element as React.ReactNode
|
|
140
|
-
);
|
|
141
|
-
const withNav = createElement(NavigationProvider, { value: navState }, withParams);
|
|
142
|
-
const withUpdates = createElement(
|
|
143
|
-
SegmentUpdateContext.Provider,
|
|
144
|
-
{ value: EMPTY_SEGMENT_UPDATES },
|
|
145
|
-
withNav
|
|
146
|
-
);
|
|
147
|
-
const wrapped = createElement(TimberNuqsAdapter, null, withUpdates);
|
|
148
|
-
const rootElement = createElement(NavigationRoot, {
|
|
149
|
-
initial: wrapped,
|
|
150
|
-
topLoaderConfig: config.topLoader,
|
|
151
|
-
});
|
|
116
|
+
// The chain is the router's own `renderTree`, not a copy: an element type
|
|
117
|
+
// that is present here and absent on the first navigation (or the
|
|
118
|
+
// reverse) changes the type at that position, and React remounts
|
|
119
|
+
// everything below it — losing the layout state partial navigation exists
|
|
120
|
+
// to preserve. Sharing the function makes that impossible by construction.
|
|
121
|
+
//
|
|
122
|
+
// `rscResult.params` is the payload's params promise, not a resolved
|
|
123
|
+
// record: the inlined Flight stream is still arriving and hydration cannot
|
|
124
|
+
// wait for it. PayloadRoot suspends on it exactly where React was going to
|
|
125
|
+
// suspend on `element` (TIM-1297).
|
|
126
|
+
const wrapped = renderTree(rscResult.element, getNavigationState(), rscResult.params);
|
|
152
127
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
'[timber] hydrateRoot called before initRouter() — bootstrap order violated'
|
|
157
|
-
);
|
|
158
|
-
}
|
|
128
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
129
|
+
if (!getRouterOrNull()) {
|
|
130
|
+
throw new Error('[timber] hydrateRoot called before initRouter() — bootstrap order violated');
|
|
159
131
|
}
|
|
160
|
-
|
|
161
|
-
hydrateRoot(document, rootElement, {
|
|
162
|
-
// Suppress recoverable hydration errors from deny/error signals
|
|
163
|
-
// inside Suspense boundaries. The server already handled these
|
|
164
|
-
// (wrapStreamWithErrorHandling closes the stream cleanly after
|
|
165
|
-
// the shell is flushed). React replays the error during hydration
|
|
166
|
-
// but the server HTML is already correct — no recovery needed.
|
|
167
|
-
onRecoverableError(error: unknown) {
|
|
168
|
-
if (isPageUnloading()) return;
|
|
169
|
-
if (process.env.NODE_ENV === 'development') {
|
|
170
|
-
queueOrShowHydrationError(error);
|
|
171
|
-
console.debug('[timber] Hydration recoverable error:', error);
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
});
|
|
175
|
-
} else {
|
|
176
|
-
// No RSC payload available — defer React root creation until the
|
|
177
|
-
// first client navigation (TIM-600).
|
|
178
|
-
//
|
|
179
|
-
// We must NOT call createRoot(document).render() here — that would
|
|
180
|
-
// take React ownership of the entire document and blank the SSR HTML.
|
|
181
|
-
// Instead, installDeferredNavigation sets up one-shot callbacks so
|
|
182
|
-
// the first navigateTransition/transitionRender call creates the root
|
|
183
|
-
// on `document` with the navigated content. After that initial render,
|
|
184
|
-
// NavigationRoot's real startTransition-based callbacks take over.
|
|
185
|
-
//
|
|
186
|
-
// This also fixes TIM-580 (navigation from SSR-only pages) because
|
|
187
|
-
// the deferred callbacks ensure NavigationRoot is mounted before the
|
|
188
|
-
// first navigation completes.
|
|
189
|
-
installDeferredNavigation((initial) => {
|
|
190
|
-
const rootElement = createElement(NavigationRoot, {
|
|
191
|
-
initial,
|
|
192
|
-
topLoaderConfig: config.topLoader,
|
|
193
|
-
});
|
|
194
|
-
const root = createRoot(document);
|
|
195
|
-
root.render(rootElement);
|
|
196
|
-
});
|
|
197
132
|
}
|
|
133
|
+
|
|
134
|
+
reactRoot.hydrate(wrapped, {
|
|
135
|
+
// Suppress recoverable hydration errors from deny/error signals
|
|
136
|
+
// inside Suspense boundaries. The server already handled these
|
|
137
|
+
// (wrapStreamWithErrorHandling closes the stream cleanly after
|
|
138
|
+
// the shell is flushed). React replays the error during hydration
|
|
139
|
+
// but the server HTML is already correct — no recovery needed.
|
|
140
|
+
onRecoverableError(error: unknown) {
|
|
141
|
+
if (isPageUnloading()) return;
|
|
142
|
+
if (process.env.NODE_ENV === 'development') {
|
|
143
|
+
queueOrShowHydrationError(error);
|
|
144
|
+
console.debug('[timber] Hydration recoverable error:', error);
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
});
|
|
198
148
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* action-dispatch.ts — server action callServer callback
|
|
8
8
|
* rsc-stream.ts — __timber_f chunk handling + ReadableStream
|
|
9
9
|
* router-init.ts — createRouter + Navigation API setup
|
|
10
|
-
* hydrate.ts — pre-hydration sequence +
|
|
10
|
+
* hydrate.ts — pre-hydration sequence + hydrating the root
|
|
11
11
|
* post-hydration.ts — history stack, segment cache, popstate, scroll
|
|
12
12
|
* hmr.ts — dev-only HMR + error forwarding
|
|
13
13
|
* scroll.ts — getScrollY helper
|
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
*
|
|
17
17
|
* 1. setupServerActions() — register callServer (independent)
|
|
18
18
|
* 2. createRscPayloadStream() — decode inlined RSC payload
|
|
19
|
-
* 3.
|
|
19
|
+
* 3. createReactRoot() + — the root host the router renders through,
|
|
20
|
+
* createTimberRouter() then the router + Navigation API
|
|
20
21
|
* 4. runPreHydration() — set params + navigation state
|
|
21
|
-
* 5. hydrateApp() —
|
|
22
|
+
* 5. hydrateApp() — hydrate the root (no-op without a payload)
|
|
22
23
|
* 6. setupPostHydration() — history stack, popstate, scroll
|
|
23
24
|
* 7. setupHmr() — dev-only HMR wiring
|
|
24
25
|
* 8. timber-ready signal — E2E test readiness indicator
|
|
@@ -41,6 +42,8 @@ import { initStaleClient } from '../stale-client.ts';
|
|
|
41
42
|
import { setupServerActions } from './action-dispatch.ts';
|
|
42
43
|
import { createRscPayloadStream } from './rsc-stream.ts';
|
|
43
44
|
import { createTimberRouter } from './router-init.ts';
|
|
45
|
+
import { createReactRoot } from '../react-root.ts';
|
|
46
|
+
import type { TopLoaderConfig } from '../top-loader.tsx';
|
|
44
47
|
import { readPublishedParams } from '../../shared/payload-root.ts';
|
|
45
48
|
import { runPreHydration, hydrateApp } from './hydrate.ts';
|
|
46
49
|
import { setupPostHydration } from './post-hydration.ts';
|
|
@@ -79,8 +82,15 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
79
82
|
// Step 2: Decode inlined RSC payload (may be null for JS-only clients)
|
|
80
83
|
const rscResult = createRscPayloadStream();
|
|
81
84
|
|
|
82
|
-
// Step 3: Create router
|
|
83
|
-
|
|
85
|
+
// Step 3: Create the React root host and the router that renders through
|
|
86
|
+
// it. The root itself is created lazily — by hydration below, or by the
|
|
87
|
+
// first navigation when there is no payload to hydrate (TIM-600).
|
|
88
|
+
const reactRoot = createReactRoot({
|
|
89
|
+
container: document,
|
|
90
|
+
topLoaderConfig: (runtimeConfig as { topLoader?: TopLoaderConfig }).topLoader,
|
|
91
|
+
});
|
|
92
|
+
const { router, navApiController, renderTree } = createTimberRouter({
|
|
93
|
+
render: reactRoot.render,
|
|
84
94
|
initialElement: rscResult?.element ?? undefined,
|
|
85
95
|
initialParams: rscResult?.params,
|
|
86
96
|
clientSegmentCache:
|
|
@@ -91,7 +101,7 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
91
101
|
runPreHydration();
|
|
92
102
|
|
|
93
103
|
// Step 5: Hydrate or set up deferred root creation
|
|
94
|
-
hydrateApp({ rscResult,
|
|
104
|
+
hydrateApp({ rscResult, reactRoot, renderTree });
|
|
95
105
|
|
|
96
106
|
// Step 6: Post-hydration wiring
|
|
97
107
|
setupPostHydration({
|
|
@@ -16,6 +16,7 @@ import { getNavigationState } from '../navigation-context.ts';
|
|
|
16
16
|
import type { NavigationApiController } from '../navigation-api.ts';
|
|
17
17
|
import { getScrollY } from './scroll.ts';
|
|
18
18
|
import type { ParamsSource } from '../../shared/payload-root.ts';
|
|
19
|
+
import { locationSearch } from '../location-search.ts';
|
|
19
20
|
|
|
20
21
|
interface PostHydrationOptions {
|
|
21
22
|
router: RouterInstance;
|
|
@@ -54,7 +55,7 @@ export function setupPostHydration({
|
|
|
54
55
|
// `initialParams`. Both came out of one split of one decoded payload root
|
|
55
56
|
// (rsc-stream.ts), so the entry cannot replay with another route's params —
|
|
56
57
|
// which is what a separately-seeded copy used to do (TIM-1037, TIM-1285).
|
|
57
|
-
router.historyStack.push(window.location.pathname +
|
|
58
|
+
router.historyStack.push(window.location.pathname + locationSearch(), {
|
|
58
59
|
payload: initialElement,
|
|
59
60
|
params: initialParams,
|
|
60
61
|
segmentInfo: initialSegmentInfo,
|
|
@@ -94,7 +95,7 @@ export function setupPostHydration({
|
|
|
94
95
|
|
|
95
96
|
const state = window.history.state;
|
|
96
97
|
const scrollY = state && typeof state.scrollY === 'number' ? state.scrollY : 0;
|
|
97
|
-
void router.handlePopState(window.location.pathname +
|
|
98
|
+
void router.handlePopState(window.location.pathname + locationSearch(), scrollY);
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
// Keep scroll position up to date as the user scrolls.
|
|
@@ -20,16 +20,26 @@ import {
|
|
|
20
20
|
getNavigationState,
|
|
21
21
|
setNavigationState,
|
|
22
22
|
} from '../navigation-context.ts';
|
|
23
|
-
import {
|
|
23
|
+
import { navigateTransition } from '../navigation-transition.ts';
|
|
24
|
+
import type { NavigationRender } from '../navigation-root.tsx';
|
|
24
25
|
import { SegmentUpdateContext, EMPTY_SEGMENT_UPDATES } from '../segment-update-context.ts';
|
|
26
|
+
import { SlotContentCacheContext, type SlotContentCache } from '../slot-content-cache-context.ts';
|
|
25
27
|
import {
|
|
26
28
|
hasNavigationApi,
|
|
27
29
|
setupNavigationApi,
|
|
28
30
|
type NavigationApiController,
|
|
29
31
|
} from '../navigation-api.ts';
|
|
30
32
|
import { getScrollY, scrollToHashTarget } from './scroll.ts';
|
|
33
|
+
import { locationSearch } from '../location-search.ts';
|
|
34
|
+
import { stripRscCacheKey } from '../../shared/rsc-cache-key.ts';
|
|
31
35
|
|
|
32
36
|
export interface RouterInitOptions {
|
|
37
|
+
/**
|
|
38
|
+
* The router's React root (`createReactRoot().render`). Every path that puts
|
|
39
|
+
* a page on screen renders through it — it is the synchronous transition
|
|
40
|
+
* around `root.render`, and the `publish` it takes runs on React's commit.
|
|
41
|
+
*/
|
|
42
|
+
render: NavigationRender;
|
|
33
43
|
initialElement?: unknown;
|
|
34
44
|
/** The params the hydration payload published, beside `initialElement`. */
|
|
35
45
|
initialParams?: ParamsSource;
|
|
@@ -39,6 +49,18 @@ export interface RouterInitOptions {
|
|
|
39
49
|
export interface RouterInitResult {
|
|
40
50
|
router: RouterInstance;
|
|
41
51
|
navApiController: NavigationApiController | null;
|
|
52
|
+
/**
|
|
53
|
+
* The client's wrapper chain around a tree — the one every navigation
|
|
54
|
+
* renders through. Hydration builds its root with this too, so the
|
|
55
|
+
* hydrated tree and the first navigated tree cannot differ in shape
|
|
56
|
+
* (a type present on one and absent on the other remounts everything
|
|
57
|
+
* below it) and share the router's slot content cache.
|
|
58
|
+
*/
|
|
59
|
+
renderTree: (
|
|
60
|
+
tree: unknown,
|
|
61
|
+
navState: NavigationState,
|
|
62
|
+
params: ParamsSource
|
|
63
|
+
) => React.ReactElement;
|
|
42
64
|
}
|
|
43
65
|
|
|
44
66
|
/**
|
|
@@ -48,7 +70,9 @@ export interface RouterInitResult {
|
|
|
48
70
|
* the initial render (methods lazily resolve the router at invocation,
|
|
49
71
|
* not render time, but initRouter must still run first).
|
|
50
72
|
*/
|
|
51
|
-
export function createTimberRouter(options
|
|
73
|
+
export function createTimberRouter(options: RouterInitOptions): RouterInitResult {
|
|
74
|
+
const { render } = options;
|
|
75
|
+
|
|
52
76
|
// Feature-detect Navigation API. When available, the navigate event
|
|
53
77
|
// replaces popstate for back/forward and catches external navigations.
|
|
54
78
|
// See design/19-client-navigation.md §"Navigation API Integration"
|
|
@@ -60,27 +84,35 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
60
84
|
// SegmentUpdateContext, not from new children props).
|
|
61
85
|
// Initialized from the hydration element so partial nav works on the
|
|
62
86
|
// very first client navigation (before any wrapPayload call).
|
|
63
|
-
let currentPayload: unknown = options
|
|
87
|
+
let currentPayload: unknown = options.initialElement ?? null;
|
|
64
88
|
|
|
65
89
|
// The params `currentPayload` is displayed with. Tracked beside it because
|
|
66
90
|
// the two render paths that do NOT receive a payload — a shallow search
|
|
67
91
|
// update and any re-wrap of the current tree — must republish the same
|
|
68
92
|
// record rather than dropping it. They are written together, in one place
|
|
69
93
|
// (`renderTree`), so they cannot come apart.
|
|
70
|
-
let currentParams: ParamsSource = options
|
|
94
|
+
let currentParams: ParamsSource = options.initialParams ?? readPublishedParams(undefined);
|
|
95
|
+
|
|
96
|
+
// The slot content the client has committed, by segmentPath. Owned here so
|
|
97
|
+
// there is exactly one per router (never a module or globalThis singleton —
|
|
98
|
+
// SegmentOutlet also executes during SSR, where a process-wide Map would be
|
|
99
|
+
// shared across requests). Read by SegmentOutlet in render, written by it
|
|
100
|
+
// at commit only. See client/slot-content-cache-context.ts (TIM-1423).
|
|
101
|
+
const slotContentCache: SlotContentCache = new Map();
|
|
71
102
|
|
|
72
103
|
/**
|
|
73
104
|
* Build the client's wrapper chain around a tree. **The only place it is
|
|
74
105
|
* built.**
|
|
75
106
|
*
|
|
76
|
-
* Every render path goes through here — hydration
|
|
77
|
-
*
|
|
78
|
-
* update, a popstate replay and a revalidation all call this. That
|
|
79
|
-
* beyond tidiness: an element type present on one render and absent
|
|
80
|
-
* next changes the type at that position, so React unmounts and
|
|
81
|
-
* everything below it, destroying the layout state partial
|
|
82
|
-
* exists to preserve (caught by
|
|
83
|
-
* when the params wrapper was
|
|
107
|
+
* Every render path goes through here — hydration (`hydrate.ts`) receives
|
|
108
|
+
* this function and builds its root with it, and a navigation, a shallow
|
|
109
|
+
* search update, a popstate replay and a revalidation all call this. That
|
|
110
|
+
* matters beyond tidiness: an element type present on one render and absent
|
|
111
|
+
* on the next changes the type at that position, so React unmounts and
|
|
112
|
+
* remounts everything below it, destroying the layout state partial
|
|
113
|
+
* navigation exists to preserve (caught by
|
|
114
|
+
* tests/e2e/segment-merge-navigation.test.ts when the params wrapper was
|
|
115
|
+
* conditional).
|
|
84
116
|
*
|
|
85
117
|
* `PayloadRoot` is *above* `currentPayload`, which on a partial navigation
|
|
86
118
|
* is the retained tree. That position is the whole design: it is the only
|
|
@@ -105,7 +137,12 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
105
137
|
{ value: segmentUpdates ?? EMPTY_SEGMENT_UPDATES },
|
|
106
138
|
withNav
|
|
107
139
|
);
|
|
108
|
-
|
|
140
|
+
const withSlotCache = createElement(
|
|
141
|
+
SlotContentCacheContext.Provider,
|
|
142
|
+
{ value: slotContentCache },
|
|
143
|
+
withUpdates
|
|
144
|
+
);
|
|
145
|
+
return createElement(TimberNuqsAdapter, null, withSlotCache);
|
|
109
146
|
}
|
|
110
147
|
|
|
111
148
|
/**
|
|
@@ -145,11 +182,11 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
145
182
|
* the patched pushState/replaceState returns).
|
|
146
183
|
*
|
|
147
184
|
* Bails when search is unchanged (scroll saves, no-ops, dual-fire).
|
|
148
|
-
* Re-wraps currentPayload with updated NavigationProvider and
|
|
149
|
-
*
|
|
185
|
+
* Re-wraps currentPayload with updated NavigationProvider and hands it to
|
|
186
|
+
* React through the root's `render` (startTransition — non-blocking).
|
|
150
187
|
*/
|
|
151
188
|
function syncShallowSearch(search?: string): void {
|
|
152
|
-
const currentSearch = search
|
|
189
|
+
const currentSearch = search != null ? stripRscCacheKey(search) : locationSearch();
|
|
153
190
|
const navState = getNavigationState();
|
|
154
191
|
if (currentSearch === navState.search) return;
|
|
155
192
|
if (!currentPayload) return;
|
|
@@ -167,7 +204,7 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
167
204
|
setNavigationState(newNavState);
|
|
168
205
|
|
|
169
206
|
publishTree(currentPayload, currentParams);
|
|
170
|
-
|
|
207
|
+
render(renderTree(currentPayload, newNavState, currentParams), null);
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
window.history.pushState = function (
|
|
@@ -222,7 +259,7 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
222
259
|
(el as HTMLElement).scrollTop = y;
|
|
223
260
|
}
|
|
224
261
|
},
|
|
225
|
-
getCurrentUrl: () => window.location.pathname +
|
|
262
|
+
getCurrentUrl: () => window.location.pathname + locationSearch(),
|
|
226
263
|
getScrollY,
|
|
227
264
|
scrollToHash: scrollToHashTarget,
|
|
228
265
|
|
|
@@ -230,8 +267,8 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
230
267
|
return createFromFetch(fetchPromise);
|
|
231
268
|
},
|
|
232
269
|
|
|
233
|
-
// Render decoded RSC tree
|
|
234
|
-
//
|
|
270
|
+
// Render a decoded RSC tree through the router's React root. Used for
|
|
271
|
+
// non-navigation renders (popstate cached replay, applyRevalidation).
|
|
235
272
|
// Wraps with NavigationProvider + TimberNuqsAdapter.
|
|
236
273
|
//
|
|
237
274
|
// For navigation renders (navigate, refresh, popstate-with-fetch),
|
|
@@ -240,24 +277,38 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
240
277
|
//
|
|
241
278
|
// navState is passed explicitly by the router — no temporal coupling
|
|
242
279
|
// with getNavigationState().
|
|
243
|
-
renderRoot: (
|
|
280
|
+
renderRoot: (
|
|
281
|
+
element: unknown,
|
|
282
|
+
navState: NavigationState,
|
|
283
|
+
params: ParamsSource,
|
|
284
|
+
commit: () => void
|
|
285
|
+
) => {
|
|
244
286
|
// These paths render what is already decided (a replay, a
|
|
245
|
-
// revalidation),
|
|
246
|
-
|
|
247
|
-
|
|
287
|
+
// revalidation), but "decided" is not "on screen": the tree goes out in
|
|
288
|
+
// a transition React may hold. The publish — the router's and this
|
|
289
|
+
// module's record of the displayed tree — runs when React commits it,
|
|
290
|
+
// the same way a navigation's does (TIM-1301, TIM-1423).
|
|
291
|
+
render(renderTree(element, navState, params), () => {
|
|
292
|
+
publishTree(element, params);
|
|
293
|
+
commit();
|
|
294
|
+
});
|
|
248
295
|
},
|
|
249
296
|
|
|
250
|
-
// Hand a navigation's tree to React in a transition
|
|
251
|
-
// The fetch runs outside any transition
|
|
252
|
-
// hands the tree over is wrapped, and in a
|
|
253
|
-
// Do NOT make this callback's caller an
|
|
254
|
-
// entanglement note in
|
|
297
|
+
// Hand a navigation's tree to React in a transition
|
|
298
|
+
// (navigation-transition.ts). The fetch runs outside any transition
|
|
299
|
+
// scope; only the render that hands the tree over is wrapped, and in a
|
|
300
|
+
// synchronous `startTransition`. Do NOT make this callback's caller an
|
|
301
|
+
// async React action — see the entanglement note in
|
|
302
|
+
// `navigateTransition` (TIM-1306, TIM-1307).
|
|
255
303
|
//
|
|
256
304
|
// The perform callback receives a wrapPayload function that wraps the
|
|
257
305
|
// decoded RSC payload with NavigationProvider + NuqsAdapter. navState
|
|
258
306
|
// is passed explicitly by the router — no getNavigationState() needed.
|
|
259
|
-
|
|
260
|
-
|
|
307
|
+
//
|
|
308
|
+
// `_url` names the pending URL the router already published to its own
|
|
309
|
+
// pending store before calling here; the transition has no use for it.
|
|
310
|
+
navigateTransition: (_url: string, perform) => {
|
|
311
|
+
return navigateTransition(async () => {
|
|
261
312
|
// What wrapPayload built, held until the transition wins. The client's
|
|
262
313
|
// record of the displayed tree is navigation state like any other, so
|
|
263
314
|
// it rides the same commit as the segment cache rather than being
|
|
@@ -287,7 +338,7 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
287
338
|
commit();
|
|
288
339
|
},
|
|
289
340
|
};
|
|
290
|
-
});
|
|
341
|
+
}, render);
|
|
291
342
|
},
|
|
292
343
|
|
|
293
344
|
_getCurrentPayload: () => currentPayload,
|
|
@@ -338,5 +389,5 @@ export function createTimberRouter(options?: RouterInitOptions): RouterInitResul
|
|
|
338
389
|
deps.navigationNavigate = (url, replace) => navApiController!.navigate(url, replace);
|
|
339
390
|
}
|
|
340
391
|
|
|
341
|
-
return { router, navApiController };
|
|
392
|
+
return { router, navApiController, renderTree };
|
|
342
393
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createGlobalContext — a React context with a process-wide singleton identity.
|
|
3
|
+
*
|
|
4
|
+
* SINGLETON GUARANTEE: Uses globalThis + Symbol.for — same pattern as
|
|
5
|
+
* NavigationContext. The RSC client bundler can duplicate a module across
|
|
6
|
+
* chunks (browser-entry graph + client-reference graph). With ESM output,
|
|
7
|
+
* each chunk gets its own module scope — a bare createContext at module
|
|
8
|
+
* level would create separate instances per chunk. globalThis guarantees a
|
|
9
|
+
* single instance regardless of duplication.
|
|
10
|
+
*
|
|
11
|
+
* See design/19-client-navigation.md §"Singleton Guarantee via globalThis"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use client';
|
|
15
|
+
|
|
16
|
+
import React from 'react';
|
|
17
|
+
|
|
18
|
+
export function createGlobalContext<T>(key: string, defaultValue: T): React.Context<T> {
|
|
19
|
+
const g = globalThis as Record<symbol, unknown>;
|
|
20
|
+
const sym = Symbol.for(key);
|
|
21
|
+
const existing = g[sym] as React.Context<T> | undefined;
|
|
22
|
+
if (existing !== undefined) return existing;
|
|
23
|
+
if (typeof React.createContext === 'function') {
|
|
24
|
+
const ctx = React.createContext<T>(defaultValue);
|
|
25
|
+
g[sym] = ctx;
|
|
26
|
+
return ctx;
|
|
27
|
+
}
|
|
28
|
+
// RSC environment — createContext not available. Return a dummy that
|
|
29
|
+
// won't be used (the consumers only render on the client / during SSR).
|
|
30
|
+
return undefined as unknown as React.Context<T>;
|
|
31
|
+
}
|
package/src/client/internal.ts
CHANGED
|
@@ -39,8 +39,7 @@ export type { SegmentInfo, StateTree } from '../shared/segment-info.ts';
|
|
|
39
39
|
export { HistoryStack } from './history.ts';
|
|
40
40
|
export type { HistoryEntry } from './history.ts';
|
|
41
41
|
|
|
42
|
-
// ── Params (
|
|
43
|
-
export { setCurrentParams, setCurrentSlotParams } from './use-segment-params.ts';
|
|
42
|
+
// ── Params (raw hooks) ───────────────────────────────────────────────────
|
|
44
43
|
export { useSearchParams } from './use-search-params.ts';
|
|
45
44
|
|
|
46
45
|
// ── Navigation context ───────────────────────────────────────────────────
|
package/src/client/link.tsx
CHANGED
|
@@ -38,6 +38,8 @@ import { getRouterOrNull } from './router-ref.ts';
|
|
|
38
38
|
import { getSsrData } from './ssr-data.ts';
|
|
39
39
|
import { mergePreservedSearchParams } from '../shared/merge-search-params.ts';
|
|
40
40
|
import { getLinkCodec } from '../params/codec-registry.ts';
|
|
41
|
+
import { locationSearch } from './location-search.ts';
|
|
42
|
+
import { stripRscCacheKey } from '../shared/rsc-cache-key.ts';
|
|
41
43
|
import type { LinkStatus } from './use-link-status.ts';
|
|
42
44
|
|
|
43
45
|
const LINK_PENDING: LinkStatus = { isPending: true };
|
|
@@ -47,16 +49,15 @@ const LINK_IDLE: LinkStatus = { isPending: false };
|
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
51
|
* Read the current URL's search string without requiring a React hook.
|
|
50
|
-
* On the client, reads window.location.search. During SSR, reads
|
|
51
|
-
* 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.
|
|
52
57
|
*/
|
|
53
58
|
function getCurrentSearch(): string {
|
|
54
|
-
if (typeof window !== 'undefined') return
|
|
55
|
-
|
|
56
|
-
if (!data) return '';
|
|
57
|
-
const sp = new URLSearchParams(data.searchParams);
|
|
58
|
-
const str = sp.toString();
|
|
59
|
-
return str ? `?${str}` : '';
|
|
59
|
+
if (typeof window !== 'undefined') return locationSearch();
|
|
60
|
+
return getSsrData()?.search ?? '';
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// ─── Types ───────────────────────────────────────────────────────
|
|
@@ -483,7 +484,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
483
484
|
// a thenable from `startTransition` hands it to `ReactSharedInternals.S`,
|
|
484
485
|
// which calls react-dom's `entangleAsyncAction`. That opens an action scope
|
|
485
486
|
// whose `currentEntangledLane` collects every transition update scheduled
|
|
486
|
-
// while it is open — including
|
|
487
|
+
// while it is open — including the router's `root.render` of `NavigationRoot`, which is a
|
|
487
488
|
// fully synchronous `startTransition` in a different component — and
|
|
488
489
|
// rendering that lane suspends on `currentEntangledActionThenable` until the
|
|
489
490
|
// action settles. `navigateTransition` awaits `decodePromise`, so a Link
|
|
@@ -498,7 +499,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
498
499
|
// `isPending` runs from the click until `router.navigate()` settles, which
|
|
499
500
|
// is after `decodePromise` — the same lifecycle the router's pending store
|
|
500
501
|
// and the TopLoader use. The clear wraps in `startTransition` so it lands
|
|
501
|
-
// on the same React lane as
|
|
502
|
+
// on the same React lane as the router's `root.render` — without it,
|
|
502
503
|
// the clear is an urgent update that paints before the transition commits,
|
|
503
504
|
// and destinations with pending Suspense boundaries flash idle for one frame
|
|
504
505
|
// while the old page is still showing (TIM-1418). The callback is
|
|
@@ -515,11 +516,10 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
515
516
|
const linkStatus = isPending ? LINK_PENDING : LINK_IDLE;
|
|
516
517
|
|
|
517
518
|
// Preserve search params from the current URL when requested.
|
|
518
|
-
//
|
|
519
|
-
//
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
// 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.
|
|
523
523
|
const internal = isInternalHref(baseHref);
|
|
524
524
|
|
|
525
525
|
// Only preserve search params for internal links — leaking current
|
|
@@ -565,7 +565,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
565
565
|
|
|
566
566
|
if (
|
|
567
567
|
resolved.pathname === window.location.pathname &&
|
|
568
|
-
resolved.search ===
|
|
568
|
+
stripRscCacheKey(resolved.search) === locationSearch() &&
|
|
569
569
|
resolved.hash
|
|
570
570
|
) {
|
|
571
571
|
return;
|
|
@@ -577,7 +577,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
577
577
|
// Keep the #fragment — the router commits it to the address bar and
|
|
578
578
|
// scrolls to the matching element after render. The hash is stripped
|
|
579
579
|
// from the RSC fetch URL inside the router (TIM-1035).
|
|
580
|
-
const absoluteHref = resolved.pathname + resolved.search + resolved.hash;
|
|
580
|
+
const absoluteHref = resolved.pathname + stripRscCacheKey(resolved.search) + resolved.hash;
|
|
581
581
|
|
|
582
582
|
const seq = ++clickSeq.current;
|
|
583
583
|
const settle = () => {
|
|
@@ -615,7 +615,7 @@ export const Link: LinkFunction = function LinkImpl(props: any) {
|
|
|
615
615
|
? mergePreservedSearchParams(baseHref, getCurrentSearch(), preserveSearchParams)
|
|
616
616
|
: resolvedHref;
|
|
617
617
|
const resolved = new URL(prefetchHref, window.location.href);
|
|
618
|
-
router.prefetch(resolved.pathname + resolved.search);
|
|
618
|
+
router.prefetch(resolved.pathname + stripRscCacheKey(resolved.search));
|
|
619
619
|
}
|
|
620
620
|
}
|
|
621
621
|
: userOnMouseEnter;
|