@tanstack/router-core 1.156.0 → 1.157.1

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 (55) hide show
  1. package/dist/cjs/index.cjs +2 -0
  2. package/dist/cjs/index.cjs.map +1 -1
  3. package/dist/cjs/index.d.cts +1 -0
  4. package/dist/cjs/isServer/client.cjs +5 -0
  5. package/dist/cjs/isServer/client.cjs.map +1 -0
  6. package/dist/cjs/isServer/client.d.cts +1 -0
  7. package/dist/cjs/isServer/development.cjs +5 -0
  8. package/dist/cjs/isServer/development.cjs.map +1 -0
  9. package/dist/cjs/isServer/development.d.cts +1 -0
  10. package/dist/cjs/isServer/server.cjs +5 -0
  11. package/dist/cjs/isServer/server.cjs.map +1 -0
  12. package/dist/cjs/isServer/server.d.cts +1 -0
  13. package/dist/cjs/isServer.d.cts +24 -0
  14. package/dist/cjs/load-matches.cjs +2 -1
  15. package/dist/cjs/load-matches.cjs.map +1 -1
  16. package/dist/cjs/location.d.cts +2 -3
  17. package/dist/cjs/router.cjs +27 -18
  18. package/dist/cjs/router.cjs.map +1 -1
  19. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  20. package/dist/cjs/utils.cjs +5 -0
  21. package/dist/cjs/utils.cjs.map +1 -1
  22. package/dist/cjs/utils.d.cts +9 -0
  23. package/dist/esm/index.d.ts +1 -0
  24. package/dist/esm/index.js +2 -0
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/isServer/client.d.ts +1 -0
  27. package/dist/esm/isServer/client.js +5 -0
  28. package/dist/esm/isServer/client.js.map +1 -0
  29. package/dist/esm/isServer/development.d.ts +1 -0
  30. package/dist/esm/isServer/development.js +5 -0
  31. package/dist/esm/isServer/development.js.map +1 -0
  32. package/dist/esm/isServer/server.d.ts +1 -0
  33. package/dist/esm/isServer/server.js +5 -0
  34. package/dist/esm/isServer/server.js.map +1 -0
  35. package/dist/esm/isServer.d.ts +24 -0
  36. package/dist/esm/load-matches.js +2 -1
  37. package/dist/esm/load-matches.js.map +1 -1
  38. package/dist/esm/location.d.ts +2 -3
  39. package/dist/esm/router.js +28 -19
  40. package/dist/esm/router.js.map +1 -1
  41. package/dist/esm/scroll-restoration.js.map +1 -1
  42. package/dist/esm/utils.d.ts +9 -0
  43. package/dist/esm/utils.js +5 -0
  44. package/dist/esm/utils.js.map +1 -1
  45. package/package.json +81 -1
  46. package/src/index.ts +1 -0
  47. package/src/isServer/client.ts +1 -0
  48. package/src/isServer/development.ts +2 -0
  49. package/src/isServer/server.ts +1 -0
  50. package/src/isServer.ts +24 -0
  51. package/src/load-matches.ts +8 -7
  52. package/src/location.ts +2 -3
  53. package/src/router.ts +58 -32
  54. package/src/scroll-restoration.ts +3 -2
  55. package/src/utils.ts +15 -0
package/src/router.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  createControlledPromise,
5
5
  decodePath,
6
6
  deepEqual,
7
+ encodeNonAscii,
7
8
  findLast,
8
9
  functionalUpdate,
9
10
  isDangerousProtocol,
@@ -25,6 +26,7 @@ import {
25
26
  trimPath,
26
27
  trimPathRight,
27
28
  } from './path'
29
+ import { isServer } from './isServer'
28
30
  import { createLRUCache } from './lru-cache'
29
31
  import { isNotFound } from './not-found'
30
32
  import { setupScrollRestoration } from './scroll-restoration'
@@ -1018,7 +1020,7 @@ export class RouterCore<
1018
1020
  (this.options.history && this.options.history !== this.history)
