@octanejs/tanstack-router 0.1.60 → 0.2.0
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/package.json +6 -6
- package/src/CatchBoundary.tsrx +2 -2
- package/src/Match.tsrx +69 -155
- package/src/MatchRoute.tsrx +8 -6
- package/src/Matches.tsrx +18 -7
- package/src/Outlet.tsrx +22 -17
- package/src/ScriptOnce.tsrx +11 -6
- package/src/Scripts.tsrx +12 -27
- package/src/Transitioner.tsrx +71 -100
- package/src/hooks.ts +9 -10
- package/src/link.ts +8 -4
- package/src/router.ts +15 -102
- package/src/scriptContentUtils.ts +23 -50
- package/src/scroll-restoration.tsrx +15 -2
- package/src/ssr/RouterClient.tsrx +1 -1
- package/src/ssr/renderRouterToStream.ts +108 -138
- package/src/ssr/renderRouterToString.ts +27 -39
- package/src/useBlocker.tsrx +8 -6
package/src/Transitioner.tsrx
CHANGED
|
@@ -1,57 +1,66 @@
|
|
|
1
|
-
// The navigation engine — port of react-router's Transitioner. It runs as a
|
|
2
|
-
//
|
|
1
|
+
// The navigation engine — port of react-router's Transitioner. It runs as a hook
|
|
2
|
+
// because a rendered sibling would place an SSR marker before a root route's
|
|
3
3
|
// document-level <html> node and outside the #__app hydration range.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
//
|
|
5
|
+
// router-core 1.171.34 owns the navigation lifecycle end to end: `load` drives
|
|
6
|
+
// `startViewTransition` → `startTransition(commit, matches)`, awaits the render
|
|
7
|
+
// acknowledgement, then emits `onLoad`/`onBeforeRouteMount`/`onResolved`/
|
|
8
|
+
// `onRendered` and commits `status`/`resolvedLocation` itself. The binding's only
|
|
9
|
+
// remaining jobs are:
|
|
10
|
+
// 1. Supply `router.startTransition(fn, expected)` as that render
|
|
11
|
+
// ACKNOWLEDGEMENT — a promise router-core awaits that resolves `true` only
|
|
12
|
+
// once `MatchesInner` has rendered the `expected` matches (via `settleOwner`).
|
|
13
|
+
// Running `fn` inside an octane transition is what lets the current page hold
|
|
14
|
+
// while the next route suspends.
|
|
15
|
+
// 2. Subscribe to history so back/forward and `Link` clicks reload; replace a
|
|
16
|
+
// non-canonical mounted URL (mirrors the server-side check in beforeLoad);
|
|
17
|
+
// and kick off the initial load — or, when hydrating an already-resolved SSR
|
|
18
|
+
// location, acknowledge the first render so `onRendered` consumers (scroll
|
|
19
|
+
// restoration, devtools) fire.
|
|
20
|
+
import { useLayoutEffect, useState, startTransition } from 'octane';
|
|
18
21
|
import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core';
|
|
19
22
|
import { useRouter } from './context.ts';
|
|
20
|
-
import { useStore } from './useStore.ts';
|
|
21
|
-
import { usePrevious } from './utils.ts';
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const [
|
|
24
|
+
// Resolve the pending render acknowledgement with whether the expected matches
|
|
25
|
+
// rendered, then clear the slot. `owner` is `router._rendered`: `[expected, resolve]`.
|
|
26
|
+
export function settleOwner(owner: Array<any>, rendered: boolean) {
|
|
27
|
+
const settle = owner[1] as ((rendered: boolean) => void) | undefined;
|
|
28
|
+
owner.length = 0;
|
|
29
|
+
settle?.(rendered);
|
|
30
|
+
}
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
const
|
|
32
|
+
export function useTransitioner() {
|
|
33
|
+
const router = useRouter() as any;
|
|
34
|
+
const [, forceRender] = useState<object>();
|
|
30
35
|
|
|
31
|
-
const
|
|
32
|
-
const isAnyPending = isLoading || isTransitioning || hasPending;
|
|
33
|
-
const previousIsAnyPending = usePrevious(isAnyPending);
|
|
34
|
-
const isPagePending = isLoading || hasPending;
|
|
35
|
-
const previousIsPagePending = usePrevious(isPagePending);
|
|
36
|
+
const acknowledgement: Array<any> = router._rendered ??= [];
|
|
36
37
|
|
|
37
|
-
router.startTransition = (fn: () => void) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
router.startTransition = (fn: () => void, expected: unknown) => new Promise<boolean>((
|
|
39
|
+
resolve,
|
|
40
|
+
) => {
|
|
41
|
+
// A newer transition supersedes an unrendered older one.
|
|
42
|
+
settleOwner(acknowledgement, false);
|
|
43
|
+
acknowledgement.push(expected, resolve);
|
|
44
|
+
// Force `MatchesInner` to re-read: it renders `acknowledgement[0]` and
|
|
45
|
+
// settles this promise from a layout effect once that commit lands.
|
|
46
|
+
forceRender({});
|
|
47
|
+
startTransition(fn);
|
|
48
|
+
});
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
// verify the current URL is in canonical form (search serialization,
|
|
47
|
-
// trailing slash) and replace it if not — mirroring the server-side redirect
|
|
48
|
-
// check in router.beforeLoad.
|
|
49
|
-
useEffect(() => {
|
|
50
|
+
useLayoutEffect(() => {
|
|
50
51
|
const unsub = router.history.subscribe(() => router.load());
|
|
51
52
|
|
|
53
|
+
// octane has no StrictMode double-invoke, so this layout effect runs once per
|
|
54
|
+
// mounted router and re-runs only when the router (or its history) is swapped
|
|
55
|
+
// on a live provider — exactly when the new router needs its initial load.
|
|
56
|
+
// Upstream guards this block with a dev-only `mounted` ref purely to absorb
|
|
57
|
+
// React's StrictMode double-invoke; keeping that ref here would instead skip
|
|
58
|
+
// the swapped-in router's load.
|
|
59
|
+
router.updateLatestLocation?.();
|
|
60
|
+
const location = router.latestLocation;
|
|
52
61
|
const nextLocation = router.buildLocation(
|
|
53
62
|
{
|
|
54
|
-
to:
|
|
63
|
+
to: location.pathname,
|
|
55
64
|
search: true,
|
|
56
65
|
params: true,
|
|
57
66
|
hash: true,
|
|
@@ -59,70 +68,32 @@ export function useTransitioner() {
|
|
|
59
68
|
_includeValidateSearch: true,
|
|
60
69
|
} as any,
|
|
61
70
|
);
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
router.commitLocation({ ...nextLocation, replace: true });
|
|
71
|
+
if (trimPathRight(location.publicHref) !== trimPathRight(nextLocation.publicHref)) {
|
|
72
|
+
router.commitLocation({ ...nextLocation, replace: true, ignoreBlocker: true });
|
|
73
|
+
return unsub;
|
|
66
74
|
}
|
|
67
75
|
|
|
68
|
-
|
|
69
|
-
}, [router, router.history]);
|
|
70
|
-
|
|
71
|
-
// Initial load. Skipped when hydrating from SSR (ssr-client triggers it) and
|
|
72
|
-
// on re-runs for the same router instance.
|
|
73
|
-
useLayoutEffect(() => {
|
|
76
|
+
const resolvedLocation = router.stores.resolvedLocation.get();
|
|
74
77
|
if (
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
resolvedLocation?.href === location.href &&
|
|
79
|
+
resolvedLocation.state.__TSR_key === location.state.__TSR_key
|
|
77
80
|
) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// The router was loading and now it's not — the new matches are in state.
|
|
87
|
-
useLayoutEffect(() => {
|
|
88
|
-
if (previousIsLoading && !isLoading) {
|
|
81
|
+
// Already resolved (SSR hydration): emit `onRendered` for scroll
|
|
82
|
+
// restoration and devtools. The match tree has already committed by the
|
|
83
|
+
// time this layout effect runs (child effects fire before the parent's),
|
|
84
|
+
// so emit directly rather than through the acknowledgement/settle path:
|
|
85
|
+
// hydration acknowledges the matches already on screen, which — unlike a
|
|
86
|
+
// navigation that commits the *next* matches — triggers no re-render for
|
|
87
|
+
// `MatchesInner`'s settle effect to run on.
|
|
89
88
|
router.emit({
|
|
90
|
-
type: '
|
|
91
|
-
...getLocationChangeInfo(
|
|
92
|
-
router.stores.location.get(),
|
|
93
|
-
router.stores.resolvedLocation.get(),
|
|
94
|
-
),
|
|
89
|
+
type: 'onRendered',
|
|
90
|
+
...getLocationChangeInfo(resolvedLocation, resolvedLocation),
|
|
95
91
|
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
useLayoutEffect(() => {
|
|
100
|
-
if (previousIsPagePending && !isPagePending) {
|
|
101
|
-
router.emit({
|
|
102
|
-
type: 'onBeforeRouteMount',
|
|
103
|
-
...getLocationChangeInfo(
|
|
104
|
-
router.stores.location.get(),
|
|
105
|
-
router.stores.resolvedLocation.get(),
|
|
106
|
-
),
|
|
92
|
+
} else if (!router._tx) {
|
|
93
|
+
router.load({ sync: true }).catch((err: unknown) => {
|
|
94
|
+
console.error(err);
|
|
107
95
|
});
|
|
108
96
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// Everything pending has drained — resolve the navigation: emit onResolved and
|
|
112
|
-
// commit status idle + resolvedLocation (what `onRendered` and scroll
|
|
113
|
-
// restoration key off).
|
|
114
|
-
useLayoutEffect(() => {
|
|
115
|
-
if (previousIsAnyPending && !isAnyPending) {
|
|
116
|
-
const changeInfo = getLocationChangeInfo(
|
|
117
|
-
router.stores.location.get(),
|
|
118
|
-
router.stores.resolvedLocation.get(),
|
|
119
|
-
);
|
|
120
|
-
router.emit({ type: 'onResolved', ...changeInfo });
|
|
121
|
-
|
|
122
|
-
batch(() => {
|
|
123
|
-
router.stores.status.set('idle');
|
|
124
|
-
router.stores.resolvedLocation.set(router.stores.location.get());
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
}, [isAnyPending, previousIsAnyPending, router]);
|
|
97
|
+
return unsub;
|
|
98
|
+
}, [router, router.history]);
|
|
128
99
|
}
|
package/src/hooks.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// The read hooks — ports of react-router's useMatch.tsx / useParams / useSearch /
|
|
2
2
|
// useLoaderData / useLoaderDeps / useRouteContext / useNavigate / useCanGoBack /
|
|
3
3
|
// Matches.tsx (useMatches / useParentMatches / useChildMatches). Everything match-
|
|
4
|
-
// shaped funnels through `useMatch`, which subscribes to ONE match store
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// shaped funnels through `useMatch`, which subscribes to ONE match store via
|
|
5
|
+
// `router.stores.getMatchStore(routeId)`:
|
|
6
|
+
// - `from` given → that routeId's match store;
|
|
7
7
|
// - no `from` → the NEAREST match via `matchContext` (the match id the enclosing
|
|
8
8
|
// `<Match>` provided) — NOT the leaf match.
|
|
9
9
|
// A missing match throws unless `shouldThrow: false` (upstream invariant).
|
|
@@ -89,9 +89,8 @@ export function useMatch(...args: any[]): any {
|
|
|
89
89
|
// octane has no rules of hooks, so the nearest-match context is read
|
|
90
90
|
// unconditionally (upstream reads a dummy context when `from` is given).
|
|
91
91
|
const nearestMatchId = useContext(matchContext);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
: router.stores.matchStores.get(nearestMatchId as string);
|
|
92
|
+
const routeId = opts.from ?? (nearestMatchId as string);
|
|
93
|
+
const matchStore = router.stores.getMatchStore(routeId);
|
|
95
94
|
|
|
96
95
|
const selector = useStructuralSharing(opts, router, subSlot(slot, 'm'));
|
|
97
96
|
const matchSelection = useStore(
|
|
@@ -302,13 +301,13 @@ export function useParentMatches(opts: any, slot: symbol | undefined): any;
|
|
|
302
301
|
export function useParentMatches(...args: any[]): any {
|
|
303
302
|
const [user, slot] = splitSlot(args);
|
|
304
303
|
const opts = user[0] ?? {};
|
|
305
|
-
const
|
|
304
|
+
const contextRouteId = useContext(matchContext);
|
|
306
305
|
return useMatches(
|
|
307
306
|
{
|
|
308
307
|
select: (matches: any[]) => {
|
|
309
308
|
matches = matches.slice(
|
|
310
309
|
0,
|
|
311
|
-
matches.findIndex((d: any) => d.
|
|
310
|
+
matches.findIndex((d: any) => d.routeId === contextRouteId),
|
|
312
311
|
);
|
|
313
312
|
return opts.select ? opts.select(matches) : matches;
|
|
314
313
|
},
|
|
@@ -330,11 +329,11 @@ export function useChildMatches(opts: any, slot: symbol | undefined): any;
|
|
|
330
329
|
export function useChildMatches(...args: any[]): any {
|
|
331
330
|
const [user, slot] = splitSlot(args);
|
|
332
331
|
const opts = user[0] ?? {};
|
|
333
|
-
const
|
|
332
|
+
const contextRouteId = useContext(matchContext);
|
|
334
333
|
return useMatches(
|
|
335
334
|
{
|
|
336
335
|
select: (matches: any[]) => {
|
|
337
|
-
matches = matches.slice(matches.findIndex((d: any) => d.
|
|
336
|
+
matches = matches.slice(matches.findIndex((d: any) => d.routeId === contextRouteId) + 1);
|
|
338
337
|
return opts.select ? opts.select(matches) : matches;
|
|
339
338
|
},
|
|
340
339
|
structuralSharing: opts.structuralSharing,
|
package/src/link.ts
CHANGED
|
@@ -195,10 +195,14 @@ export function useLinkProps(...args: any[]): Record<string, any> {
|
|
|
195
195
|
if (!pathIsFuzzyEqual) return false;
|
|
196
196
|
}
|
|
197
197
|
if (activeOptions?.includeSearch ?? true) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
// router-core 1.171.34 deepEqual: positional (partial, explicitUndefined)
|
|
199
|
+
// instead of an options object; the 4th arg is explicitUndefined directly.
|
|
200
|
+
const searchTest = deepEqual(
|
|
201
|
+
currentLocation.search,
|
|
202
|
+
next.search,
|
|
203
|
+
!activeOptions?.exact,
|
|
204
|
+
activeOptions?.explicitUndefined,
|
|
205
|
+
);
|
|
202
206
|
if (!searchTest) return false;
|
|
203
207
|
}
|
|
204
208
|
if (activeOptions?.includeHash) return currentLocation.hash === next.hash;
|
package/src/router.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
createNonReactiveReadonlyStore,
|
|
13
13
|
} from '@tanstack/router-core';
|
|
14
14
|
import { createAtom, batch } from '@tanstack/store';
|
|
15
|
-
import { startTransition } from 'octane';
|
|
16
15
|
import type { RouterHistory } from '@tanstack/history';
|
|
17
16
|
import type {
|
|
18
17
|
AnyRoute,
|
|
@@ -23,11 +22,12 @@ import type {
|
|
|
23
22
|
|
|
24
23
|
const isServerEnv = typeof document === 'undefined';
|
|
25
24
|
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
25
|
+
// Store factory for RouterCore — the only framework-specific wiring. Client
|
|
26
|
+
// stores are reactive @tanstack/store atoms (framework-agnostic; `createAtom`
|
|
27
|
+
// lives in @tanstack/store, not @tanstack/react-store); server snapshots use the
|
|
28
|
+
// non-reactive factory. router-core 1.171.34 drives concurrent navigation through
|
|
29
|
+
// `router.startTransition` (see Transitioner), so `batch` is a plain atomic batch
|
|
30
|
+
// rather than one wrapped in an octane transition.
|
|
31
31
|
const octaneStoreFactory = (opts: { isServer?: boolean }) => {
|
|
32
32
|
if (opts?.isServer ?? isServerEnv) {
|
|
33
33
|
return {
|
|
@@ -39,7 +39,7 @@ const octaneStoreFactory = (opts: { isServer?: boolean }) => {
|
|
|
39
39
|
return {
|
|
40
40
|
createMutableStore: createAtom,
|
|
41
41
|
createReadonlyStore: createAtom,
|
|
42
|
-
batch
|
|
42
|
+
batch,
|
|
43
43
|
};
|
|
44
44
|
};
|
|
45
45
|
|
|
@@ -65,102 +65,15 @@ export class Router<
|
|
|
65
65
|
TDehydrated
|
|
66
66
|
>,
|
|
67
67
|
) {
|
|
68
|
+
// router-core 1.171.34 owns the navigation lifecycle end to end: it drives
|
|
69
|
+
// `startViewTransition` → `startTransition(commit, matches)` and awaits the
|
|
70
|
+
// render acknowledgement (supplied by `useTransitioner`), then emits
|
|
71
|
+
// onLoad/onBeforeRouteMount/onResolved/onRendered and commits
|
|
72
|
+
// `status`/`resolvedLocation` itself. So `await router.load()` is already a
|
|
73
|
+
// render-readiness boundary and the previous view-transition/load wrapper is
|
|
74
|
+
// no longer needed. HTTP status is derived on demand by the SSR layer
|
|
75
|
+
// (`getSsrStatus`), not stored on the router.
|
|
68
76
|
super(options, octaneStoreFactory);
|
|
69
|
-
|
|
70
|
-
// router-core starts the resolved-match commit through startViewTransition,
|
|
71
|
-
// whose browser callback may run after router-core's load promise resolves.
|
|
72
|
-
// Track those callbacks so `await router.load()` is a real render-readiness
|
|
73
|
-
// boundary: the active match tree is committed before a consumer's first
|
|
74
|
-
// render or hydration pass.
|
|
75
|
-
const coreLoad = this.load.bind(this);
|
|
76
|
-
const coreStartViewTransition = this.startViewTransition.bind(this);
|
|
77
|
-
const pendingViewCommits = new Set<Promise<void>>();
|
|
78
|
-
const activeLoadScopes = new Set<Set<Promise<void>>>();
|
|
79
|
-
|
|
80
|
-
this.startViewTransition = (fn: () => Promise<void>) => {
|
|
81
|
-
let resolveCommit!: () => void;
|
|
82
|
-
let rejectCommit!: (error: unknown) => void;
|
|
83
|
-
const commit = new Promise<void>((resolve, reject) => {
|
|
84
|
-
resolveCommit = resolve;
|
|
85
|
-
rejectCommit = reject;
|
|
86
|
-
});
|
|
87
|
-
pendingViewCommits.add(commit);
|
|
88
|
-
for (const scope of activeLoadScopes) scope.add(commit);
|
|
89
|
-
// Keep only unsettled callbacks globally. A later load waits for a prior
|
|
90
|
-
// callback so no mutation can land after its readiness boundary, but only
|
|
91
|
-
// the active scopes above own (and therefore propagate) this failure.
|
|
92
|
-
void commit.then(
|
|
93
|
-
() => pendingViewCommits.delete(commit),
|
|
94
|
-
() => pendingViewCommits.delete(commit),
|
|
95
|
-
);
|
|
96
|
-
|
|
97
|
-
const runCommit = async () => {
|
|
98
|
-
try {
|
|
99
|
-
await fn();
|
|
100
|
-
resolveCommit();
|
|
101
|
-
} catch (error) {
|
|
102
|
-
rejectCommit(error);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
try {
|
|
107
|
-
coreStartViewTransition(runCommit);
|
|
108
|
-
} catch (error) {
|
|
109
|
-
rejectCommit(error);
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
this.load = async (...args: any[]) => {
|
|
115
|
-
const prerequisiteCommits = new Set(pendingViewCommits);
|
|
116
|
-
const viewCommits = new Set<Promise<void>>();
|
|
117
|
-
activeLoadScopes.add(viewCommits);
|
|
118
|
-
let hasLoadError = false;
|
|
119
|
-
let loadError: unknown;
|
|
120
|
-
let result: void;
|
|
121
|
-
try {
|
|
122
|
-
result = await coreLoad(...args);
|
|
123
|
-
} catch (error) {
|
|
124
|
-
hasLoadError = true;
|
|
125
|
-
loadError = error;
|
|
126
|
-
} finally {
|
|
127
|
-
// All commits started by this core load are now registered. Stop
|
|
128
|
-
// accepting commits from later navigations before awaiting this scope.
|
|
129
|
-
activeLoadScopes.delete(viewCommits);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
let hasCommitError = false;
|
|
133
|
-
let commitError: unknown;
|
|
134
|
-
const [, outcomes] = await Promise.all([
|
|
135
|
-
Promise.allSettled(prerequisiteCommits),
|
|
136
|
-
Promise.allSettled(viewCommits),
|
|
137
|
-
]);
|
|
138
|
-
const rejected = outcomes.find((outcome) => outcome.status === 'rejected');
|
|
139
|
-
if (rejected?.status === 'rejected') {
|
|
140
|
-
hasCommitError = true;
|
|
141
|
-
commitError = rejected.reason;
|
|
142
|
-
}
|
|
143
|
-
if (hasLoadError) throw loadError;
|
|
144
|
-
if (hasCommitError) throw commitError;
|
|
145
|
-
|
|
146
|
-
// RouterCore derives the final HTTP status immediately after its internal
|
|
147
|
-
// load promise resolves. A platform-deferred View Transition can commit the
|
|
148
|
-
// new tree only after that point, so finalize every branch once the
|
|
149
|
-
// render-ready tree is present. RouterCore has already committed a
|
|
150
|
-
// redirect and its HTTP status together, so preserve that authoritative
|
|
151
|
-
// status; otherwise a successful tree must also clear a stale 404/500.
|
|
152
|
-
const state = this.state;
|
|
153
|
-
const statusCode =
|
|
154
|
-
state.redirect != null
|
|
155
|
-
? state.statusCode
|
|
156
|
-
: this.hasNotFoundMatch()
|
|
157
|
-
? 404
|
|
158
|
-
: state.matches.some((match: any) => match.status === 'error')
|
|
159
|
-
? 500
|
|
160
|
-
: 200;
|
|
161
|
-
this.stores.statusCode.set(statusCode);
|
|
162
|
-
return result;
|
|
163
|
-
};
|
|
164
77
|
}
|
|
165
78
|
}
|
|
166
79
|
|
|
@@ -1,64 +1,37 @@
|
|
|
1
|
-
import { deepEqual } from '@tanstack/router-core';
|
|
1
|
+
import { composeSsrBodyScripts, deepEqual, getSsrBodyScriptParts } from '@tanstack/router-core';
|
|
2
2
|
import { isServer } from '@tanstack/router-core/isServer';
|
|
3
3
|
import { useRouter } from './context';
|
|
4
4
|
import { splitSlot, subSlot } from './internal';
|
|
5
5
|
import { useStore } from './useStore';
|
|
6
|
-
import type { AnyRouteMatch,
|
|
6
|
+
import type { AnyRouteMatch, RouterManagedTag } from '@tanstack/router-core';
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
tag: 'script',
|
|
15
|
-
attrs: { ...attrs, nonce },
|
|
16
|
-
children,
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
const manifest = router.ssr?.manifest;
|
|
20
|
-
if (manifest) {
|
|
21
|
-
for (const match of matches) {
|
|
22
|
-
for (const asset of manifest.routes[match.routeId]?.scripts ?? []) {
|
|
23
|
-
scripts.push({
|
|
24
|
-
tag: 'script',
|
|
25
|
-
attrs: { ...asset.attrs, nonce },
|
|
26
|
-
children: asset.children,
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return scripts;
|
|
33
|
-
}
|
|
8
|
+
// router-core 1.171.34 composes body scripts through `getSsrBodyScriptParts` +
|
|
9
|
+
// `composeSsrBodyScripts`, and the initial hydration `<Scripts>` take moved from
|
|
10
|
+
// the removed `serverSsr.takeBufferedScripts()` (a single buffered tag) to
|
|
11
|
+
// `serverSsr.takeInitialHydrationScriptTags()` (hydration tags plus the streaming
|
|
12
|
+
// boundary), which `composeSsrBodyScripts` interleaves.
|
|
13
|
+
const routeScriptAttrs = { suppressHydrationWarning: true };
|
|
34
14
|
|
|
35
15
|
export function useScripts(...args: Array<unknown>): Array<RouterManagedTag> {
|
|
36
16
|
const [, slot] = splitSlot(args);
|
|
37
17
|
const router = useRouter();
|
|
18
|
+
const nonce = router.options.ssr?.nonce;
|
|
19
|
+
const getScripts = (matches: Array<AnyRouteMatch>) =>
|
|
20
|
+
composeSsrBodyScripts(
|
|
21
|
+
getSsrBodyScriptParts(matches, router.ssr?.manifest, nonce, routeScriptAttrs),
|
|
22
|
+
);
|
|
38
23
|
|
|
39
24
|
if (isServer ?? router.isServer) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
children:
|
|
50
|
-
typeof buffered.children === 'string'
|
|
51
|
-
? buffered.children.replace(/;document\.currentScript\.remove\(\)$/, '')
|
|
52
|
-
: buffered.children,
|
|
53
|
-
},
|
|
54
|
-
...scripts,
|
|
55
|
-
];
|
|
25
|
+
return composeSsrBodyScripts(
|
|
26
|
+
getSsrBodyScriptParts(
|
|
27
|
+
router.stores.matches.get(),
|
|
28
|
+
router.ssr?.manifest,
|
|
29
|
+
nonce,
|
|
30
|
+
routeScriptAttrs,
|
|
31
|
+
),
|
|
32
|
+
router.serverSsr?.takeInitialHydrationScriptTags(),
|
|
33
|
+
);
|
|
56
34
|
}
|
|
57
35
|
|
|
58
|
-
return useStore(
|
|
59
|
-
router.stores.matches,
|
|
60
|
-
(matches: Array<AnyRouteMatch>) => getScripts(router, matches),
|
|
61
|
-
deepEqual,
|
|
62
|
-
subSlot(slot, 'body:scripts'),
|
|
63
|
-
);
|
|
36
|
+
return useStore(router.stores.matches, getScripts, deepEqual, subSlot(slot, 'body:scripts'));
|
|
64
37
|
}
|
|
@@ -1,15 +1,28 @@
|
|
|
1
|
+
// Port of react-router's scroll-restoration.tsx. router-core's server build of
|
|
2
|
+
// `getScrollRestorationScriptForRouter` returns the inline restore script unless
|
|
3
|
+
// a `scrollRestoration` function declines the location; its client build always
|
|
4
|
+
// returns null. Upstream's client renders null because its ScriptOnce removes
|
|
5
|
+
// itself before React hydrates. Octane hydration claims the server's arm, so the
|
|
6
|
+
// client takes the arm the server took and ScriptOnce adopts, then drops, the
|
|
7
|
+
// server's script.
|
|
1
8
|
import {
|
|
2
9
|
getScrollRestorationScriptForRouter,
|
|
3
10
|
} from '@tanstack/router-core/scroll-restoration-script';
|
|
11
|
+
import type { AnyRouter } from '@tanstack/router-core';
|
|
4
12
|
import { useRouter } from './context.ts';
|
|
5
13
|
import { ScriptOnce } from './ScriptOnce.tsrx';
|
|
6
14
|
|
|
15
|
+
function rendersScript(router: AnyRouter): boolean {
|
|
16
|
+
const option = router.options.scrollRestoration;
|
|
17
|
+
return typeof option !== 'function' || option({ location: router.latestLocation });
|
|
18
|
+
}
|
|
19
|
+
|
|
7
20
|
export function ScrollRestorationScript() @{
|
|
8
21
|
const router = useRouter();
|
|
9
22
|
const script = getScrollRestorationScriptForRouter(router);
|
|
10
23
|
|
|
11
|
-
@if (script) {
|
|
12
|
-
<ScriptOnce children={script} />
|
|
24
|
+
@if (script !== null || rendersScript(router)) {
|
|
25
|
+
<ScriptOnce children={script ?? ''} />
|
|
13
26
|
} @else {
|
|
14
27
|
<></>
|
|
15
28
|
}
|
|
@@ -16,7 +16,7 @@ const ready = {
|
|
|
16
16
|
function getHydrationPromise(router: AnyRouter) {
|
|
17
17
|
let promise = hydrationPromises.get(router);
|
|
18
18
|
if (!promise) {
|
|
19
|
-
promise = router.stores.
|
|
19
|
+
promise = router.stores.ids.get().length ? ready as PromiseLike<void> : hydrate(router);
|
|
20
20
|
hydrationPromises.set(router, promise);
|
|
21
21
|
}
|
|
22
22
|
return promise;
|