@tanstack/router-core 1.121.0-alpha.3 → 1.121.0-alpha.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
- "version": "1.121.0-alpha.3",
3
+ "version": "1.121.0-alpha.5",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
package/src/index.ts CHANGED
@@ -243,7 +243,6 @@ export type {
243
243
  ControllablePromise,
244
244
  InjectedHtmlEntry,
245
245
  RouterErrorSerializer,
246
- MatchedRoutesResult,
247
246
  EmitFn,
248
247
  LoadFn,
249
248
  GetMatchFn,
package/src/link.ts CHANGED
@@ -424,7 +424,7 @@ export type ToSubOptionsProps<
424
424
  hash?: true | Updater<string>
425
425
  state?: true | NonNullableUpdater<ParsedHistoryState, HistoryState>
426
426
  from?: FromPathOption<TRouter, TFrom> & {}
427
- relative?: 'route' | 'path'
427
+ unsafeRelative?: 'route' | 'path'
428
428
  }
429
429
 
430
430
  export type ParamsReducerFn<
package/src/redirect.ts CHANGED
@@ -64,6 +64,14 @@ export function redirect<
64
64
  opts: RedirectOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo>,
65
65
  ): Redirect<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> {
66
66
  opts.statusCode = opts.statusCode || opts.code || 307
67
+
68
+ if (!opts.reloadDocument) {
69
+ try {
70
+ new URL(`${opts.href}`)
71
+ opts.reloadDocument = true
72
+ } catch {}
73
+ }
74
+
67
75
  const headers = new Headers(opts.headers || {})
68
76
 
69
77
  const response = new Response(null, {
package/src/router.ts CHANGED
@@ -521,11 +521,6 @@ export interface RouterErrorSerializer<TSerializedError> {
521
521
  deserialize: (err: TSerializedError) => unknown
522
522
  }
523
523
 
524
- export interface MatchedRoutesResult {
525
- matchedRoutes: Array<AnyRoute>
526
- routeParams: Record<string, string>
527
- }
528
-
529
524
  export type PreloadRouteFn<
530
525
  TRouteTree extends AnyRoute,
531
526
  TTrailingSlashOption extends TrailingSlashOption,
@@ -1071,9 +1066,9 @@ export class RouterCore<
1071
1066
  } as ParsedLocation,
1072
1067
  opts,
1073
1068
  )
1074
- } else {
1075
- return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
1076
1069
  }
1070
+
1071
+ return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
1077
1072
  }
1078
1073
 