1019
1021
  ) {
1020
1022
  if (!this.options.history) {
1021
- if (!this.isServer) {
1023
+ if (!(isServer ?? this.isServer)) {
1022
1024
  this.history = createBrowserHistory() as TRouterHistory
1023
1025
  }
1024
1026
  } else {
@@ -1028,7 +1030,11 @@ export class RouterCore<
1028
1030
 
1029
1031
  this.origin = this.options.origin
1030
1032
  if (!this.origin) {
1031
- if (!this.isServer && window?.origin && window.origin !== 'null') {
1033
+ if (
1034
+ !(isServer ?? this.isServer) &&
1035
+ window?.origin &&
1036
+ window.origin !== 'null'
1037
+ ) {
1032
1038
  this.origin = window.origin
1033
1039
  } else {
1034
1040
  // fallback for the server, can be overridden by calling router.update({origin}) on the server
@@ -1044,7 +1050,7 @@ export class RouterCore<
1044
1050
  this.routeTree = this.options.routeTree as TRouteTree
1045
1051
  let processRouteTreeResult: ProcessRouteTreeResult<TRouteTree>
1046
1052
  if (
1047
- this.isServer &&
1053
+ (isServer ?? this.isServer) &&
1048
1054
  globalThis.__TSR_CACHE__ &&
1049
1055
  globalThis.__TSR_CACHE__.routeTree === this.routeTree
1050
1056
  ) {
@@ -1055,7 +1061,10 @@ export class RouterCore<
1055
1061
  this.resolvePathCache = createLRUCache(1000)
1056
1062
  processRouteTreeResult = this.buildRouteTree()
1057
1063
  // only cache if nothing else is cached yet
1058
- if (this.isServer && globalThis.__TSR_CACHE__ === undefined) {
1064
+ if (
1065
+ (isServer ?? this.isServer) &&
1066
+ globalThis.__TSR_CACHE__ === undefined
1067
+ ) {
1059
1068
  globalThis.__TSR_CACHE__ = {
1060
1069
  routeTree: this.routeTree,
1061
1070
  processRouteTreeResult: processRouteTreeResult as any,
@@ -1091,7 +1100,8 @@ export class RouterCore<
1091
1100
  this.basepath = nextBasepath
1092
1101
 
1093
1102
  const rewrites: Array<LocationRewrite> = []
1094
- if (trimPath(nextBasepath) !== '') {
1103
+ const trimmed = trimPath(nextBasepath)
1104
+ if (trimmed && trimmed !== '/') {
1095
1105
  rewrites.push(
1096
1106
  rewriteBasepath({
1097
1107
  basepath: nextBasepath,
@@ -1237,8 +1247,8 @@ export class RouterCore<
1237
1247
  return {
1238
1248
  href: fullPath,
1239
1249
  publicHref: href,
1240
- url: url,
1241
1250
  pathname: decodePath(url.pathname),
1251
+ external: !!this.rewrite && url.origin !== this.origin,
1242
1252
  searchStr,
1243
1253
  search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
1244
1254
  hash: decodePath(url.hash.split('#').reverse()[0] ?? ''),
@@ -1479,7 +1489,7 @@ export class RouterCore<
1479
1489
 
1480
1490
  match = {
1481
1491
  id: matchId,
1482
- ssr: this.isServer ? undefined : route.options.ssr,
1492
+ ssr: (isServer ?? this.isServer) ? undefined : route.options.ssr,
1483
1493
  index,
1484
1494
  routeId: route.id,
1485
1495
  params: previousMatch
@@ -1878,22 +1888,43 @@ export class RouterCore<
1878
1888
  // Create the full path of the location
1879
1889
  const fullPath = `${nextPathname}${searchStr}${hashStr}`
1880
1890
 
1881
- // Create the new href with full origin
1882
- const url = new URL(fullPath, this.origin)
1883
-
1884
- // If a rewrite function is provided, use it to rewrite the URL
1885
- const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
1891
+ // Compute href and publicHref without URL construction when no rewrite
1892
+ let href: string
1893
+ let publicHref: string
1894
+ let external = false
1895
+
1896
+ if (this.rewrite) {
1897
+ // With rewrite, we need to construct URL to apply the rewrite
1898
+ const url = new URL(fullPath, this.origin)
1899
+ const rewrittenUrl = executeRewriteOutput(this.rewrite, url)
1900
+ href = url.href.replace(url.origin, '')
1901
+ // If rewrite changed the origin, publicHref needs full URL
1902
+ // Otherwise just use the path components
1903
+ if (rewrittenUrl.origin !== this.origin) {
1904
+ publicHref = rewrittenUrl.href
1905
+ external = true
1906
+ } else {
1907
+ publicHref =
1908
+ rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash
1909
+ }
1910
+ } else {
1911
+ // Fast path: no rewrite, skip URL construction entirely
1912
+ // fullPath is already the correct href (origin-stripped)
1913
+ // We need to encode non-ASCII (unicode) characters for the href
1914
+ // since decodePath decoded them from the interpolated path
1915
+ href = encodeNonAscii(fullPath)
1916
+ publicHref = href
1917
+ }
1886
1918
 
1887
1919
  return {
1888
- publicHref:
1889
- rewrittenUrl.pathname + rewrittenUrl.search + rewrittenUrl.hash,
1890
- href: fullPath,
1891
- url: rewrittenUrl,
1920
+ publicHref,
1921
+ href,
1892
1922
  pathname: nextPathname,
1893
1923
  search: nextSearch,
1894
1924
  searchStr,
1895
1925
  state: nextState as any,
1896
1926
  hash: hash ?? '',
1927
+ external,
1897
1928
  unmaskOnReload: dest.unmaskOnReload,
1898
1929
  }
1899
1930
  }
@@ -2006,9 +2037,6 @@ export class RouterCore<
2006
2037
  maskedLocation,
2007
2038
  // eslint-disable-next-line prefer-const
2008
2039
  hashScrollIntoView,
2009
- // don't pass url into history since it is a URL instance that cannot be serialized
2010
- // eslint-disable-next-line prefer-const
2011
- url: _url,
2012
2040
  ...nextHistory
2013
2041
  } = next
2014
2042
 
@@ -2154,8 +2182,9 @@ export class RouterCore<
2154
2182
  // be a complete path (possibly with basepath)
2155
2183
  if (to !== undefined || !href) {
2156
2184
  const location = this.buildLocation({ to, ...rest } as any)
2157
- href = href ?? location.url.href
2158
- publicHref = publicHref ?? location.url.href
2185
+ // Use publicHref which contains the path (origin-stripped is fine for reload)
2186
+ href = href ?? location.publicHref
2187
+ publicHref = publicHref ?? location.publicHref
2159
2188
  }
2160
2189
 
2161
2190
  // Use publicHref when available and href is not a full URL,
@@ -2215,7 +2244,7 @@ export class RouterCore<
2215
2244
  this.cancelMatches()
2216
2245
  this.updateLatestLocation()
2217
2246
 
2218
- if (this.isServer) {
2247
+ if (isServer ?? this.isServer) {
2219
2248
  // for SPAs on the initial load, this is handled by the Transitioner
2220
2249
  const nextLocation = this.buildLocation({
2221
2250
  to: this.latestLocation.pathname,
@@ -2226,10 +2255,9 @@ export class RouterCore<
2226
2255
  _includeValidateSearch: true,
2227
2256
  })
2228
2257
 
2229
- if (
2230
- this.latestLocation.publicHref !== nextLocation.publicHref ||
2231
- nextLocation.url.origin !== this.origin
2232
- ) {
2258
+ // Check if location changed - origin check is unnecessary since buildLocation
2259
+ // always uses this.origin when constructing URLs
2260
+ if (this.latestLocation.publicHref !== nextLocation.publicHref) {
2233
2261
  const href = this.getParsedLocationHref(nextLocation)
2234
2262
 
2235
2263
  throw redirect({ href })
@@ -2365,7 +2393,7 @@ export class RouterCore<
2365
2393
  } catch (err) {
2366
2394
  if (isRedirect(err)) {
2367
2395
  redirect = err
2368
- if (!this.isServer) {
2396
+ if (!(isServer ?? this.isServer)) {
2369
2397
  this.navigate({
2370
2398
  ...redirect.options,
2371
2399
  replace: true,
@@ -2553,11 +2581,9 @@ export class RouterCore<
2553
2581
  }
2554
2582
 
2555
2583
  getParsedLocationHref = (location: ParsedLocation) => {
2556
- let href = location.url.href
2557
- if (this.origin && location.url.origin === this.origin) {
2558
- href = href.replace(this.origin, '') || '/'
2559
- }
2560
- return href
2584
+ // For redirects and external use, we need publicHref (with rewrite output applied)
2585
+ // href is the internal path after rewrite input, publicHref is user-facing
2586
+ return location.publicHref || '/'
2561
2587
  }
2562
2588
 
2563
2589
  resolveRedirect = (redirect: AnyRedirect): AnyRedirect => {
@@ -1,4 +1,5 @@
1
1
  import { functionalUpdate } from './utils'
2
+ import { isServer } from './isServer'
2
3
  import type { AnyRouter } from './router'
3
4
  import type { ParsedLocation } from './location'
4
5
  import type { NonNullableUpdater } from './utils'
@@ -217,7 +218,7 @@ export function restoreScroll({
217
218
  /** Setup global listeners and hooks to support scroll restoration. */
218
219
  /** Setup global listeners and hooks to support scroll restoration. */
219
220
  export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
220
- if (!scrollRestorationCache && !router.isServer) {
221
+ if (!scrollRestorationCache && !(isServer ?? router.isServer)) {
221
222
  return
222
223
  }
223
224
  const shouldScrollRestoration =
@@ -228,7 +229,7 @@ export function setupScrollRestoration(router: AnyRouter, force?: boolean) {
228
229
  }
229
230
 
230
231
  if (
231
- router.isServer ||
232
+ (isServer ?? router.isServer) ||
232
233
  router.isScrollRestorationSetup ||
233
234
  !scrollRestorationCache
234
235
  ) {
package/src/utils.ts CHANGED
@@ -613,6 +613,21 @@ export function decodePath(path: string, decodeIgnore?: Array<string>): string {
613
613
  return result
614
614
  }
615
615
 
616
+ /**
617
+ * Encodes non-ASCII (unicode) characters in a path while preserving
618
+ * already percent-encoded sequences. This is used to generate proper
619
+ * href values without constructing URL objects.
620
+ *
621
+ * Unlike encodeURI, this won't double-encode percent-encoded sequences
622
+ * like %2F or %25 because it only targets non-ASCII characters.
623
+ */
624
+ export function encodeNonAscii(path: string): string {
625
+ // eslint-disable-next-line no-control-regex
626
+ if (!/[^\u0000-\u007F]/.test(path)) return path
627
+ // eslint-disable-next-line no-control-regex
628
+ return path.replace(/[^\u0000-\u007F]/gu, encodeURIComponent)
629
+ }
630
+
616
631
  /**
617
632
  * Builds the dev-mode CSS styles URL for route-scoped CSS collection.
618
633
  * Used by HeadContent components in all framework implementations to construct