@tanstack/router-core 1.168.17 → 1.169.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.
Files changed (109) hide show
  1. package/dist/cjs/Matches.cjs +1 -1
  2. package/dist/cjs/Matches.cjs.map +1 -1
  3. package/dist/cjs/config.cjs +1 -1
  4. package/dist/cjs/config.cjs.map +1 -1
  5. package/dist/cjs/defer.cjs +1 -1
  6. package/dist/cjs/defer.cjs.map +1 -1
  7. package/dist/cjs/index.cjs +1 -0
  8. package/dist/cjs/index.d.cts +1 -1
  9. package/dist/cjs/isServer/client.cjs +1 -1
  10. package/dist/cjs/isServer/client.cjs.map +1 -1
  11. package/dist/cjs/isServer/development.cjs +1 -1
  12. package/dist/cjs/isServer/development.cjs.map +1 -1
  13. package/dist/cjs/isServer/server.cjs +1 -1
  14. package/dist/cjs/isServer/server.cjs.map +1 -1
  15. package/dist/cjs/link.cjs +1 -1
  16. package/dist/cjs/link.cjs.map +1 -1
  17. package/dist/cjs/load-matches.cjs +19 -19
  18. package/dist/cjs/load-matches.cjs.map +1 -1
  19. package/dist/cjs/new-process-route-tree.cjs +36 -48
  20. package/dist/cjs/new-process-route-tree.cjs.map +1 -1
  21. package/dist/cjs/new-process-route-tree.d.cts +4 -26
  22. package/dist/cjs/path.cjs +1 -22
  23. package/dist/cjs/path.cjs.map +1 -1
  24. package/dist/cjs/root.cjs +1 -1
  25. package/dist/cjs/root.cjs.map +1 -1
  26. package/dist/cjs/route.cjs.map +1 -1
  27. package/dist/cjs/route.d.cts +1 -32
  28. package/dist/cjs/router.cjs +36 -21
  29. package/dist/cjs/router.cjs.map +1 -1
  30. package/dist/cjs/router.d.cts +3 -4
  31. package/dist/cjs/scroll-restoration-script/server.cjs +1 -1
  32. package/dist/cjs/scroll-restoration-script/server.cjs.map +1 -1
  33. package/dist/cjs/scroll-restoration.cjs +6 -6
  34. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  35. package/dist/cjs/searchParams.cjs +2 -2
  36. package/dist/cjs/searchParams.cjs.map +1 -1
  37. package/dist/cjs/ssr/constants.cjs +2 -2
  38. package/dist/cjs/ssr/constants.cjs.map +1 -1
  39. package/dist/cjs/ssr/serializer/RawStream.cjs +12 -12
  40. package/dist/cjs/ssr/serializer/RawStream.cjs.map +1 -1
  41. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs +1 -1
  42. package/dist/cjs/ssr/serializer/ShallowErrorPlugin.cjs.map +1 -1
  43. package/dist/cjs/ssr/serializer/seroval-plugins.cjs +1 -1
  44. package/dist/cjs/ssr/serializer/seroval-plugins.cjs.map +1 -1
  45. package/dist/cjs/ssr/ssr-server.cjs +9 -9
  46. package/dist/cjs/ssr/ssr-server.cjs.map +1 -1
  47. package/dist/cjs/ssr/transformStreamWithRouter.cjs +6 -6
  48. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -1
  49. package/dist/cjs/utils.cjs +12 -7
  50. package/dist/cjs/utils.cjs.map +1 -1
  51. package/dist/cjs/utils.d.cts +1 -0
  52. package/dist/esm/Matches.js +1 -1
  53. package/dist/esm/Matches.js.map +1 -1
  54. package/dist/esm/config.js +1 -1
  55. package/dist/esm/config.js.map +1 -1
  56. package/dist/esm/defer.js +1 -1
  57. package/dist/esm/defer.js.map +1 -1
  58. package/dist/esm/index.d.ts +1 -1
  59. package/dist/esm/index.js +2 -2
  60. package/dist/esm/isServer/client.js +1 -1
  61. package/dist/esm/isServer/client.js.map +1 -1
  62. package/dist/esm/isServer/development.js +1 -1
  63. package/dist/esm/isServer/development.js.map +1 -1
  64. package/dist/esm/isServer/server.js +1 -1
  65. package/dist/esm/isServer/server.js.map +1 -1
  66. package/dist/esm/link.js +1 -1
  67. package/dist/esm/link.js.map +1 -1
  68. package/dist/esm/load-matches.js +19 -19
  69. package/dist/esm/load-matches.js.map +1 -1
  70. package/dist/esm/new-process-route-tree.d.ts +4 -26
  71. package/dist/esm/new-process-route-tree.js +36 -49
  72. package/dist/esm/new-process-route-tree.js.map +1 -1
  73. package/dist/esm/path.js +1 -22
  74. package/dist/esm/path.js.map +1 -1
  75. package/dist/esm/root.js +1 -1
  76. package/dist/esm/root.js.map +1 -1
  77. package/dist/esm/route.d.ts +1 -32
  78. package/dist/esm/route.js.map +1 -1
  79. package/dist/esm/router.d.ts +3 -4
  80. package/dist/esm/router.js +38 -23
  81. package/dist/esm/router.js.map +1 -1
  82. package/dist/esm/scroll-restoration-script/server.js +1 -1
  83. package/dist/esm/scroll-restoration-script/server.js.map +1 -1
  84. package/dist/esm/scroll-restoration.js +6 -6
  85. package/dist/esm/scroll-restoration.js.map +1 -1
  86. package/dist/esm/searchParams.js +2 -2
  87. package/dist/esm/searchParams.js.map +1 -1
  88. package/dist/esm/ssr/constants.js +2 -2
  89. package/dist/esm/ssr/constants.js.map +1 -1
  90. package/dist/esm/ssr/serializer/RawStream.js +10 -10
  91. package/dist/esm/ssr/serializer/RawStream.js.map +1 -1
  92. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js +1 -1
  93. package/dist/esm/ssr/serializer/ShallowErrorPlugin.js.map +1 -1
  94. package/dist/esm/ssr/serializer/seroval-plugins.js +1 -1
  95. package/dist/esm/ssr/serializer/seroval-plugins.js.map +1 -1
  96. package/dist/esm/ssr/ssr-server.js +9 -9
  97. package/dist/esm/ssr/ssr-server.js.map +1 -1
  98. package/dist/esm/ssr/transformStreamWithRouter.js +6 -6
  99. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -1
  100. package/dist/esm/utils.d.ts +1 -0
  101. package/dist/esm/utils.js +12 -8
  102. package/dist/esm/utils.js.map +1 -1
  103. package/package.json +1 -1
  104. package/src/index.ts +1 -0
  105. package/src/new-process-route-tree.ts +42 -77
  106. package/src/path.ts +1 -27
  107. package/src/route.ts +5 -34
  108. package/src/router.ts +76 -54
  109. package/src/utils.ts +7 -0
