@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
|
@@ -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 ───────────────────────────────────────────────────
|