@tanstack/router-core 1.120.7 → 1.121.0-alpha.11
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/fileRoute.d.cts +6 -2
- package/dist/cjs/index.cjs +3 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +7 -7
- package/dist/cjs/link.cjs.map +1 -1
- package/dist/cjs/link.d.cts +18 -1
- package/dist/cjs/path.cjs +130 -16
- package/dist/cjs/path.cjs.map +1 -1
- package/dist/cjs/path.d.cts +17 -0
- package/dist/cjs/redirect.cjs +17 -7
- package/dist/cjs/redirect.cjs.map +1 -1
- package/dist/cjs/redirect.d.cts +13 -7
- package/dist/cjs/route.cjs +12 -1
- package/dist/cjs/route.cjs.map +1 -1
- package/dist/cjs/route.d.cts +18 -27
- package/dist/cjs/router.cjs +395 -335
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +48 -8
- package/dist/cjs/typePrimitives.d.cts +2 -2
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/cjs/utils.d.cts +3 -0
- package/dist/esm/fileRoute.d.ts +6 -2
- package/dist/esm/index.d.ts +7 -7
- package/dist/esm/index.js +5 -2
- package/dist/esm/link.d.ts +18 -1
- package/dist/esm/link.js.map +1 -1
- package/dist/esm/path.d.ts +17 -0
- package/dist/esm/path.js +130 -16
- package/dist/esm/path.js.map +1 -1
- package/dist/esm/redirect.d.ts +13 -7
- package/dist/esm/redirect.js +17 -7
- package/dist/esm/redirect.js.map +1 -1
- package/dist/esm/route.d.ts +18 -27
- package/dist/esm/route.js +12 -1
- package/dist/esm/route.js.map +1 -1
- package/dist/esm/router.d.ts +48 -8
- package/dist/esm/router.js +398 -338
- package/dist/esm/router.js.map +1 -1
- package/dist/esm/typePrimitives.d.ts +2 -2
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +2 -2
- package/src/fileRoute.ts +90 -1
- package/src/index.ts +14 -8
- package/src/link.ts +97 -11
- package/src/path.ts +181 -16
- package/src/redirect.ts +39 -16
- package/src/route.ts +91 -64
- package/src/router.ts +569 -434
- package/src/typePrimitives.ts +2 -2
- package/src/utils.ts +15 -0
package/src/router.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { isNotFound } from './not-found'
|
|
|
28
28
|
import { setupScrollRestoration } from './scroll-restoration'
|
|
29
29
|
import { defaultParseSearch, defaultStringifySearch } from './searchParams'
|
|
30
30
|
import { rootRouteId } from './root'
|
|
31
|
-
import { isRedirect
|
|
31
|
+
import { isRedirect } from './redirect'
|
|
32
32
|
import type { SearchParser, SearchSerializer } from './searchParams'
|
|
33
33
|
import type { AnyRedirect, ResolvedRedirect } from './redirect'
|
|
34
34
|
import type {
|
|
@@ -165,6 +165,14 @@ export interface RouterOptions<
|
|
|
165
165
|
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-delay)
|
|
166
166
|
*/
|
|
167
167
|
defaultPreloadDelay?: number
|
|
168
|
+
/**
|
|
169
|
+
* The default `preloadIntentProximity` a route should use if no preloadIntentProximity is provided.
|
|
170
|
+
*
|
|
171
|
+
* @default 0
|
|
172
|
+
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#defaultpreloadintentproximity-property)
|
|
173
|
+
* @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/preloading#preload-intent-proximity)
|
|
174
|
+
*/
|
|
175
|
+
defaultPreloadIntentProximity?: number
|
|
168
176
|
/**
|
|
169
177
|
* The default `pendingMs` a route should use if no pendingMs is provided.
|
|
170
178
|
*
|
|
@@ -407,7 +415,7 @@ export interface RouterState<
|
|
|
407
415
|
location: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
408
416
|
resolvedLocation?: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
409
417
|
statusCode: number
|
|
410
|
-
redirect?:
|
|
418
|
+
redirect?: AnyRedirect
|
|
411
419
|
}
|
|
412
420
|
|
|
413
421
|
export interface BuildNextOptions {
|
|
@@ -425,8 +433,9 @@ export interface BuildNextOptions {
|
|
|
425
433
|
unmaskOnReload?: boolean
|
|
426
434
|
}
|
|
427
435
|
from?: string
|
|
428
|
-
_fromLocation?: ParsedLocation
|
|
429
436
|
href?: string
|
|
437
|
+
_fromLocation?: ParsedLocation
|
|
438
|
+
unsafeRelative?: 'path'
|
|
430
439
|
}
|
|
431
440
|
|
|
432
441
|
type NavigationEventInfo = {
|
|
@@ -513,11 +522,6 @@ export interface RouterErrorSerializer<TSerializedError> {
|
|
|
513
522
|
deserialize: (err: TSerializedError) => unknown
|
|
514
523
|
}
|
|
515
524
|
|
|
516
|
-
export interface MatchedRoutesResult {
|
|
517
|
-
matchedRoutes: Array<AnyRoute>
|
|
518
|
-
routeParams: Record<string, string>
|
|
519
|
-
}
|
|
520
|
-
|
|
521
525
|
export type PreloadRouteFn<
|
|
522
526
|
TRouteTree extends AnyRoute,
|
|
523
527
|
TTrailingSlashOption extends TrailingSlashOption,
|
|
@@ -593,8 +597,8 @@ export type ParseLocationFn<TRouteTree extends AnyRoute> = (
|
|
|
593
597
|
) => ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
594
598
|
|
|
595
599
|
export type GetMatchRoutesFn = (
|
|
596
|
-
|
|
597
|
-
|
|
600
|
+
pathname: string,
|
|
601
|
+
routePathname: string | undefined,
|
|
598
602
|
) => {
|
|
599
603
|
matchedRoutes: Array<AnyRoute>
|
|
600
604
|
routeParams: Record<string, string>
|
|
@@ -836,6 +840,8 @@ export class RouterCore<
|
|
|
836
840
|
// router can be used in a non-react environment if necessary
|
|
837
841
|
startTransition: StartTransitionFn = (fn) => fn()
|
|
838
842
|
|
|
843
|
+
isShell = false
|
|
844
|
+
|
|
839
845
|
update: UpdateFn<
|
|
840
846
|
TRouteTree,
|
|
841
847
|
TTrailingSlashOption,
|
|
@@ -882,7 +888,6 @@ export class RouterCore<
|
|
|
882
888
|
}
|
|
883
889
|
|
|
884
890
|
if (
|
|
885
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
886
891
|
!this.history ||
|
|
887
892
|
(this.options.history && this.options.history !== this.history)
|
|
888
893
|
) {
|
|
@@ -901,7 +906,6 @@ export class RouterCore<
|
|
|
901
906
|
this.buildRouteTree()
|
|
902
907
|
}
|
|
903
908
|
|
|
904
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
905
909
|
if (!this.__store) {
|
|
906
910
|
this.__store = new Store(getInitialRouterState(this.latestLocation), {
|
|
907
911
|
onUpdate: () => {
|
|
@@ -920,13 +924,16 @@ export class RouterCore<
|
|
|
920
924
|
if (
|
|
921
925
|
typeof window !== 'undefined' &&
|
|
922
926
|
'CSS' in window &&
|
|
923
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
924
927
|
typeof window.CSS?.supports === 'function'
|
|
925
928
|
) {
|
|
926
929
|
this.isViewTransitionTypesSupported = window.CSS.supports(
|
|
927
930
|
'selector(:active-view-transition-type(a)',
|
|
928
931
|
)
|
|
929
932
|
}
|
|
933
|
+
|
|
934
|
+
if ((this.latestLocation.search as any).__TSS_SHELL) {
|
|
935
|
+
this.isShell = true
|
|
936
|
+
}
|
|
930
937
|
}
|
|
931
938
|
|
|
932
939
|
get state() {
|
|
@@ -934,124 +941,29 @@ export class RouterCore<
|
|
|
934
941
|
}
|
|
935
942
|
|
|
936
943
|
buildRouteTree = () => {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
if (notFoundRoute) {
|
|
942
|
-
notFoundRoute.init({
|
|
943
|
-
originalIndex: 99999999999,
|
|
944
|
-
defaultSsr: this.options.defaultSsr,
|
|
945
|
-
})
|
|
946
|
-
;(this.routesById as any)[notFoundRoute.id] = notFoundRoute
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
const recurseRoutes = (childRoutes: Array<AnyRoute>) => {
|
|
950
|
-
childRoutes.forEach((childRoute, i) => {
|
|
951
|
-
childRoute.init({
|
|
944
|
+
const { routesById, routesByPath, flatRoutes } = processRouteTree({
|
|
945
|
+
routeTree: this.routeTree,
|
|
946
|
+
initRoute: (route, i) => {
|
|
947
|
+
route.init({
|
|
952
948
|
originalIndex: i,
|
|
953
949
|
defaultSsr: this.options.defaultSsr,
|
|
954
950
|
})
|
|
955
|
-
|
|
956
|
-
const existingRoute = (this.routesById as any)[childRoute.id]
|
|
957
|
-
|
|
958
|
-
invariant(
|
|
959
|
-
!existingRoute,
|
|
960
|
-
`Duplicate routes found with id: ${String(childRoute.id)}`,
|
|
961
|
-
)
|
|
962
|
-
;(this.routesById as any)[childRoute.id] = childRoute
|
|
963
|
-
|
|
964
|
-
if (!childRoute.isRoot && childRoute.path) {
|
|
965
|
-
const trimmedFullPath = trimPathRight(childRoute.fullPath)
|
|
966
|
-
if (
|
|
967
|
-
!(this.routesByPath as any)[trimmedFullPath] ||
|
|
968
|
-
childRoute.fullPath.endsWith('/')
|
|
969
|
-
) {
|
|
970
|
-
;(this.routesByPath as any)[trimmedFullPath] = childRoute
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
const children = childRoute.children
|
|
975
|
-
|
|
976
|
-
if (children?.length) {
|
|
977
|
-
recurseRoutes(children)
|
|
978
|
-
}
|
|
979
|
-
})
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
recurseRoutes([this.routeTree])
|
|
983
|
-
|
|
984
|
-
const scoredRoutes: Array<{
|
|
985
|
-
child: AnyRoute
|
|
986
|
-
trimmed: string
|
|
987
|
-
parsed: ReturnType<typeof parsePathname>
|
|
988
|
-
index: number
|
|
989
|
-
scores: Array<number>
|
|
990
|
-
}> = []
|
|
991
|
-
|
|
992
|
-
const routes: Array<AnyRoute> = Object.values(this.routesById)
|
|
993
|
-
|
|
994
|
-
routes.forEach((d, i) => {
|
|
995
|
-
if (d.isRoot || !d.path) {
|
|
996
|
-
return
|
|
997
|
-
}
|
|
998
|
-
|
|
999
|
-
const trimmed = trimPathLeft(d.fullPath)
|
|
1000
|
-
const parsed = parsePathname(trimmed)
|
|
1001
|
-
|
|
1002
|
-
while (parsed.length > 1 && parsed[0]?.value === '/') {
|
|
1003
|
-
parsed.shift()
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
const scores = parsed.map((segment) => {
|
|
1007
|
-
if (segment.value === '/') {
|
|
1008
|
-
return 0.75
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
if (segment.type === 'param') {
|
|
1012
|
-
return 0.5
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (segment.type === 'wildcard') {
|
|
1016
|
-
return 0.25
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
return 1
|
|
1020
|
-
})
|
|
1021
|
-
|
|
1022
|
-
scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
|
|
951
|
+
},
|
|
1023
952
|
})
|
|
1024
953
|
|
|
1025
|
-
this.
|
|
1026
|
-
|
|
1027
|
-
|
|
954
|
+
this.routesById = routesById as RoutesById<TRouteTree>
|
|
955
|
+
this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
|
|
956
|
+
this.flatRoutes = flatRoutes as Array<AnyRoute>
|
|
1028
957
|
|
|
1029
|
-
|
|
1030
|
-
for (let i = 0; i < minLength; i++) {
|
|
1031
|
-
if (a.scores[i] !== b.scores[i]) {
|
|
1032
|
-
return b.scores[i]! - a.scores[i]!
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Sort by length of score
|
|
1037
|
-
if (a.scores.length !== b.scores.length) {
|
|
1038
|
-
return b.scores.length - a.scores.length
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
// Sort by min available parsed value
|
|
1042
|
-
for (let i = 0; i < minLength; i++) {
|
|
1043
|
-
if (a.parsed[i]!.value !== b.parsed[i]!.value) {
|
|
1044
|
-
return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
958
|
+
const notFoundRoute = this.options.notFoundRoute
|
|
1047
959
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
d.child.rank = i
|
|
1053
|
-
return d.child
|
|
960
|
+
if (notFoundRoute) {
|
|
961
|
+
notFoundRoute.init({
|
|
962
|
+
originalIndex: 99999999999,
|
|
963
|
+
defaultSsr: this.options.defaultSsr,
|
|
1054
964
|
})
|
|
965
|
+
this.routesById[notFoundRoute.id] = notFoundRoute
|
|
966
|
+
}
|
|
1055
967
|
}
|
|
1056
968
|
|
|
1057
969
|
subscribe: SubscribeFn = (eventType, fn) => {
|
|
@@ -1155,9 +1067,9 @@ export class RouterCore<
|
|
|
1155
1067
|
} as ParsedLocation,
|
|
1156
1068
|
opts,
|
|
1157
1069
|
)
|
|
1158
|
-
} else {
|
|
1159
|
-
return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
|
|
1160
1070
|
}
|
|
1071
|
+
|
|
1072
|
+
return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
|
|
1161
1073
|
}
|
|
1162
1074
|
|
|
1163
1075
|
private matchRoutesInternal(
|
|
@@ -1165,8 +1077,8 @@ export class RouterCore<
|
|
|
1165
1077
|
opts?: MatchRoutesOpts,
|
|
1166
1078
|
): Array<AnyRouteMatch> {
|
|
1167
1079
|
const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
|
|
1168
|
-
next,
|
|
1169
|
-
opts?.dest,
|
|
1080
|
+
next.pathname,
|
|
1081
|
+
opts?.dest?.to as string,
|
|
1170
1082
|
)
|
|
1171
1083
|
let isGlobalNotFound = false
|
|
1172
1084
|
|
|
@@ -1418,7 +1330,8 @@ export class RouterCore<
|
|
|
1418
1330
|
const route = this.looseRoutesById[match.routeId]!
|
|
1419
1331
|
const existingMatch = this.getMatch(match.id)
|
|
1420
1332
|
|
|
1421
|
-
// only execute `context` if we are not
|
|
1333
|
+
// only execute `context` if we are not calling from router.buildLocation
|
|
1334
|
+
|
|
1422
1335
|
if (!existingMatch && opts?._buildLocation !== true) {
|
|
1423
1336
|
const parentMatch = matches[index - 1]
|
|
1424
1337
|
const parentContext = getParentContext(parentMatch)
|
|
@@ -1452,47 +1365,19 @@ export class RouterCore<
|
|
|
1452
1365
|
return matches
|
|
1453
1366
|
}
|
|
1454
1367
|
|
|
1455
|
-
getMatchedRoutes: GetMatchRoutesFn = (
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
let foundRoute: AnyRoute | undefined =
|
|
1469
|
-
dest?.to !== undefined ? this.routesByPath[dest.to!] : undefined
|
|
1470
|
-
if (foundRoute) {
|
|
1471
|
-
routeParams = getMatchedParams(foundRoute)!
|
|
1472
|
-
} else {
|
|
1473
|
-
foundRoute = this.flatRoutes.find((route) => {
|
|
1474
|
-
const matchedParams = getMatchedParams(route)
|
|
1475
|
-
|
|
1476
|
-
if (matchedParams) {
|
|
1477
|
-
routeParams = matchedParams
|
|
1478
|
-
return true
|
|
1479
|
-
}
|
|
1480
|
-
|
|
1481
|
-
return false
|
|
1482
|
-
})
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
let routeCursor: AnyRoute =
|
|
1486
|
-
foundRoute || (this.routesById as any)[rootRouteId]
|
|
1487
|
-
|
|
1488
|
-
const matchedRoutes: Array<AnyRoute> = [routeCursor]
|
|
1489
|
-
|
|
1490
|
-
while (routeCursor.parentRoute) {
|
|
1491
|
-
routeCursor = routeCursor.parentRoute
|
|
1492
|
-
matchedRoutes.unshift(routeCursor)
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
return { matchedRoutes, routeParams, foundRoute }
|
|
1368
|
+
getMatchedRoutes: GetMatchRoutesFn = (
|
|
1369
|
+
pathname: string,
|
|
1370
|
+
routePathname: string | undefined,
|
|
1371
|
+
) => {
|
|
1372
|
+
return getMatchedRoutes({
|
|
1373
|
+
pathname,
|
|
1374
|
+
routePathname,
|
|
1375
|
+
basepath: this.basepath,
|
|
1376
|
+
caseSensitive: this.options.caseSensitive,
|
|
1377
|
+
routesByPath: this.routesByPath,
|
|
1378
|
+
routesById: this.routesById,
|
|
1379
|
+
flatRoutes: this.flatRoutes,
|
|
1380
|
+
})
|
|
1496
1381
|
}
|
|
1497
1382
|
|
|
1498
1383
|
cancelMatch = (id: string) => {
|
|
@@ -1515,75 +1400,67 @@ export class RouterCore<
|
|
|
1515
1400
|
dest: BuildNextOptions & {
|
|
1516
1401
|
unmaskOnReload?: boolean
|
|
1517
1402
|
} = {},
|
|
1518
|
-
matchedRoutesResult?: MatchedRoutesResult,
|
|
1519
1403
|
): ParsedLocation => {
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
: this.state.matches
|
|
1523
|
-
|
|
1524
|
-
const fromMatch =
|
|
1525
|
-
dest.from != null
|
|
1526
|
-
? fromMatches.find((d) =>
|
|
1527
|
-
matchPathname(this.basepath, trimPathRight(d.pathname), {
|
|
1528
|
-
to: dest.from,
|
|
1529
|
-
caseSensitive: false,
|
|
1530
|
-
fuzzy: false,
|
|
1531
|
-
}),
|
|
1532
|
-
)
|
|
1533
|
-
: undefined
|
|
1534
|
-
|
|
1535
|
-
const fromPath = fromMatch?.pathname || this.latestLocation.pathname
|
|
1404
|
+
// We allow the caller to override the current location
|
|
1405
|
+
const currentLocation = dest._fromLocation || this.latestLocation
|
|
1536
1406
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
)
|
|
1407
|
+
const allFromMatches = this.matchRoutes(currentLocation, {
|
|
1408
|
+
_buildLocation: true,
|
|
1409
|
+
})
|
|
1541
1410
|
|
|
1542
|
-
const
|
|
1543
|
-
|
|
1544
|
-
|
|
1411
|
+
const lastMatch = last(allFromMatches)!
|
|
1412
|
+
|
|
1413
|
+
// First let's find the starting pathname
|
|
1414
|
+
// By default, start with the current location
|
|
1415
|
+
let fromPath = lastMatch.fullPath
|
|
1416
|
+
|
|
1417
|
+
// If there is a to, it means we are changing the path in some way
|
|
1418
|
+
// So we need to find the relative fromPath
|
|
1419
|
+
if (dest.unsafeRelative === 'path') {
|
|
1420
|
+
fromPath = currentLocation.pathname
|
|
1421
|
+
} else if (dest.to && dest.from) {
|
|
1422
|
+
fromPath = dest.from
|
|
1423
|
+
const existingFrom = [...allFromMatches].reverse().find((d) => {
|
|
1424
|
+
return (
|
|
1425
|
+
d.fullPath === fromPath || d.fullPath === joinPaths([fromPath, '/'])
|
|
1426
|
+
)
|
|
1427
|
+
})
|
|
1545
1428
|
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
let pathname: string
|
|
1550
|
-
if (dest.to) {
|
|
1551
|
-
const resolvePathTo =
|
|
1552
|
-
fromMatch?.fullPath ||
|
|
1553
|
-
last(fromMatches)?.fullPath ||
|
|
1554
|
-
this.latestLocation.pathname
|
|
1555
|
-
pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`)
|
|
1556
|
-
} else {
|
|
1557
|
-
const fromRouteByFromPathRouteId =
|
|
1558
|
-
this.routesById[
|
|
1559
|
-
stayingMatches?.find((route) => {
|
|
1560
|
-
const interpolatedPath = interpolatePath({
|
|
1561
|
-
path: route.fullPath,
|
|
1562
|
-
params: matchedRoutesResult?.routeParams ?? {},
|
|
1563
|
-
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1564
|
-
}).interpolatedPath
|
|
1565
|
-
const pathname = joinPaths([this.basepath, interpolatedPath])
|
|
1566
|
-
return pathname === fromPath
|
|
1567
|
-
})?.id as keyof this['routesById']
|
|
1568
|
-
]
|
|
1569
|
-
pathname = this.resolvePathWithBase(
|
|
1570
|
-
fromPath,
|
|
1571
|
-
fromRouteByFromPathRouteId?.to ?? fromPath,
|
|
1572
|
-
)
|
|
1429
|
+
if (!existingFrom) {
|
|
1430
|
+
console.warn(`Could not find match for from: ${dest.from}`)
|
|
1431
|
+
}
|
|
1573
1432
|
}
|
|
1574
1433
|
|
|
1575
|
-
|
|
1434
|
+
// From search should always use the current location
|
|
1435
|
+
const fromSearch = lastMatch.search
|
|
1436
|
+
// Same with params. It can't hurt to provide as many as possible
|
|
1437
|
+
const fromParams = { ...lastMatch.params }
|
|
1438
|
+
|
|
1439
|
+
// Resolve the next to
|
|
1440
|
+
const nextTo = dest.to
|
|
1441
|
+
? this.resolvePathWithBase(fromPath, `${dest.to}`)
|
|
1442
|
+
: fromPath
|
|
1576
1443
|
|
|
1444
|
+
// Resolve the next params
|
|
1577
1445
|
let nextParams =
|
|
1578
1446
|
(dest.params ?? true) === true
|
|
1579
|
-
?
|
|
1447
|
+
? fromParams
|
|
1580
1448
|
: {
|
|
1581
|
-
...
|
|
1582
|
-
...functionalUpdate(dest.params as any,
|
|
1449
|
+
...fromParams,
|
|
1450
|
+
...functionalUpdate(dest.params as any, fromParams),
|
|
1583
1451
|
}
|
|
1584
1452
|
|
|
1453
|
+
const destRoutes = this.matchRoutes(
|
|
1454
|
+
nextTo,
|
|
1455
|
+
{},
|
|
1456
|
+
{
|
|
1457
|
+
_buildLocation: true,
|
|
1458
|
+
},
|
|
1459
|
+
).map((d) => this.looseRoutesById[d.routeId]!)
|
|
1460
|
+
|
|
1461
|
+
// If there are any params, we need to stringify them
|
|
1585
1462
|
if (Object.keys(nextParams).length > 0) {
|
|
1586
|
-
|
|
1463
|
+
destRoutes
|
|
1587
1464
|
.map((route) => {
|
|
1588
1465
|
return (
|
|
1589
1466
|
route.options.params?.stringify ?? route.options.stringifyParams
|
|
@@ -1595,25 +1472,27 @@ export class RouterCore<
|
|
|
1595
1472
|
})
|
|
1596
1473
|
}
|
|
1597
1474
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1475
|
+
// Interpolate the next to into the next pathname
|
|
1476
|
+
const nextPathname = interpolatePath({
|
|
1477
|
+
path: nextTo,
|
|
1600
1478
|
params: nextParams ?? {},
|
|
1601
1479
|
leaveWildcards: false,
|
|
1602
1480
|
leaveParams: opts.leaveParams,
|
|
1603
1481
|
decodeCharMap: this.pathParamsDecodeCharMap,
|
|
1604
1482
|
}).interpolatedPath
|
|
1605
1483
|
|
|
1606
|
-
|
|
1484
|
+
// Resolve the next search
|
|
1485
|
+
let nextSearch = fromSearch
|
|
1607
1486
|
if (opts._includeValidateSearch && this.options.search?.strict) {
|
|
1608
1487
|
let validatedSearch = {}
|
|
1609
|
-
|
|
1488
|
+
destRoutes.forEach((route) => {
|
|
1610
1489
|
try {
|
|
1611
1490
|
if (route.options.validateSearch) {
|
|
1612
1491
|
validatedSearch = {
|
|
1613
1492
|
...validatedSearch,
|
|
1614
1493
|
...(validateSearch(route.options.validateSearch, {
|
|
1615
1494
|
...validatedSearch,
|
|
1616
|
-
...
|
|
1495
|
+
...nextSearch,
|
|
1617
1496
|
}) ?? {}),
|
|
1618
1497
|
}
|
|
1619
1498
|
}
|
|
@@ -1621,137 +1500,52 @@ export class RouterCore<
|
|
|
1621
1500
|
// ignore errors here because they are already handled in matchRoutes
|
|
1622
1501
|
}
|
|
1623
1502
|
})
|
|
1624
|
-
|
|
1503
|
+
nextSearch = validatedSearch
|
|
1625
1504
|
}
|
|
1626
1505
|
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
if (route.options.search?.middlewares) {
|
|
1634
|
-
middlewares.push(...route.options.search.middlewares)
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
|
-
// TODO remove preSearchFilters and postSearchFilters in v2
|
|
1638
|
-
else if (
|
|
1639
|
-
route.options.preSearchFilters ||
|
|
1640
|
-
route.options.postSearchFilters
|
|
1641
|
-
) {
|
|
1642
|
-
const legacyMiddleware: SearchMiddleware<any> = ({
|
|
1643
|
-
search,
|
|
1644
|
-
next,
|
|
1645
|
-
}) => {
|
|
1646
|
-
let nextSearch = search
|
|
1647
|
-
if (
|
|
1648
|
-
'preSearchFilters' in route.options &&
|
|
1649
|
-
route.options.preSearchFilters
|
|
1650
|
-
) {
|
|
1651
|
-
nextSearch = route.options.preSearchFilters.reduce(
|
|
1652
|
-
(prev, next) => next(prev),
|
|
1653
|
-
search,
|
|
1654
|
-
)
|
|
1655
|
-
}
|
|
1656
|
-
const result = next(nextSearch)
|
|
1657
|
-
if (
|
|
1658
|
-
'postSearchFilters' in route.options &&
|
|
1659
|
-
route.options.postSearchFilters
|
|
1660
|
-
) {
|
|
1661
|
-
return route.options.postSearchFilters.reduce(
|
|
1662
|
-
(prev, next) => next(prev),
|
|
1663
|
-
result,
|
|
1664
|
-
)
|
|
1665
|
-
}
|
|
1666
|
-
return result
|
|
1667
|
-
}
|
|
1668
|
-
middlewares.push(legacyMiddleware)
|
|
1669
|
-
}
|
|
1670
|
-
if (opts._includeValidateSearch && route.options.validateSearch) {
|
|
1671
|
-
const validate: SearchMiddleware<any> = ({ search, next }) => {
|
|
1672
|
-
const result = next(search)
|
|
1673
|
-
try {
|
|
1674
|
-
const validatedSearch = {
|
|
1675
|
-
...result,
|
|
1676
|
-
...(validateSearch(
|
|
1677
|
-
route.options.validateSearch,
|
|
1678
|
-
result,
|
|
1679
|
-
) ?? {}),
|
|
1680
|
-
}
|
|
1681
|
-
return validatedSearch
|
|
1682
|
-
} catch {
|
|
1683
|
-
// ignore errors here because they are already handled in matchRoutes
|
|
1684
|
-
return result
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
middlewares.push(validate)
|
|
1688
|
-
}
|
|
1689
|
-
return acc.concat(middlewares)
|
|
1690
|
-
},
|
|
1691
|
-
[] as Array<SearchMiddleware<any>>,
|
|
1692
|
-
) ?? []
|
|
1693
|
-
|
|
1694
|
-
// the chain ends here since `next` is not called
|
|
1695
|
-
const final: SearchMiddleware<any> = ({ search }) => {
|
|
1696
|
-
if (!dest.search) {
|
|
1697
|
-
return {}
|
|
1698
|
-
}
|
|
1699
|
-
if (dest.search === true) {
|
|
1700
|
-
return search
|
|
1701
|
-
}
|
|
1702
|
-
return functionalUpdate(dest.search, search)
|
|
1703
|
-
}
|
|
1704
|
-
allMiddlewares.push(final)
|
|
1705
|
-
|
|
1706
|
-
const applyNext = (index: number, currentSearch: any): any => {
|
|
1707
|
-
// no more middlewares left, return the current search
|
|
1708
|
-
if (index >= allMiddlewares.length) {
|
|
1709
|
-
return currentSearch
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1712
|
-
const middleware = allMiddlewares[index]!
|
|
1713
|
-
|
|
1714
|
-
const next = (newSearch: any): any => {
|
|
1715
|
-
return applyNext(index + 1, newSearch)
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
return middleware({ search: currentSearch, next })
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
// Start applying middlewares
|
|
1722
|
-
return applyNext(0, search)
|
|
1723
|
-
}
|
|
1506
|
+
nextSearch = applySearchMiddleware({
|
|
1507
|
+
search: nextSearch,
|
|
1508
|
+
dest,
|
|
1509
|
+
destRoutes,
|
|
1510
|
+
_includeValidateSearch: opts._includeValidateSearch,
|
|
1511
|
+
})
|
|
1724
1512
|
|
|
1725
|
-
|
|
1513
|
+
// Replace the equal deep
|
|
1514
|
+
nextSearch = replaceEqualDeep(fromSearch, nextSearch)
|
|
1726
1515
|
|
|
1727
|
-
|
|
1728
|
-
const searchStr = this.options.stringifySearch(
|
|
1516
|
+
// Stringify the next search
|
|
1517
|
+
const searchStr = this.options.stringifySearch(nextSearch)
|
|
1729
1518
|
|
|
1519
|
+
// Resolve the next hash
|
|
1730
1520
|
const hash =
|
|
1731
1521
|
dest.hash === true
|
|
1732
|
-
?
|
|
1522
|
+
? currentLocation.hash
|
|
1733
1523
|
: dest.hash
|
|
1734
|
-
? functionalUpdate(dest.hash,
|
|
1524
|
+
? functionalUpdate(dest.hash, currentLocation.hash)
|
|
1735
1525
|
: undefined
|
|
1736
1526
|
|
|
1527
|
+
// Resolve the next hash string
|
|
1737
1528
|
const hashStr = hash ? `#${hash}` : ''
|
|
1738
1529
|
|
|
1530
|
+
// Resolve the next state
|
|
1739
1531
|
let nextState =
|
|
1740
1532
|
dest.state === true
|
|
1741
|
-
?
|
|
1533
|
+
? currentLocation.state
|
|
1742
1534
|
: dest.state
|
|
1743
|
-
? functionalUpdate(dest.state,
|
|
1535
|
+
? functionalUpdate(dest.state, currentLocation.state)
|
|
1744
1536
|
: {}
|
|
1745
1537
|
|
|
1746
|
-
|
|
1538
|
+
// Replace the equal deep
|
|
1539
|
+
nextState = replaceEqualDeep(currentLocation.state, nextState)
|
|
1747
1540
|
|
|
1541
|
+
// Return the next location
|
|
1748
1542
|
return {
|
|
1749
|
-
pathname,
|
|
1750
|
-
search,
|
|
1543
|
+
pathname: nextPathname,
|
|
1544
|
+
search: nextSearch,
|
|
1751
1545
|
searchStr,
|
|
1752
1546
|
state: nextState as any,
|
|
1753
1547
|
hash: hash ?? '',
|
|
1754
|
-
href: `${
|
|
1548
|
+
href: `${nextPathname}${searchStr}${hashStr}`,
|
|
1755
1549
|
unmaskOnReload: dest.unmaskOnReload,
|
|
1756
1550
|
}
|
|
1757
1551
|
}
|
|
@@ -1761,6 +1555,7 @@ export class RouterCore<
|
|
|
1761
1555
|
maskedDest?: BuildNextOptions,
|
|
1762
1556
|
) => {
|
|
1763
1557
|
const next = build(dest)
|
|
1558
|
+
|
|
1764
1559
|
let maskedNext = maskedDest ? build(maskedDest) : undefined
|
|
1765
1560
|
|
|
1766
1561
|
if (!maskedNext) {
|
|
@@ -1792,16 +1587,12 @@ export class RouterCore<
|
|
|
1792
1587
|
}
|
|
1793
1588
|
}
|
|
1794
1589
|
|
|
1795
|
-
const nextMatches = this.getMatchedRoutes(next, dest)
|
|
1796
|
-
const final = build(dest, nextMatches)
|
|
1797
|
-
|
|
1798
1590
|
if (maskedNext) {
|
|
1799
|
-
const
|
|
1800
|
-
|
|
1801
|
-
final.maskedLocation = maskedFinal
|
|
1591
|
+
const maskedFinal = build(maskedDest)
|
|
1592
|
+
next.maskedLocation = maskedFinal
|
|
1802
1593
|
}
|
|
1803
1594
|
|
|
1804
|
-
return
|
|
1595
|
+
return next
|
|
1805
1596
|
}
|
|
1806
1597
|
|
|
1807
1598
|
if (opts.mask) {
|
|
@@ -1938,6 +1729,13 @@ export class RouterCore<
|
|
|
1938
1729
|
}
|
|
1939
1730
|
|
|
1940
1731
|
navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
|
|
1732
|
+
if (!reloadDocument && href) {
|
|
1733
|
+
try {
|
|
1734
|
+
new URL(`${href}`)
|
|
1735
|
+
reloadDocument = true
|
|
1736
|
+
} catch {}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1941
1739
|
if (reloadDocument) {
|
|
1942
1740
|
if (!href) {
|
|
1943
1741
|
const location = this.buildLocation({ to, ...rest } as any)
|
|
@@ -1960,10 +1758,30 @@ export class RouterCore<
|
|
|
1960
1758
|
|
|
1961
1759
|
latestLoadPromise: undefined | Promise<void>
|
|
1962
1760
|
|
|
1963
|
-
|
|
1761
|
+
beforeLoad = () => {
|
|
1762
|
+
// Cancel any pending matches
|
|
1763
|
+
this.cancelMatches()
|
|
1964
1764
|
this.latestLocation = this.parseLocation(this.latestLocation)
|
|
1965
1765
|
|
|
1966
|
-
|
|
1766
|
+
// Match the routes
|
|
1767
|
+
const pendingMatches = this.matchRoutes(this.latestLocation)
|
|
1768
|
+
|
|
1769
|
+
// Ingest the new matches
|
|
1770
|
+
this.__store.setState((s) => ({
|
|
1771
|
+
...s,
|
|
1772
|
+
status: 'pending',
|
|
1773
|
+
isLoading: true,
|
|
1774
|
+
location: this.latestLocation,
|
|
1775
|
+
pendingMatches,
|
|
1776
|
+
// If a cached moved to pendingMatches, remove it from cachedMatches
|
|
1777
|
+
cachedMatches: s.cachedMatches.filter((d) => {
|
|
1778
|
+
return !pendingMatches.find((e) => e.id === d.id)
|
|
1779
|
+
}),
|
|
1780
|
+
}))
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
|
|
1784
|
+
let redirect: AnyRedirect | undefined
|
|
1967
1785
|
let notFound: NotFoundError | undefined
|
|
1968
1786
|
|
|
1969
1787
|
let loadPromise: Promise<void>
|
|
@@ -1972,36 +1790,10 @@ export class RouterCore<
|
|
|
1972
1790
|
loadPromise = new Promise<void>((resolve) => {
|
|
1973
1791
|
this.startTransition(async () => {
|
|
1974
1792
|
try {
|
|
1793
|
+
this.beforeLoad()
|
|
1975
1794
|
const next = this.latestLocation
|
|
1976
1795
|
const prevLocation = this.state.resolvedLocation
|
|
1977
1796
|
|
|
1978
|
-
// Cancel any pending matches
|
|
1979
|
-
this.cancelMatches()
|
|
1980
|
-
|
|
1981
|
-
let pendingMatches!: Array<AnyRouteMatch>
|
|
1982
|
-
|
|
1983
|
-
batch(() => {
|
|
1984
|
-
// this call breaks a route context of destination route after a redirect
|
|
1985
|
-
// we should be fine not eagerly calling this since we call it later
|
|
1986
|
-
// this.clearExpiredCache()
|
|
1987
|
-
|
|
1988
|
-
// Match the routes
|
|
1989
|
-
pendingMatches = this.matchRoutes(next)
|
|
1990
|
-
|
|
1991
|
-
// Ingest the new matches
|
|
1992
|
-
this.__store.setState((s) => ({
|
|
1993
|
-
...s,
|
|
1994
|
-
status: 'pending',
|
|
1995
|
-
isLoading: true,
|
|
1996
|
-
location: next,
|
|
1997
|
-
pendingMatches,
|
|
1998
|
-
// If a cached moved to pendingMatches, remove it from cachedMatches
|
|
1999
|
-
cachedMatches: s.cachedMatches.filter((d) => {
|
|
2000
|
-
return !pendingMatches.find((e) => e.id === d.id)
|
|
2001
|
-
}),
|
|
2002
|
-
}))
|
|
2003
|
-
})
|
|
2004
|
-
|
|
2005
1797
|
if (!this.state.redirect) {
|
|
2006
1798
|
this.emit({
|
|
2007
1799
|
type: 'onBeforeNavigate',
|
|
@@ -2022,7 +1814,7 @@ export class RouterCore<
|
|
|
2022
1814
|
|
|
2023
1815
|
await this.loadMatches({
|
|
2024
1816
|
sync: opts?.sync,
|
|
2025
|
-
matches: pendingMatches
|
|
1817
|
+
matches: this.state.pendingMatches as Array<AnyRouteMatch>,
|
|
2026
1818
|
location: next,
|
|
2027
1819
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
2028
1820
|
onReady: async () => {
|
|
@@ -2083,11 +1875,11 @@ export class RouterCore<
|
|
|
2083
1875
|
},
|
|
2084
1876
|
})
|
|
2085
1877
|
} catch (err) {
|
|
2086
|
-
if (
|
|
1878
|
+
if (isRedirect(err)) {
|
|
2087
1879
|
redirect = err
|
|
2088
1880
|
if (!this.isServer) {
|
|
2089
1881
|
this.navigate({
|
|
2090
|
-
...redirect,
|
|
1882
|
+
...redirect.options,
|
|
2091
1883
|
replace: true,
|
|
2092
1884
|
ignoreBlocker: true,
|
|
2093
1885
|
})
|
|
@@ -2099,7 +1891,7 @@ export class RouterCore<
|
|
|
2099
1891
|
this.__store.setState((s) => ({
|
|
2100
1892
|
...s,
|
|
2101
1893
|
statusCode: redirect
|
|
2102
|
-
? redirect.
|
|
1894
|
+
? redirect.status
|
|
2103
1895
|
: notFound
|
|
2104
1896
|
? 404
|
|
2105
1897
|
: s.matches.some((d) => d.status === 'error')
|
|
@@ -2255,13 +2047,15 @@ export class RouterCore<
|
|
|
2255
2047
|
}
|
|
2256
2048
|
|
|
2257
2049
|
const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
|
|
2258
|
-
if (
|
|
2259
|
-
if (
|
|
2260
|
-
|
|
2050
|
+
if (isRedirect(err) || isNotFound(err)) {
|
|
2051
|
+
if (isRedirect(err)) {
|
|
2052
|
+
if (err.redirectHandled) {
|
|
2053
|
+
if (!err.options.reloadDocument) {
|
|
2054
|
+
throw err
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2261
2057
|
}
|
|
2262
|
-
}
|
|
2263
2058
|
|
|
2264
|
-
if (isRedirect(err) || isNotFound(err)) {
|
|
2265
2059
|
updateMatch(match.id, (prev) => ({
|
|
2266
2060
|
...prev,
|
|
2267
2061
|
status: isRedirect(err)
|
|
@@ -2285,7 +2079,9 @@ export class RouterCore<
|
|
|
2285
2079
|
|
|
2286
2080
|
if (isRedirect(err)) {
|
|
2287
2081
|
rendered = true
|
|
2288
|
-
err =
|
|
2082
|
+
err.options._fromLocation = location
|
|
2083
|
+
err.redirectHandled = true
|
|
2084
|
+
err = this.resolveRedirect(err)
|
|
2289
2085
|
throw err
|
|
2290
2086
|
} else if (isNotFound(err)) {
|
|
2291
2087
|
this._handleNotFound(matches, err, {
|
|
@@ -2589,7 +2385,7 @@ export class RouterCore<
|
|
|
2589
2385
|
!this.state.matches.find((d) => d.id === matchId),
|
|
2590
2386
|
}))
|
|
2591
2387
|
|
|
2592
|
-
const executeHead = () => {
|
|
2388
|
+
const executeHead = async () => {
|
|
2593
2389
|
const match = this.getMatch(matchId)
|
|
2594
2390
|
// in case of a redirecting match during preload, the match does not exist
|
|
2595
2391
|
if (!match) {
|
|
@@ -2601,21 +2397,17 @@ export class RouterCore<
|
|
|
2601
2397
|
params: match.params,
|
|
2602
2398
|
loaderData: match.loaderData,
|
|
2603
2399
|
}
|
|
2604
|
-
const headFnContent =
|
|
2400
|
+
const headFnContent =
|
|
2401
|
+
await route.options.head?.(assetContext)
|
|
2605
2402
|
const meta = headFnContent?.meta
|
|
2606
2403
|
const links = headFnContent?.links
|
|
2607
2404
|
const headScripts = headFnContent?.scripts
|
|
2608
2405
|
|
|
2609
|
-
const scripts =
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
links,
|
|
2615
|
-
headScripts,
|
|
2616
|
-
headers,
|
|
2617
|
-
scripts,
|
|
2618
|
-
}))
|
|
2406
|
+
const scripts =
|
|
2407
|
+
await route.options.scripts?.(assetContext)
|
|
2408
|
+
const headers =
|
|
2409
|
+
await route.options.headers?.(assetContext)
|
|
2410
|
+
return { meta, links, headScripts, headers, scripts }
|
|
2619
2411
|
}
|
|
2620
2412
|
|
|
2621
2413
|
const runLoader = async () => {
|
|
@@ -2662,17 +2454,19 @@ export class RouterCore<
|
|
|
2662
2454
|
// to be preloaded before we resolve the match
|
|
2663
2455
|
await route._componentsPromise
|
|
2664
2456
|
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2457
|
+
updateMatch(matchId, (prev) => ({
|
|
2458
|
+
...prev,
|
|
2459
|
+
error: undefined,
|
|
2460
|
+
status: 'success',
|
|
2461
|
+
isFetching: false,
|
|
2462
|
+
updatedAt: Date.now(),
|
|
2463
|
+
loaderData,
|
|
2464
|
+
}))
|
|
2465
|
+
const head = await executeHead()
|
|
2466
|
+
updateMatch(matchId, (prev) => ({
|
|
2467
|
+
...prev,
|
|
2468
|
+
...head,
|
|
2469
|
+
}))
|
|
2676
2470
|
} catch (e) {
|
|
2677
2471
|
let error = e
|
|
2678
2472
|
|
|
@@ -2689,16 +2483,14 @@ export class RouterCore<
|
|
|
2689
2483
|
onErrorError,
|
|
2690
2484
|
)
|
|
2691
2485
|
}
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
executeHead()
|
|
2701
|
-
})
|
|
2486
|
+
const head = await executeHead()
|
|
2487
|
+
updateMatch(matchId, (prev) => ({
|
|
2488
|
+
...prev,
|
|
2489
|
+
error,
|
|
2490
|
+
status: 'error',
|
|
2491
|
+
isFetching: false,
|
|
2492
|
+
...head,
|
|
2493
|
+
}))
|
|
2702
2494
|
}
|
|
2703
2495
|
|
|
2704
2496
|
this.serverSsr?.onMatchSettled({
|
|
@@ -2706,13 +2498,13 @@ export class RouterCore<
|
|
|
2706
2498
|
match: this.getMatch(matchId)!,
|
|
2707
2499
|
})
|
|
2708
2500
|
} catch (err) {
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
})
|
|
2501
|
+
const head = await executeHead()
|
|
2502
|
+
|
|
2503
|
+
updateMatch(matchId, (prev) => ({
|
|
2504
|
+
...prev,
|
|
2505
|
+
loaderPromise: undefined,
|
|
2506
|
+
...head,
|
|
2507
|
+
}))
|
|
2716
2508
|
handleRedirectAndNotFound(this.getMatch(matchId)!, err)
|
|
2717
2509
|
}
|
|
2718
2510
|
}
|
|
@@ -2738,8 +2530,8 @@ export class RouterCore<
|
|
|
2738
2530
|
loaderPromise: undefined,
|
|
2739
2531
|
}))
|
|
2740
2532
|
} catch (err) {
|
|
2741
|
-
if (
|
|
2742
|
-
await this.navigate(err)
|
|
2533
|
+
if (isRedirect(err)) {
|
|
2534
|
+
await this.navigate(err.options)
|
|
2743
2535
|
}
|
|
2744
2536
|
}
|
|
2745
2537
|
})()
|
|
@@ -2752,7 +2544,11 @@ export class RouterCore<
|
|
|
2752
2544
|
// if the loader did not run, still update head.
|
|
2753
2545
|
// reason: parent's beforeLoad may have changed the route context
|
|
2754
2546
|
// and only now do we know the route context (and that the loader would not run)
|
|
2755
|
-
executeHead()
|
|
2547
|
+
const head = await executeHead()
|
|
2548
|
+
updateMatch(matchId, (prev) => ({
|
|
2549
|
+
...prev,
|
|
2550
|
+
...head,
|
|
2551
|
+
}))
|
|
2756
2552
|
}
|
|
2757
2553
|
}
|
|
2758
2554
|
if (!loaderIsRunningAsync) {
|
|
@@ -2829,11 +2625,14 @@ export class RouterCore<
|
|
|
2829
2625
|
return this.load({ sync: opts?.sync })
|
|
2830
2626
|
}
|
|
2831
2627
|
|
|
2832
|
-
resolveRedirect = (
|
|
2833
|
-
|
|
2628
|
+
resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
|
|
2629
|
+
if (!redirect.options.href) {
|
|
2630
|
+
redirect.options.href = this.buildLocation(redirect.options).href
|
|
2631
|
+
redirect.headers.set('Location', redirect.options.href)
|
|
2632
|
+
}
|
|
2834
2633
|
|
|
2835
|
-
if (!redirect.
|
|
2836
|
-
redirect.
|
|
2634
|
+
if (!redirect.headers.get('Location')) {
|
|
2635
|
+
redirect.headers.set('Location', redirect.options.href)
|
|
2837
2636
|
}
|
|
2838
2637
|
|
|
2839
2638
|
return redirect
|
|
@@ -2968,11 +2767,12 @@ export class RouterCore<
|
|
|
2968
2767
|
return matches
|
|
2969
2768
|
} catch (err) {
|
|
2970
2769
|
if (isRedirect(err)) {
|
|
2971
|
-
if (err.reloadDocument) {
|
|
2770
|
+
if (err.options.reloadDocument) {
|
|
2972
2771
|
return undefined
|
|
2973
2772
|
}
|
|
2773
|
+
|
|
2974
2774
|
return await this.preloadRoute({
|
|
2975
|
-
...
|
|
2775
|
+
...err.options,
|
|
2976
2776
|
_fromLocation: next,
|
|
2977
2777
|
})
|
|
2978
2778
|
}
|
|
@@ -3206,3 +3006,338 @@ function routeNeedsPreload(route: AnyRoute) {
|
|
|
3206
3006
|
}
|
|
3207
3007
|
return false
|
|
3208
3008
|
}
|
|
3009
|
+
|
|
3010
|
+
interface RouteLike {
|
|
3011
|
+
id: string
|
|
3012
|
+
isRoot?: boolean
|
|
3013
|
+
path?: string
|
|
3014
|
+
fullPath: string
|
|
3015
|
+
rank?: number
|
|
3016
|
+
parentRoute?: RouteLike
|
|
3017
|
+
children?: Array<RouteLike>
|
|
3018
|
+
options?: {
|
|
3019
|
+
caseSensitive?: boolean
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
export function processRouteTree<TRouteLike extends RouteLike>({
|
|
3024
|
+
routeTree,
|
|
3025
|
+
initRoute,
|
|
3026
|
+
}: {
|
|
3027
|
+
routeTree: TRouteLike
|
|
3028
|
+
initRoute?: (route: TRouteLike, index: number) => void
|
|
3029
|
+
}) {
|
|
3030
|
+
const routesById = {} as Record<string, TRouteLike>
|
|
3031
|
+
const routesByPath = {} as Record<string, TRouteLike>
|
|
3032
|
+
|
|
3033
|
+
const recurseRoutes = (childRoutes: Array<TRouteLike>) => {
|
|
3034
|
+
childRoutes.forEach((childRoute, i) => {
|
|
3035
|
+
initRoute?.(childRoute, i)
|
|
3036
|
+
|
|
3037
|
+
const existingRoute = routesById[childRoute.id]
|
|
3038
|
+
|
|
3039
|
+
invariant(
|
|
3040
|
+
!existingRoute,
|
|
3041
|
+
`Duplicate routes found with id: ${String(childRoute.id)}`,
|
|
3042
|
+
)
|
|
3043
|
+
|
|
3044
|
+
routesById[childRoute.id] = childRoute
|
|
3045
|
+
|
|
3046
|
+
if (!childRoute.isRoot && childRoute.path) {
|
|
3047
|
+
const trimmedFullPath = trimPathRight(childRoute.fullPath)
|
|
3048
|
+
if (
|
|
3049
|
+
!routesByPath[trimmedFullPath] ||
|
|
3050
|
+
childRoute.fullPath.endsWith('/')
|
|
3051
|
+
) {
|
|
3052
|
+
routesByPath[trimmedFullPath] = childRoute
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
|
|
3056
|
+
const children = childRoute.children as Array<TRouteLike>
|
|
3057
|
+
|
|
3058
|
+
if (children?.length) {
|
|
3059
|
+
recurseRoutes(children)
|
|
3060
|
+
}
|
|
3061
|
+
})
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
recurseRoutes([routeTree])
|
|
3065
|
+
|
|
3066
|
+
const scoredRoutes: Array<{
|
|
3067
|
+
child: TRouteLike
|
|
3068
|
+
trimmed: string
|
|
3069
|
+
parsed: ReturnType<typeof parsePathname>
|
|
3070
|
+
index: number
|
|
3071
|
+
scores: Array<number>
|
|
3072
|
+
}> = []
|
|
3073
|
+
|
|
3074
|
+
const routes: Array<TRouteLike> = Object.values(routesById)
|
|
3075
|
+
|
|
3076
|
+
routes.forEach((d, i) => {
|
|
3077
|
+
if (d.isRoot || !d.path) {
|
|
3078
|
+
return
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
const trimmed = trimPathLeft(d.fullPath)
|
|
3082
|
+
const parsed = parsePathname(trimmed)
|
|
3083
|
+
|
|
3084
|
+
// Removes the leading slash if it is not the only remaining segment
|
|
3085
|
+
while (parsed.length > 1 && parsed[0]?.value === '/') {
|
|
3086
|
+
parsed.shift()
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
const scores = parsed.map((segment) => {
|
|
3090
|
+
if (segment.value === '/') {
|
|
3091
|
+
return 0.75
|
|
3092
|
+
}
|
|
3093
|
+
|
|
3094
|
+
if (
|
|
3095
|
+
segment.type === 'param' &&
|
|
3096
|
+
segment.prefixSegment &&
|
|
3097
|
+
segment.suffixSegment
|
|
3098
|
+
) {
|
|
3099
|
+
return 0.55
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
if (segment.type === 'param' && segment.prefixSegment) {
|
|
3103
|
+
return 0.52
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3106
|
+
if (segment.type === 'param' && segment.suffixSegment) {
|
|
3107
|
+
return 0.51
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
if (segment.type === 'param') {
|
|
3111
|
+
return 0.5
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
if (
|
|
3115
|
+
segment.type === 'wildcard' &&
|
|
3116
|
+
segment.prefixSegment &&
|
|
3117
|
+
segment.suffixSegment
|
|
3118
|
+
) {
|
|
3119
|
+
return 0.3
|
|
3120
|
+
}
|
|
3121
|
+
|
|
3122
|
+
if (segment.type === 'wildcard' && segment.prefixSegment) {
|
|
3123
|
+
return 0.27
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
if (segment.type === 'wildcard' && segment.suffixSegment) {
|
|
3127
|
+
return 0.26
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
if (segment.type === 'wildcard') {
|
|
3131
|
+
return 0.25
|
|
3132
|
+
}
|
|
3133
|
+
|
|
3134
|
+
return 1
|
|
3135
|
+
})
|
|
3136
|
+
|
|
3137
|
+
scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
|
|
3138
|
+
})
|
|
3139
|
+
|
|
3140
|
+
const flatRoutes = scoredRoutes
|
|
3141
|
+
.sort((a, b) => {
|
|
3142
|
+
const minLength = Math.min(a.scores.length, b.scores.length)
|
|
3143
|
+
|
|
3144
|
+
// Sort by min available score
|
|
3145
|
+
for (let i = 0; i < minLength; i++) {
|
|
3146
|
+
if (a.scores[i] !== b.scores[i]) {
|
|
3147
|
+
return b.scores[i]! - a.scores[i]!
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// Sort by length of score
|
|
3152
|
+
if (a.scores.length !== b.scores.length) {
|
|
3153
|
+
return b.scores.length - a.scores.length
|
|
3154
|
+
}
|
|
3155
|
+
|
|
3156
|
+
// Sort by min available parsed value
|
|
3157
|
+
for (let i = 0; i < minLength; i++) {
|
|
3158
|
+
if (a.parsed[i]!.value !== b.parsed[i]!.value) {
|
|
3159
|
+
return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
// Sort by original index
|
|
3164
|
+
return a.index - b.index
|
|
3165
|
+
})
|
|
3166
|
+
.map((d, i) => {
|
|
3167
|
+
d.child.rank = i
|
|
3168
|
+
return d.child
|
|
3169
|
+
})
|
|
3170
|
+
|
|
3171
|
+
return { routesById, routesByPath, flatRoutes }
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
export function getMatchedRoutes<TRouteLike extends RouteLike>({
|
|
3175
|
+
pathname,
|
|
3176
|
+
routePathname,
|
|
3177
|
+
basepath,
|
|
3178
|
+
caseSensitive,
|
|
3179
|
+
routesByPath,
|
|
3180
|
+
routesById,
|
|
3181
|
+
flatRoutes,
|
|
3182
|
+
}: {
|
|
3183
|
+
pathname: string
|
|
3184
|
+
routePathname?: string
|
|
3185
|
+
basepath: string
|
|
3186
|
+
caseSensitive?: boolean
|
|
3187
|
+
routesByPath: Record<string, TRouteLike>
|
|
3188
|
+
routesById: Record<string, TRouteLike>
|
|
3189
|
+
flatRoutes: Array<TRouteLike>
|
|
3190
|
+
}) {
|
|
3191
|
+
let routeParams: Record<string, string> = {}
|
|
3192
|
+
const trimmedPath = trimPathRight(pathname)
|
|
3193
|
+
const getMatchedParams = (route: TRouteLike) => {
|
|
3194
|
+
const result = matchPathname(basepath, trimmedPath, {
|
|
3195
|
+
to: route.fullPath,
|
|
3196
|
+
caseSensitive: route.options?.caseSensitive ?? caseSensitive,
|
|
3197
|
+
fuzzy: true,
|
|
3198
|
+
})
|
|
3199
|
+
return result
|
|
3200
|
+
}
|
|
3201
|
+
|
|
3202
|
+
let foundRoute: TRouteLike | undefined =
|
|
3203
|
+
routePathname !== undefined ? routesByPath[routePathname] : undefined
|
|
3204
|
+
if (foundRoute) {
|
|
3205
|
+
routeParams = getMatchedParams(foundRoute)!
|
|
3206
|
+
} else {
|
|
3207
|
+
foundRoute = flatRoutes.find((route) => {
|
|
3208
|
+
const matchedParams = getMatchedParams(route)
|
|
3209
|
+
|
|
3210
|
+
if (matchedParams) {
|
|
3211
|
+
routeParams = matchedParams
|
|
3212
|
+
return true
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
return false
|
|
3216
|
+
})
|
|
3217
|
+
}
|
|
3218
|
+
|
|
3219
|
+
let routeCursor: TRouteLike = foundRoute || routesById[rootRouteId]!
|
|
3220
|
+
|
|
3221
|
+
const matchedRoutes: Array<TRouteLike> = [routeCursor]
|
|
3222
|
+
|
|
3223
|
+
while (routeCursor.parentRoute) {
|
|
3224
|
+
routeCursor = routeCursor.parentRoute as TRouteLike
|
|
3225
|
+
matchedRoutes.unshift(routeCursor)
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
return { matchedRoutes, routeParams, foundRoute }
|
|
3229
|
+
}
|
|
3230
|
+
|
|
3231
|
+
function applySearchMiddleware({
|
|
3232
|
+
search,
|
|
3233
|
+
dest,
|
|
3234
|
+
destRoutes,
|
|
3235
|
+
_includeValidateSearch,
|
|
3236
|
+
}: {
|
|
3237
|
+
search: any
|
|
3238
|
+
dest: BuildNextOptions
|
|
3239
|
+
destRoutes: Array<AnyRoute>
|
|
3240
|
+
_includeValidateSearch: boolean | undefined
|
|
3241
|
+
}) {
|
|
3242
|
+
const allMiddlewares =
|
|
3243
|
+
destRoutes.reduce(
|
|
3244
|
+
(acc, route) => {
|
|
3245
|
+
const middlewares: Array<SearchMiddleware<any>> = []
|
|
3246
|
+
|
|
3247
|
+
if ('search' in route.options) {
|
|
3248
|
+
if (route.options.search?.middlewares) {
|
|
3249
|
+
middlewares.push(...route.options.search.middlewares)
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
// TODO remove preSearchFilters and postSearchFilters in v2
|
|
3253
|
+
else if (
|
|
3254
|
+
route.options.preSearchFilters ||
|
|
3255
|
+
route.options.postSearchFilters
|
|
3256
|
+
) {
|
|
3257
|
+
const legacyMiddleware: SearchMiddleware<any> = ({
|
|
3258
|
+
search,
|
|
3259
|
+
next,
|
|
3260
|
+
}) => {
|
|
3261
|
+
let nextSearch = search
|
|
3262
|
+
|
|
3263
|
+
if (
|
|
3264
|
+
'preSearchFilters' in route.options &&
|
|
3265
|
+
route.options.preSearchFilters
|
|
3266
|
+
) {
|
|
3267
|
+
nextSearch = route.options.preSearchFilters.reduce(
|
|
3268
|
+
(prev, next) => next(prev),
|
|
3269
|
+
search,
|
|
3270
|
+
)
|
|
3271
|
+
}
|
|
3272
|
+
|
|
3273
|
+
const result = next(nextSearch)
|
|
3274
|
+
|
|
3275
|
+
if (
|
|
3276
|
+
'postSearchFilters' in route.options &&
|
|
3277
|
+
route.options.postSearchFilters
|
|
3278
|
+
) {
|
|
3279
|
+
return route.options.postSearchFilters.reduce(
|
|
3280
|
+
(prev, next) => next(prev),
|
|
3281
|
+
result,
|
|
3282
|
+
)
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
return result
|
|
3286
|
+
}
|
|
3287
|
+
middlewares.push(legacyMiddleware)
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
if (_includeValidateSearch && route.options.validateSearch) {
|
|
3291
|
+
const validate: SearchMiddleware<any> = ({ search, next }) => {
|
|
3292
|
+
const result = next(search)
|
|
3293
|
+
try {
|
|
3294
|
+
const validatedSearch = {
|
|
3295
|
+
...result,
|
|
3296
|
+
...(validateSearch(route.options.validateSearch, result) ?? {}),
|
|
3297
|
+
}
|
|
3298
|
+
return validatedSearch
|
|
3299
|
+
} catch {
|
|
3300
|
+
// ignore errors here because they are already handled in matchRoutes
|
|
3301
|
+
return result
|
|
3302
|
+
}
|
|
3303
|
+
}
|
|
3304
|
+
|
|
3305
|
+
middlewares.push(validate)
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
return acc.concat(middlewares)
|
|
3309
|
+
},
|
|
3310
|
+
[] as Array<SearchMiddleware<any>>,
|
|
3311
|
+
) ?? []
|
|
3312
|
+
|
|
3313
|
+
// the chain ends here since `next` is not called
|
|
3314
|
+
const final: SearchMiddleware<any> = ({ search }) => {
|
|
3315
|
+
if (!dest.search) {
|
|
3316
|
+
return {}
|
|
3317
|
+
}
|
|
3318
|
+
if (dest.search === true) {
|
|
3319
|
+
return search
|
|
3320
|
+
}
|
|
3321
|
+
return functionalUpdate(dest.search, search)
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
allMiddlewares.push(final)
|
|
3325
|
+
|
|
3326
|
+
const applyNext = (index: number, currentSearch: any): any => {
|
|
3327
|
+
// no more middlewares left, return the current search
|
|
3328
|
+
if (index >= allMiddlewares.length) {
|
|
3329
|
+
return currentSearch
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
const middleware = allMiddlewares[index]!
|
|
3333
|
+
|
|
3334
|
+
const next = (newSearch: any): any => {
|
|
3335
|
+
return applyNext(index + 1, newSearch)
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
return middleware({ search: currentSearch, next })
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
// Start applying middlewares
|
|
3342
|
+
return applyNext(0, search)
|
|
3343
|
+
}
|