1079
1074
  private matchRoutesInternal(
@@ -1334,7 +1329,8 @@ export class RouterCore<
1334
1329
  const route = this.looseRoutesById[match.routeId]!
1335
1330
  const existingMatch = this.getMatch(match.id)
1336
1331
 
1337
- // only execute `context` if we are not just building a location
1332
+ // only execute `context` if we are not calling from router.buildLocation
1333
+
1338
1334
  if (!existingMatch && opts?._buildLocation !== true) {
1339
1335
  const parentMatch = matches[index - 1]
1340
1336
  const parentContext = getParentContext(parentMatch)
@@ -1403,75 +1399,64 @@ export class RouterCore<
1403
1399
  dest: BuildNextOptions & {
1404
1400
  unmaskOnReload?: boolean
1405
1401
  } = {},
1406
- matchedRoutesResult?: MatchedRoutesResult,
1407
1402
  ): ParsedLocation => {
1408
- const fromMatches = dest._fromLocation
1409
- ? this.matchRoutes(dest._fromLocation, { _buildLocation: true })
1410
- : this.state.matches
1411
-
1412
- const fromMatch =
1413
- dest.from != null
1414
- ? fromMatches.find((d) =>
1415
- matchPathname(this.basepath, trimPathRight(d.pathname), {
1416
- to: dest.from,
1417
- caseSensitive: false,
1418
- fuzzy: false,
1419
- }),
1420
- )
1421
- : undefined
1403
+ // We allow the caller to override the current location
1404
+ const currentLocation = dest._fromLocation || this.latestLocation
1422
1405
 
1423
- const fromPath = fromMatch?.pathname || this.latestLocation.pathname
1406
+ const allFromMatches = this.matchRoutes(currentLocation, {
1407
+ _buildLocation: true,
1408
+ })
1424
1409
 
1425
- invariant(
1426
- dest.from == null || fromMatch != null,
1427
- 'Could not find match for from: ' + dest.from,
1428
- )
1410
+ const lastMatch = last(allFromMatches)!
1429
1411
 
1430
- const fromSearch = this.state.pendingMatches?.length
1431
- ? last(this.state.pendingMatches)?.search
1432
- : last(fromMatches)?.search || this.latestLocation.search
1412
+ // First let's find the starting pathname
1413
+ // By default, start with the current location
1414
+ let fromId = lastMatch.fullPath
1433
1415
 
1434
- const stayingMatches = matchedRoutesResult?.matchedRoutes.filter((d) =>
1435
- fromMatches.find((e) => e.routeId === d.id),
1436
- )
1437
- let pathname: string
1438
- if (dest.to) {
1439
- const resolvePathTo =
1440
- fromMatch?.fullPath ||
1441
- last(fromMatches)?.fullPath ||
1442
- this.latestLocation.pathname
1443
- pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`)
1444
- } else {
1445
- const fromRouteByFromPathRouteId =
1446
- this.routesById[
1447
- stayingMatches?.find((route) => {
1448
- const interpolatedPath = interpolatePath({
1449
- path: route.fullPath,
1450
- params: matchedRoutesResult?.routeParams ?? {},
1451
- decodeCharMap: this.pathParamsDecodeCharMap,
1452
- }).interpolatedPath
1453
- const pathname = joinPaths([this.basepath, interpolatedPath])
1454
- return pathname === fromPath
1455
- })?.id as keyof this['routesById']
1456
- ]
1457
- pathname = this.resolvePathWithBase(
1458
- fromPath,
1459
- fromRouteByFromPathRouteId?.to ?? fromPath,
1460
- )
1416
+ // If there is a to, it means we are changing the path in some way
1417
+ // So we need to find the relative fromPath
1418
+ if (dest.to && dest.from) {
1419
+ fromId = dest.from
1420
+ }
1421
+
1422
+ const existingFrom = [...allFromMatches].reverse().find((d) => {
1423
+ return d.fullPath === fromId || d.fullPath === joinPaths([fromId, '/'])
1424
+ })
1425
+
1426
+ if (!existingFrom) {
1427
+ console.warn(`Could not find match for from: ${dest.from}`)
1461
1428
  }
1462
1429
 
1463
- const prevParams = { ...last(fromMatches)?.params }
1430
+ // From search should always use the current location
1431
+ const fromSearch = lastMatch.search
1432
+ // Same with params. It can't hurt to provide as many as possible
1433
+ const fromParams = { ...lastMatch.params }
1434
+
1435
+ // Resolve the next to
1436
+ const nextTo = dest.to
1437
+ ? this.resolvePathWithBase(fromId, `${dest.to}`)
1438
+ : fromId
1464
1439
 
1440
+ // Resolve the next params
1465
1441
  let nextParams =
1466
1442
  (dest.params ?? true) === true
1467
- ? prevParams
1443
+ ? fromParams
1468
1444
  : {
1469
- ...prevParams,
1470
- ...functionalUpdate(dest.params as any, prevParams),
1445
+ ...fromParams,
1446
+ ...functionalUpdate(dest.params as any, fromParams),
1471
1447
  }
1472
1448
 
1449
+ const destRoutes = this.matchRoutes(
1450
+ nextTo,
1451
+ {},
1452
+ {
1453
+ _buildLocation: true,
1454
+ },
1455
+ ).map((d) => this.looseRoutesById[d.routeId]!)
1456
+
1457
+ // If there are any params, we need to stringify them
1473
1458
  if (Object.keys(nextParams).length > 0) {
1474
- matchedRoutesResult?.matchedRoutes
1459
+ destRoutes
1475
1460
  .map((route) => {
1476
1461
  return (
1477
1462
  route.options.params?.stringify ?? route.options.stringifyParams
@@ -1483,25 +1468,27 @@ export class RouterCore<
1483
1468
  })
1484
1469
  }
1485
1470
 
1486
- pathname = interpolatePath({
1487
- path: pathname,
1471
+ // Interpolate the next to into the next pathname
1472
+ const nextPathname = interpolatePath({
1473
+ path: nextTo,
1488
1474
  params: nextParams ?? {},
1489
1475
  leaveWildcards: false,
1490
1476
  leaveParams: opts.leaveParams,
1491
1477
  decodeCharMap: this.pathParamsDecodeCharMap,
1492
1478
  }).interpolatedPath
1493
1479
 
1494
- let search = fromSearch
1480
+ // Resolve the next search
1481
+ let nextSearch = fromSearch
1495
1482
  if (opts._includeValidateSearch && this.options.search?.strict) {
1496
1483
  let validatedSearch = {}
1497
- matchedRoutesResult?.matchedRoutes.forEach((route) => {
1484
+ destRoutes.forEach((route) => {
1498
1485
  try {
1499
1486
  if (route.options.validateSearch) {
1500
1487
  validatedSearch = {
1501
1488
  ...validatedSearch,
1502
1489
  ...(validateSearch(route.options.validateSearch, {
1503
1490
  ...validatedSearch,
1504
- ...search,
1491
+ ...nextSearch,
1505
1492
  }) ?? {}),
1506
1493
  }
1507
1494
  }
@@ -1509,137 +1496,52 @@ export class RouterCore<
1509
1496
  // ignore errors here because they are already handled in matchRoutes
1510
1497
  }
1511
1498
  })
1512
- search = validatedSearch
1499
+ nextSearch = validatedSearch
1513
1500
  }
1514
1501
 
1515
- const applyMiddlewares = (search: any) => {
1516
- const allMiddlewares =
1517
- matchedRoutesResult?.matchedRoutes.reduce(
1518
- (acc, route) => {
1519
- const middlewares: Array<SearchMiddleware<any>> = []
1520
- if ('search' in route.options) {
1521
- if (route.options.search?.middlewares) {
1522
- middlewares.push(...route.options.search.middlewares)
1523
- }
1524
- }
1525
- // TODO remove preSearchFilters and postSearchFilters in v2
1526
- else if (
1527
- route.options.preSearchFilters ||
1528
- route.options.postSearchFilters
1529
- ) {
1530
- const legacyMiddleware: SearchMiddleware<any> = ({
1531
- search,
1532
- next,
1533
- }) => {
1534
- let nextSearch = search
1535
- if (
1536
- 'preSearchFilters' in route.options &&
1537
- route.options.preSearchFilters
1538
- ) {
1539
- nextSearch = route.options.preSearchFilters.reduce(
1540
- (prev, next) => next(prev),
1541
- search,
1542
- )
1543
- }
1544
- const result = next(nextSearch)
1545
- if (
1546
- 'postSearchFilters' in route.options &&
1547
- route.options.postSearchFilters
1548
- ) {
1549
- return route.options.postSearchFilters.reduce(
1550
- (prev, next) => next(prev),
1551
- result,
1552
- )
1553
- }
1554
- return result
1555
- }
1556
- middlewares.push(legacyMiddleware)
1557
- }
1558
- if (opts._includeValidateSearch && route.options.validateSearch) {
1559
- const validate: SearchMiddleware<any> = ({ search, next }) => {
1560
- const result = next(search)
1561
- try {
1562
- const validatedSearch = {
1563
- ...result,
1564
- ...(validateSearch(
1565
- route.options.validateSearch,
1566
- result,
1567
- ) ?? {}),
1568
- }
1569
- return validatedSearch
1570
- } catch {
1571
- // ignore errors here because they are already handled in matchRoutes
1572
- return result
1573
- }
1574
- }
1575
- middlewares.push(validate)
1576
- }
1577
- return acc.concat(middlewares)
1578
- },
1579
- [] as Array<SearchMiddleware<any>>,
1580
- ) ?? []
1581
-
1582
- // the chain ends here since `next` is not called
1583
- const final: SearchMiddleware<any> = ({ search }) => {
1584
- if (!dest.search) {
1585
- return {}
1586
- }
1587
- if (dest.search === true) {
1588
- return search
1589
- }
1590
- return functionalUpdate(dest.search, search)
1591
- }
1592
- allMiddlewares.push(final)
1593
-
1594
- const applyNext = (index: number, currentSearch: any): any => {
1595
- // no more middlewares left, return the current search
1596
- if (index >= allMiddlewares.length) {
1597
- return currentSearch
1598
- }
1599
-
1600
- const middleware = allMiddlewares[index]!
1601
-
1602
- const next = (newSearch: any): any => {
1603
- return applyNext(index + 1, newSearch)
1604
- }
1605
-
1606
- return middleware({ search: currentSearch, next })
1607
- }
1608
-
1609
- // Start applying middlewares
1610
- return applyNext(0, search)
1611
- }
1502
+ nextSearch = applySearchMiddleware({
1503
+ search: nextSearch,
1504
+ dest,
1505
+ destRoutes,
1506
+ _includeValidateSearch: opts._includeValidateSearch,
1507
+ })
1612
1508
 
1613
- search = applyMiddlewares(search)
1509
+ // Replace the equal deep
1510
+ nextSearch = replaceEqualDeep(fromSearch, nextSearch)
1614
1511
 
1615
- search = replaceEqualDeep(fromSearch, search)
1616
- const searchStr = this.options.stringifySearch(search)
1512
+ // Stringify the next search
1513
+ const searchStr = this.options.stringifySearch(nextSearch)
1617
1514
 
1515
+ // Resolve the next hash
1618
1516
  const hash =
1619
1517
  dest.hash === true
1620
- ? this.latestLocation.hash
1518
+ ? currentLocation.hash
1621
1519
  : dest.hash
1622
- ? functionalUpdate(dest.hash, this.latestLocation.hash)
1520
+ ? functionalUpdate(dest.hash, currentLocation.hash)
1623
1521
  : undefined
1624
1522
 
1523
+ // Resolve the next hash string
1625
1524
  const hashStr = hash ? `#${hash}` : ''
1626
1525
 
1526
+ // Resolve the next state
1627
1527
  let nextState =
1628
1528
  dest.state === true
1629
- ? this.latestLocation.state
1529
+ ? currentLocation.state
1630
1530
  : dest.state
1631
- ? functionalUpdate(dest.state, this.latestLocation.state)
1531
+ ? functionalUpdate(dest.state, currentLocation.state)
1632
1532
  : {}
1633
1533
 
1634
- nextState = replaceEqualDeep(this.latestLocation.state, nextState)
1534
+ // Replace the equal deep
1535
+ nextState = replaceEqualDeep(currentLocation.state, nextState)
1635
1536
 
1537
+ // Return the next location
1636
1538
  return {
1637
- pathname,
1638
- search,
1539
+ pathname: nextPathname,
1540
+ search: nextSearch,
1639
1541
  searchStr,
1640
1542
  state: nextState as any,
1641
1543
  hash: hash ?? '',
1642
- href: `${pathname}${searchStr}${hashStr}`,
1544
+ href: `${nextPathname}${searchStr}${hashStr}`,
1643
1545
  unmaskOnReload: dest.unmaskOnReload,
1644
1546
  }
1645
1547
  }
@@ -1649,6 +1551,7 @@ export class RouterCore<
1649
1551
  maskedDest?: BuildNextOptions,
1650
1552
  ) => {
1651
1553
  const next = build(dest)
1554
+
1652
1555
  let maskedNext = maskedDest ? build(maskedDest) : undefined
1653
1556
 
1654
1557
  if (!maskedNext) {
@@ -1680,22 +1583,12 @@ export class RouterCore<
1680
1583
  }
1681
1584
  }
1682
1585
 
1683
- const nextMatches = this.getMatchedRoutes(
1684
- next.pathname,
1685
- dest.to as string,
1686
- )
1687
- const final = build(dest, nextMatches)
1688
-
1689
1586
  if (maskedNext) {
1690
- const maskedMatches = this.getMatchedRoutes(
1691
- maskedNext.pathname,
1692
- maskedDest?.to as string,
1693
- )
1694
- const maskedFinal = build(maskedDest, maskedMatches)
1695
- final.maskedLocation = maskedFinal
1587
+ const maskedFinal = build(maskedDest)
1588
+ next.maskedLocation = maskedFinal
1696
1589
  }
1697
1590
 
1698
- return final
1591
+ return next
1699
1592
  }
