@tanstack/router-core 1.155.0 → 1.156.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/new-process-route-tree.cjs.map +1 -1
- package/dist/cjs/new-process-route-tree.d.cts +13 -8
- package/dist/cjs/router.cjs +116 -49
- package/dist/cjs/router.cjs.map +1 -1
- package/dist/cjs/router.d.cts +18 -4
- package/dist/esm/new-process-route-tree.d.ts +13 -8
- package/dist/esm/new-process-route-tree.js.map +1 -1
- package/dist/esm/router.d.ts +18 -4
- package/dist/esm/router.js +117 -50
- package/dist/esm/router.js.map +1 -1
- package/package.json +1 -1
- package/src/new-process-route-tree.ts +12 -8
- package/src/router.ts +166 -48
package/package.json
CHANGED
|
@@ -778,6 +778,17 @@ export function trimPathRight(path: string) {
|
|
|
778
778
|
return path === '/' ? path : path.replace(/\/{1,}$/, '')
|
|
779
779
|
}
|
|
780
780
|
|
|
781
|
+
export interface ProcessRouteTreeResult<
|
|
782
|
+
TRouteLike extends Extract<RouteLike, { fullPath: string }> & { id: string },
|
|
783
|
+
> {
|
|
784
|
+
/** Should be considered a black box, needs to be provided to all matching functions in this module. */
|
|
785
|
+
processedTree: ProcessedTree<TRouteLike, any, any>
|
|
786
|
+
/** A lookup map of routes by their unique IDs. */
|
|
787
|
+
routesById: Record<string, TRouteLike>
|
|
788
|
+
/** A lookup map of routes by their trimmed full paths. */
|
|
789
|
+
routesByPath: Record<string, TRouteLike>
|
|
790
|
+
}
|
|
791
|
+
|
|
781
792
|
/**
|
|
782
793
|
* Processes a route tree into a segment trie for efficient path matching.
|
|
783
794
|
* Also builds lookup maps for routes by ID and by trimmed full path.
|
|
@@ -791,14 +802,7 @@ export function processRouteTree<
|
|
|
791
802
|
caseSensitive: boolean = false,
|
|
792
803
|
/** Optional callback invoked for each route during processing. */
|
|
793
804
|
initRoute?: (route: TRouteLike, index: number) => void,
|
|
794
|
-
): {
|
|
795
|
-
/** Should be considered a black box, needs to be provided to all matching functions in this module. */
|
|
796
|
-
processedTree: ProcessedTree<TRouteLike, any, any>
|
|
797
|
-
/** A lookup map of routes by their unique IDs. */
|
|
798
|
-
routesById: Record<string, TRouteLike>
|
|
799
|
-
/** A lookup map of routes by their trimmed full paths. */
|
|
800
|
-
routesByPath: Record<string, TRouteLike>
|
|
801
|
-
} {
|
|
805
|
+
): ProcessRouteTreeResult<TRouteLike> {
|
|
802
806
|
const segmentTree = createStaticNode<TRouteLike>(routeTree.fullPath)
|
|
803
807
|
const data = new Uint16Array(6)
|
|
804
808
|
const routesById = {} as Record<string, TRouteLike>
|
package/src/router.ts
CHANGED
|
@@ -38,7 +38,11 @@ import {
|
|
|
38
38
|
executeRewriteOutput,
|
|
39
39
|
rewriteBasepath,
|
|
40
40
|
} from './rewrite'
|
|
41
|
-
import type {
|
|
41
|
+
import type { LRUCache } from './lru-cache'
|
|
42
|
+
import type {
|
|
43
|
+
ProcessRouteTreeResult,
|
|
44
|
+
ProcessedTree,
|
|
45
|
+
} from './new-process-route-tree'
|
|
42
46
|
import type { SearchParser, SearchSerializer } from './searchParams'
|
|
43
47
|
import type { AnyRedirect, ResolvedRedirect } from './redirect'
|
|
44
48
|
import type {
|
|
@@ -589,7 +593,6 @@ export type SubscribeFn = <TType extends keyof RouterEvents>(
|
|
|
589
593
|
export interface MatchRoutesOpts {
|
|
590
594
|
preload?: boolean
|
|
591
595
|
throwOnError?: boolean
|
|
592
|
-
_buildLocation?: boolean
|
|
593
596
|
dest?: BuildNextOptions
|
|
594
597
|
}
|
|
595
598
|
|
|
@@ -873,6 +876,17 @@ export type CreateRouterFn = <
|
|
|
873
876
|
TDehydrated
|
|
874
877
|
>
|
|
875
878
|
|
|
879
|
+
declare global {
|
|
880
|
+
// eslint-disable-next-line no-var
|
|
881
|
+
var __TSR_CACHE__:
|
|
882
|
+
| {
|
|
883
|
+
routeTree: AnyRoute
|
|
884
|
+
processRouteTreeResult: ProcessRouteTreeResult<AnyRoute>
|
|
885
|
+
resolvePathCache: LRUCache<string, string>
|
|
886
|
+
}
|
|
887
|
+
| undefined
|
|
888
|
+
}
|
|
889
|
+
|
|
876
890
|
/**
|
|
877
891
|
* Core, framework-agnostic router engine that powers TanStack Router.
|
|
878
892
|
*
|
|
@@ -923,6 +937,7 @@ export class RouterCore<
|
|
|
923
937
|
routesById!: RoutesById<TRouteTree>
|
|
924
938
|
routesByPath!: RoutesByPath<TRouteTree>
|
|
925
939
|
processedTree!: ProcessedTree<TRouteTree, any, any>
|
|
940
|
+
resolvePathCache!: LRUCache<string, string>
|
|
926
941
|
isServer!: boolean
|
|
927
942
|
pathParamsDecoder?: (encoded: string) => string
|
|
928
943
|
|
|
@@ -1027,7 +1042,28 @@ export class RouterCore<
|
|
|
1027
1042
|
|
|
1028
1043
|
if (this.options.routeTree !== this.routeTree) {
|
|
1029
1044
|
this.routeTree = this.options.routeTree as TRouteTree
|
|
1030
|
-
|
|
1045
|
+
let processRouteTreeResult: ProcessRouteTreeResult<TRouteTree>
|
|
1046
|
+
if (
|
|
1047
|
+
this.isServer &&
|
|
1048
|
+
globalThis.__TSR_CACHE__ &&
|
|
1049
|
+
globalThis.__TSR_CACHE__.routeTree === this.routeTree
|
|
1050
|
+
) {
|
|
1051
|
+
const cached = globalThis.__TSR_CACHE__
|
|
1052
|
+
this.resolvePathCache = cached.resolvePathCache
|
|
1053
|
+
processRouteTreeResult = cached.processRouteTreeResult as any
|
|
1054
|
+
} else {
|
|
1055
|
+
this.resolvePathCache = createLRUCache(1000)
|
|
1056
|
+
processRouteTreeResult = this.buildRouteTree()
|
|
1057
|
+
// only cache if nothing else is cached yet
|
|
1058
|
+
if (this.isServer && globalThis.__TSR_CACHE__ === undefined) {
|
|
1059
|
+
globalThis.__TSR_CACHE__ = {
|
|
1060
|
+
routeTree: this.routeTree,
|
|
1061
|
+
processRouteTreeResult: processRouteTreeResult as any,
|
|
1062
|
+
resolvePathCache: this.resolvePathCache,
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
this.setRoutes(processRouteTreeResult)
|
|
1031
1067
|
}
|
|
1032
1068
|
|
|
1033
1069
|
if (!this.__store && this.latestLocation) {
|
|
@@ -1110,7 +1146,7 @@ export class RouterCore<
|
|
|
1110
1146
|
}
|
|
1111
1147
|
|
|
1112
1148
|
buildRouteTree = () => {
|
|
1113
|
-
const
|
|
1149
|
+
const result = processRouteTree(
|
|
1114
1150
|
this.routeTree,
|
|
1115
1151
|
this.options.caseSensitive,
|
|
1116
1152
|
(route, i) => {
|
|
@@ -1120,9 +1156,17 @@ export class RouterCore<
|
|
|
1120
1156
|
},
|
|
1121
1157
|
)
|
|
1122
1158
|
if (this.options.routeMasks) {
|
|
1123
|
-
processRouteMasks(this.options.routeMasks, processedTree)
|
|
1159
|
+
processRouteMasks(this.options.routeMasks, result.processedTree)
|
|
1124
1160
|
}
|
|
1125
1161
|
|
|
1162
|
+
return result
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
setRoutes({
|
|
1166
|
+
routesById,
|
|
1167
|
+
routesByPath,
|
|
1168
|
+
processedTree,
|
|
1169
|
+
}: ProcessRouteTreeResult<TRouteTree>) {
|
|
1126
1170
|
this.routesById = routesById as RoutesById<TRouteTree>
|
|
1127
1171
|
this.routesByPath = routesByPath as RoutesByPath<TRouteTree>
|
|
1128
1172
|
this.processedTree = processedTree
|
|
@@ -1222,8 +1266,6 @@ export class RouterCore<
|
|
|
1222
1266
|
return location
|
|
1223
1267
|
}
|
|
1224
1268
|
|
|
1225
|
-
resolvePathCache = createLRUCache<string, string>(1000)
|
|
1226
|
-
|
|
1227
1269
|
/** Resolve a path against the router basepath and trailing-slash policy. */
|
|
1228
1270
|
resolvePathWithBase = (from: string, path: string) => {
|
|
1229
1271
|
const resolvedPath = resolvePath({
|
|
@@ -1390,35 +1432,19 @@ export class RouterCore<
|
|
|
1390
1432
|
let paramsError: unknown = undefined
|
|
1391
1433
|
|
|
1392
1434
|
if (!existingMatch) {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1435
|
+
try {
|
|
1436
|
+
extractStrictParams(route, usedParams, parsedParams!, strictParams)
|
|
1437
|
+
} catch (err: any) {
|
|
1438
|
+
if (isNotFound(err) || isRedirect(err)) {
|
|
1439
|
+
paramsError = err
|
|
1440
|
+
} else {
|
|
1441
|
+
paramsError = new PathParamError(err.message, {
|
|
1442
|
+
cause: err,
|
|
1443
|
+
})
|
|
1398
1444
|
}
|
|
1399
|
-
} else {
|
|
1400
|
-
const strictParseParams =
|
|
1401
|
-
route.options.params?.parse ?? route.options.parseParams
|
|
1402
1445
|
|
|
1403
|
-
if (
|
|
1404
|
-
|
|
1405
|
-
Object.assign(
|
|
1406
|
-
strictParams,
|
|
1407
|
-
strictParseParams(strictParams as Record<string, string>),
|
|
1408
|
-
)
|
|
1409
|
-
} catch (err: any) {
|
|
1410
|
-
if (isNotFound(err) || isRedirect(err)) {
|
|
1411
|
-
paramsError = err
|
|
1412
|
-
} else {
|
|
1413
|
-
paramsError = new PathParamError(err.message, {
|
|
1414
|
-
cause: err,
|
|
1415
|
-
})
|
|
1416
|
-
}
|
|
1417
|
-
|
|
1418
|
-
if (opts?.throwOnError) {
|
|
1419
|
-
throw paramsError
|
|
1420
|
-
}
|
|
1421
|
-
}
|
|
1446
|
+
if (opts?.throwOnError) {
|
|
1447
|
+
throw paramsError
|
|
1422
1448
|
}
|
|
1423
1449
|
}
|
|
1424
1450
|
}
|
|
@@ -1519,7 +1545,7 @@ export class RouterCore<
|
|
|
1519
1545
|
|
|
1520
1546
|
// only execute `context` if we are not calling from router.buildLocation
|
|
1521
1547
|
|
|
1522
|
-
if (!existingMatch
|
|
1548
|
+
if (!existingMatch) {
|
|
1523
1549
|
const parentMatch = matches[index - 1]
|
|
1524
1550
|
const parentContext = getParentContext(parentMatch)
|
|
1525
1551
|
|
|
@@ -1563,6 +1589,80 @@ export class RouterCore<
|
|
|
1563
1589
|
})
|
|
1564
1590
|
}
|
|
1565
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* Lightweight route matching for buildLocation.
|
|
1594
|
+
* Only computes fullPath, accumulated search, and params - skipping expensive
|
|
1595
|
+
* operations like AbortController, ControlledPromise, loaderDeps, and full match objects.
|
|
1596
|
+
*/
|
|
1597
|
+
private matchRoutesLightweight(location: ParsedLocation): {
|
|
1598
|
+
matchedRoutes: ReadonlyArray<AnyRoute>
|
|
1599
|
+
fullPath: string
|
|
1600
|
+
search: Record<string, unknown>
|
|
1601
|
+
params: Record<string, unknown>
|
|
1602
|
+
} {
|
|
1603
|
+
const { matchedRoutes, routeParams, parsedParams } = this.getMatchedRoutes(
|
|
1604
|
+
location.pathname,
|
|
1605
|
+
)
|
|
1606
|
+
const lastRoute = last(matchedRoutes)!
|
|
1607
|
+
|
|
1608
|
+
// I don't know if we should run the full search middleware chain, or just validateSearch
|
|
1609
|
+
// // Accumulate search validation through the route chain
|
|
1610
|
+
// const accumulatedSearch: Record<string, unknown> = applySearchMiddleware({
|
|
1611
|
+
// search: { ...location.search },
|
|
1612
|
+
// dest: location,
|
|
1613
|
+
// destRoutes: matchedRoutes,
|
|
1614
|
+
// _includeValidateSearch: true,
|
|
1615
|
+
// })
|
|
1616
|
+
|
|
1617
|
+
// Accumulate search validation through route chain
|
|
1618
|
+
const accumulatedSearch = { ...location.search }
|
|
1619
|
+
for (const route of matchedRoutes) {
|
|
1620
|
+
try {
|
|
1621
|
+
Object.assign(
|
|
1622
|
+
accumulatedSearch,
|
|
1623
|
+
validateSearch(route.options.validateSearch, accumulatedSearch),
|
|
1624
|
+
)
|
|
1625
|
+
} catch {
|
|
1626
|
+
// Ignore errors, we're not actually routing
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// Determine params: reuse from state if possible, otherwise parse
|
|
1631
|
+
const lastStateMatch = last(this.state.matches)
|
|
1632
|
+
const canReuseParams =
|
|
1633
|
+
lastStateMatch &&
|
|
1634
|
+
lastStateMatch.routeId === lastRoute.id &&
|
|
1635
|
+
location.pathname === this.state.location.pathname
|
|
1636
|
+
|
|
1637
|
+
let params: Record<string, unknown>
|
|
1638
|
+
if (canReuseParams) {
|
|
1639
|
+
params = lastStateMatch.params
|
|
1640
|
+
} else {
|
|
1641
|
+
// Parse params through the route chain
|
|
1642
|
+
const strictParams: Record<string, unknown> = { ...routeParams }
|
|
1643
|
+
for (const route of matchedRoutes) {
|
|
1644
|
+
try {
|
|
1645
|
+
extractStrictParams(
|
|
1646
|
+
route,
|
|
1647
|
+
routeParams,
|
|
1648
|
+
parsedParams ?? {},
|
|
1649
|
+
strictParams,
|
|
1650
|
+
)
|
|
1651
|
+
} catch {
|
|
1652
|
+
// Ignore errors, we're not actually routing
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
params = strictParams
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
return {
|
|
1659
|
+
matchedRoutes,
|
|
1660
|
+
fullPath: lastRoute.fullPath,
|
|
1661
|
+
search: accumulatedSearch,
|
|
1662
|
+
params,
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1566
1666
|
cancelMatch = (id: string) => {
|
|
1567
1667
|
const match = this.getMatch(id)
|
|
1568
1668
|
|
|
@@ -1607,13 +1707,9 @@ export class RouterCore<
|
|
|
1607
1707
|
const currentLocation =
|
|
1608
1708
|
dest._fromLocation || this.pendingBuiltLocation || this.latestLocation
|
|
1609
1709
|
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
// Now let's find the starting pathname
|
|
1615
|
-
// This should default to the current location if no from is provided
|
|
1616
|
-
const lastMatch = last(allCurrentLocationMatches)!
|
|
1710
|
+
// Use lightweight matching - only computes what buildLocation needs
|
|
1711
|
+
// (fullPath, search, params) without creating full match objects
|
|
1712
|
+
const lightweightResult = this.matchRoutesLightweight(currentLocation)
|
|
1617
1713
|
|
|
1618
1714
|
// check that from path exists in the current route tree
|
|
1619
1715
|
// do this check only on navigations during test or development
|
|
@@ -1624,12 +1720,12 @@ export class RouterCore<
|
|
|
1624
1720
|
) {
|
|
1625
1721
|
const allFromMatches = this.getMatchedRoutes(dest.from).matchedRoutes
|
|
1626
1722
|
|
|
1627
|
-
const matchedFrom = findLast(
|
|
1723
|
+
const matchedFrom = findLast(lightweightResult.matchedRoutes, (d) => {
|
|
1628
1724
|
return comparePaths(d.fullPath, dest.from!)
|
|
1629
1725
|
})
|
|
1630
1726
|
|
|
1631
1727
|
const matchedCurrent = findLast(allFromMatches, (d) => {
|
|
1632
|
-
return comparePaths(d.fullPath,
|
|
1728
|
+
return comparePaths(d.fullPath, lightweightResult.fullPath)
|
|
1633
1729
|
})
|
|
1634
1730
|
|
|
1635
1731
|
// for from to be invalid it shouldn't just be unmatched to currentLocation
|
|
@@ -1642,15 +1738,15 @@ export class RouterCore<
|
|
|
1642
1738
|
const defaultedFromPath =
|
|
1643
1739
|
dest.unsafeRelative === 'path'
|
|
1644
1740
|
? currentLocation.pathname
|
|
1645
|
-
: (dest.from ??
|
|
1741
|
+
: (dest.from ?? lightweightResult.fullPath)
|
|
1646
1742
|
|
|
1647
1743
|
// ensure this includes the basePath if set
|
|
1648
1744
|
const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
|
|
1649
1745
|
|
|
1650
1746
|
// From search should always use the current location
|
|
1651
|
-
const fromSearch =
|
|
1747
|
+
const fromSearch = lightweightResult.search
|
|
1652
1748
|
// Same with params. It can't hurt to provide as many as possible
|
|
1653
|
-
const fromParams = { ...
|
|
1749
|
+
const fromParams = { ...lightweightResult.params }
|
|
1654
1750
|
|
|
1655
1751
|
// Resolve the next to
|
|
1656
1752
|
// ensure this includes the basePath if set
|
|
@@ -2799,7 +2895,7 @@ function applySearchMiddleware({
|
|
|
2799
2895
|
_includeValidateSearch,
|
|
2800
2896
|
}: {
|
|
2801
2897
|
search: any
|
|
2802
|
-
dest:
|
|
2898
|
+
dest: { search?: unknown }
|
|
2803
2899
|
destRoutes: ReadonlyArray<AnyRoute>
|
|
2804
2900
|
_includeValidateSearch: boolean | undefined
|
|
2805
2901
|
}) {
|
|
@@ -2934,3 +3030,25 @@ function findGlobalNotFoundRouteId(
|
|
|
2934
3030
|
}
|
|
2935
3031
|
return rootRouteId
|
|
2936
3032
|
}
|
|
3033
|
+
|
|
3034
|
+
function extractStrictParams(
|
|
3035
|
+
route: AnyRoute,
|
|
3036
|
+
referenceParams: Record<string, unknown>,
|
|
3037
|
+
parsedParams: Record<string, unknown>,
|
|
3038
|
+
accumulatedParams: Record<string, unknown>,
|
|
3039
|
+
) {
|
|
3040
|
+
const parseParams = route.options.params?.parse ?? route.options.parseParams
|
|
3041
|
+
if (parseParams) {
|
|
3042
|
+
if (route.options.skipRouteOnParseError) {
|
|
3043
|
+
// Use pre-parsed params from route matching for skipRouteOnParseError routes
|
|
3044
|
+
for (const key in referenceParams) {
|
|
3045
|
+
if (key in parsedParams) {
|
|
3046
|
+
accumulatedParams[key] = parsedParams[key]
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
} else {
|
|
3050
|
+
const result = parseParams(accumulatedParams as Record<string, string>)
|
|
3051
|
+
Object.assign(accumulatedParams, result)
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
}
|