@tanstack/router-core 1.163.3 → 1.166.4
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 +187 -96
- package/dist/cjs/load-matches.cjs.map +1 -1
- package/dist/cjs/load-matches.d.cts +1 -0
- package/dist/cjs/router.cjs +2 -0
- package/dist/cjs/router.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.d.ts +1 -0
- package/dist/esm/load-matches.js +187 -96
- package/dist/esm/load-matches.js.map +1 -1
- package/dist/esm/router.js +2 -0
- package/dist/esm/router.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 +285 -133
- package/src/router.ts +2 -0
- 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,13 @@ 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
|
|
36
|
+
forceStaleReload?: boolean
|
|
35
37
|
onReady?: () => Promise<void>
|
|
36
38
|
sync?: boolean
|
|
37
|
-
/** mutable state, scoped to a `loadMatches` call */
|
|
38
|
-
matchPromises: Array<Promise<AnyRouteMatch>>
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
const triggerOnReady = (inner: InnerLoadContext): void | Promise<void> => {
|
|
@@ -74,64 +74,45 @@ const buildMatchContext = (
|
|
|
74
74
|
return context
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
const
|
|
77
|
+
const getNotFoundBoundaryIndex = (
|
|
78
78
|
inner: InnerLoadContext,
|
|
79
79
|
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
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// For BEFORE_LOAD errors that will walk up to a parent route,
|
|
98
|
-
// don't require notFoundComponent on the current (child) route —
|
|
99
|
-
// an ancestor will handle it. Only enforce the invariant when
|
|
100
|
-
// we've reached a route that won't walk up further.
|
|
101
|
-
const willWalkUp = routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute
|
|
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
|
-
)
|
|
80
|
+
): number | undefined => {
|
|
81
|
+
if (!inner.matches.length) {
|
|
82
|
+
return undefined
|
|
109
83
|
}
|
|
110
84
|
|
|
111
|
-
|
|
112
|
-
const
|
|
85
|
+
const requestedRouteId = err.routeId
|
|
86
|
+
const matchedRootIndex = inner.matches.findIndex(
|
|
87
|
+
(m) => m.routeId === inner.router.routeTree.id,
|
|
88
|
+
)
|
|
89
|
+
const rootIndex = matchedRootIndex >= 0 ? matchedRootIndex : 0
|
|
113
90
|
|
|
114
|
-
|
|
91
|
+
let startIndex = requestedRouteId
|
|
92
|
+
? inner.matches.findIndex((match) => match.routeId === requestedRouteId)
|
|
93
|
+
: (inner.firstBadMatchIndex ?? inner.matches.length - 1)
|
|
115
94
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
status: 'notFound',
|
|
120
|
-
error: err,
|
|
121
|
-
isFetching: false,
|
|
122
|
-
}))
|
|
95
|
+
if (startIndex < 0) {
|
|
96
|
+
startIndex = rootIndex
|
|
97
|
+
}
|
|
123
98
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
99
|
+
for (let i = startIndex; i >= 0; i--) {
|
|
100
|
+
const match = inner.matches[i]!
|
|
101
|
+
const route = inner.router.looseRoutesById[match.routeId]!
|
|
102
|
+
if (route.options.notFoundComponent) {
|
|
103
|
+
return i
|
|
104
|
+
}
|
|
127
105
|
}
|
|
106
|
+
|
|
107
|
+
// If no boundary component is found, preserve explicit routeId targeting behavior,
|
|
108
|
+
// otherwise default to root for untargeted notFounds.
|
|
109
|
+
return requestedRouteId ? startIndex : rootIndex
|
|
128
110
|
}
|
|
129
111
|
|
|
130
112
|
const handleRedirectAndNotFound = (
|
|
131
113
|
inner: InnerLoadContext,
|
|
132
114
|
match: AnyRouteMatch | undefined,
|
|
133
115
|
err: unknown,
|
|
134
|
-
routerCode?: string,
|
|
135
116
|
): void => {
|
|
136
117
|
if (!isRedirect(err) && !isNotFound(err)) return
|
|
137
118
|
|
|
@@ -146,19 +127,26 @@ const handleRedirectAndNotFound = (
|
|
|
146
127
|
match._nonReactive.beforeLoadPromise = undefined
|
|
147
128
|
match._nonReactive.loaderPromise = undefined
|
|
148
129
|
|
|
149
|
-
const status = isRedirect(err) ? 'redirected' : 'notFound'
|
|
150
|
-
|
|
151
130
|
match._nonReactive.error = err
|
|
152
131
|
|
|
153
132
|
inner.updateMatch(match.id, (prev) => ({
|
|
154
133
|
...prev,
|
|
155
|
-
status
|
|
134
|
+
status: isRedirect(err)
|
|
135
|
+
? 'redirected'
|
|
136
|
+
: prev.status === 'pending'
|
|
137
|
+
? 'success'
|
|
138
|
+
: prev.status,
|
|
156
139
|
context: buildMatchContext(inner, match.index),
|
|
157
140
|
isFetching: false,
|
|
158
141
|
error: err,
|
|
159
142
|
}))
|
|
160
143
|
|
|
161
144
|
if (isNotFound(err) && !err.routeId) {
|
|
145
|
+
// Stamp the throwing match's routeId so that the finalization step in
|
|
146
|
+
// loadMatches knows where the notFound originated. The actual boundary
|
|
147
|
+
// resolution (walking up to the nearest notFoundComponent) is deferred to
|
|
148
|
+
// the finalization step, where firstBadMatchIndex is stable and
|
|
149
|
+
// headMaxIndex can be capped correctly.
|
|
162
150
|
err.routeId = match.routeId
|
|
163
151
|
}
|
|
164
152
|
|
|
@@ -170,18 +158,19 @@ const handleRedirectAndNotFound = (
|
|
|
170
158
|
err.options._fromLocation = inner.location
|
|
171
159
|
err.redirectHandled = true
|
|
172
160
|
err = inner.router.resolveRedirect(err)
|
|
173
|
-
throw err
|
|
174
|
-
} else {
|
|
175
|
-
_handleNotFound(inner, err, routerCode)
|
|
176
|
-
throw err
|
|
177
161
|
}
|
|
162
|
+
|
|
163
|
+
throw err
|
|
178
164
|
}
|
|
179
165
|
|
|
180
166
|
const shouldSkipLoader = (
|
|
181
167
|
inner: InnerLoadContext,
|
|
182
168
|
matchId: string,
|
|
183
169
|
): boolean => {
|
|
184
|
-
const match = inner.router.getMatch(matchId)
|
|
170
|
+
const match = inner.router.getMatch(matchId)
|
|
171
|
+
if (!match) {
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
185
174
|
// upon hydration, we skip the loader if the match has been dehydrated on the server
|
|
186
175
|
if (!(isServer ?? inner.router.isServer) && match._nonReactive.dehydrated) {
|
|
187
176
|
return true
|
|
@@ -194,6 +183,21 @@ const shouldSkipLoader = (
|
|
|
194
183
|
return false
|
|
195
184
|
}
|
|
196
185
|
|
|
186
|
+
const syncMatchContext = (
|
|
187
|
+
inner: InnerLoadContext,
|
|
188
|
+
matchId: string,
|
|
189
|
+
index: number,
|
|
190
|
+
): void => {
|
|
191
|
+
const nextContext = buildMatchContext(inner, index)
|
|
192
|
+
|
|
193
|
+
inner.updateMatch(matchId, (prev) => {
|
|
194
|
+
return {
|
|
195
|
+
...prev,
|
|
196
|
+
context: nextContext,
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
197
201
|
const handleSerialError = (
|
|
198
202
|
inner: InnerLoadContext,
|
|
199
203
|
index: number,
|
|
@@ -212,23 +216,13 @@ const handleSerialError = (
|
|
|
212
216
|
|
|
213
217
|
err.routerCode = routerCode
|
|
214
218
|
inner.firstBadMatchIndex ??= index
|
|
215
|
-
handleRedirectAndNotFound(
|
|
216
|
-
inner,
|
|
217
|
-
inner.router.getMatch(matchId),
|
|
218
|
-
err,
|
|
219
|
-
routerCode,
|
|
220
|
-
)
|
|
219
|
+
handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
|
|
221
220
|
|
|
222
221
|
try {
|
|
223
222
|
route.options.onError?.(err)
|
|
224
223
|
} catch (errorHandlerErr) {
|
|
225
224
|
err = errorHandlerErr
|
|
226
|
-
handleRedirectAndNotFound(
|
|
227
|
-
inner,
|
|
228
|
-
inner.router.getMatch(matchId),
|
|
229
|
-
err,
|
|
230
|
-
routerCode,
|
|
231
|
-
)
|
|
225
|
+
handleRedirectAndNotFound(inner, inner.router.getMatch(matchId), err)
|
|
232
226
|
}
|
|
233
227
|
|
|
234
228
|
inner.updateMatch(matchId, (prev) => {
|
|
@@ -245,6 +239,10 @@ const handleSerialError = (
|
|
|
245
239
|
abortController: new AbortController(),
|
|
246
240
|
}
|
|
247
241
|
})
|
|
242
|
+
|
|
243
|
+
if (!inner.preload && !isRedirect(err) && !isNotFound(err)) {
|
|
244
|
+
inner.serialError ??= err
|
|
245
|
+
}
|
|
248
246
|
}
|
|
249
247
|
|
|
250
248
|
const isBeforeLoadSsr = (
|
|
@@ -500,8 +498,6 @@ const executeBeforeLoad = (
|
|
|
500
498
|
|
|
501
499
|
batch(() => {
|
|
502
500
|
pending()
|
|
503
|
-
// Only store __beforeLoadContext here, don't update context yet
|
|
504
|
-
// Context will be updated in loadRouteMatch after loader completes
|
|
505
501
|
inner.updateMatch(matchId, (prev) => ({
|
|
506
502
|
...prev,
|
|
507
503
|
__beforeLoadContext: beforeLoadContext,
|
|
@@ -606,11 +602,12 @@ const executeHead = (
|
|
|
606
602
|
|
|
607
603
|
const getLoaderContext = (
|
|
608
604
|
inner: InnerLoadContext,
|
|
605
|
+
matchPromises: Array<Promise<AnyRouteMatch>>,
|
|
609
606
|
matchId: string,
|
|
610
607
|
index: number,
|
|
611
608
|
route: AnyRoute,
|
|
612
609
|
): LoaderFnContext => {
|
|
613
|
-
const parentMatchPromise =
|
|
610
|
+
const parentMatchPromise = matchPromises[index - 1] as any
|
|
614
611
|
const { params, loaderDeps, abortController, cause } =
|
|
615
612
|
inner.router.getMatch(matchId)!
|
|
616
613
|
|
|
@@ -639,6 +636,7 @@ const getLoaderContext = (
|
|
|
639
636
|
|
|
640
637
|
const runLoader = async (
|
|
641
638
|
inner: InnerLoadContext,
|
|
639
|
+
matchPromises: Array<Promise<AnyRouteMatch>>,
|
|
642
640
|
matchId: string,
|
|
643
641
|
index: number,
|
|
644
642
|
route: AnyRoute,
|
|
@@ -660,7 +658,7 @@ const runLoader = async (
|
|
|
660
658
|
|
|
661
659
|
// Kick off the loader!
|
|
662
660
|
const loaderResult = route.options.loader?.(
|
|
663
|
-
getLoaderContext(inner, matchId, index, route),
|
|
661
|
+
getLoaderContext(inner, matchPromises, matchId, index, route),
|
|
664
662
|
)
|
|
665
663
|
const loaderResultIsPromise =
|
|
666
664
|
route.options.loader && isPromise(loaderResult)
|
|
@@ -775,11 +773,13 @@ const runLoader = async (
|
|
|
775
773
|
|
|
776
774
|
const loadRouteMatch = async (
|
|
777
775
|
inner: InnerLoadContext,
|
|
776
|
+
matchPromises: Array<Promise<AnyRouteMatch>>,
|
|
778
777
|
index: number,
|
|
779
778
|
): Promise<AnyRouteMatch> => {
|
|
780
779
|
async function handleLoader(
|
|
781
780
|
preload: boolean,
|
|
782
781
|
prevMatch: AnyRouteMatch,
|
|
782
|
+
previousRouteMatchId: string | undefined,
|
|
783
783
|
match: AnyRouteMatch,
|
|
784
784
|
route: AnyRoute,
|
|
785
785
|
) {
|
|
@@ -798,20 +798,29 @@ const loadRouteMatch = async (
|
|
|
798
798
|
// if provided.
|
|
799
799
|
const shouldReload =
|
|
800
800
|
typeof shouldReloadOption === 'function'
|
|
801
|
-
? shouldReloadOption(
|
|
801
|
+
? shouldReloadOption(
|
|
802
|
+
getLoaderContext(inner, matchPromises, matchId, index, route),
|
|
803
|
+
)
|
|
802
804
|
: shouldReloadOption
|
|
803
805
|
|
|
804
806
|
// If the route is successful and still fresh, just resolve
|
|
805
807
|
const { status, invalid } = match
|
|
808
|
+
const staleMatchShouldReload =
|
|
809
|
+
age > staleAge &&
|
|
810
|
+
(!!inner.forceStaleReload ||
|
|
811
|
+
match.cause === 'enter' ||
|
|
812
|
+
(previousRouteMatchId !== undefined &&
|
|
813
|
+
previousRouteMatchId !== match.id))
|
|
806
814
|
loaderShouldRunAsync =
|
|
807
|
-
status === 'success' &&
|
|
815
|
+
status === 'success' &&
|
|
816
|
+
(invalid || (shouldReload ?? staleMatchShouldReload))
|
|
808
817
|
if (preload && route.options.preload === false) {
|
|
809
818
|
// Do nothing
|
|
810
819
|
} else if (loaderShouldRunAsync && !inner.sync) {
|
|
811
820
|
loaderIsRunningAsync = true
|
|
812
821
|
;(async () => {
|
|
813
822
|
try {
|
|
814
|
-
await runLoader(inner, matchId, index, route)
|
|
823
|
+
await runLoader(inner, matchPromises, matchId, index, route)
|
|
815
824
|
const match = inner.router.getMatch(matchId)!
|
|
816
825
|
match._nonReactive.loaderPromise?.resolve()
|
|
817
826
|
match._nonReactive.loadPromise?.resolve()
|
|
@@ -823,7 +832,9 @@ const loadRouteMatch = async (
|
|
|
823
832
|
}
|
|
824
833
|
})()
|
|
825
834
|
} else if (status !== 'success' || (loaderShouldRunAsync && inner.sync)) {
|
|
826
|
-
await runLoader(inner, matchId, index, route)
|
|
835
|
+
await runLoader(inner, matchPromises, matchId, index, route)
|
|
836
|
+
} else {
|
|
837
|
+
syncMatchContext(inner, matchId, index)
|
|
827
838
|
}
|
|
828
839
|
}
|
|
829
840
|
|
|
@@ -833,11 +844,22 @@ const loadRouteMatch = async (
|
|
|
833
844
|
const route = inner.router.looseRoutesById[routeId]!
|
|
834
845
|
|
|
835
846
|
if (shouldSkipLoader(inner, matchId)) {
|
|
847
|
+
const match = inner.router.getMatch(matchId)
|
|
848
|
+
if (!match) {
|
|
849
|
+
return inner.matches[index]!
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
syncMatchContext(inner, matchId, index)
|
|
853
|
+
|
|
836
854
|
if (isServer ?? inner.router.isServer) {
|
|
837
855
|
return inner.router.getMatch(matchId)!
|
|
838
856
|
}
|
|
839
857
|
} else {
|
|
840
858
|
const prevMatch = inner.router.getMatch(matchId)! // This is where all of the stale-while-revalidate magic happens
|
|
859
|
+
const previousRouteMatchId =
|
|
860
|
+
inner.router.state.matches[index]?.routeId === routeId
|
|
861
|
+
? inner.router.state.matches[index]!.id
|
|
862
|
+
: inner.router.state.matches.find((d) => d.routeId === routeId)?.id
|
|
841
863
|
const preload = resolvePreload(inner, matchId)
|
|
842
864
|
|
|
843
865
|
// there is a loaderPromise, so we are in the middle of a load
|
|
@@ -856,7 +878,13 @@ const loadRouteMatch = async (
|
|
|
856
878
|
}
|
|
857
879
|
|
|
858
880
|
if (match.status === 'pending') {
|
|
859
|
-
await handleLoader(
|
|
881
|
+
await handleLoader(
|
|
882
|
+
preload,
|
|
883
|
+
prevMatch,
|
|
884
|
+
previousRouteMatchId,
|
|
885
|
+
match,
|
|
886
|
+
route,
|
|
887
|
+
)
|
|
860
888
|
}
|
|
861
889
|
} else {
|
|
862
890
|
const nextPreload =
|
|
@@ -870,7 +898,7 @@ const loadRouteMatch = async (
|
|
|
870
898
|
}))
|
|
871
899
|
}
|
|
872
900
|
|
|
873
|
-
await handleLoader(preload, prevMatch, match, route)
|
|
901
|
+
await handleLoader(preload, prevMatch, previousRouteMatchId, match, route)
|
|
874
902
|
}
|
|
875
903
|
}
|
|
876
904
|
const match = inner.router.getMatch(matchId)!
|
|
@@ -902,13 +930,13 @@ export async function loadMatches(arg: {
|
|
|
902
930
|
location: ParsedLocation
|
|
903
931
|
matches: Array<AnyRouteMatch>
|
|
904
932
|
preload?: boolean
|
|
933
|
+
forceStaleReload?: boolean
|
|
905
934
|
onReady?: () => Promise<void>
|
|
906
935
|
updateMatch: UpdateMatchFn
|
|
907
936
|
sync?: boolean
|
|
908
937
|
}): Promise<Array<MakeRouteMatch>> {
|
|
909
|
-
const inner: InnerLoadContext =
|
|
910
|
-
|
|
911
|
-
})
|
|
938
|
+
const inner: InnerLoadContext = arg
|
|
939
|
+
const matchPromises: Array<Promise<AnyRouteMatch>> = []
|
|
912
940
|
|
|
913
941
|
// make sure the pending component is immediately rendered when hydrating a match that is not SSRed
|
|
914
942
|
// 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 +947,201 @@ export async function loadMatches(arg: {
|
|
|
919
947
|
triggerOnReady(inner)
|
|
920
948
|
}
|
|
921
949
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
950
|
+
let beforeLoadNotFound: NotFoundError | undefined
|
|
951
|
+
|
|
952
|
+
// Execute all beforeLoads one by one
|
|
953
|
+
for (let i = 0; i < inner.matches.length; i++) {
|
|
954
|
+
try {
|
|
925
955
|
const beforeLoad = handleBeforeLoad(inner, i)
|
|
926
956
|
if (isPromise(beforeLoad)) await beforeLoad
|
|
957
|
+
} catch (err) {
|
|
958
|
+
if (isRedirect(err)) {
|
|
959
|
+
throw err
|
|
960
|
+
}
|
|
961
|
+
if (isNotFound(err)) {
|
|
962
|
+
beforeLoadNotFound = err
|
|
963
|
+
} else {
|
|
964
|
+
if (!inner.preload) throw err
|
|
965
|
+
}
|
|
966
|
+
break
|
|
927
967
|
}
|
|
928
968
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
for (let i = 0; i < max; i++) {
|
|
932
|
-
inner.matchPromises.push(loadRouteMatch(inner, i))
|
|
969
|
+
if (inner.serialError) {
|
|
970
|
+
break
|
|
933
971
|
}
|
|
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)
|
|
972
|
+
}
|
|
944
973
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
974
|
+
// Execute loaders once, with max index adapted for beforeLoad notFound handling.
|
|
975
|
+
const baseMaxIndexExclusive = inner.firstBadMatchIndex ?? inner.matches.length
|
|
976
|
+
|
|
977
|
+
const boundaryIndex =
|
|
978
|
+
beforeLoadNotFound && !inner.preload
|
|
979
|
+
? getNotFoundBoundaryIndex(inner, beforeLoadNotFound)
|
|
980
|
+
: undefined
|
|
981
|
+
|
|
982
|
+
const maxIndexExclusive =
|
|
983
|
+
beforeLoadNotFound && inner.preload
|
|
984
|
+
? 0
|
|
985
|
+
: boundaryIndex !== undefined
|
|
986
|
+
? Math.min(boundaryIndex + 1, baseMaxIndexExclusive)
|
|
987
|
+
: baseMaxIndexExclusive
|
|
988
|
+
|
|
989
|
+
let firstNotFound: NotFoundError | undefined
|
|
990
|
+
let firstUnhandledRejection: unknown
|
|
991
|
+
|
|
992
|
+
for (let i = 0; i < maxIndexExclusive; i++) {
|
|
993
|
+
matchPromises.push(loadRouteMatch(inner, matchPromises, i))
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
try {
|
|
997
|
+
await Promise.all(matchPromises)
|
|
998
|
+
} catch {
|
|
999
|
+
const settled = await Promise.allSettled(matchPromises)
|
|
1000
|
+
|
|
1001
|
+
for (const result of settled) {
|
|
1002
|
+
if (result.status !== 'rejected') continue
|
|
1003
|
+
|
|
1004
|
+
const reason = result.reason
|
|
1005
|
+
if (isRedirect(reason)) {
|
|
1006
|
+
throw reason
|
|
950
1007
|
}
|
|
951
|
-
if (
|
|
952
|
-
firstNotFound
|
|
1008
|
+
if (isNotFound(reason)) {
|
|
1009
|
+
firstNotFound ??= reason
|
|
1010
|
+
} else {
|
|
1011
|
+
firstUnhandledRejection ??= reason
|
|
953
1012
|
}
|
|
954
1013
|
}
|
|
955
1014
|
|
|
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
|
-
}
|
|
1015
|
+
if (firstUnhandledRejection !== undefined) {
|
|
1016
|
+
throw firstUnhandledRejection
|
|
974
1017
|
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const notFoundToThrow =
|
|
1021
|
+
firstNotFound ??
|
|
1022
|
+
(beforeLoadNotFound && !inner.preload ? beforeLoadNotFound : undefined)
|
|
1023
|
+
|
|
1024
|
+
let headMaxIndex = inner.serialError
|
|
1025
|
+
? (inner.firstBadMatchIndex ?? 0)
|
|
1026
|
+
: inner.matches.length - 1
|
|
1027
|
+
|
|
1028
|
+
if (!notFoundToThrow && beforeLoadNotFound && inner.preload) {
|
|
1029
|
+
return inner.matches
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
if (notFoundToThrow) {
|
|
1033
|
+
// Determine once which matched route will actually render the
|
|
1034
|
+
// notFoundComponent, then pass this precomputed index through the remaining
|
|
1035
|
+
// finalization steps.
|
|
1036
|
+
// This can differ from the throwing route when routeId targets an ancestor
|
|
1037
|
+
// boundary (or when bubbling resolves to a parent/root boundary).
|
|
1038
|
+
const renderedBoundaryIndex = getNotFoundBoundaryIndex(
|
|
1039
|
+
inner,
|
|
1040
|
+
notFoundToThrow,
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
invariant(
|
|
1044
|
+
renderedBoundaryIndex !== undefined,
|
|
1045
|
+
'Could not find match for notFound boundary',
|
|
1046
|
+
)
|
|
1047
|
+
const boundaryMatch = inner.matches[renderedBoundaryIndex]!
|
|
975
1048
|
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1049
|
+
const boundaryRoute = inner.router.looseRoutesById[boundaryMatch.routeId]!
|
|
1050
|
+
const defaultNotFoundComponent = (inner.router.options as any)
|
|
1051
|
+
?.defaultNotFoundComponent
|
|
1052
|
+
|
|
1053
|
+
// Ensure a notFoundComponent exists on the boundary route
|
|
1054
|
+
if (!boundaryRoute.options.notFoundComponent && defaultNotFoundComponent) {
|
|
1055
|
+
boundaryRoute.options.notFoundComponent = defaultNotFoundComponent
|
|
979
1056
|
}
|
|
980
1057
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1058
|
+
notFoundToThrow.routeId = boundaryMatch.routeId
|
|
1059
|
+
|
|
1060
|
+
const boundaryIsRoot = boundaryMatch.routeId === inner.router.routeTree.id
|
|
1061
|
+
|
|
1062
|
+
inner.updateMatch(boundaryMatch.id, (prev) => ({
|
|
1063
|
+
...prev,
|
|
1064
|
+
...(boundaryIsRoot
|
|
1065
|
+
? // For root boundary, use globalNotFound so the root component's
|
|
1066
|
+
// shell still renders and <Outlet> handles the not-found display,
|
|
1067
|
+
// instead of replacing the entire root shell via status='notFound'.
|
|
1068
|
+
{ status: 'success' as const, globalNotFound: true, error: undefined }
|
|
1069
|
+
: // For non-root boundaries, set status:'notFound' so MatchInner
|
|
1070
|
+
// renders the notFoundComponent directly.
|
|
1071
|
+
{ status: 'notFound' as const, error: notFoundToThrow }),
|
|
1072
|
+
isFetching: false,
|
|
1073
|
+
}))
|
|
1074
|
+
|
|
1075
|
+
headMaxIndex = renderedBoundaryIndex
|
|
1076
|
+
|
|
1077
|
+
// Ensure the rendering boundary route chunk (and its lazy components, including
|
|
1078
|
+
// lazy notFoundComponent) is loaded before we continue to head execution/render.
|
|
1079
|
+
await loadRouteChunk(boundaryRoute)
|
|
1080
|
+
} else if (!inner.preload) {
|
|
1081
|
+
// Clear stale root global-not-found state on normal navigations that do not
|
|
1082
|
+
// throw notFound. This must live here (instead of only in runLoader success)
|
|
1083
|
+
// because the root loader may be skipped when data is still fresh.
|
|
1084
|
+
const rootMatch = inner.matches[0]!
|
|
1085
|
+
// `rootMatch` is the next match for this navigation. If it is not global
|
|
1086
|
+
// not-found, then any currently stored root global-not-found is stale.
|
|
1087
|
+
if (!rootMatch.globalNotFound) {
|
|
1088
|
+
// `currentRootMatch` is the current store state (from the previous
|
|
1089
|
+
// navigation/load). Update only when a stale flag is actually present.
|
|
1090
|
+
const currentRootMatch = inner.router.getMatch(rootMatch.id)
|
|
1091
|
+
if (currentRootMatch?.globalNotFound) {
|
|
1092
|
+
inner.updateMatch(rootMatch.id, (prev) => ({
|
|
1093
|
+
...prev,
|
|
1094
|
+
globalNotFound: false,
|
|
1095
|
+
error: undefined,
|
|
1096
|
+
}))
|
|
1097
|
+
}
|
|
988
1098
|
}
|
|
989
|
-
|
|
990
|
-
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// When a serial error occurred (e.g. beforeLoad threw a regular Error),
|
|
1102
|
+
// the erroring route's lazy chunk wasn't loaded because loaders were skipped.
|
|
1103
|
+
// We need to load it so the code-split errorComponent is available for rendering.
|
|
1104
|
+
if (inner.serialError && inner.firstBadMatchIndex !== undefined) {
|
|
1105
|
+
const errorRoute =
|
|
1106
|
+
inner.router.looseRoutesById[
|
|
1107
|
+
inner.matches[inner.firstBadMatchIndex]!.routeId
|
|
1108
|
+
]!
|
|
1109
|
+
await loadRouteChunk(errorRoute)
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// serially execute heads once after loaders/notFound handling, ensuring
|
|
1113
|
+
// all head functions get a chance even if one throws.
|
|
1114
|
+
for (let i = 0; i <= headMaxIndex; i++) {
|
|
1115
|
+
const match = inner.matches[i]!
|
|
1116
|
+
const { id: matchId, routeId } = match
|
|
1117
|
+
const route = inner.router.looseRoutesById[routeId]!
|
|
1118
|
+
try {
|
|
1119
|
+
const headResult = executeHead(inner, matchId, route)
|
|
1120
|
+
if (headResult) {
|
|
1121
|
+
const head = await headResult
|
|
1122
|
+
inner.updateMatch(matchId, (prev) => ({
|
|
1123
|
+
...prev,
|
|
1124
|
+
...head,
|
|
1125
|
+
}))
|
|
1126
|
+
}
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
console.error(`Error executing head for route ${routeId}:`, err)
|
|
991
1129
|
}
|
|
992
1130
|
}
|
|
1131
|
+
|
|
1132
|
+
const readyPromise = triggerOnReady(inner)
|
|
1133
|
+
if (isPromise(readyPromise)) {
|
|
1134
|
+
await readyPromise
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (notFoundToThrow) {
|
|
1138
|
+
throw notFoundToThrow
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
if (inner.serialError && !inner.preload && !inner.onReady) {
|
|
1142
|
+
throw inner.serialError
|
|
1143
|
+
}
|
|
1144
|
+
|
|
993
1145
|
return inner.matches
|
|
994
1146
|
}
|
|
995
1147
|
|
package/src/router.ts
CHANGED
|
@@ -2364,6 +2364,7 @@ export class RouterCore<
|
|
|
2364
2364
|
let redirect: AnyRedirect | undefined
|
|
2365
2365
|
let notFound: NotFoundError | undefined
|
|
2366
2366
|
let loadPromise: Promise<void>
|
|
2367
|
+
const previousLocation = this.state.resolvedLocation ?? this.state.location
|
|
2367
2368
|
|
|
2368
2369
|
// eslint-disable-next-line prefer-const
|
|
2369
2370
|
loadPromise = new Promise<void>((resolve) => {
|
|
@@ -2394,6 +2395,7 @@ export class RouterCore<
|
|
|
2394
2395
|
await loadMatches({
|
|
2395
2396
|
router: this,
|
|
2396
2397
|
sync: opts?.sync,
|
|
2398
|
+
forceStaleReload: previousLocation.href === next.href,
|
|
2397
2399
|
matches: this.state.pendingMatches as Array<AnyRouteMatch>,
|
|
2398
2400
|
location: next,
|
|
2399
2401
|
updateMatch: this.updateMatch,
|