@tanstack/router-core 1.163.3 → 1.166.2
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/cjs/load-matches.cjs +156 -92
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-client.cjs +15 -17
- package/dist/cjs/ssr/ssr-client.cjs.map +1 -1
- package/dist/cjs/ssr/ssr-server.cjs +3 -0
- package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
- package/dist/cjs/ssr/types.d.cts +1 -0
- package/dist/esm/load-matches.js +156 -92
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/ssr/ssr-client.js +15 -17
- package/dist/esm/ssr/ssr-client.js.map +1 -1
- package/dist/esm/ssr/ssr-server.js +3 -0
- package/dist/esm/ssr/ssr-server.js.map +1 -1
- package/dist/esm/ssr/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/load-matches.ts +234 -127
- package/src/ssr/ssr-client.ts +20 -17
- package/src/ssr/ssr-server.ts +3 -0
- package/src/ssr/types.ts +1 -0
package/src/load-matches.ts
CHANGED
|
@@ -29,13 +29,12 @@ type InnerLoadContext = {
|
|
|
29
29
|
firstBadMatchIndex?: number
|
|
30
30
|
/** mutable state, scoped to a `loadMatches` call */
|
|
31
31
|
rendered?: boolean
|
|
32
|
+
serialError?: unknown
|
|
32
33
|
updateMatch: UpdateMatchFn
|
|
33
34
|
matches: Array<AnyRouteMatch>
|
|
34
35
|
preload?: boolean
|
|
35
36
|
onReady?: () => Promise<void>
|
|
36
37
|
sync?: boolean
|
|
37
|
-
/** mutable state, scoped to a `loadMatches` call */
|
|
38
|
-
matchPromises: Array<Promise<AnyRouteMatch>>
|
|
39
38
|
}
|
|
40
39
|
|
|
41
40
|
const triggerOnReady = (inner: InnerLoadContext): void | Promise<void> => {
|
|
@@ -74,64 +73,45 @@ const buildMatchContext = (
|
|
|
74
73
|
return context
|
|
75
74
|
}
|
|
76
75
|
|
|
77
|
-
const
|
|
76
|
+
const getNotFoundBoundaryIndex = (
|
|
78
77
|
inner: InnerLoadContext,
|
|
79
78
|
err: NotFoundError,
|
|
80
|
-
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
// First check if a specific route is requested to show the error
|
|
84
|
-
const routeCursor =
|
|
85
|
-
inner.router.routesById[err.routeId ?? ''] ?? inner.router.routeTree
|
|
86
|
-
|
|
87
|
-
// Ensure a NotFoundComponent exists on the route
|
|
88
|
-
if (
|
|
89
|
-
!routeCursor.options.notFoundComponent &&
|
|
90
|
-
(inner.router.options as any)?.defaultNotFoundComponent
|
|
91
|
-
) {
|
|
92
|
-
routeCursor.options.notFoundComponent = (
|
|
93
|
-
inner.router.options as any
|
|
94
|
-
).defaultNotFoundComponent
|
|
79
|
+
): number | undefined => {
|
|
80
|
+
if (!inner.matches.length) {
|
|
81
|
+
return undefined
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (!willWalkUp) {
|
|
104
|
-
// Ensure we have a notFoundComponent
|
|
105
|
-
invariant(
|
|
106
|
-
routeCursor.options.notFoundComponent,
|
|
107
|
-
'No notFoundComponent found. Please set a notFoundComponent on your route or provide a defaultNotFoundComponent to the router.',
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Find the match for this route
|
|
112
|
-
const matchForRoute = inner.matches.find((m) => m.routeId === routeCursor.id)
|
|
84
|
+
const requestedRouteId = err.routeId
|
|
85
|
+
const matchedRootIndex = inner.matches.findIndex(
|
|
86
|
+
(m) => m.routeId === inner.router.routeTree.id,
|
|
87
|
+
)
|
|
88
|
+
const rootIndex = matchedRootIndex >= 0 ? matchedRootIndex : 0
|
|
113
89
|
|
|
114
|
-
|
|
90
|
+
let startIndex = requestedRouteId
|
|
91
|
+
? inner.matches.findIndex((match) => match.routeId === requestedRouteId)
|
|
92
|
+
: (inner.firstBadMatchIndex ?? inner.matches.length - 1)
|
|
115
93
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
status: 'notFound',
|
|
120
|
-
error: err,
|
|
121
|
-
isFetching: false,
|
|
122
|
-
}))
|
|
94
|
+
if (startIndex < 0) {
|
|
95
|
+
startIndex = rootIndex
|
|
96
|
+
}
|
|
123
97
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
98
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
99
|
+
const match = inner.matches[i]!
|
|
100
|
+
const route = inner.router.looseRoutesById[match.routeId]!
|
|
101
|
+
if (route.options.notFoundComponent) {
|
|
102
|
+
return i
|
|
103
|
+
}
|
|
127
104
|
}
|
|
105
|
+
|
|
106
|
+
// If no boundary component is found, preserve explicit routeId targeting behavior,
|
|
107
|
+
// otherwise default to root for untargeted notFounds.
|
|
108
|
+
return requestedRouteId ? startIndex : rootIndex
|
|
128
109
|
}
|
|
129
110
|
|
|
130
111
|
const handleRedirectAndNotFound = (
|
|
131
112
|
inner: InnerLoadContext,
|
|
132
113
|
match: AnyRouteMatch | undefined,
|
|
133
114
|
err: unknown,
|
|
134
|
-
routerCode?: string,
|
|
135
115
|
): void => {
|
|
136
116
|
if (!isRedirect(err) && !isNotFound(err)) return
|
|
137
117
|
|
|
@@ -146,19 +126,26 @@ const handleRedirectAndNotFound = (
|
|
|
146
126
|
match._nonReactive.beforeLoadPromise = undefined
|
|
147
127
|
match._nonReactive.loaderPromise = undefined
|
|
148
128
|
|
|
149
|
-
const status = isRedirect(err) ? 'redirected' : 'notFound'
|
|
150
|
-
|
|
151
129
|
match._nonReactive.error = err
|
|
152
130
|
|
|
153
131
|
inner.updateMatch(match.id, (prev) => ({
|
|
154
132
|
...prev,
|
|
155
|
-
status
|
|
133
|
+
status: isRedirect(err)
|
|
134
|
+
? 'redirected'
|
|
135
|
+
: prev.status === 'pending'
|
|
136
|
+
? 'success'
|
|
137
|
+
: prev.status,
|
|
156
138
|
context: buildMatchContext(inner, match.index),
|
|
157
139
|
isFetching: false,
|
|
158
140
|
error: err,
|
|
159
141
|
}))
|
|
160
142
|
|
|
161
143
|
if (isNotFound(err) && !err.routeId) {
|
|
144
|
+
// Stamp the throwing match's routeId so that the finalization step in
|
|
145
|
+
// loadMatches knows where the notFound originated. The actual boundary
|
|
146
|
+
// resolution (walking up to the nearest notFoundComponent) is deferred to
|
|
147
|
+
// the finalization step, where firstBadMatchIndex is stable and
|
|
148
|
+
// headMaxIndex can be capped correctly.
|
|
162
149
|
err.routeId = match.routeId
|
|
163
150
|
}
|
|
164
151
|
|
|
@@ -170,11 +157,9 @@ const handleRedirectAndNotFound = (
|
|
|
170
157
|
err.options._fromLocation = inner.location
|
|
171
158
|
err.redirectHandled = true
|
|
172
159
|
err = inner.router.resolveRedirect(err)
|
|
173
|
-
throw err
|
|
174
|
-
} else {
|
|
175
|
-
_handleNotFound(inner, err, routerCode)
|
|
176
|
-
throw err
|
|
177
160
|
}
|
|
161
|
+
|
|
162
|
+
throw err
|
|
178
163
|
}
|
|
179
164
|
|
|
180
165
|
const shouldSkipLoader = (
|
|
@@ -212,23 +197,13 @@ const handleSerialError = (
|
|
|
212
197
|
|
|
213
198
|
err.routerCode = routerCode
|
|
214
199
|
inner.firstBadMatchIndex ??= index
|
|
215
|
-
handleRedirectAndNotFound(
|
|
216
|
-
inner,
|
|
217
|
-
inner.router.getMatch(matchId),
|
|
218
|
-
err,
|
|
219
|
-
routerCode,
|
|
220
|
-
)
|
|
200
|
+
handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
|
|
221
201
|
|
|
222
202
|
try {
|
|
223
203
|
route.options.onError?.(err)
|
|
224
204
|
} catch (errorHandlerErr) {
|
|
225
205
|
err = errorHandlerErr
|
|
226
|
-
handleRedirectAndNotFound(
|
|
227
|
-
inner,
|
|
228
|
-
inner.router.getMatch(matchId),
|
|
229
|
-
err,
|
|
230
|
-
routerCode,
|
|
231
|
-
)
|
|
206
|
+
handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
|
|
232
207
|
}
|
|
233
208
|
|
|
234
209
|
inner.updateMatch(matchId, (prev) => {
|
|
@@ -245,6 +220,10 @@ const handleSerialError = (
|
|
|
245
220
|
abortController: new AbortController(),
|
|
246
221
|
}
|
|
247
222
|
})
|
|
223
|
+
|
|
224
|
+
if (!inner.preload && !isRedirect(err) && !isNotFound(err)) {
|
|
225
|
+
inner.serialError ??= err
|
|
226
|
+
}
|
|
248
227
|
}
|
|
249
228
|
|
|
250
229
|
const isBeforeLoadSsr = (
|
|
@@ -606,11 +585,12 @@ const executeHead = (
|
|
|
606
585
|
|
|
607
586
|
const getLoaderContext = (
|
|
608
587
|
inner: InnerLoadContext,
|
|
588
|
+
matchPromises: Array<Promise<AnyRouteMatch>>,
|
|
609
589
|
matchId: string,
|
|
610
590
|
index: number,
|
|
611
591
|
route: AnyRoute,
|
|
612
592
|
): LoaderFnContext => {
|
|
613
|
-
const parentMatchPromise =
|
|
593
|
+
const parentMatchPromise = matchPromises[index - 1] as any
|
|
614
594
|
const { params, loaderDeps, abortController, cause } =
|
|
615
595
|
inner.router.getMatch(matchId)!
|
|
616
596
|
|
|
@@ -639,6 +619,7 @@ const getLoaderContext = (
|
|
|
639
619
|
|
|
640
620
|
const runLoader = async (
|
|
641
621
|
inner: InnerLoadContext,
|
|
622
|
+
matchPromises: Array<Promise<AnyRouteMatch>>,
|
|
642
623
|
matchId: string,
|
|
643
624
|
index: number,
|
|
644
625
|
route: AnyRoute,
|
|
@@ -660,7 +641,7 @@ const runLoader = async (
|
|
|
660
641
|
|
|
661
642
|
// Kick off the loader!
|
|
662
643
|
const loaderResult = route.options.loader?.(
|
|
663
|
-
getLoaderContext(inner, matchId, index, route),
|
|
644
|
+
getLoaderContext(inner, matchPromises, matchId, index, route),
|
|
664
645
|
)
|
|
665
646
|
const loaderResultIsPromise =
|
|
666
647
|
route.options.loader && isPromise(loaderResult)
|
|
@@ -775,6 +756,7 @@ const runLoader = async (
|
|
|
775
756
|
|
|
776
757
|
const loadRouteMatch = async (
|
|
777
758
|
inner: InnerLoadContext,
|
|
759
|
+
matchPromises: Array<Promise<AnyRouteMatch>>,
|
|
778
760
|
index: number,
|
|
779
761
|
): Promise<AnyRouteMatch> => {
|
|
780
762
|
async function handleLoader(
|
|
@@ -798,7 +780,9 @@ const loadRouteMatch = async (
|
|
|
798
780
|
// if provided.
|
|
799
781
|
const shouldReload =
|
|
800
782
|
typeof shouldReloadOption === 'function'
|
|
801
|
-
? shouldReloadOption(
|
|
783
|
+
? shouldReloadOption(
|
|
784
|
+
getLoaderContext(inner, matchPromises, matchId, index, route),
|
|
785
|
+
)
|
|
802
786
|
: shouldReloadOption
|
|
803
787
|
|
|
804
788
|
// If the route is successful and still fresh, just resolve
|
|
@@ -811,7 +795,7 @@ const loadRouteMatch = async (
|
|
|
811
795
|
loaderIsRunningAsync = true
|
|
812
796
|
;(async () => {
|
|
813
797
|
try {
|
|
814
|
-
await runLoader(inner, matchId, index, route)
|
|
798
|
+
await runLoader(inner, matchPromises, matchId, index, route)
|
|
815
799
|
const match = inner.router.getMatch(matchId)!
|
|
816
800
|
match._nonReactive.loaderPromise?.resolve()
|
|
817
801
|
match._nonReactive.loadPromise?.resolve()
|
|
@@ -823,7 +807,7 @@ const loadRouteMatch = async (
|
|
|
823
807
|
}
|
|
824
808
|
})()
|
|
825
809
|
} else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
|
|
826
|
-
await runLoader(inner, matchId, index, route)
|
|
810
|
+
await runLoader(inner, matchPromises, matchId, index, route)
|
|
827
811
|
}
|
|
828
812
|
}
|
|
829
813
|
|
|
@@ -906,9 +890,8 @@ export async function loadMatches(arg: {
|
|
|
906
890
|
updateMatch: UpdateMatchFn
|
|
907
891
|
sync?: boolean
|
|
908
892
|
}): Promise<Array<MakeRouteMatch>> {
|
|
909
|
-
const inner: InnerLoadContext =
|
|
910
|
-
|
|
911
|
-
})
|
|
893
|
+
const inner: InnerLoadContext = arg
|
|
894
|
+
const matchPromises: Array<Promise<AnyRouteMatch>> = []
|
|
912
895
|
|
|
913
896
|
// make sure the pending component is immediately rendered when hydrating a match that is not SSRed
|
|
914
897
|
// the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
|
|
@@ -919,77 +902,201 @@ export async function loadMatches(arg: {
|
|
|
919
902
|
triggerOnReady(inner)
|
|
920
903
|
}
|
|
921
904
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
905
|
+
let beforeLoadNotFound: NotFoundError | undefined
|
|
906
|
+
|
|
907
|
+
// Execute all beforeLoads one by one
|
|
908
|
+
for (let i = 0; i < inner.matches.length; i++) {
|
|
909
|
+
try {
|
|
925
910
|
const beforeLoad = handleBeforeLoad(inner, i)
|
|
926
911
|
if (isPromise(beforeLoad)) await beforeLoad
|
|
912
|
+
} catch (err) {
|
|
913
|
+
if (isRedirect(err)) {
|
|
914
|
+
throw err
|
|
915
|
+
}
|
|
916
|
+
if (isNotFound(err)) {
|
|
917
|
+
beforeLoadNotFound = err
|
|
918
|
+
} else {
|
|
919
|
+
if (!inner.preload) throw err
|
|
920
|
+
}
|
|
921
|
+
break
|
|
927
922
|
}
|
|
928
923
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
for (let i = 0; i < max; i++) {
|
|
932
|
-
inner.matchPromises.push(loadRouteMatch(inner, i))
|
|
924
|
+
if (inner.serialError) {
|
|
925
|
+
break
|
|
933
926
|
}
|
|
934
|
-
|
|
935
|
-
const results = await Promise.allSettled(inner.matchPromises)
|
|
936
|
-
|
|
937
|
-
const failures = results
|
|
938
|
-
// TODO when we drop support for TS 5.4, we can use the built-in type guard for PromiseRejectedResult
|
|
939
|
-
.filter(
|
|
940
|
-
(result): result is PromiseRejectedResult =>
|
|
941
|
-
result.status === 'rejected',
|
|
942
|
-
)
|
|
943
|
-
.map((result) => result.reason)
|
|
927
|
+
}
|
|
944
928
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
929
|
+
// Execute loaders once, with max index adapted for beforeLoad notFound handling.
|
|
930
|
+
const baseMaxIndexExclusive = inner.firstBadMatchIndex ?? inner.matches.length
|
|
931
|
+
|
|
932
|
+
const boundaryIndex =
|
|
933
|
+
beforeLoadNotFound && !inner.preload
|
|
934
|
+
? getNotFoundBoundaryIndex(inner, beforeLoadNotFound)
|
|
935
|
+
: undefined
|
|
936
|
+
|
|
937
|
+
const maxIndexExclusive =
|
|
938
|
+
beforeLoadNotFound && inner.preload
|
|
939
|
+
? 0
|
|
940
|
+
: boundaryIndex !== undefined
|
|
941
|
+
? Math.min(boundaryIndex + 1, baseMaxIndexExclusive)
|
|
942
|
+
: baseMaxIndexExclusive
|
|
943
|
+
|
|
944
|
+
let firstNotFound: NotFoundError | undefined
|
|
945
|
+
let firstUnhandledRejection: unknown
|
|
946
|
+
|
|
947
|
+
for (let i = 0; i < maxIndexExclusive; i++) {
|
|
948
|
+
matchPromises.push(loadRouteMatch(inner, matchPromises, i))
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
try {
|
|
952
|
+
await Promise.all(matchPromises)
|
|
953
|
+
} catch {
|
|
954
|
+
const settled = await Promise.allSettled(matchPromises)
|
|
955
|
+
|
|
956
|
+
for (const result of settled) {
|
|
957
|
+
if (result.status !== 'rejected') continue
|
|
958
|
+
|
|
959
|
+
const reason = result.reason
|
|
960
|
+
if (isRedirect(reason)) {
|
|
961
|
+
throw reason
|
|
950
962
|
}
|
|
951
|
-
if (
|
|
952
|
-
firstNotFound
|
|
963
|
+
if (isNotFound(reason)) {
|
|
964
|
+
firstNotFound ??= reason
|
|
965
|
+
} else {
|
|
966
|
+
firstUnhandledRejection ??= reason
|
|
953
967
|
}
|
|
954
968
|
}
|
|
955
969
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
for (const match of inner.matches) {
|
|
959
|
-
const { id: matchId, routeId } = match
|
|
960
|
-
const route = inner.router.looseRoutesById[routeId]!
|
|
961
|
-
try {
|
|
962
|
-
const headResult = executeHead(inner, matchId, route)
|
|
963
|
-
if (headResult) {
|
|
964
|
-
const head = await headResult
|
|
965
|
-
inner.updateMatch(matchId, (prev) => ({
|
|
966
|
-
...prev,
|
|
967
|
-
...head,
|
|
968
|
-
}))
|
|
969
|
-
}
|
|
970
|
-
} catch (err) {
|
|
971
|
-
// Log error but continue executing other head functions
|
|
972
|
-
console.error(`Error executing head for route ${routeId}:`, err)
|
|
973
|
-
}
|
|
970
|
+
if (firstUnhandledRejection !== undefined) {
|
|
971
|
+
throw firstUnhandledRejection
|
|
974
972
|
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const notFoundToThrow =
|
|
976
|
+
firstNotFound ??
|
|
977
|
+
(beforeLoadNotFound && !inner.preload ? beforeLoadNotFound : undefined)
|
|
978
|
+
|
|
979
|
+
let headMaxIndex = inner.serialError
|
|
980
|
+
? (inner.firstBadMatchIndex ?? 0)
|
|
981
|
+
: inner.matches.length - 1
|
|
982
|
+
|
|
983
|
+
if (!notFoundToThrow && beforeLoadNotFound && inner.preload) {
|
|
984
|
+
return inner.matches
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
if (notFoundToThrow) {
|
|
988
|
+
// Determine once which matched route will actually render the
|
|
989
|
+
// notFoundComponent, then pass this precomputed index through the remaining
|
|
990
|
+
// finalization steps.
|
|
991
|
+
// This can differ from the throwing route when routeId targets an ancestor
|
|
992
|
+
// boundary (or when bubbling resolves to a parent/root boundary).
|
|
993
|
+
const renderedBoundaryIndex = getNotFoundBoundaryIndex(
|
|
994
|
+
inner,
|
|
995
|
+
notFoundToThrow,
|
|
996
|
+
)
|
|
997
|
+
|
|
998
|
+
invariant(
|
|
999
|
+
renderedBoundaryIndex !== undefined,
|
|
1000
|
+
'Could not find match for notFound boundary',
|
|
1001
|
+
)
|
|
1002
|
+
const boundaryMatch = inner.matches[renderedBoundaryIndex]!
|
|
1003
|
+
|
|
1004
|
+
const boundaryRoute = inner.router.looseRoutesById[boundaryMatch.routeId]!
|
|
1005
|
+
const defaultNotFoundComponent = (inner.router.options as any)
|
|
1006
|
+
?.defaultNotFoundComponent
|
|
975
1007
|
|
|
976
|
-
//
|
|
977
|
-
if (
|
|
978
|
-
|
|
1008
|
+
// Ensure a notFoundComponent exists on the boundary route
|
|
1009
|
+
if (!boundaryRoute.options.notFoundComponent && defaultNotFoundComponent) {
|
|
1010
|
+
boundaryRoute.options.notFoundComponent = defaultNotFoundComponent
|
|
979
1011
|
}
|
|
980
1012
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1013
|
+
notFoundToThrow.routeId = boundaryMatch.routeId
|
|
1014
|
+
|
|
1015
|
+
const boundaryIsRoot = boundaryMatch.routeId === inner.router.routeTree.id
|
|
1016
|
+
|
|
1017
|
+
inner.updateMatch(boundaryMatch.id, (prev) => ({
|
|
1018
|
+
...prev,
|
|
1019
|
+
...(boundaryIsRoot
|
|
1020
|
+
? // For root boundary, use globalNotFound so the root component's
|
|
1021
|
+
// shell still renders and <Outlet> handles the not-found display,
|
|
1022
|
+
// instead of replacing the entire root shell via status='notFound'.
|
|
1023
|
+
{ status: 'success' as const, globalNotFound: true, error: undefined }
|
|
1024
|
+
: // For non-root boundaries, set status:'notFound' so MatchInner
|
|
1025
|
+
// renders the notFoundComponent directly.
|
|
1026
|
+
{ status: 'notFound' as const, error: notFoundToThrow }),
|
|
1027
|
+
isFetching: false,
|
|
1028
|
+
}))
|
|
1029
|
+
|
|
1030
|
+
headMaxIndex = renderedBoundaryIndex
|
|
1031
|
+
|
|
1032
|
+
// Ensure the rendering boundary route chunk (and its lazy components, including
|
|
1033
|
+
// lazy notFoundComponent) is loaded before we continue to head execution/render.
|
|
1034
|
+
await loadRouteChunk(boundaryRoute)
|
|
1035
|
+
} else if (!inner.preload) {
|
|
1036
|
+
// Clear stale root global-not-found state on normal navigations that do not
|
|
1037
|
+
// throw notFound. This must live here (instead of only in runLoader success)
|
|
1038
|
+
// because the root loader may be skipped when data is still fresh.
|
|
1039
|
+
const rootMatch = inner.matches[0]!
|
|
1040
|
+
// `rootMatch` is the next match for this navigation. If it is not global
|
|
1041
|
+
// not-found, then any currently stored root global-not-found is stale.
|
|
1042
|
+
if (!rootMatch.globalNotFound) {
|
|
1043
|
+
// `currentRootMatch` is the current store state (from the previous
|
|
1044
|
+
// navigation/load). Update only when a stale flag is actually present.
|
|
1045
|
+
const currentRootMatch = inner.router.getMatch(rootMatch.id)
|
|
1046
|
+
if (currentRootMatch?.globalNotFound) {
|
|
1047
|
+
inner.updateMatch(rootMatch.id, (prev) => ({
|
|
1048
|
+
...prev,
|
|
1049
|
+
globalNotFound: false,
|
|
1050
|
+
error: undefined,
|
|
1051
|
+
}))
|
|
1052
|
+
}
|
|
988
1053
|
}
|
|
989
|
-
|
|
990
|
-
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// When a serial error occurred (e.g. beforeLoad threw a regular Error),
|
|
1057
|
+
// the erroring route's lazy chunk wasn't loaded because loaders were skipped.
|
|
1058
|
+
// We need to load it so the code-split errorComponent is available for rendering.
|
|
1059
|
+
if (inner.serialError && inner.firstBadMatchIndex !== undefined) {
|
|
1060
|
+
const errorRoute =
|
|
1061
|
+
inner.router.looseRoutesById[
|
|
1062
|
+
inner.matches[inner.firstBadMatchIndex]!.routeId
|
|
1063
|
+
]!
|
|
1064
|
+
await loadRouteChunk(errorRoute)
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// serially execute heads once after loaders/notFound handling, ensuring
|
|
1068
|
+
// all head functions get a chance even if one throws.
|
|
1069
|
+
for (let i = 0; i <= headMaxIndex; i++) {
|
|
1070
|
+
const match = inner.matches[i]!
|
|
1071
|
+
const { id: matchId, routeId } = match
|
|
1072
|
+
const route = inner.router.looseRoutesById[routeId]!
|
|
1073
|
+
try {
|
|
1074
|
+
const headResult = executeHead(inner, matchId, route)
|
|
1075
|
+
if (headResult) {
|
|
1076
|
+
const head = await headResult
|
|
1077
|
+
inner.updateMatch(matchId, (prev) => ({
|
|
1078
|
+
...prev,
|
|
1079
|
+
...head,
|
|
1080
|
+
}))
|
|
1081
|
+
}
|
|
1082
|
+
} catch (err) {
|
|
1083
|
+
console.error(`Error executing head for route ${routeId}:`, err)
|
|
991
1084
|
}
|
|
992
1085
|
}
|
|
1086
|
+
|
|
1087
|
+
const readyPromise = triggerOnReady(inner)
|
|
1088
|
+
if (isPromise(readyPromise)) {
|
|
1089
|
+
await readyPromise
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (notFoundToThrow) {
|
|
1093
|
+
throw notFoundToThrow
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (inner.serialError && !inner.preload && !inner.onReady) {
|
|
1097
|
+
throw inner.serialError
|
|
1098
|
+
}
|
|
1099
|
+
|
|
993
1100
|
return inner.matches
|
|
994
1101
|
}
|
|
995
1102
|
|
package/src/ssr/ssr-client.ts
CHANGED
|
@@ -28,6 +28,14 @@ function hydrateMatch(
|
|
|
28
28
|
match.ssr = deyhydratedMatch.ssr
|
|
29
29
|
match.updatedAt = deyhydratedMatch.u
|
|
30
30
|
match.error = deyhydratedMatch.e
|
|
31
|
+
// Only hydrate global-not-found when a defined value is present in the
|
|
32
|
+
// dehydrated payload. If omitted, preserve the value computed from the
|
|
33
|
+
// current client location (important for SPA fallback HTML served at unknown
|
|
34
|
+
// URLs, where dehydrated matches may come from `/` but client matching marks
|
|
35
|
+
// root as globalNotFound).
|
|
36
|
+
if (deyhydratedMatch.g !== undefined) {
|
|
37
|
+
match.globalNotFound = deyhydratedMatch.g
|
|
38
|
+
}
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
export async function hydrate(router: AnyRouter): Promise<any> {
|
|
@@ -82,10 +90,9 @@ export async function hydrate(router: AnyRouter): Promise<any> {
|
|
|
82
90
|
|
|
83
91
|
// kick off loading the route chunks
|
|
84
92
|
const routeChunkPromise = Promise.all(
|
|
85
|
-
matches.map((match) =>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}),
|
|
93
|
+
matches.map((match) =>
|
|
94
|
+
router.loadRouteChunk(router.looseRoutesById[match.routeId]!),
|
|
95
|
+
),
|
|
89
96
|
)
|
|
90
97
|
|
|
91
98
|
function setMatchForcePending(match: AnyRouteMatch) {
|
|
@@ -146,12 +153,10 @@ export async function hydrate(router: AnyRouter): Promise<any> {
|
|
|
146
153
|
}
|
|
147
154
|
})
|
|
148
155
|
|
|
149
|
-
router.__store.setState((s) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
})
|
|
156
|
+
router.__store.setState((s) => ({
|
|
157
|
+
...s,
|
|
158
|
+
matches,
|
|
159
|
+
}))
|
|
155
160
|
|
|
156
161
|
// Allow the user to handle custom hydration data
|
|
157
162
|
await router.options.hydrate?.(dehydratedData)
|
|
@@ -277,13 +282,11 @@ export async function hydrate(router: AnyRouter): Promise<any> {
|
|
|
277
282
|
}))
|
|
278
283
|
}
|
|
279
284
|
// hide the pending component once the load is finished
|
|
280
|
-
router.updateMatch(match.id, (prev) => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
}
|
|
286
|
-
})
|
|
285
|
+
router.updateMatch(match.id, (prev) => ({
|
|
286
|
+
...prev,
|
|
287
|
+
_displayPending: undefined,
|
|
288
|
+
displayPendingPromise: undefined,
|
|
289
|
+
}))
|
|
287
290
|
})
|
|
288
291
|
})
|
|
289
292
|
}
|
package/src/ssr/ssr-server.ts
CHANGED