package/src/path.ts CHANGED
@@ -167,33 +167,7 @@ export function resolvePath({
167
167
  }
168
168
  }
169
169
 
170
- let segment
171
- let joined = ''
172
- for (let i = 0; i < baseSegments.length; i++) {
173
- if (i > 0) joined += '/'
174
- const part = baseSegments[i]!
175
- if (!part) continue
176
- segment = parseSegment(part, 0, segment)
177
- const kind = segment[0]
178
- if (kind === SEGMENT_TYPE_PATHNAME) {
179
- joined += part
180
- continue
181
- }
182
- const end = segment[5]
183
- const prefix = part.substring(0, segment[1])
184
- const suffix = part.substring(segment[4], end)
185
- const value = part.substring(segment[2], segment[3])
186
- if (kind === SEGMENT_TYPE_PARAM) {
187
- joined += prefix || suffix ? `${prefix}{$${value}}${suffix}` : `$${value}`
188
- } else if (kind === SEGMENT_TYPE_WILDCARD) {
189
- joined += prefix || suffix ? `${prefix}{$}${suffix}` : '$'
190
- } else {
191
- // SEGMENT_TYPE_OPTIONAL_PARAM
192
- joined += `${prefix}{-$${value}}${suffix}`
193
- }
194
- }
195
- joined = cleanPath(joined)
196
- const result = joined || '/'
170
+ const result = cleanPath(baseSegments.join('/')) || '/'
197
171
  if (key && cache) cache.set(key, result)