1700
1593
 
1701
1594
  if (opts.mask) {
@@ -2873,6 +2766,7 @@ export class RouterCore<
2873
2766
  if (err.options.reloadDocument) {
2874
2767
  return undefined
2875
2768
  }
2769
+
2876
2770
  return await this.preloadRoute({
2877
2771
  ...err.options,
2878
2772
  _fromLocation: next,
@@ -3329,3 +3223,117 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3329
3223
 
3330
3224
  return { matchedRoutes, routeParams, foundRoute }
3331
3225
  }
3226
+
3227
+ function applySearchMiddleware({
3228
+ search,
3229
+ dest,
3230
+ destRoutes,
3231
+ _includeValidateSearch,
3232
+ }: {
3233
+ search: any
3234
+ dest: BuildNextOptions
3235
+ destRoutes: Array<AnyRoute>
3236
+ _includeValidateSearch: boolean | undefined
3237
+ }) {
3238
+ const allMiddlewares =
3239
+ destRoutes.reduce(
3240
+ (acc, route) => {
3241
+ const middlewares: Array<SearchMiddleware<any>> = []
3242
+
3243
+ if ('search' in route.options) {
3244
+ if (route.options.search?.middlewares) {
3245
+ middlewares.push(...route.options.search.middlewares)
3246
+ }
3247
+ }
3248
+ // TODO remove preSearchFilters and postSearchFilters in v2
3249
+ else if (
3250
+ route.options.preSearchFilters ||
3251
+ route.options.postSearchFilters
3252
+ ) {
3253
+ const legacyMiddleware: SearchMiddleware<any> = ({
3254
+ search,
3255
+ next,
3256
+ }) => {
3257
+ let nextSearch = search
3258
+
3259
+ if (
3260
+ 'preSearchFilters' in route.options &&
3261
+ route.options.preSearchFilters
3262
+ ) {
3263
+ nextSearch = route.options.preSearchFilters.reduce(
3264
+ (prev, next) => next(prev),
3265
+ search,
3266
+ )
3267
+ }
3268
+
3269
+ const result = next(nextSearch)
3270
+
3271
+ if (
3272
+ 'postSearchFilters' in route.options &&
3273
+ route.options.postSearchFilters
3274
+ ) {
3275
+ return route.options.postSearchFilters.reduce(
3276
+ (prev, next) => next(prev),
3277
+ result,
3278
+ )
3279
+ }
3280
+
3281
+ return result
3282
+ }
3283
+ middlewares.push(legacyMiddleware)
3284
+ }
3285
+
3286
+ if (_includeValidateSearch && route.options.validateSearch) {
3287
+ const validate: SearchMiddleware<any> = ({ search, next }) => {
3288
+ const result = next(search)
3289
+ try {
3290
+ const validatedSearch = {
3291
+ ...result,
3292
+ ...(validateSearch(route.options.validateSearch, result) ?? {}),
3293
+ }
3294
+ return validatedSearch
3295
+ } catch {
3296
+ // ignore errors here because they are already handled in matchRoutes
3297
+ return result
3298
+ }
3299
+ }
3300
+
3301
+ middlewares.push(validate)
3302
+ }
3303
+
3304
+ return acc.concat(middlewares)
3305
+ },
3306
+ [] as Array<SearchMiddleware<any>>,
3307
+ ) ?? []
3308
+
3309
+ // the chain ends here since `next` is not called
3310
+ const final: SearchMiddleware<any> = ({ search }) => {
3311
+ if (!dest.search) {
3312
+ return {}
3313
+ }
3314
+ if (dest.search === true) {
3315
+ return search
3316
+ }
3317
+ return functionalUpdate(dest.search, search)
3318
+ }
3319
+
3320
+ allMiddlewares.push(final)
3321
+
3322
+ const applyNext = (index: number, currentSearch: any): any => {
3323
+ // no more middlewares left, return the current search
3324
+ if (index >= allMiddlewares.length) {
3325
+ return currentSearch
3326
+ }
3327
+
3328
+ const middleware = allMiddlewares[index]!
3329
+
3330
+ const next = (newSearch: any): any => {
3331
+ return applyNext(index + 1, newSearch)
3332
+ }
3333
+
3334
+ return middleware({ search: currentSearch, next })
3335
+ }
3336
+
3337
+ // Start applying middlewares
3338
+ return applyNext(0, search)
3339
+ }