@tanstack/react-router 1.18.3 → 1.18.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/Matches.cjs +75 -24
- package/dist/cjs/Matches.cjs.map +1 -1
- package/dist/cjs/Matches.d.cts +2 -3
- package/dist/cjs/RouterProvider.cjs +1 -10
- package/dist/cjs/RouterProvider.cjs.map +1 -1
- package/dist/cjs/awaited.cjs +2 -4
- package/dist/cjs/awaited.cjs.map +1 -1
- package/dist/cjs/fileRoute.cjs.map +1 -1
- package/dist/cjs/fileRoute.d.cts +6 -5
- package/dist/cjs/link.cjs.map +1 -1
- package/dist/cjs/not-found.cjs.map +1 -1
- package/dist/cjs/not-found.d.cts +11 -1
- package/dist/cjs/redirects.cjs +1 -1
- package/dist/cjs/redirects.cjs.map +1 -1
- package/dist/cjs/route.cjs +3 -2
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +25 -24
- package/dist/cjs/router.cjs +172 -143
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +12 -8
- package/dist/esm/Matches.d.ts +2 -3
- package/dist/esm/Matches.js +67 -16
- package/dist/esm/Matches.js.map +1 -1
- package/dist/esm/RouterProvider.js +1 -10
- package/dist/esm/RouterProvider.js.map +1 -1
- package/dist/esm/awaited.js +2 -4
- package/dist/esm/awaited.js.map +1 -1
- package/dist/esm/fileRoute.d.ts +6 -5
- package/dist/esm/fileRoute.js.map +1 -1
- package/dist/esm/link.js.map +1 -1
- package/dist/esm/not-found.d.ts +11 -1
- package/dist/esm/not-found.js.map +1 -1
- package/dist/esm/redirects.js +1 -1
- package/dist/esm/redirects.js.map +1 -1
- package/dist/esm/route.d.ts +25 -24
- package/dist/esm/route.js +3 -2
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +12 -8
- package/dist/esm/router.js +163 -134
- package/dist/esm/router.js.map +1 -1
- package/package.json +1 -1
- package/src/Matches.tsx +100 -27
- package/src/RouterProvider.tsx +1 -12
- package/src/awaited.tsx +3 -5
- package/src/fileRoute.ts +8 -3
- package/src/link.tsx +0 -2
- package/src/not-found.tsx +11 -1
- package/src/redirects.ts +2 -1
- package/src/route.ts +55 -114
- package/src/router.ts +267 -175
package/src/router.ts
CHANGED
|
@@ -14,11 +14,11 @@ import {
|
|
|
14
14
|
AnySearchSchema,
|
|
15
15
|
AnyRoute,
|
|
16
16
|
AnyContext,
|
|
17
|
-
AnyPathParams,
|
|
18
17
|
RouteMask,
|
|
19
18
|
Route,
|
|
20
19
|
LoaderFnContext,
|
|
21
20
|
rootRouteId,
|
|
21
|
+
NotFoundRouteComponent,
|
|
22
22
|
} from './route'
|
|
23
23
|
import {
|
|
24
24
|
FullSearchSchema,
|
|
@@ -67,10 +67,10 @@ import {
|
|
|
67
67
|
import invariant from 'tiny-invariant'
|
|
68
68
|
import { AnyRedirect, isRedirect } from './redirects'
|
|
69
69
|
import { NotFoundError, isNotFound } from './not-found'
|
|
70
|
-
import { ResolveRelativePath, ToOptions } from './link'
|
|
70
|
+
import { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
|
|
71
71
|
import { NoInfer } from '@tanstack/react-store'
|
|
72
72
|
import warning from 'tiny-warning'
|
|
73
|
-
import { DeferredPromiseState } from '
|
|
73
|
+
import { DeferredPromiseState } from './defer'
|
|
74
74
|
|
|
75
75
|
//
|
|
76
76
|
|
|
@@ -85,7 +85,7 @@ export interface Register {
|
|
|
85
85
|
// router: Router
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
export type AnyRouter = Router<AnyRoute, any>
|
|
88
|
+
export type AnyRouter = Router<AnyRoute, any, any>
|
|
89
89
|
|
|
90
90
|
export type RegisteredRouter = Register extends {
|
|
91
91
|
router: infer TRouter extends AnyRouter
|
|
@@ -125,6 +125,7 @@ export interface RouterOptions<
|
|
|
125
125
|
defaultStaleTime?: number
|
|
126
126
|
defaultPreloadStaleTime?: number
|
|
127
127
|
defaultPreloadGcTime?: number
|
|
128
|
+
notFoundMode?: 'root' | 'fuzzy'
|
|
128
129
|
defaultGcTime?: number
|
|
129
130
|
caseSensitive?: boolean
|
|
130
131
|
routeTree?: TRouteTree
|
|
@@ -142,9 +143,9 @@ export interface RouterOptions<
|
|
|
142
143
|
* See https://tanstack.com/router/v1/docs/guide/not-found-errors#migrating-from-notfoundroute for more info.
|
|
143
144
|
*/
|
|
144
145
|
notFoundRoute?: AnyRoute
|
|
146
|
+
defaultNotFoundComponent?: NotFoundRouteComponent
|
|
145
147
|
transformer?: RouterTransformer
|
|
146
148
|
errorSerializer?: RouterErrorSerializer<TSerializedError>
|
|
147
|
-
globalNotFound?: RouteComponent
|
|
148
149
|
}
|
|
149
150
|
|
|
150
151
|
export interface RouterTransformer {
|
|
@@ -166,6 +167,7 @@ export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
|
|
|
166
167
|
location: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
167
168
|
resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
168
169
|
lastUpdated: number
|
|
170
|
+
statusCode: number
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
|
|
@@ -193,7 +195,7 @@ export interface DehydratedRouterState {
|
|
|
193
195
|
|
|
194
196
|
export type DehydratedRouteMatch = Pick<
|
|
195
197
|
RouteMatch,
|
|
196
|
-
'id' | 'status' | 'updatedAt' | '
|
|
198
|
+
'id' | 'status' | 'updatedAt' | 'loaderData'
|
|
197
199
|
>
|
|
198
200
|
|
|
199
201
|
export interface DehydratedRouter {
|
|
@@ -380,6 +382,9 @@ export class Router<
|
|
|
380
382
|
this.state.isTransitioning || this.state.isLoading
|
|
381
383
|
? 'pending'
|
|
382
384
|
: 'idle',
|
|
385
|
+
cachedMatches: this.state.cachedMatches.filter(
|
|
386
|
+
(d) => !['redirected'].includes(d.status),
|
|
387
|
+
),
|
|
383
388
|
}
|
|
384
389
|
},
|
|
385
390
|
})
|
|
@@ -586,7 +591,7 @@ export class Router<
|
|
|
586
591
|
matchRoutes = <TRouteTree extends AnyRoute>(
|
|
587
592
|
pathname: string,
|
|
588
593
|
locationSearch: AnySearchSchema,
|
|
589
|
-
opts?: { throwOnError?: boolean; debug?: boolean },
|
|
594
|
+
opts?: { preload?: boolean; throwOnError?: boolean; debug?: boolean },
|
|
590
595
|
): RouteMatch<TRouteTree>[] => {
|
|
591
596
|
let routeParams: Record<string, string> = {}
|
|
592
597
|
|
|
@@ -611,7 +616,7 @@ export class Router<
|
|
|
611
616
|
})
|
|
612
617
|
|
|
613
618
|
let routeCursor: AnyRoute =
|
|
614
|
-
foundRoute || (this.routesById as any)[
|
|
619
|
+
foundRoute || (this.routesById as any)[rootRouteId]
|
|
615
620
|
|
|
616
621
|
let matchedRoutes: AnyRoute[] = [routeCursor]
|
|
617
622
|
|
|
@@ -639,6 +644,23 @@ export class Router<
|
|
|
639
644
|
if (routeCursor) matchedRoutes.unshift(routeCursor)
|
|
640
645
|
}
|
|
641
646
|
|
|
647
|
+
const globalNotFoundRouteId = (() => {
|
|
648
|
+
if (!isGlobalNotFound) {
|
|
649
|
+
return undefined
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
if (this.options.notFoundMode !== 'root') {
|
|
653
|
+
for (let i = matchedRoutes.length - 1; i >= 0; i--) {
|
|
654
|
+
const route = matchedRoutes[i]!
|
|
655
|
+
if (route.children) {
|
|
656
|
+
return route.id
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
return rootRouteId
|
|
662
|
+
})()
|
|
663
|
+
|
|
642
664
|
// Existing matches are matches that are already loaded along with
|
|
643
665
|
// pending matches that are still loading
|
|
644
666
|
|
|
@@ -677,6 +699,7 @@ export class Router<
|
|
|
677
699
|
// which is used to uniquely identify the route match in state
|
|
678
700
|
|
|
679
701
|
const parentMatch = matches[index - 1]
|
|
702
|
+
const isLast = index === matchedRoutes.length - 1
|
|
680
703
|
|
|
681
704
|
const [preMatchSearch, searchError]: [Record<string, any>, any] = (() => {
|
|
682
705
|
// Validate the search params and stabilize them
|
|
@@ -737,7 +760,7 @@ export class Router<
|
|
|
737
760
|
// Waste not, want not. If we already have a match for this route,
|
|
738
761
|
// reuse it. This is important for layout routes, which might stick
|
|
739
762
|
// around between navigation actions that only change leaf routes.
|
|
740
|
-
|
|
763
|
+
let existingMatch = getRouteMatch(this.state, matchId)
|
|
741
764
|
|
|
742
765
|
const cause = this.state.matches.find((d) => d.id === matchId)
|
|
743
766
|
? 'stay'
|
|
@@ -747,10 +770,6 @@ export class Router<
|
|
|
747
770
|
? {
|
|
748
771
|
...existingMatch,
|
|
749
772
|
cause,
|
|
750
|
-
notFoundError:
|
|
751
|
-
isGlobalNotFound && route.id === rootRouteId
|
|
752
|
-
? { global: true }
|
|
753
|
-
: undefined,
|
|
754
773
|
params: routeParams,
|
|
755
774
|
}
|
|
756
775
|
: {
|
|
@@ -775,15 +794,16 @@ export class Router<
|
|
|
775
794
|
loaderDeps,
|
|
776
795
|
invalid: false,
|
|
777
796
|
preload: false,
|
|
778
|
-
notFoundError:
|
|
779
|
-
isGlobalNotFound && route.id === rootRouteId
|
|
780
|
-
? { global: true }
|
|
781
|
-
: undefined,
|
|
782
797
|
links: route.options.links?.(),
|
|
783
798
|
scripts: route.options.scripts?.(),
|
|
784
799
|
staticData: route.options.staticData || {},
|
|
785
800
|
}
|
|
786
801
|
|
|
802
|
+
if (!opts?.preload) {
|
|
803
|
+
// If we have a global not found, mark the right match as global not found
|
|
804
|
+
match.globalNotFound = globalNotFoundRouteId === route.id
|
|
805
|
+
}
|
|
806
|
+
|
|
787
807
|
// Regardless of whether we're reusing an existing match or creating
|
|
788
808
|
// a new one, we need to update the match's search params
|
|
789
809
|
match.search = replaceEqualDeep(match.search, preMatchSearch)
|
|
@@ -796,9 +816,7 @@ export class Router<
|
|
|
796
816
|
return matches as any
|
|
797
817
|
}
|
|
798
818
|
|
|
799
|
-
cancelMatch = (id: string) => {
|
|
800
|
-
getRouteMatch(this.state, id)?.abortController?.abort()
|
|
801
|
-
}
|
|
819
|
+
cancelMatch = (id: string) => {}
|
|
802
820
|
|
|
803
821
|
cancelMatches = () => {
|
|
804
822
|
this.state.pendingMatches?.forEach((match) => {
|
|
@@ -813,16 +831,23 @@ export class Router<
|
|
|
813
831
|
} = {},
|
|
814
832
|
matches?: AnyRouteMatch[],
|
|
815
833
|
): ParsedLocation => {
|
|
834
|
+
// if (dest.href) {
|
|
835
|
+
// return {
|
|
836
|
+
// pathname: dest.href,
|
|
837
|
+
// search: {},
|
|
838
|
+
// searchStr: '',
|
|
839
|
+
// state: {},
|
|
840
|
+
// hash: '',
|
|
841
|
+
// href: dest.href,
|
|
842
|
+
// unmaskOnReload: dest.unmaskOnReload,
|
|
843
|
+
// }
|
|
844
|
+
// }
|
|
845
|
+
|
|
816
846
|
const relevantMatches = this.state.pendingMatches || this.state.matches
|
|
817
847
|
const fromSearch =
|
|
818
848
|
relevantMatches[relevantMatches.length - 1]?.search ||
|
|
819
849
|
this.latestLocation.search
|
|
820
850
|
|
|
821
|
-
let pathname = this.resolvePathWithBase(
|
|
822
|
-
dest.from ?? this.latestLocation.pathname,
|
|
823
|
-
`${dest.to ?? ''}`,
|
|
824
|
-
)
|
|
825
|
-
|
|
826
851
|
const fromMatches = this.matchRoutes(
|
|
827
852
|
this.latestLocation.pathname,
|
|
828
853
|
fromSearch,
|
|
@@ -831,6 +856,15 @@ export class Router<
|
|
|
831
856
|
fromMatches?.find((e) => e.routeId === d.routeId),
|
|
832
857
|
)
|
|
833
858
|
|
|
859
|
+
const fromRoute = this.looseRoutesById[last(fromMatches)?.routeId]
|
|
860
|
+
|
|
861
|
+
let pathname = dest.to
|
|
862
|
+
? this.resolvePathWithBase(
|
|
863
|
+
dest.from ?? this.latestLocation.pathname,
|
|
864
|
+
`${dest.to}`,
|
|
865
|
+
)
|
|
866
|
+
: fromRoute?.fullPath
|
|
867
|
+
|
|
834
868
|
const prevParams = { ...last(fromMatches)?.params }
|
|
835
869
|
|
|
836
870
|
let nextParams =
|
|
@@ -1002,7 +1036,7 @@ export class Router<
|
|
|
1002
1036
|
|
|
1003
1037
|
// If the next urls are the same and we're not replacing,
|
|
1004
1038
|
// do nothing
|
|
1005
|
-
if (!isSameUrl
|
|
1039
|
+
if (!isSameUrl) {
|
|
1006
1040
|
let { maskedLocation, ...nextHistory } = next
|
|
1007
1041
|
|
|
1008
1042
|
if (maskedLocation) {
|
|
@@ -1097,18 +1131,19 @@ export class Router<
|
|
|
1097
1131
|
|
|
1098
1132
|
loadMatches = async ({
|
|
1099
1133
|
checkLatest,
|
|
1134
|
+
location,
|
|
1100
1135
|
matches,
|
|
1101
1136
|
preload,
|
|
1102
1137
|
}: {
|
|
1103
1138
|
checkLatest: () => Promise<void> | undefined
|
|
1139
|
+
location: ParsedLocation
|
|
1104
1140
|
matches: AnyRouteMatch[]
|
|
1105
1141
|
preload?: boolean
|
|
1106
1142
|
}): Promise<RouteMatch[]> => {
|
|
1107
1143
|
let latestPromise
|
|
1108
1144
|
let firstBadMatchIndex: number | undefined
|
|
1109
1145
|
|
|
1110
|
-
const updateMatch = (match: AnyRouteMatch) => {
|
|
1111
|
-
// const isPreload = this.state.cachedMatches.find((d) => d.id === match.id)
|
|
1146
|
+
const updateMatch = (match: AnyRouteMatch, opts?: { remove?: boolean }) => {
|
|
1112
1147
|
const isPending = this.state.pendingMatches?.find(
|
|
1113
1148
|
(d) => d.id === match.id,
|
|
1114
1149
|
)
|
|
@@ -1123,29 +1158,45 @@ export class Router<
|
|
|
1123
1158
|
|
|
1124
1159
|
this.__store.setState((s) => ({
|
|
1125
1160
|
...s,
|
|
1126
|
-
[matchesKey]:
|
|
1127
|
-
d.id
|
|
1128
|
-
|
|
1161
|
+
[matchesKey]: opts?.remove
|
|
1162
|
+
? s[matchesKey]?.filter((d) => d.id !== match.id)
|
|
1163
|
+
: s[matchesKey]?.map((d) => (d.id === match.id ? match : d)),
|
|
1129
1164
|
}))
|
|
1130
1165
|
}
|
|
1131
1166
|
|
|
1167
|
+
const handleMatchSpecialError = (match: AnyRouteMatch, err: any) => {
|
|
1168
|
+
match = {
|
|
1169
|
+
...match,
|
|
1170
|
+
status: isRedirect(err)
|
|
1171
|
+
? 'redirected'
|
|
1172
|
+
: isNotFound(err)
|
|
1173
|
+
? 'notFound'
|
|
1174
|
+
: 'error',
|
|
1175
|
+
isFetching: false,
|
|
1176
|
+
error: err,
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
updateMatch(match)
|
|
1180
|
+
|
|
1181
|
+
if (!err.routeId) {
|
|
1182
|
+
err.routeId = match.routeId
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
throw err
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1132
1188
|
// Check each match middleware to see if the route can be accessed
|
|
1133
1189
|
for (let [index, match] of matches.entries()) {
|
|
1134
1190
|
const parentMatch = matches[index - 1]
|
|
1135
1191
|
const route = this.looseRoutesById[match.routeId]!
|
|
1136
1192
|
const abortController = new AbortController()
|
|
1137
1193
|
|
|
1138
|
-
const
|
|
1194
|
+
const handleSerialError = (err: any, code: string) => {
|
|
1139
1195
|
err.routerCode = code
|
|
1140
1196
|
firstBadMatchIndex = firstBadMatchIndex ?? index
|
|
1141
1197
|
|
|
1142
|
-
if (isRedirect(err)) {
|
|
1143
|
-
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
if (isNotFound(err)) {
|
|
1147
|
-
err.routeId = match.routeId
|
|
1148
|
-
throw err
|
|
1198
|
+
if (isRedirect(err) || isNotFound(err)) {
|
|
1199
|
+
handleMatchSpecialError(match, err)
|
|
1149
1200
|
}
|
|
1150
1201
|
|
|
1151
1202
|
try {
|
|
@@ -1153,8 +1204,8 @@ export class Router<
|
|
|
1153
1204
|
} catch (errorHandlerErr) {
|
|
1154
1205
|
err = errorHandlerErr
|
|
1155
1206
|
|
|
1156
|
-
if (isRedirect(
|
|
1157
|
-
|
|
1207
|
+
if (isRedirect(err) || isNotFound(err)) {
|
|
1208
|
+
handleMatchSpecialError(match, errorHandlerErr)
|
|
1158
1209
|
}
|
|
1159
1210
|
}
|
|
1160
1211
|
|
|
@@ -1167,15 +1218,19 @@ export class Router<
|
|
|
1167
1218
|
}
|
|
1168
1219
|
}
|
|
1169
1220
|
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
}
|
|
1221
|
+
if (match.paramsError) {
|
|
1222
|
+
handleSerialError(match.paramsError, 'PARSE_PARAMS')
|
|
1223
|
+
}
|
|
1174
1224
|
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1225
|
+
if (match.searchError) {
|
|
1226
|
+
handleSerialError(match.searchError, 'VALIDATE_SEARCH')
|
|
1227
|
+
}
|
|
1178
1228
|
|
|
1229
|
+
// if (match.globalNotFound && !preload) {
|
|
1230
|
+
// handleSerialError(notFound({ _global: true }), 'NOT_FOUND')
|
|
1231
|
+
// }
|
|
1232
|
+
|
|
1233
|
+
try {
|
|
1179
1234
|
const parentContext = parentMatch?.context ?? this.options.context ?? {}
|
|
1180
1235
|
|
|
1181
1236
|
const pendingMs =
|
|
@@ -1192,16 +1247,15 @@ export class Router<
|
|
|
1192
1247
|
params: match.params,
|
|
1193
1248
|
preload: !!preload,
|
|
1194
1249
|
context: parentContext,
|
|
1195
|
-
location
|
|
1196
|
-
// TOOD: just expose state and router, etc
|
|
1250
|
+
location,
|
|
1197
1251
|
navigate: (opts) =>
|
|
1198
1252
|
this.navigate({ ...opts, from: match.pathname } as any),
|
|
1199
1253
|
buildLocation: this.buildLocation,
|
|
1200
1254
|
cause: preload ? 'preload' : match.cause,
|
|
1201
1255
|
})) ?? ({} as any)
|
|
1202
1256
|
|
|
1203
|
-
if (isRedirect(beforeLoadContext)) {
|
|
1204
|
-
|
|
1257
|
+
if (isRedirect(beforeLoadContext) || isNotFound(beforeLoadContext)) {
|
|
1258
|
+
handleSerialError(beforeLoadContext, 'BEFORE_LOAD')
|
|
1205
1259
|
}
|
|
1206
1260
|
|
|
1207
1261
|
const context = {
|
|
@@ -1217,7 +1271,7 @@ export class Router<
|
|
|
1217
1271
|
pendingPromise,
|
|
1218
1272
|
}
|
|
1219
1273
|
} catch (err) {
|
|
1220
|
-
|
|
1274
|
+
handleSerialError(err, 'BEFORE_LOAD')
|
|
1221
1275
|
break
|
|
1222
1276
|
}
|
|
1223
1277
|
}
|
|
@@ -1232,13 +1286,8 @@ export class Router<
|
|
|
1232
1286
|
const route = this.looseRoutesById[match.routeId]!
|
|
1233
1287
|
|
|
1234
1288
|
const handleError = (err: any) => {
|
|
1235
|
-
if (isRedirect(err)) {
|
|
1236
|
-
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
if (isNotFound(err)) {
|
|
1240
|
-
err.routeId = match.routeId
|
|
1241
|
-
throw err
|
|
1289
|
+
if (isRedirect(err) || isNotFound(err)) {
|
|
1290
|
+
handleMatchSpecialError(match, err)
|
|
1242
1291
|
}
|
|
1243
1292
|
}
|
|
1244
1293
|
|
|
@@ -1254,11 +1303,6 @@ export class Router<
|
|
|
1254
1303
|
route.options.pendingMs ?? this.options.defaultPendingMs
|
|
1255
1304
|
const pendingMinMs =
|
|
1256
1305
|
route.options.pendingMinMs ?? this.options.defaultPendingMinMs
|
|
1257
|
-
const shouldPending =
|
|
1258
|
-
!preload &&
|
|
1259
|
-
typeof pendingMs === 'number' &&
|
|
1260
|
-
(route.options.pendingComponent ??
|
|
1261
|
-
this.options.defaultPendingComponent)
|
|
1262
1306
|
|
|
1263
1307
|
const loaderContext: LoaderFnContext = {
|
|
1264
1308
|
params: match.params,
|
|
@@ -1267,72 +1311,69 @@ export class Router<
|
|
|
1267
1311
|
parentMatchPromise,
|
|
1268
1312
|
abortController: match.abortController,
|
|
1269
1313
|
context: match.context,
|
|
1270
|
-
location
|
|
1314
|
+
location,
|
|
1271
1315
|
navigate: (opts) =>
|
|
1272
1316
|
this.navigate({ ...opts, from: match.pathname } as any),
|
|
1273
1317
|
cause: preload ? 'preload' : match.cause,
|
|
1318
|
+
route,
|
|
1274
1319
|
}
|
|
1275
1320
|
|
|
1276
1321
|
const fetch = async () => {
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1322
|
+
try {
|
|
1323
|
+
if (match.isFetching) {
|
|
1324
|
+
loadPromise = getRouteMatch(this.state, match.id)?.loadPromise
|
|
1325
|
+
} else {
|
|
1326
|
+
// If the user doesn't want the route to reload, just
|
|
1327
|
+
// resolve with the existing loader data
|
|
1328
|
+
|
|
1329
|
+
// if (match.fetchCount && match.status === 'success') {
|
|
1330
|
+
// resolve()
|
|
1331
|
+
// }
|
|
1332
|
+
|
|
1333
|
+
// Otherwise, load the route
|
|
1334
|
+
matches[index] = match = {
|
|
1335
|
+
...match,
|
|
1336
|
+
isFetching: true,
|
|
1337
|
+
fetchCount: match.fetchCount + 1,
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
const lazyPromise =
|
|
1341
|
+
route.lazyFn?.().then((lazyRoute) => {
|
|
1342
|
+
Object.assign(route.options, lazyRoute.options)
|
|
1343
|
+
}) || Promise.resolve()
|
|
1344
|
+
|
|
1345
|
+
// If for some reason lazy resolves more lazy components...
|
|
1346
|
+
// We'll wait for that before pre attempt to preload any
|
|
1347
|
+
// components themselves.
|
|
1348
|
+
const componentsPromise = lazyPromise.then(() =>
|
|
1349
|
+
Promise.all(
|
|
1350
|
+
componentTypes.map(async (type) => {
|
|
1351
|
+
const component = route.options[type]
|
|
1352
|
+
|
|
1353
|
+
if ((component as any)?.preload) {
|
|
1354
|
+
await (component as any).preload()
|
|
1355
|
+
}
|
|
1356
|
+
}),
|
|
1357
|
+
),
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
// Kick off the loader!
|
|
1361
|
+
const loaderPromise = route.options.loader?.(loaderContext)
|
|
1362
|
+
|
|
1363
|
+
loadPromise = Promise.all([
|
|
1364
|
+
componentsPromise,
|
|
1365
|
+
loaderPromise,
|
|
1366
|
+
lazyPromise,
|
|
1367
|
+
]).then((d) => d[1])
|
|
1285
1368
|
}
|
|
1286
1369
|
|
|
1287
|
-
// Otherwise, load the route
|
|
1288
1370
|
matches[index] = match = {
|
|
1289
1371
|
...match,
|
|
1290
|
-
|
|
1291
|
-
fetchCount: match.fetchCount + 1,
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
const lazyPromise =
|
|
1295
|
-
route.lazyFn?.().then((lazyRoute) => {
|
|
1296
|
-
Object.assign(route.options, lazyRoute.options)
|
|
1297
|
-
}) || Promise.resolve()
|
|
1298
|
-
|
|
1299
|
-
// If for some reason lazy resolves more lazy components...
|
|
1300
|
-
// We'll wait for that before pre attempt to preload any
|
|
1301
|
-
// components themselves.
|
|
1302
|
-
const componentsPromise = lazyPromise.then(() =>
|
|
1303
|
-
Promise.all(
|
|
1304
|
-
componentTypes.map(async (type) => {
|
|
1305
|
-
const component = route.options[type]
|
|
1306
|
-
|
|
1307
|
-
if ((component as any)?.preload) {
|
|
1308
|
-
await (component as any).preload()
|
|
1309
|
-
}
|
|
1310
|
-
}),
|
|
1311
|
-
),
|
|
1312
|
-
)
|
|
1313
|
-
|
|
1314
|
-
// wrap loader into an async function to be able to catch synchronous exceptions
|
|
1315
|
-
async function loader() {
|
|
1316
|
-
return await route.options.loader?.(loaderContext)
|
|
1372
|
+
loadPromise,
|
|
1317
1373
|
}
|
|
1318
|
-
// Kick off the loader!
|
|
1319
|
-
const loaderPromise = loader()
|
|
1320
|
-
|
|
1321
|
-
loadPromise = Promise.all([
|
|
1322
|
-
componentsPromise,
|
|
1323
|
-
loaderPromise,
|
|
1324
|
-
lazyPromise,
|
|
1325
|
-
]).then((d) => d[1])
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
matches[index] = match = {
|
|
1329
|
-
...match,
|
|
1330
|
-
loadPromise,
|
|
1331
|
-
}
|
|
1332
1374
|
|
|
1333
|
-
|
|
1375
|
+
updateMatch(match)
|
|
1334
1376
|
|
|
1335
|
-
try {
|
|
1336
1377
|
const loaderData = await loadPromise
|
|
1337
1378
|
if ((latestPromise = checkLatest())) return await latestPromise
|
|
1338
1379
|
|
|
@@ -1367,6 +1408,7 @@ export class Router<
|
|
|
1367
1408
|
}
|
|
1368
1409
|
} catch (error) {
|
|
1369
1410
|
if ((latestPromise = checkLatest())) return await latestPromise
|
|
1411
|
+
|
|
1370
1412
|
handleError(error)
|
|
1371
1413
|
|
|
1372
1414
|
try {
|
|
@@ -1414,10 +1456,44 @@ export class Router<
|
|
|
1414
1456
|
!!preload && !this.state.matches.find((d) => d.id === match.id),
|
|
1415
1457
|
}
|
|
1416
1458
|
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1459
|
+
// If the route is successful and still fresh, just resolve
|
|
1460
|
+
if (
|
|
1461
|
+
match.status === 'success' &&
|
|
1462
|
+
(match.invalid || (shouldReload ?? age > staleAge))
|
|
1463
|
+
) {
|
|
1464
|
+
;(async () => {
|
|
1465
|
+
try {
|
|
1466
|
+
await fetch()
|
|
1467
|
+
} catch (err) {
|
|
1468
|
+
console.info('Background Fetching Error', err)
|
|
1469
|
+
|
|
1470
|
+
if (isRedirect(err)) {
|
|
1471
|
+
const isActive = (
|
|
1472
|
+
this.state.pendingMatches || this.state.matches
|
|
1473
|
+
).find((d) => d.id === match.id)
|
|
1474
|
+
|
|
1475
|
+
// Redirects should not be persisted
|
|
1476
|
+
handleError(err)
|
|
1477
|
+
|
|
1478
|
+
// If the route is still active, redirect
|
|
1479
|
+
if (isActive) {
|
|
1480
|
+
this.handleRedirect(err)
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
})()
|
|
1485
|
+
|
|
1486
|
+
return resolve()
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const shouldPending =
|
|
1490
|
+
!preload &&
|
|
1491
|
+
typeof pendingMs === 'number' &&
|
|
1492
|
+
(route.options.pendingComponent ??
|
|
1493
|
+
this.options.defaultPendingComponent)
|
|
1494
|
+
|
|
1495
|
+
if (match.status !== 'success') {
|
|
1496
|
+
try {
|
|
1421
1497
|
if (shouldPending) {
|
|
1422
1498
|
match.pendingPromise?.then(async () => {
|
|
1423
1499
|
if ((latestPromise = checkLatest())) return latestPromise
|
|
@@ -1433,14 +1509,10 @@ export class Router<
|
|
|
1433
1509
|
})
|
|
1434
1510
|
}
|
|
1435
1511
|
|
|
1436
|
-
// Critical Fetching, we need to await
|
|
1437
1512
|
await fetch()
|
|
1438
|
-
}
|
|
1439
|
-
|
|
1440
|
-
fetch()
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
reject(err)
|
|
1441
1515
|
}
|
|
1442
|
-
} catch (err) {
|
|
1443
|
-
reject(err)
|
|
1444
1516
|
}
|
|
1445
1517
|
|
|
1446
1518
|
resolve()
|
|
@@ -1511,18 +1583,28 @@ export class Router<
|
|
|
1511
1583
|
})
|
|
1512
1584
|
|
|
1513
1585
|
try {
|
|
1586
|
+
let redirected: AnyRedirect
|
|
1587
|
+
let notFound: NotFoundError
|
|
1588
|
+
|
|
1514
1589
|
try {
|
|
1515
1590
|
// Load the matches
|
|
1516
1591
|
await this.loadMatches({
|
|
1517
1592
|
matches: pendingMatches,
|
|
1593
|
+
location: next,
|
|
1518
1594
|
checkLatest: () => this.checkLatest(promise),
|
|
1519
1595
|
})
|
|
1520
1596
|
} catch (err) {
|
|
1521
1597
|
if (isRedirect(err)) {
|
|
1598
|
+
redirected = err
|
|
1522
1599
|
this.handleRedirect(err)
|
|
1523
1600
|
} else if (isNotFound(err)) {
|
|
1601
|
+
notFound = err
|
|
1524
1602
|
this.handleNotFound(pendingMatches, err)
|
|
1525
1603
|
}
|
|
1604
|
+
|
|
1605
|
+
// Swallow all other errors that happen inside
|
|
1606
|
+
// of loadMatches. These errors will be handled
|
|
1607
|
+
// as state on each match.
|
|
1526
1608
|
}
|
|
1527
1609
|
|
|
1528
1610
|
// Only apply the latest transition
|
|
@@ -1552,6 +1634,12 @@ export class Router<
|
|
|
1552
1634
|
...s.cachedMatches,
|
|
1553
1635
|
...exitingMatches.filter((d) => d.status !== 'error'),
|
|
1554
1636
|
],
|
|
1637
|
+
statusCode:
|
|
1638
|
+
redirected?.code || notFound
|
|
1639
|
+
? 404
|
|
1640
|
+
: s.matches.some((d) => d.status === 'error')
|
|
1641
|
+
? 500
|
|
1642
|
+
: 200,
|
|
1555
1643
|
}))
|
|
1556
1644
|
this.cleanCache()
|
|
1557
1645
|
})
|
|
@@ -1583,6 +1671,8 @@ export class Router<
|
|
|
1583
1671
|
return latestPromise
|
|
1584
1672
|
}
|
|
1585
1673
|
|
|
1674
|
+
console.log('Load Error', err)
|
|
1675
|
+
|
|
1586
1676
|
reject(err)
|
|
1587
1677
|
}
|
|
1588
1678
|
})
|
|
@@ -1596,12 +1686,9 @@ export class Router<
|
|
|
1596
1686
|
if (!err.href) {
|
|
1597
1687
|
err.href = this.buildLocation(err as any).href
|
|
1598
1688
|
}
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
throw err
|
|
1689
|
+
if (!isServer) {
|
|
1690
|
+
this.navigate({ ...(err as any), replace: true })
|
|
1602
1691
|
}
|
|
1603
|
-
|
|
1604
|
-
this.navigate(err as any)
|
|
1605
1692
|
}
|
|
1606
1693
|
|
|
1607
1694
|
cleanCache = () => {
|
|
@@ -1630,13 +1717,19 @@ export class Router<
|
|
|
1630
1717
|
})
|
|
1631
1718
|
}
|
|
1632
1719
|
|
|
1633
|
-
preloadRoute = async
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1720
|
+
preloadRoute = async <
|
|
1721
|
+
TFrom extends RoutePaths<TRouteTree> | string = string,
|
|
1722
|
+
TTo extends string = '',
|
|
1723
|
+
TMaskFrom extends RoutePaths<TRouteTree> | string = TFrom,
|
|
1724
|
+
TMaskTo extends string = '',
|
|
1725
|
+
>(
|
|
1726
|
+
opts: NavigateOptions<TRouteTree, TFrom, TTo, TMaskFrom, TMaskTo>,
|
|
1727
|
+
): Promise<AnyRouteMatch[] | undefined> => {
|
|
1728
|
+
let next = this.buildLocation(opts as any)
|
|
1637
1729
|
|
|
1638
1730
|
let matches = this.matchRoutes(next.pathname, next.search, {
|
|
1639
1731
|
throwOnError: true,
|
|
1732
|
+
preload: true,
|
|
1640
1733
|
})
|
|
1641
1734
|
|
|
1642
1735
|
const loadedMatchIds = Object.fromEntries(
|
|
@@ -1661,17 +1754,18 @@ export class Router<
|
|
|
1661
1754
|
try {
|
|
1662
1755
|
matches = await this.loadMatches({
|
|
1663
1756
|
matches,
|
|
1757
|
+
location: next,
|
|
1664
1758
|
preload: true,
|
|
1665
1759
|
checkLatest: () => undefined,
|
|
1666
1760
|
})
|
|
1667
1761
|
|
|
1668
1762
|
return matches
|
|
1669
1763
|
} catch (err) {
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
console.error(err)
|
|
1764
|
+
if (isRedirect(err)) {
|
|
1765
|
+
return await this.preloadRoute(err as any)
|
|
1673
1766
|
}
|
|
1674
|
-
|
|
1767
|
+
// Preload errors are not fatal, but we should still log them
|
|
1768
|
+
console.error(err)
|
|
1675
1769
|
return undefined
|
|
1676
1770
|
}
|
|
1677
1771
|
}
|
|
@@ -1801,15 +1895,7 @@ export class Router<
|
|
|
1801
1895
|
return {
|
|
1802
1896
|
state: {
|
|
1803
1897
|
dehydratedMatches: this.state.matches.map((d) => ({
|
|
1804
|
-
...pick(d, [
|
|
1805
|
-
'id',
|
|
1806
|
-
'status',
|
|
1807
|
-
'updatedAt',
|
|
1808
|
-
'loaderData',
|
|
1809
|
-
// Not-founds that occur during SSR don't require the client to load data before
|
|
1810
|
-
// triggering in order to prevent the flicker of the loading component
|
|
1811
|
-
'notFoundError',
|
|
1812
|
-
]),
|
|
1898
|
+
...pick(d, ['id', 'status', 'updatedAt', 'loaderData']),
|
|
1813
1899
|
// If an error occurs server-side during SSRing,
|
|
1814
1900
|
// send a small subset of the error to the client
|
|
1815
1901
|
error: d.error
|
|
@@ -1879,43 +1965,48 @@ export class Router<
|
|
|
1879
1965
|
})
|
|
1880
1966
|
}
|
|
1881
1967
|
|
|
1882
|
-
// Finds a match that has a notFoundComponent
|
|
1883
1968
|
handleNotFound = (matches: AnyRouteMatch[], err: NotFoundError) => {
|
|
1884
1969
|
const matchesByRouteId = Object.fromEntries(
|
|
1885
1970
|
matches.map((match) => [match.routeId, match]),
|
|
1886
1971
|
) as Record<string, AnyRouteMatch>
|
|
1887
1972
|
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1973
|
+
// Start at the route that errored or default to the root route
|
|
1974
|
+
let routeCursor =
|
|
1975
|
+
(err.global
|
|
1976
|
+
? this.looseRoutesById[rootRouteId]
|
|
1977
|
+
: this.looseRoutesById[err.routeId]) ||
|
|
1978
|
+
this.looseRoutesById[rootRouteId]!
|
|
1979
|
+
|
|
1980
|
+
// Go up the tree until we find a route with a notFoundComponent or we hit the root
|
|
1981
|
+
while (
|
|
1982
|
+
!routeCursor.options.notFoundComponent &&
|
|
1983
|
+
!this.options.defaultNotFoundComponent &&
|
|
1984
|
+
routeCursor.id !== rootRouteId
|
|
1985
|
+
) {
|
|
1986
|
+
routeCursor = routeCursor?.parentRoute
|
|
1901
1987
|
|
|
1902
|
-
|
|
1903
|
-
|
|
1988
|
+
invariant(
|
|
1989
|
+
routeCursor,
|
|
1990
|
+
'Found invalid route tree while trying to find not-found handler.',
|
|
1991
|
+
)
|
|
1992
|
+
}
|
|
1904
1993
|
|
|
1905
|
-
|
|
1906
|
-
invariant(match, 'Could not find match for route: ' + currentRoute.id)
|
|
1907
|
-
match.notFoundError = err
|
|
1994
|
+
let match = matchesByRouteId[routeCursor.id]
|
|
1908
1995
|
|
|
1909
|
-
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1996
|
+
invariant(match, 'Could not find match for route: ' + routeCursor.id)
|
|
1912
1997
|
|
|
1913
|
-
//
|
|
1914
|
-
|
|
1998
|
+
// Assign the error to the match
|
|
1999
|
+
Object.assign(match, {
|
|
2000
|
+
status: 'notFound',
|
|
2001
|
+
error: err,
|
|
2002
|
+
isFetching: false,
|
|
2003
|
+
} as AnyRouteMatch)
|
|
1915
2004
|
}
|
|
1916
2005
|
|
|
1917
2006
|
hasNotFoundMatch = () => {
|
|
1918
|
-
return this.__store.state.matches.some(
|
|
2007
|
+
return this.__store.state.matches.some(
|
|
2008
|
+
(d) => d.status === 'notFound' || d.globalNotFound,
|
|
2009
|
+
)
|
|
1919
2010
|
}
|
|
1920
2011
|
|
|
1921
2012
|
// resolveMatchPromise = (matchId: string, key: string, value: any) => {
|
|
@@ -1957,6 +2048,7 @@ export function getInitialRouterState(
|
|
|
1957
2048
|
pendingMatches: [],
|
|
1958
2049
|
cachedMatches: [],
|
|
1959
2050
|
lastUpdated: 0,
|
|
2051
|
+
statusCode: 200,
|
|
1960
2052
|
}
|
|
1961
2053
|
}
|
|
1962
2054
|
|