198
172
  return result
199
173
  }
package/src/route.ts CHANGED
@@ -173,9 +173,11 @@ export type ResolveParams<
173
173
 
174
174
  export type ParseParamsFn<in out TPath extends string, in out TParams> = (
175
175
  rawParams: Expand<ResolveParams<TPath>>,
176
- ) => TParams extends ResolveParams<TPath, any>
177
- ? TParams
178
- : ResolveParams<TPath, any>
176
+ ) =>
177
+ | (TParams extends ResolveParams<TPath, any>
178
+ ? TParams
179
+ : ResolveParams<TPath, any>)
180
+ | false
179
181
 
180
182
  export type StringifyParamsFn<in out TPath extends string, in out TParams> = (
181
183
  params: TParams,
@@ -1248,37 +1250,6 @@ export interface UpdatableRouteOptions<
1248
1250
  in out TBeforeLoadFn,
1249
1251
  >
1250
1252
  extends UpdatableStaticRouteOption, UpdatableRouteOptionsExtensions {
1251
- /**
1252
- * Options to control route matching behavior with runtime code.
1253
- *
1254
- * @experimental 🚧 this feature is subject to change
1255
- *
1256
- * @link https://tanstack.com/router/latest/docs/framework/react/api/router/RouteOptionsType
1257
- */
1258
- skipRouteOnParseError?: {
1259
- /**
1260
- * If `true`, skip this route during matching if `params.parse` fails.
1261
- *
1262
- * Without this option, a `/$param` route could match *any* value for `param`,
1263
- * and only later during the route lifecycle would `params.parse` run and potentially
1264
- * show the `errorComponent` if validation failed.
1265
- *
1266
- * With this option enabled, the route will only match if `params.parse` succeeds.
1267
- * If it fails, the router will continue trying to match other routes, potentially
1268
- * finding a different route that works, or ultimately showing the `notFoundComponent`.
1269
- *
1270
- * @default false
1271
- */
1272
- params?: boolean
1273
- /**
1274
- * In cases where multiple routes would need to run `params.parse` during matching
1275
- * to determine which route to pick, this priority number can be used as a tie-breaker
1276
- * for which route to try first. Higher number = higher priority.
1277
- *
1278
- * @default 0
1279
- */
1280
- priority?: number
1281
- }
1282
1253
  /**
1283
1254
  * If true, this route will be matched as case-sensitive
1284
1255
  *
package/src/router.ts CHANGED
@@ -8,12 +8,14 @@ import {
8
8
  encodePathLikeUrl,
9
9
  findLast,
10
10
  functionalUpdate,
11
+ hasKeys,
11
12
  isDangerousProtocol,
12
13
  last,
13
14
  nullReplaceEqualDeep,
14
15
  replaceEqualDeep,
15
16
  } from './utils'
16
17
  import {
18
+ buildRouteBranch,
17
19
  findFlatMatch,
18
20
  findRouteMatch,
19
21
  findSingleMatch,
@@ -732,8 +734,6 @@ export type GetMatchRoutesFn = (pathname: string) => {
732
734
  matchedRoutes: ReadonlyArray<AnyRoute>
733
735
  /** exhaustive params, still in their string form */
734
736
  routeParams: Record<string, string>
735
- /** partial params, parsed from routeParams during matching */
736
- parsedParams: Record<string, unknown> | undefined
737
737
  foundRoute: AnyRoute | undefined
738
738
  parseError?: unknown
739
739
  }
@@ -972,6 +972,7 @@ export class RouterCore<
972
972
  routesByPath!: RoutesByPath<TRouteTree>
973
973
  processedTree!: ProcessedTree<TRouteTree, any, any>
974
974
  resolvePathCache!: LRUCache<string, string>
975
+ private routeBranchCache = new WeakMap<AnyRoute, ReadonlyArray<AnyRoute>>()
975
976
  isServer!: boolean
976
977
  pathParamsDecoder?: (encoded: string) => string
977
978
  protocolAllowlist!: Set<string>
@@ -1343,15 +1344,23 @@ export class RouterCore<
1343
1344
  return location
1344
1345
  }
1345
1346
 
1346
- /** Resolve a path against the router basepath and trailing-slash policy. */
1347
+ /** Resolve a path using the router's trailing-slash policy. */
1347
1348
  resolvePathWithBase = (from: string, path: string) => {
1348
- const resolvedPath = resolvePath({
1349
+ return resolvePath({
1349
1350
  base: from,
1350
- to: cleanPath(path),
1351
+ to: path.includes('//') ? cleanPath(path) : path,
1351
1352
  trailingSlash: this.options.trailingSlash,
1352
1353
  cache: this.resolvePathCache,
1353
1354
  })
1354
- return resolvedPath
1355
+ }
1356
+
1357
+ private getRouteBranch(route: AnyRoute) {
1358
+ let branch = this.routeBranchCache.get(route)
1359
+ if (!branch) {
1360
+ branch = buildRouteBranch(route)
1361
+ this.routeBranchCache.set(route, branch)
1362
+ }
1363
+ return branch
1355
1364
  }
1356
1365
 
1357
1366
  get looseRoutesById() {
@@ -1391,7 +1400,7 @@ export class RouterCore<
1391
1400
  opts?: MatchRoutesOpts,
1392
1401
  ): Array<AnyRouteMatch> {
1393
1402
  const matchedRoutesResult = this.getMatchedRoutes(next.pathname)
1394
- const { foundRoute, routeParams, parsedParams } = matchedRoutesResult
1403
+ const { foundRoute, routeParams } = matchedRoutesResult
1395
1404
  let { matchedRoutes } = matchedRoutesResult
1396
1405
  let isGlobalNotFound = false
1397
1406
 
@@ -1517,7 +1526,7 @@ export class RouterCore<
1517
1526
 
1518
1527
  if (!existingMatch) {
1519
1528
  try {
1520
- extractStrictParams(route, usedParams, parsedParams!, strictParams)
1529
+ extractStrictParams(route, strictParams)
1521
1530
  } catch (err: any) {
1522
1531
  if (isNotFound(err) || isRedirect(err)) {
1523
1532
  paramsError = err
@@ -1687,7 +1696,7 @@ export class RouterCore<
1687
1696
  search: Record<string, unknown>
1688
1697
  params: Record<string, unknown>
1689
1698
  } {
1690
- const { matchedRoutes, routeParams, parsedParams } = this.getMatchedRoutes(
1699
+ const { matchedRoutes, routeParams } = this.getMatchedRoutes(
1691
1700
  location.pathname,
1692
1701
  )
1693
1702
  const lastRoute = last(matchedRoutes)!
@@ -1734,12 +1743,7 @@ export class RouterCore<
1734
1743
  )
1735
1744
  for (const route of matchedRoutes) {
1736
1745
  try {
1737
- extractStrictParams(
1738
- route,
1739
- routeParams,
1740
- parsedParams ?? {},
1741
- strictParams,
1742
- )
1746
+ extractStrictParams(route, strictParams)
1743
1747
  } catch {
1744
1748
  // Ignore errors, we're not actually routing
1745
1749
  }
@@ -1835,9 +1839,7 @@ export class RouterCore<
1835
1839
  dest.unsafeRelative === 'path'
1836
1840
  ? currentLocation.pathname
1837
1841
  : (dest.from ?? lightweightResult.fullPath)
1838
-
1839
- // ensure this includes the basePath if set
1840
- const fromPath = this.resolvePathWithBase(defaultedFromPath, '.')
1842
+ const destTo = dest.to ? `${dest.to}` : undefined
1841
1843
 
1842
1844
  // From search should always use the current location
1843
1845
  const fromSearch = lightweightResult.search
@@ -1847,11 +1849,15 @@ export class RouterCore<
1847
1849
  lightweightResult.params,
1848
1850
  )
1849
1851
 
1850
- // Resolve the next to
1851
- // ensure this includes the basePath if set
1852
- const nextTo = dest.to
1853
- ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1854
- : this.resolvePathWithBase(fromPath, '.')
1852
+ const isAbsoluteTo = destTo?.charCodeAt(0) === 47
1853
+ const sourcePath = isAbsoluteTo
1854
+ ? '/'
1855
+ : this.resolvePathWithBase(defaultedFromPath, '.')
1856
+
1857
+ // Resolve the destination. Absolute destinations don't need the source path.
1858
+ const nextTo = destTo
1859
+ ? this.resolvePathWithBase(sourcePath, destTo)
1860
+ : sourcePath
1855
1861
 
1856
1862
  // Resolve the next params
1857
1863
  const nextParams =
@@ -1864,24 +1870,33 @@ export class RouterCore<
1864
1870
  functionalUpdate(dest.params as any, fromParams),
1865
1871
  )
1866
1872
 
1867
- // Use lightweight getMatchedRoutes instead of matchRoutesInternal
1868
- // This avoids creating full match objects (AbortController, ControlledPromise, etc.)
1869
- // which are expensive and not needed for buildLocation
1870
- const destMatchResult = this.getMatchedRoutes(nextTo)
1871
- let destRoutes = destMatchResult.matchedRoutes
1872
-
1873
- // Compute globalNotFoundRouteId using the same logic as matchRoutesInternal
1874
- const isGlobalNotFound =
1875
- !destMatchResult.foundRoute ||
1876
- (destMatchResult.foundRoute.path !== '/' &&
1877
- destMatchResult.routeParams['**'])
1873
+ const destRoute = this.routesByPath[
1874
+ trimPathRight(nextTo) as keyof typeof this.routesByPath
1875
+ ] as AnyRoute | undefined
1876
+
1877
+ let destRoutes: ReadonlyArray<AnyRoute>
1878
+ if (destRoute) {
1879
+ destRoutes = this.getRouteBranch(destRoute)
1880
+ } else if (nextTo.includes('$')) {
1881
+ // Route templates must match routesByPath exactly. A miss here is a
1882
+ // typed destination mismatch, not a concrete URL to route-match.
1883
+ destRoutes = []
1884
+ } else {
1885
+ const destMatchResult = this.getMatchedRoutes(nextTo)
1886
+ destRoutes = destMatchResult.matchedRoutes
1878
1887
 
1879
- if (isGlobalNotFound && this.options.notFoundRoute) {
1880
- destRoutes = [...destRoutes, this.options.notFoundRoute]
1888
+ if (
1889
+ this.options.notFoundRoute &&
1890
+ (!destMatchResult.foundRoute ||
1891
+ (destMatchResult.foundRoute.path !== '/' &&
1892
+ destMatchResult.routeParams['**']))
1893
+ ) {
1894
+ destRoutes = [...destRoutes, this.options.notFoundRoute]
1895
+ }
1881
1896
  }
1882
1897
 
1883
1898
  // If there are any params, we need to stringify them
1884
- if (Object.keys(nextParams).length > 0) {
1899
+ if (destRoutes.length && hasKeys(nextParams)) {
1885
1900
  for (const route of destRoutes) {
1886
1901
  const fn =
1887
1902
  route.options.params?.stringify ?? route.options.stringifyParams
@@ -1900,8 +1915,7 @@ export class RouterCore<
1900
1915
  }
1901
1916
 
1902
1917
  const nextPathname = opts.leaveParams
1903
- ? // Use the original template path for interpolation
1904
- // This preserves the original parameter syntax including optional parameters
1918
+ ? // Keep path params uninterpolated for matchRoute/template matching.
1905
1919
  nextTo
1906
1920
  : decodePath(
1907
1921
  interpolatePath({
@@ -1912,6 +1926,24 @@ export class RouterCore<
1912
1926
  }).interpolatedPath,
1913
1927
  ).path
1914
1928
 
1929
+ if (
1930
+ process.env.NODE_ENV !== 'production' &&
1931
+ destRoute &&
1932
+ !opts.leaveParams
1933
+ ) {
1934
+ try {
1935
+ const roundTrip = this.getMatchedRoutes(nextPathname)
1936
+ if (roundTrip.foundRoute?.id !== destRoute.id) {
1937
+ console.warn(
1938
+ `Generated path "${nextPathname}" for route "${destRoute.id}" did not match the same route after params.stringify.`,
1939
+ )
1940
+ }
1941
+ } catch {
1942
+ // Ignore roundtrip validation errors. The generated location will be
1943
+ // handled by the normal navigation flow.
1944
+ }
1945
+ }
1946
+
1915
1947
  // Resolve the next search
1916
1948
  let nextSearch = fromSearch
1917
1949
  if (opts._includeValidateSearch && this.options.search?.strict) {
@@ -3015,17 +3047,15 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3015
3047
  const trimmedPath = trimPathRight(pathname)
3016
3048
 
3017
3049
  let foundRoute: TRouteLike | undefined = undefined
3018
- let parsedParams: Record<string, unknown> | undefined = undefined
3019
3050
  const match = findRouteMatch<TRouteLike>(trimmedPath, processedTree, true)
3020
3051
  if (match) {
3021
3052
  foundRoute = match.route
3022
3053
  Object.assign(routeParams, match.rawParams) // Copy params, because they're cached
3023
- parsedParams = Object.assign(Object.create(null), match.parsedParams)
3024
3054
  }
3025
3055
 
3026
3056
  const matchedRoutes = match?.branch || [routesById[rootRouteId]!]
3027
3057
 
3028
- return { matchedRoutes, routeParams, foundRoute, parsedParams }
3058
+ return { matchedRoutes, routeParams, foundRoute }
3029
3059
  }
3030
3060
 
3031
3061
  /**
@@ -3177,22 +3207,14 @@ function findGlobalNotFoundRouteId(
3177
3207
 
3178
3208
  function extractStrictParams(
3179
3209
  route: AnyRoute,
3180
- referenceParams: Record<string, unknown>,
3181
- parsedParams: Record<string, unknown>,
3182
3210
  accumulatedParams: Record<string, unknown>,
3183
3211
  ) {
3184
3212
  const parseParams = route.options.params?.parse ?? route.options.parseParams
3185
3213
  if (parseParams) {
3186
- if (route.options.skipRouteOnParseError) {
3187
- // Use pre-parsed params from route matching for skipRouteOnParseError routes
3188
- for (const key in referenceParams) {
3189
- if (key in parsedParams) {
3190
- accumulatedParams[key] = parsedParams[key]
3191
- }
3192
- }
3193
- } else {
3194
- const result = parseParams(accumulatedParams as Record<string, string>)
3195
- Object.assign(accumulatedParams, result)
3214
+ const result = parseParams(accumulatedParams as Record<string, string>)
3215
+ if (result === false) {
3216
+ throw new Error('Route params.parse returned false for a matched route')
3196
3217
  }
3218
+ Object.assign(accumulatedParams, result)
3197
3219
  }
3198
3220
  }
package/src/utils.ts CHANGED
@@ -215,6 +215,13 @@ export function functionalUpdate<TPrevious, TResult = TPrevious>(
215
215
  const hasOwn = Object.prototype.hasOwnProperty
216
216
  const isEnumerable = Object.prototype.propertyIsEnumerable
217
217
 
218
+ export function hasKeys(obj: Record<string, unknown>) {
219
+ for (const key in obj) {
220
+ if (hasOwn.call(obj, key)) return true
221
+ }
222
+ return false
223
+ }
224
+
218
225
  const createNull = () => Object.create(null)
219
226
  export const nullReplaceEqualDeep: typeof replaceEqualDeep = (prev, next) =>
220
227
  replaceEqualDeep(prev, next, createNull)