@tanstack/router-core 1.121.0-alpha.27 → 1.121.0-alpha.28

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 (165) hide show
  1. package/dist/cjs/Matches.cjs.map +1 -1
  2. package/dist/cjs/Matches.d.cts +31 -1
  3. package/dist/cjs/RouterProvider.d.cts +2 -1
  4. package/dist/cjs/defer.cjs +1 -1
  5. package/dist/cjs/defer.cjs.map +1 -1
  6. package/dist/cjs/global.d.cts +7 -0
  7. package/dist/cjs/index.cjs +1 -2
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/index.d.cts +6 -6
  10. package/dist/cjs/link.cjs.map +1 -1
  11. package/dist/cjs/link.d.cts +12 -0
  12. package/dist/cjs/lru-cache.cjs +62 -0
  13. package/dist/cjs/lru-cache.cjs.map +1 -0
  14. package/dist/cjs/lru-cache.d.cts +5 -0
  15. package/dist/cjs/not-found.cjs +1 -1
  16. package/dist/cjs/not-found.cjs.map +1 -1
  17. package/dist/cjs/path.cjs +316 -148
  18. package/dist/cjs/path.cjs.map +1 -1
  19. package/dist/cjs/path.d.cts +18 -24
  20. package/dist/cjs/qss.cjs.map +1 -1
  21. package/dist/cjs/redirect.cjs +3 -0
  22. package/dist/cjs/redirect.cjs.map +1 -1
  23. package/dist/cjs/route.cjs +6 -12
  24. package/dist/cjs/route.cjs.map +1 -1
  25. package/dist/cjs/route.d.cts +29 -9
  26. package/dist/cjs/router.cjs +453 -272
  27. package/dist/cjs/router.cjs.map +1 -1
  28. package/dist/cjs/router.d.cts +55 -85
  29. package/dist/cjs/scroll-restoration.cjs +20 -13
  30. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  31. package/dist/cjs/scroll-restoration.d.cts +9 -1
  32. package/dist/cjs/searchMiddleware.cjs.map +1 -1
  33. package/dist/cjs/searchParams.cjs.map +1 -1
  34. package/dist/cjs/ssr/client.cjs +10 -0
  35. package/dist/cjs/ssr/client.cjs.map +1 -0
  36. package/dist/cjs/ssr/client.d.cts +5 -0
  37. package/dist/cjs/ssr/createRequestHandler.cjs +50 -0
  38. package/dist/cjs/ssr/createRequestHandler.cjs.map +1 -0
  39. package/dist/cjs/ssr/createRequestHandler.d.cts +9 -0
  40. package/dist/cjs/ssr/handlerCallback.cjs +7 -0
  41. package/dist/cjs/ssr/handlerCallback.cjs.map +1 -0
  42. package/dist/cjs/ssr/handlerCallback.d.cts +9 -0
  43. package/dist/cjs/ssr/headers.cjs +39 -0
  44. package/dist/cjs/ssr/headers.cjs.map +1 -0
  45. package/dist/cjs/ssr/headers.d.cts +5 -0
  46. package/dist/cjs/ssr/json.cjs +14 -0
  47. package/dist/cjs/ssr/json.cjs.map +1 -0
  48. package/dist/cjs/ssr/json.d.cts +4 -0
  49. package/dist/cjs/ssr/seroval-plugins.cjs +34 -0
  50. package/dist/cjs/ssr/seroval-plugins.cjs.map +1 -0
  51. package/dist/cjs/ssr/seroval-plugins.d.cts +10 -0
  52. package/dist/cjs/ssr/server.cjs +13 -0
  53. package/dist/cjs/ssr/server.cjs.map +1 -0
  54. package/dist/cjs/ssr/server.d.cts +6 -0
  55. package/dist/cjs/ssr/ssr-client.cjs +159 -0
  56. package/dist/cjs/ssr/ssr-client.cjs.map +1 -0
  57. package/dist/cjs/ssr/ssr-client.d.cts +29 -0
  58. package/dist/cjs/ssr/ssr-server.cjs +107 -0
  59. package/dist/cjs/ssr/ssr-server.cjs.map +1 -0
  60. package/dist/cjs/ssr/ssr-server.d.cts +18 -0
  61. package/dist/cjs/ssr/transformStreamWithRouter.cjs +183 -0
  62. package/dist/cjs/ssr/transformStreamWithRouter.cjs.map +1 -0
  63. package/dist/cjs/ssr/transformStreamWithRouter.d.cts +6 -0
  64. package/dist/cjs/ssr/tsrScript.cjs +4 -0
  65. package/dist/cjs/ssr/tsrScript.cjs.map +1 -0
  66. package/dist/cjs/ssr/tsrScript.d.cts +0 -0
  67. package/dist/cjs/utils.cjs +7 -25
  68. package/dist/cjs/utils.cjs.map +1 -1
  69. package/dist/cjs/utils.d.cts +1 -6
  70. package/dist/esm/Matches.d.ts +31 -1
  71. package/dist/esm/Matches.js.map +1 -1
  72. package/dist/esm/RouterProvider.d.ts +2 -1
  73. package/dist/esm/defer.js +1 -1
  74. package/dist/esm/defer.js.map +1 -1
  75. package/dist/esm/global.d.ts +7 -0
  76. package/dist/esm/index.d.ts +6 -6
  77. package/dist/esm/index.js +2 -3
  78. package/dist/esm/link.d.ts +12 -0
  79. package/dist/esm/link.js.map +1 -1
  80. package/dist/esm/lru-cache.d.ts +5 -0
  81. package/dist/esm/lru-cache.js +62 -0
  82. package/dist/esm/lru-cache.js.map +1 -0
  83. package/dist/esm/not-found.js +1 -1
  84. package/dist/esm/not-found.js.map +1 -1
  85. package/dist/esm/path.d.ts +18 -24
  86. package/dist/esm/path.js +316 -148
  87. package/dist/esm/path.js.map +1 -1
  88. package/dist/esm/qss.js.map +1 -1
  89. package/dist/esm/redirect.js +3 -0
  90. package/dist/esm/redirect.js.map +1 -1
  91. package/dist/esm/route.d.ts +29 -9
  92. package/dist/esm/route.js +6 -12
  93. package/dist/esm/route.js.map +1 -1
  94. package/dist/esm/router.d.ts +55 -85
  95. package/dist/esm/router.js +462 -281
  96. package/dist/esm/router.js.map +1 -1
  97. package/dist/esm/scroll-restoration.d.ts +9 -1
  98. package/dist/esm/scroll-restoration.js +20 -13
  99. package/dist/esm/scroll-restoration.js.map +1 -1
  100. package/dist/esm/searchMiddleware.js.map +1 -1
  101. package/dist/esm/searchParams.js.map +1 -1
  102. package/dist/esm/ssr/client.d.ts +5 -0
  103. package/dist/esm/ssr/client.js +10 -0
  104. package/dist/esm/ssr/client.js.map +1 -0
  105. package/dist/esm/ssr/createRequestHandler.d.ts +9 -0
  106. package/dist/esm/ssr/createRequestHandler.js +50 -0
  107. package/dist/esm/ssr/createRequestHandler.js.map +1 -0
  108. package/dist/esm/ssr/handlerCallback.d.ts +9 -0
  109. package/dist/esm/ssr/handlerCallback.js +7 -0
  110. package/dist/esm/ssr/handlerCallback.js.map +1 -0
  111. package/dist/esm/ssr/headers.d.ts +5 -0
  112. package/dist/esm/ssr/headers.js +39 -0
  113. package/dist/esm/ssr/headers.js.map +1 -0
  114. package/dist/esm/ssr/json.d.ts +4 -0
  115. package/dist/esm/ssr/json.js +14 -0
  116. package/dist/esm/ssr/json.js.map +1 -0
  117. package/dist/esm/ssr/seroval-plugins.d.ts +10 -0
  118. package/dist/esm/ssr/seroval-plugins.js +34 -0
  119. package/dist/esm/ssr/seroval-plugins.js.map +1 -0
  120. package/dist/esm/ssr/server.d.ts +6 -0
  121. package/dist/esm/ssr/server.js +13 -0
  122. package/dist/esm/ssr/server.js.map +1 -0
  123. package/dist/esm/ssr/ssr-client.d.ts +29 -0
  124. package/dist/esm/ssr/ssr-client.js +159 -0
  125. package/dist/esm/ssr/ssr-client.js.map +1 -0
  126. package/dist/esm/ssr/ssr-server.d.ts +18 -0
  127. package/dist/esm/ssr/ssr-server.js +107 -0
  128. package/dist/esm/ssr/ssr-server.js.map +1 -0
  129. package/dist/esm/ssr/transformStreamWithRouter.d.ts +6 -0
  130. package/dist/esm/ssr/transformStreamWithRouter.js +183 -0
  131. package/dist/esm/ssr/transformStreamWithRouter.js.map +1 -0
  132. package/dist/esm/ssr/tsrScript.d.ts +0 -0
  133. package/dist/esm/ssr/tsrScript.js +5 -0
  134. package/dist/esm/ssr/tsrScript.js.map +1 -0
  135. package/dist/esm/utils.d.ts +1 -6
  136. package/dist/esm/utils.js +8 -26
  137. package/dist/esm/utils.js.map +1 -1
  138. package/package.json +29 -2
  139. package/src/Matches.ts +40 -1
  140. package/src/RouterProvider.ts +2 -1
  141. package/src/global.ts +9 -0
  142. package/src/index.ts +12 -20
  143. package/src/link.ts +12 -0
  144. package/src/lru-cache.ts +68 -0
  145. package/src/path.ts +424 -174
  146. package/src/redirect.ts +3 -0
  147. package/src/route.ts +44 -13
  148. package/src/router.ts +580 -312
  149. package/src/scroll-restoration.ts +30 -18
  150. package/src/ssr/client.ts +5 -0
  151. package/src/ssr/createRequestHandler.ts +74 -0
  152. package/src/ssr/handlerCallback.ts +15 -0
  153. package/src/ssr/headers.ts +51 -0
  154. package/src/ssr/json.ts +18 -0
  155. package/src/ssr/seroval-plugins.ts +43 -0
  156. package/src/ssr/server.ts +10 -0
  157. package/src/ssr/ssr-client.ts +242 -0
  158. package/src/ssr/ssr-server.ts +132 -0
  159. package/src/ssr/transformStreamWithRouter.ts +259 -0
  160. package/src/ssr/tsrScript.ts +7 -0
  161. package/src/utils.ts +10 -39
  162. package/src/vite-env.d.ts +4 -0
  163. package/dist/cjs/serializer.d.cts +0 -22
  164. package/dist/esm/serializer.d.ts +0 -22
  165. package/src/serializer.ts +0 -32
package/src/router.ts CHANGED
@@ -14,6 +14,10 @@ import {
14
14
  replaceEqualDeep,
15
15
  } from './utils'
16
16
  import {
17
+ SEGMENT_TYPE_OPTIONAL_PARAM,
18
+ SEGMENT_TYPE_PARAM,
19
+ SEGMENT_TYPE_PATHNAME,
20
+ SEGMENT_TYPE_WILDCARD,
17
21
  cleanPath,
18
22
  interpolatePath,
19
23
  joinPaths,
@@ -28,7 +32,9 @@ import { isNotFound } from './not-found'
28
32
  import { setupScrollRestoration } from './scroll-restoration'
29
33
  import { defaultParseSearch, defaultStringifySearch } from './searchParams'
30
34
  import { rootRouteId } from './root'
31
- import { isRedirect } from './redirect'
35
+ import { isRedirect, redirect } from './redirect'
36
+ import { createLRUCache } from './lru-cache'
37
+ import type { ParsePathnameCache, Segment } from './path'
32
38
  import type { SearchParser, SearchSerializer } from './searchParams'
33
39
  import type { AnyRedirect, ResolvedRedirect } from './redirect'
34
40
  import type {
@@ -38,6 +44,7 @@ import type {
38
44
  RouterHistory,
39
45
  } from '@tanstack/history'
40
46
  import type {
47
+ Awaitable,
41
48
  ControlledPromise,
42
49
  NoInfer,
43
50
  NonNullableUpdater,
@@ -45,7 +52,6 @@ import type {
45
52
  Updater,
46
53
  } from './utils'
47
54
  import type { ParsedLocation } from './location'
48
- import type { DeferredPromiseState } from './defer'
49
55
  import type {
50
56
  AnyContext,
51
57
  AnyRoute,
@@ -56,6 +62,7 @@ import type {
56
62
  RouteContextOptions,
57
63
  RouteMask,
58
64
  SearchMiddleware,
65
+ SsrContextOptions,
59
66
  } from './route'
60
67
  import type {
61
68
  FullSearchSchema,
@@ -76,17 +83,10 @@ import type {
76
83
  NavigateFn,
77
84
  } from './RouterProvider'
78
85
  import type { Manifest } from './manifest'
79
- import type { StartSerializer } from './serializer'
80
86
  import type { AnySchema, AnyValidator } from './validators'
81
87
  import type { NavigateOptions, ResolveRelativePath, ToOptions } from './link'
82
88
  import type { NotFoundError } from './not-found'
83
89
 
84
- declare global {
85
- interface Window {
86
- __TSR_ROUTER__?: AnyRouter
87
- }
88
- }
89
-
90
90
  export type ControllablePromise<T = any> = Promise<T> & {
91
91
  resolve: (value: T) => void
92
92
  reject: (value?: any) => void
@@ -286,16 +286,14 @@ export interface RouterOptions<
286
286
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#dehydrate-method)
287
287
  * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
288
288
  */
289
- dehydrate?: () => TDehydrated
289
+ dehydrate?: () => Awaitable<TDehydrated>
290
290
  /**
291
291
  * A function that will be called when the router is hydrated.
292
292
  *
293
- * The return value of this function will be serialized and stored in the router's dehydrated state.
294
- *
295
293
  * @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#hydrate-method)
296
294
  * @link [Guide](https://tanstack.com/router/latest/docs/framework/react/guide/external-data-loading#critical-dehydrationhydration)
297
295
  */
298
- hydrate?: (dehydrated: TDehydrated) => void
296
+ hydrate?: (dehydrated: TDehydrated) => Awaitable<void>
299
297
  /**
300
298
  * An array of route masks that will be used to mask routes in the route tree.
301
299
  *
@@ -343,7 +341,22 @@ export interface RouterOptions<
343
341
  */
344
342
  isServer?: boolean
345
343
 
346
- defaultSsr?: boolean
344
+ /**
345
+ * @default false
346
+ */
347
+ isShell?: boolean
348
+
349
+ /**
350
+ * @default false
351
+ */
352
+ isPrerendering?: boolean
353
+
354
+ /**
355
+ * The default `ssr` a route should use if no `ssr` is provided.
356
+ *
357
+ * @default true
358
+ */
359
+ defaultSsr?: boolean | 'data-only'
347
360
 
348
361
  search?: {
349
362
  /**
@@ -399,6 +412,17 @@ export interface RouterOptions<
399
412
  * @default ['window']
400
413
  */
401
414
  scrollToTopSelectors?: Array<string | (() => Element | null | undefined)>
415
+
416
+ /**
417
+ * When `true`, disables the global catch boundary that normally wraps all route matches.
418
+ * This allows unhandled errors to bubble up to top-level error handlers in the browser.
419
+ *
420
+ * Useful for testing tools (like Storybook Test Runner), error reporting services,
421
+ * and debugging scenarios where you want errors to reach the browser's global error handlers.
422
+ *
423
+ * @default false
424
+ */
425
+ disableGlobalCatchBoundary?: boolean
402
426
  }
403
427
 
404
428
  export interface RouterState<
@@ -436,6 +460,7 @@ export interface BuildNextOptions {
436
460
  href?: string
437
461
  _fromLocation?: ParsedLocation
438
462
  unsafeRelative?: 'path'
463
+ _isNavigate?: boolean
439
464
  }
440
465
 
441
466
  type NavigationEventInfo = {
@@ -446,7 +471,7 @@ type NavigationEventInfo = {
446
471
  hashChanged: boolean
447
472
  }
448
473
 
449
- export type RouterEvents = {
474
+ export interface RouterEvents {
450
475
  onBeforeNavigate: {
451
476
  type: 'onBeforeNavigate'
452
477
  } & NavigationEventInfo
@@ -462,10 +487,6 @@ export type RouterEvents = {
462
487
  onBeforeRouteMount: {
463
488
  type: 'onBeforeRouteMount'
464
489
  } & NavigationEventInfo
465
- onInjectedHtml: {
466
- type: 'onInjectedHtml'
467
- promise: Promise<string>
468
- }
469
490
  onRendered: {
470
491
  type: 'onRendered'
471
492
  } & NavigationEventInfo
@@ -480,6 +501,11 @@ export type RouterListener<TRouterEvent extends RouterEvent> = {
480
501
  fn: ListenerFn<TRouterEvent>
481
502
  }
482
503
 
504
+ export type SubscribeFn = <TType extends keyof RouterEvents>(
505
+ eventType: TType,
506
+ fn: ListenerFn<RouterEvents[TType]>,
507
+ ) => () => void
508
+
483
509
  export interface MatchRoutesOpts {
484
510
  preload?: boolean
485
511
  throwOnError?: boolean
@@ -517,11 +543,6 @@ export type RouterConstructorOptions<
517
543
  > &
518
544
  RouterContextOptions<TRouteTree>
519
545
 
520
- export interface RouterErrorSerializer<TSerializedError> {
521
- serialize: (err: unknown) => TSerializedError
522
- deserialize: (err: TSerializedError) => unknown
523
- }
524
-
525
546
  export type PreloadRouteFn<
526
547
  TRouteTree extends AnyRoute,
527
548
  TTrailingSlashOption extends TrailingSlashOption,
@@ -589,6 +610,7 @@ export type UpdateFn<
589
610
  export type InvalidateFn<TRouter extends AnyRouter> = (opts?: {
590
611
  filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
591
612
  sync?: boolean
613
+ forcePending?: boolean
592
614
  }) => Promise<void>
593
615
 
594
616
  export type ParseLocationFn<TRouteTree extends AnyRoute> = (
@@ -617,17 +639,15 @@ export type CommitLocationFn = ({
617
639
 
618
640
  export type StartTransitionFn = (fn: () => void) => void
619
641
 
620
- export type SubscribeFn = <TType extends keyof RouterEvents>(
621
- eventType: TType,
622
- fn: ListenerFn<RouterEvents[TType]>,
623
- ) => () => void
624
-
625
642
  export interface MatchRoutesFn {
626
643
  (
627
644
  pathname: string,
628
- locationSearch: AnySchema,
645
+ locationSearch?: AnySchema,
629
646
  opts?: MatchRoutesOpts,
630
- ): Array<AnyRouteMatch>
647
+ ): Array<MakeRouteMatchUnion>
648
+ /**
649
+ * @deprecated use the following signature instead
650
+ */
631
651
  (next: ParsedLocation, opts?: MatchRoutesOpts): Array<AnyRouteMatch>
632
652
  (
633
653
  pathnameOrNext: string | ParsedLocation,
@@ -641,7 +661,7 @@ export type GetMatchFn = (matchId: string) => AnyRouteMatch | undefined
641
661
  export type UpdateMatchFn = (
642
662
  id: string,
643
663
  updater: (match: AnyRouteMatch) => AnyRouteMatch,
644
- ) => AnyRouteMatch
664
+ ) => void
645
665
 
646
666
  export type LoadRouteChunkFn = (route: AnyRoute) => Promise<Array<void>>
647
667
 
@@ -651,16 +671,16 @@ export type ClearCacheFn<TRouter extends AnyRouter> = (opts?: {
651
671
  filter?: (d: MakeRouteMatchUnion<TRouter>) => boolean
652
672
  }) => void
653
673
 
654
- export interface ServerSrr {
674
+ export interface ServerSsr {
655
675
  injectedHtml: Array<InjectedHtmlEntry>
656
676
  injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
657
677
  injectScript: (
658
678
  getScript: () => string | Promise<string>,
659
679
  opts?: { logScript?: boolean },
660
680
  ) => Promise<void>
661
- streamValue: (key: string, value: any) => void
662
- streamedKeys: Set<string>
663
- onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
681
+ isDehydrated: () => boolean
682
+ onRenderFinished: (listener: () => void) => void
683
+ dehydrate: () => Promise<void>
664
684
  }
665
685
 
666
686
  export type AnyRouterWithContext<TContext> = RouterCore<
@@ -703,29 +723,6 @@ export function defaultSerializeError(err: unknown) {
703
723
  data: err,
704
724
  }
705
725
  }
706
- export interface ExtractedBaseEntry {
707
- dataType: '__beforeLoadContext' | 'loaderData'
708
- type: string
709
- path: Array<string>
710
- id: number
711
- matchIndex: number
712
- }
713
-
714
- export interface ExtractedStream extends ExtractedBaseEntry {
715
- type: 'stream'
716
- streamState: StreamState
717
- }
718
-
719
- export interface ExtractedPromise extends ExtractedBaseEntry {
720
- type: 'promise'
721
- promiseState: DeferredPromiseState<any>
722
- }
723
-
724
- export type ExtractedEntry = ExtractedStream | ExtractedPromise
725
-
726
- export type StreamState = {
727
- promises: Array<ControlledPromise<string | null>>
728
- }
729
726
 
730
727
  export type TrailingSlashOption = 'always' | 'never' | 'preserve'
731
728
 
@@ -831,7 +828,7 @@ export class RouterCore<
831
828
  })
832
829
 
833
830
  if (typeof document !== 'undefined') {
834
- ;(window as any).__TSR_ROUTER__ = this
831
+ self.__TSR_ROUTER__ = this
835
832
  }
836
833
  }
837
834
 
@@ -840,7 +837,13 @@ export class RouterCore<
840
837
  // router can be used in a non-react environment if necessary
841
838
  startTransition: StartTransitionFn = (fn) => fn()
842
839
 
843
- isShell = false
840
+ isShell() {
841
+ return !!this.options.isShell
842
+ }
843
+
844
+ isPrerendering() {
845
+ return !!this.options.isPrerendering
846
+ }
844
847
 
845
848
  update: UpdateFn<
846
849
  TRouteTree,
@@ -930,10 +933,6 @@ export class RouterCore<
930
933
  'selector(:active-view-transition-type(a)',
931
934
  )
932
935
  }
933
-
934
- if ((this.latestLocation.search as any).__TSS_SHELL) {
935
- this.isShell = true
936
- }
937
936
  }
938
937
 
939
938
  get state() {
@@ -946,7 +945,6 @@ export class RouterCore<
946
945
  initRoute: (route, i) => {
947
946
  route.init({
948
947
  originalIndex: i,
949
- defaultSsr: this.options.defaultSsr,
950
948
  })
951
949
  },
952
950
  })
@@ -960,7 +958,6 @@ export class RouterCore<
960
958
  if (notFoundRoute) {
961
959
  notFoundRoute.init({
962
960
  originalIndex: 99999999999,
963
- defaultSsr: this.options.defaultSsr,
964
961
  })
965
962
  this.routesById[notFoundRoute.id] = notFoundRoute
966
963
  }
@@ -1017,7 +1014,8 @@ export class RouterCore<
1017
1014
  if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
1018
1015
  // Sync up the location keys
1019
1016
  const parsedTempLocation = parse(__tempLocation) as any
1020
- parsedTempLocation.state.key = location.state.key
1017
+ parsedTempLocation.state.key = location.state.key // TODO: Remove in v2 - use __TSR_key instead
1018
+ parsedTempLocation.state.__TSR_key = location.state.__TSR_key
1021
1019
 
1022
1020
  delete parsedTempLocation.state.__tempLocation
1023
1021
 
@@ -1037,6 +1035,7 @@ export class RouterCore<
1037
1035
  to: cleanPath(path),
1038
1036
  trailingSlash: this.options.trailingSlash,
1039
1037
  caseSensitive: this.options.caseSensitive,
1038
+ parseCache: this.parsePathnameCache,
1040
1039
  })
1041
1040
  return resolvedPath
1042
1041
  }
@@ -1045,15 +1044,6 @@ export class RouterCore<
1045
1044
  return this.routesById as Record<string, AnyRoute>
1046
1045
  }
1047
1046
 
1048
- /**
1049
- @deprecated use the following signature instead
1050
- ```ts
1051
- matchRoutes (
1052
- next: ParsedLocation,
1053
- opts?: { preload?: boolean; throwOnError?: boolean },
1054
- ): Array<AnyRouteMatch>;
1055
- ```
1056
- */
1057
1047
  matchRoutes: MatchRoutesFn = (
1058
1048
  pathnameOrNext: string | ParsedLocation,
1059
1049
  locationSearchOrOpts?: AnySchema | MatchRoutesOpts,
@@ -1227,6 +1217,7 @@ export class RouterCore<
1227
1217
  params: routeParams,
1228
1218
  leaveWildcards: true,
1229
1219
  decodeCharMap: this.pathParamsDecodeCharMap,
1220
+ parseCache: this.parsePathnameCache,
1230
1221
  }).interpolatedPath + loaderDepsHash
1231
1222
 
1232
1223
  // Waste not, want not. If we already have a match for this route,
@@ -1287,7 +1278,7 @@ export class RouterCore<
1287
1278
  error: undefined,
1288
1279
  paramsError: parseErrors[index],
1289
1280
  __routeContext: {},
1290
- __beforeLoadContext: {},
1281
+ __beforeLoadContext: undefined,
1291
1282
  context: {},
1292
1283
  abortController: new AbortController(),
1293
1284
  fetchCount: 0,
@@ -1365,6 +1356,9 @@ export class RouterCore<
1365
1356
  return matches
1366
1357
  }
1367
1358
 
1359
+ /** a cache for `parsePathname` */
1360
+ private parsePathnameCache: ParsePathnameCache = createLRUCache(1000)
1361
+
1368
1362
  getMatchedRoutes: GetMatchRoutesFn = (
1369
1363
  pathname: string,
1370
1364
  routePathname: string | undefined,
@@ -1377,6 +1371,7 @@ export class RouterCore<
1377
1371
  routesByPath: this.routesByPath,
1378
1372
  routesById: this.routesById,
1379
1373
  flatRoutes: this.flatRoutes,
1374
+ parseCache: this.parsePathnameCache,
1380
1375
  })
1381
1376
  }
1382
1377
 
@@ -1386,7 +1381,13 @@ export class RouterCore<
1386
1381
  if (!match) return
1387
1382
 
1388
1383
  match.abortController.abort()
1389
- clearTimeout(match.pendingTimeout)
1384
+ this.updateMatch(id, (prev) => {
1385
+ clearTimeout(prev.pendingTimeout)
1386
+ return {
1387
+ ...prev,
1388
+ pendingTimeout: undefined,
1389
+ }
1390
+ })
1390
1391
  }
1391
1392
 
1392
1393
  cancelMatches = () => {
@@ -1404,30 +1405,52 @@ export class RouterCore<
1404
1405
  // We allow the caller to override the current location
1405
1406
  const currentLocation = dest._fromLocation || this.latestLocation
1406
1407
 
1407
- const allFromMatches = this.matchRoutes(currentLocation, {
1408
+ const allCurrentLocationMatches = this.matchRoutes(currentLocation, {
1408
1409
  _buildLocation: true,
1409
1410
  })
1410
1411
 
1411
- const lastMatch = last(allFromMatches)!
1412
+ const lastMatch = last(allCurrentLocationMatches)!
1412
1413
 
1413
1414
  // First let's find the starting pathname
1414
1415
  // By default, start with the current location
1415
1416
  let fromPath = lastMatch.fullPath
1417
+ const toPath = dest.to
1418
+ ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1419
+ : this.resolvePathWithBase(fromPath, '.')
1416
1420
 
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
1421
+ const routeIsChanging =
1422
+ !!dest.to &&
1423
+ !comparePaths(dest.to.toString(), fromPath) &&
1424
+ !comparePaths(toPath, fromPath)
1425
+
1426
+ // If the route is changing we need to find the relative fromPath
1419
1427
  if (dest.unsafeRelative === 'path') {
1420
1428
  fromPath = currentLocation.pathname
1421
- } else if (dest.to && dest.from) {
1429
+ } else if (routeIsChanging && dest.from) {
1422
1430
  fromPath = dest.from
1423
- const existingFrom = [...allFromMatches].reverse().find((d) => {
1424
- return (
1425
- d.fullPath === fromPath || d.fullPath === joinPaths([fromPath, '/'])
1426
- )
1427
- })
1428
1431
 
1429
- if (!existingFrom) {
1430
- console.warn(`Could not find match for from: ${dest.from}`)
1432
+ // do this check only on navigations during test or development
1433
+ if (process.env.NODE_ENV !== 'production' && dest._isNavigate) {
1434
+ const allFromMatches = this.getMatchedRoutes(
1435
+ dest.from,
1436
+ undefined,
1437
+ ).matchedRoutes
1438
+
1439
+ const matchedFrom = [...allCurrentLocationMatches]
1440
+ .reverse()
1441
+ .find((d) => {
1442
+ return comparePaths(d.fullPath, fromPath)
1443
+ })
1444
+
1445
+ const matchedCurrent = [...allFromMatches].reverse().find((d) => {
1446
+ return comparePaths(d.fullPath, currentLocation.pathname)
1447
+ })
1448
+
1449
+ // for from to be invalid it shouldn't just be unmatched to currentLocation
1450
+ // but the currentLocation should also be unmatched to from
1451
+ if (!matchedFrom && !matchedCurrent) {
1452
+ console.warn(`Could not find match for from: ${fromPath}`)
1453
+ }
1431
1454
  }
1432
1455
  }
1433
1456
 
@@ -1439,19 +1462,28 @@ export class RouterCore<
1439
1462
  // Resolve the next to
1440
1463
  const nextTo = dest.to
1441
1464
  ? this.resolvePathWithBase(fromPath, `${dest.to}`)
1442
- : fromPath
1465
+ : this.resolvePathWithBase(fromPath, '.')
1443
1466
 
1444
1467
  // Resolve the next params
1445
1468
  let nextParams =
1446
- (dest.params ?? true) === true
1447
- ? fromParams
1448
- : {
1449
- ...fromParams,
1450
- ...functionalUpdate(dest.params as any, fromParams),
1451
- }
1469
+ dest.params === false || dest.params === null
1470
+ ? {}
1471
+ : (dest.params ?? true) === true
1472
+ ? fromParams
1473
+ : {
1474
+ ...fromParams,
1475
+ ...functionalUpdate(dest.params as any, fromParams),
1476
+ }
1477
+
1478
+ // Interpolate the path first to get the actual resolved path, then match against that
1479
+ const interpolatedNextTo = interpolatePath({
1480
+ path: nextTo,
1481
+ params: nextParams ?? {},
1482
+ parseCache: this.parsePathnameCache,
1483
+ }).interpolatedPath
1452
1484
 
1453
1485
  const destRoutes = this.matchRoutes(
1454
- nextTo,
1486
+ interpolatedNextTo,
1455
1487
  {},
1456
1488
  {
1457
1489
  _buildLocation: true,
@@ -1472,13 +1504,15 @@ export class RouterCore<
1472
1504
  })
1473
1505
  }
1474
1506
 
1475
- // Interpolate the next to into the next pathname
1476
1507
  const nextPathname = interpolatePath({
1508
+ // Use the original template path for interpolation
1509
+ // This preserves the original parameter syntax including optional parameters
1477
1510
  path: nextTo,
1478
1511
  params: nextParams ?? {},
1479
1512
  leaveWildcards: false,
1480
1513
  leaveParams: opts.leaveParams,
1481
1514
  decodeCharMap: this.pathParamsDecodeCharMap,
1515
+ parseCache: this.parsePathnameCache,
1482
1516
  }).interpolatedPath
1483
1517
 
1484
1518
  // Resolve the next search
@@ -1562,11 +1596,16 @@ export class RouterCore<
1562
1596
  let params = {}
1563
1597
 
1564
1598
  const foundMask = this.options.routeMasks?.find((d) => {
1565
- const match = matchPathname(this.basepath, next.pathname, {
1566
- to: d.from,
1567
- caseSensitive: false,
1568
- fuzzy: false,
1569
- })
1599
+ const match = matchPathname(
1600
+ this.basepath,
1601
+ next.pathname,
1602
+ {
1603
+ to: d.from,
1604
+ caseSensitive: false,
1605
+ fuzzy: false,
1606
+ },
1607
+ this.parsePathnameCache,
1608
+ )
1570
1609
 
1571
1610
  if (match) {
1572
1611
  params = match
@@ -1617,7 +1656,8 @@ export class RouterCore<
1617
1656
  // temporarily add the previous values to the next state so they don't affect
1618
1657
  // the comparison
1619
1658
  const ignoredProps = [
1620
- 'key',
1659
+ 'key', // TODO: Remove in v2 - use __TSR_key instead
1660
+ '__TSR_key',
1621
1661
  '__TSR_index',
1622
1662
  '__hashScrollIntoViewOptions',
1623
1663
  ] as const
@@ -1658,7 +1698,8 @@ export class RouterCore<
1658
1698
  ...nextHistory.state,
1659
1699
  __tempKey: undefined!,
1660
1700
  __tempLocation: undefined!,
1661
- key: undefined!,
1701
+ __TSR_key: undefined!,
1702
+ key: undefined!, // TODO: Remove in v2 - use __TSR_key instead
1662
1703
  },
1663
1704
  },
1664
1705
  },
@@ -1705,6 +1746,7 @@ export class RouterCore<
1705
1746
  }: BuildNextOptions & CommitLocationOptions = {}) => {
1706
1747
  if (href) {
1707
1748
  const currentIndex = this.history.location.state.__TSR_index
1749
+
1708
1750
  const parsed = parseHref(href, {
1709
1751
  __TSR_index: replace ? currentIndex : currentIndex + 1,
1710
1752
  })
@@ -1718,6 +1760,7 @@ export class RouterCore<
1718
1760
  ...(rest as any),
1719
1761
  _includeValidateSearch: true,
1720
1762
  })
1763
+
1721
1764
  return this.commitLocation({
1722
1765
  ...location,
1723
1766
  viewTransition,
@@ -1746,13 +1789,14 @@ export class RouterCore<
1746
1789
  } else {
1747
1790
  window.location.href = href
1748
1791
  }
1749
- return
1792
+ return Promise.resolve()
1750
1793
  }
1751
1794
 
1752
1795
  return this.buildAndCommitLocation({
1753
1796
  ...rest,
1754
1797
  href,
1755
1798
  to: to as string,
1799
+ _isNavigate: true,
1756
1800
  })
1757
1801
  }
1758
1802
 
@@ -1763,6 +1807,34 @@ export class RouterCore<
1763
1807
  this.cancelMatches()
1764
1808
  this.latestLocation = this.parseLocation(this.latestLocation)
1765
1809
 
1810
+ if (this.isServer) {
1811
+ // for SPAs on the initial load, this is handled by the Transitioner
1812
+ const nextLocation = this.buildLocation({
1813
+ to: this.latestLocation.pathname,
1814
+ search: true,
1815
+ params: true,
1816
+ hash: true,
1817
+ state: true,
1818
+ _includeValidateSearch: true,
1819
+ })
1820
+
1821
+ // Normalize URLs for comparison to handle encoding differences
1822
+ // Browser history always stores encoded URLs while buildLocation may produce decoded URLs
1823
+ const normalizeUrl = (url: string) => {
1824
+ try {
1825
+ return encodeURI(decodeURI(url))
1826
+ } catch {
1827
+ return url
1828
+ }
1829
+ }
1830
+
1831
+ if (
1832
+ trimPath(normalizeUrl(this.latestLocation.href)) !==
1833
+ trimPath(normalizeUrl(nextLocation.href))
1834
+ ) {
1835
+ throw redirect({ href: nextLocation.href })
1836
+ }
1837
+ }
1766
1838
  // Match the routes
1767
1839
  const pendingMatches = this.matchRoutes(this.latestLocation)
1768
1840
 
@@ -1770,20 +1842,20 @@ export class RouterCore<
1770
1842
  this.__store.setState((s) => ({
1771
1843
  ...s,
1772
1844
  status: 'pending',
1845
+ statusCode: 200,
1773
1846
  isLoading: true,
1774
1847
  location: this.latestLocation,
1775
1848
  pendingMatches,
1776
1849
  // 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
- }),
1850
+ cachedMatches: s.cachedMatches.filter(
1851
+ (d) => !pendingMatches.some((e) => e.id === d.id),
1852
+ ),
1780
1853
  }))
1781
1854
  }
1782
1855
 
1783
1856
  load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1784
1857
  let redirect: AnyRedirect | undefined
1785
1858
  let notFound: NotFoundError | undefined
1786
-
1787
1859
  let loadPromise: Promise<void>
1788
1860
 
1789
1861
  // eslint-disable-next-line prefer-const
@@ -1834,14 +1906,14 @@ export class RouterCore<
1834
1906
  const newMatches = s.pendingMatches || s.matches
1835
1907
 
1836
1908
  exitingMatches = previousMatches.filter(
1837
- (match) => !newMatches.find((d) => d.id === match.id),
1909
+ (match) => !newMatches.some((d) => d.id === match.id),
1838
1910
  )
1839
1911
  enteringMatches = newMatches.filter(
1840
1912
  (match) =>
1841
- !previousMatches.find((d) => d.id === match.id),
1913
+ !previousMatches.some((d) => d.id === match.id),
1842
1914
  )
1843
1915
  stayingMatches = previousMatches.filter((match) =>
1844
- newMatches.find((d) => d.id === match.id),
1916
+ newMatches.some((d) => d.id === match.id),
1845
1917
  )
1846
1918
 
1847
1919
  return {
@@ -1980,37 +2052,29 @@ export class RouterCore<
1980
2052
  }
1981
2053
 
1982
2054
  updateMatch: UpdateMatchFn = (id, updater) => {
1983
- let updated!: AnyRouteMatch
1984
- const isPending = this.state.pendingMatches?.find((d) => d.id === id)
1985
- const isMatched = this.state.matches.find((d) => d.id === id)
1986
- const isCached = this.state.cachedMatches.find((d) => d.id === id)
1987
-
1988
- const matchesKey = isPending
2055
+ const matchesKey = this.state.pendingMatches?.some((d) => d.id === id)
1989
2056
  ? 'pendingMatches'
1990
- : isMatched
2057
+ : this.state.matches.some((d) => d.id === id)
1991
2058
  ? 'matches'
1992
- : isCached
2059
+ : this.state.cachedMatches.some((d) => d.id === id)
1993
2060
  ? 'cachedMatches'
1994
2061
  : ''
1995
2062
 
1996
2063
  if (matchesKey) {
1997
2064
  this.__store.setState((s) => ({
1998
2065
  ...s,
1999
- [matchesKey]: s[matchesKey]?.map((d) =>
2000
- d.id === id ? (updated = updater(d)) : d,
2001
- ),
2066
+ [matchesKey]: s[matchesKey]?.map((d) => (d.id === id ? updater(d) : d)),
2002
2067
  }))
2003
2068
  }
2004
-
2005
- return updated
2006
2069
  }
2007
2070
 
2008
2071
  getMatch: GetMatchFn = (matchId: string) => {
2009
- return [
2010
- ...this.state.cachedMatches,
2011
- ...(this.state.pendingMatches ?? []),
2012
- ...this.state.matches,
2013
- ].find((d) => d.id === matchId)
2072
+ const findFn = (d: { id: string }) => d.id === matchId
2073
+ return (
2074
+ this.state.cachedMatches.find(findFn) ??
2075
+ this.state.pendingMatches?.find(findFn) ??
2076
+ this.state.matches.find(findFn)
2077
+ )
2014
2078
  }
2015
2079
 
2016
2080
  loadMatches = async ({
@@ -2043,7 +2107,13 @@ export class RouterCore<
2043
2107
  }
2044
2108
 
2045
2109
  const resolvePreload = (matchId: string) => {
2046
- return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
2110
+ return !!(allPreload && !this.state.matches.some((d) => d.id === matchId))
2111
+ }
2112
+
2113
+ // make sure the pending component is immediately rendered when hydrating a match that is not SSRed
2114
+ // the pending component was already rendered on the server and we want to keep it shown on the client until minPendingMs is reached
2115
+ if (!this.isServer && this.state.matches.some((d) => d._forcePending)) {
2116
+ triggerOnReady()
2047
2117
  }
2048
2118
 
2049
2119
  const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
@@ -2056,6 +2126,9 @@ export class RouterCore<
2056
2126
  }
2057
2127
  }
2058
2128
 
2129
+ match.beforeLoadPromise?.resolve()
2130
+ match.loaderPromise?.resolve()
2131
+
2059
2132
  updateMatch(match.id, (prev) => ({
2060
2133
  ...prev,
2061
2134
  status: isRedirect(err)
@@ -2073,8 +2146,6 @@ export class RouterCore<
2073
2146
  ;(err as any).routeId = match.routeId
2074
2147
  }
2075
2148
 
2076
- match.beforeLoadPromise?.resolve()
2077
- match.loaderPromise?.resolve()
2078
2149
  match.loadPromise?.resolve()
2079
2150
 
2080
2151
  if (isRedirect(err)) {
@@ -2087,15 +2158,26 @@ export class RouterCore<
2087
2158
  this._handleNotFound(matches, err, {
2088
2159
  updateMatch,
2089
2160
  })
2090
- this.serverSsr?.onMatchSettled({
2091
- router: this,
2092
- match: this.getMatch(match.id)!,
2093
- })
2094
2161
  throw err
2095
2162
  }
2096
2163
  }
2097
2164
  }
2098
2165
 
2166
+ const shouldSkipLoader = (matchId: string) => {
2167
+ const match = this.getMatch(matchId)!
2168
+ // upon hydration, we skip the loader if the match has been dehydrated on the server
2169
+ if (!this.isServer && match._dehydrated) {
2170
+ return true
2171
+ }
2172
+
2173
+ if (this.isServer) {
2174
+ if (match.ssr === false) {
2175
+ return true
2176
+ }
2177
+ }
2178
+ return false
2179
+ }
2180
+
2099
2181
  try {
2100
2182
  await new Promise<void>((resolveAll, rejectAll) => {
2101
2183
  ;(async () => {
@@ -2145,12 +2227,78 @@ export class RouterCore<
2145
2227
  for (const [index, { id: matchId, routeId }] of matches.entries()) {
2146
2228
  const existingMatch = this.getMatch(matchId)!
2147
2229
  const parentMatchId = matches[index - 1]?.id
2230
+ const parentMatch = parentMatchId
2231
+ ? this.getMatch(parentMatchId)!
2232
+ : undefined
2148
2233
 
2149
2234
  const route = this.looseRoutesById[routeId]!
2150
2235
 
2151
2236
  const pendingMs =
2152
2237
  route.options.pendingMs ?? this.options.defaultPendingMs
2153
2238
 
2239
+ // on the server, determine whether SSR the current match or not
2240
+ if (this.isServer) {
2241
+ let ssr: boolean | 'data-only'
2242
+ // in SPA mode, only SSR the root route
2243
+ if (this.isShell()) {
2244
+ ssr = matchId === rootRouteId
2245
+ } else {
2246
+ const defaultSsr = this.options.defaultSsr ?? true
2247
+ if (parentMatch?.ssr === false) {
2248
+ ssr = false
2249
+ } else {
2250
+ let tempSsr: boolean | 'data-only'
2251
+ if (route.options.ssr === undefined) {
2252
+ tempSsr = defaultSsr
2253
+ } else if (typeof route.options.ssr === 'function') {
2254
+ const { search, params } = this.getMatch(matchId)!
2255
+
2256
+ function makeMaybe(value: any, error: any) {
2257
+ if (error) {
2258
+ return { status: 'error' as const, error }
2259
+ }
2260
+ return { status: 'success' as const, value }
2261
+ }
2262
+
2263
+ const ssrFnContext: SsrContextOptions<any, any, any> = {
2264
+ search: makeMaybe(search, existingMatch.searchError),
2265
+ params: makeMaybe(params, existingMatch.paramsError),
2266
+ location,
2267
+ matches: matches.map((match) => ({
2268
+ index: match.index,
2269
+ pathname: match.pathname,
2270
+ fullPath: match.fullPath,
2271
+ staticData: match.staticData,
2272
+ id: match.id,
2273
+ routeId: match.routeId,
2274
+ search: makeMaybe(match.search, match.searchError),
2275
+ params: makeMaybe(match.params, match.paramsError),
2276
+ ssr: match.ssr,
2277
+ })),
2278
+ }
2279
+ tempSsr =
2280
+ (await route.options.ssr(ssrFnContext)) ?? defaultSsr
2281
+ } else {
2282
+ tempSsr = route.options.ssr
2283
+ }
2284
+
2285
+ if (tempSsr === true && parentMatch?.ssr === 'data-only') {
2286
+ ssr = 'data-only'
2287
+ } else {
2288
+ ssr = tempSsr
2289
+ }
2290
+ }
2291
+ }
2292
+ updateMatch(matchId, (prev) => ({
2293
+ ...prev,
2294
+ ssr,
2295
+ }))
2296
+ }
2297
+
2298
+ if (shouldSkipLoader(matchId)) {
2299
+ continue
2300
+ }
2301
+
2154
2302
  const shouldPending = !!(
2155
2303
  onReady &&
2156
2304
  !this.isServer &&
@@ -2165,25 +2313,43 @@ export class RouterCore<
2165
2313
  )
2166
2314
 
2167
2315
  let executeBeforeLoad = true
2168
- if (
2169
- // If we are in the middle of a load, either of these will be present
2170
- // (not to be confused with `loadPromise`, which is always defined)
2171
- existingMatch.beforeLoadPromise ||
2172
- existingMatch.loaderPromise
2173
- ) {
2174
- if (shouldPending) {
2175
- setTimeout(() => {
2316
+ const setupPendingTimeout = () => {
2317
+ if (
2318
+ shouldPending &&
2319
+ this.getMatch(matchId)!.pendingTimeout === undefined
2320
+ ) {
2321
+ const pendingTimeout = setTimeout(() => {
2176
2322
  try {
2177
2323
  // Update the match and prematurely resolve the loadMatches promise so that
2178
2324
  // the pending component can start rendering
2179
2325
  triggerOnReady()
2180
2326
  } catch {}
2181
2327
  }, pendingMs)
2328
+ updateMatch(matchId, (prev) => ({
2329
+ ...prev,
2330
+ pendingTimeout,
2331
+ }))
2182
2332
  }
2333
+ }
2334
+ if (
2335
+ // If we are in the middle of a load, either of these will be present
2336
+ // (not to be confused with `loadPromise`, which is always defined)
2337
+ existingMatch.beforeLoadPromise ||
2338
+ existingMatch.loaderPromise
2339
+ ) {
2340
+ setupPendingTimeout()
2183
2341
 
2184
2342
  // Wait for the beforeLoad to resolve before we continue
2185
2343
  await existingMatch.beforeLoadPromise
2186
- executeBeforeLoad = this.getMatch(matchId)!.status !== 'success'
2344
+ const match = this.getMatch(matchId)!
2345
+ if (match.status === 'error') {
2346
+ executeBeforeLoad = true
2347
+ } else if (
2348
+ match.preload &&
2349
+ (match.status === 'redirected' || match.status === 'notFound')
2350
+ ) {
2351
+ handleRedirectAndNotFound(match, match.error)
2352
+ }
2187
2353
  }
2188
2354
  if (executeBeforeLoad) {
2189
2355
  // If we are not in the middle of a load OR the previous load failed, start it
@@ -2199,21 +2365,6 @@ export class RouterCore<
2199
2365
  beforeLoadPromise: createControlledPromise<void>(),
2200
2366
  }
2201
2367
  })
2202
- const abortController = new AbortController()
2203
-
2204
- let pendingTimeout: ReturnType<typeof setTimeout>
2205
-
2206
- if (shouldPending) {
2207
- // If we might show a pending component, we need to wait for the
2208
- // pending promise to resolve before we start showing that state
2209
- pendingTimeout = setTimeout(() => {
2210
- try {
2211
- // Update the match and prematurely resolve the loadMatches promise so that
2212
- // the pending component can start rendering
2213
- triggerOnReady()
2214
- } catch {}
2215
- }, pendingMs)
2216
- }
2217
2368
 
2218
2369
  const { paramsError, searchError } = this.getMatch(matchId)!
2219
2370
 
@@ -2225,19 +2376,20 @@ export class RouterCore<
2225
2376
  handleSerialError(index, searchError, 'VALIDATE_SEARCH')
2226
2377
  }
2227
2378
 
2228
- const getParentMatchContext = () =>
2229
- parentMatchId
2230
- ? this.getMatch(parentMatchId)!.context
2231
- : (this.options.context ?? {})
2379
+ setupPendingTimeout()
2380
+
2381
+ const abortController = new AbortController()
2382
+
2383
+ const parentMatchContext =
2384
+ parentMatch?.context ?? this.options.context ?? {}
2232
2385
 
2233
2386
  updateMatch(matchId, (prev) => ({
2234
2387
  ...prev,
2235
2388
  isFetching: 'beforeLoad',
2236
2389
  fetchCount: prev.fetchCount + 1,
2237
2390
  abortController,
2238
- pendingTimeout,
2239
2391
  context: {
2240
- ...getParentMatchContext(),
2392
+ ...parentMatchContext,
2241
2393
  ...prev.__routeContext,
2242
2394
  },
2243
2395
  }))
@@ -2268,8 +2420,7 @@ export class RouterCore<
2268
2420
  }
2269
2421
 
2270
2422
  const beforeLoadContext =
2271
- (await route.options.beforeLoad?.(beforeLoadFnContext)) ??
2272
- {}
2423
+ await route.options.beforeLoad?.(beforeLoadFnContext)
2273
2424
 
2274
2425
  if (
2275
2426
  isRedirect(beforeLoadContext) ||
@@ -2283,7 +2434,7 @@ export class RouterCore<
2283
2434
  ...prev,
2284
2435
  __beforeLoadContext: beforeLoadContext,
2285
2436
  context: {
2286
- ...getParentMatchContext(),
2437
+ ...parentMatchContext,
2287
2438
  ...prev.__routeContext,
2288
2439
  ...beforeLoadContext,
2289
2440
  },
@@ -2312,21 +2463,78 @@ export class RouterCore<
2312
2463
  validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
2313
2464
  matchPromises.push(
2314
2465
  (async () => {
2315
- const { loaderPromise: prevLoaderPromise } =
2316
- this.getMatch(matchId)!
2317
-
2318
2466
  let loaderShouldRunAsync = false
2319
2467
  let loaderIsRunningAsync = false
2468
+ const route = this.looseRoutesById[routeId]!
2469
+
2470
+ const executeHead = async () => {
2471
+ const match = this.getMatch(matchId)
2472
+ // in case of a redirecting match during preload, the match does not exist
2473
+ if (!match) {
2474
+ return
2475
+ }
2476
+ const assetContext = {
2477
+ matches,
2478
+ match,
2479
+ params: match.params,
2480
+ loaderData: match.loaderData,
2481
+ }
2482
+ const headFnContent =
2483
+ await route.options.head?.(assetContext)
2484
+ const meta = headFnContent?.meta
2485
+ const links = headFnContent?.links
2486
+ const headScripts = headFnContent?.scripts
2487
+ const styles = headFnContent?.styles
2488
+
2489
+ const scripts = await route.options.scripts?.(assetContext)
2490
+ const headers = await route.options.headers?.(assetContext)
2491
+ return {
2492
+ meta,
2493
+ links,
2494
+ headScripts,
2495
+ headers,
2496
+ scripts,
2497
+ styles,
2498
+ }
2499
+ }
2320
2500
 
2321
- if (prevLoaderPromise) {
2322
- await prevLoaderPromise
2501
+ const potentialPendingMinPromise = async () => {
2502
+ const latestMatch = this.getMatch(matchId)!
2503
+ if (latestMatch.minPendingPromise) {
2504
+ await latestMatch.minPendingPromise
2505
+ }
2506
+ }
2507
+
2508
+ const prevMatch = this.getMatch(matchId)!
2509
+ if (shouldSkipLoader(matchId)) {
2510
+ if (this.isServer) {
2511
+ const head = await executeHead()
2512
+ updateMatch(matchId, (prev) => ({
2513
+ ...prev,
2514
+ ...head,
2515
+ }))
2516
+ return this.getMatch(matchId)!
2517
+ }
2518
+ }
2519
+ // there is a loaderPromise, so we are in the middle of a load
2520
+ else if (prevMatch.loaderPromise) {
2521
+ // do not block if we already have stale data we can show
2522
+ // but only if the ongoing load is not a preload since error handling is different for preloads
2523
+ // and we don't want to swallow errors
2524
+ if (
2525
+ prevMatch.status === 'success' &&
2526
+ !sync &&
2527
+ !prevMatch.preload
2528
+ ) {
2529
+ return this.getMatch(matchId)!
2530
+ }
2531
+ await prevMatch.loaderPromise
2323
2532
  const match = this.getMatch(matchId)!
2324
2533
  if (match.error) {
2325
2534
  handleRedirectAndNotFound(match, match.error)
2326
2535
  }
2327
2536
  } else {
2328
2537
  const parentMatchPromise = matchPromises[index - 1] as any
2329
- const route = this.looseRoutesById[routeId]!
2330
2538
 
2331
2539
  const getLoaderContext = (): LoaderFnContext => {
2332
2540
  const {
@@ -2382,34 +2590,9 @@ export class RouterCore<
2382
2590
  loaderPromise: createControlledPromise<void>(),
2383
2591
  preload:
2384
2592
  !!preload &&
2385
- !this.state.matches.find((d) => d.id === matchId),
2593
+ !this.state.matches.some((d) => d.id === matchId),
2386
2594
  }))
2387
2595
 
2388
- const executeHead = async () => {
2389
- const match = this.getMatch(matchId)
2390
- // in case of a redirecting match during preload, the match does not exist
2391
- if (!match) {
2392
- return
2393
- }
2394
- const assetContext = {
2395
- matches,
2396
- match,
2397
- params: match.params,
2398
- loaderData: match.loaderData,
2399
- }
2400
- const headFnContent =
2401
- await route.options.head?.(assetContext)
2402
- const meta = headFnContent?.meta
2403
- const links = headFnContent?.links
2404
- const headScripts = headFnContent?.scripts
2405
-
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 }
2411
- }
2412
-
2413
2596
  const runLoader = async () => {
2414
2597
  try {
2415
2598
  // If the Matches component rendered
@@ -2417,17 +2600,16 @@ export class RouterCore<
2417
2600
  // a minimum duration, we''ll wait for it to resolve
2418
2601
  // before committing to the match and resolving
2419
2602
  // the loadPromise
2420
- const potentialPendingMinPromise = async () => {
2421
- const latestMatch = this.getMatch(matchId)!
2422
-
2423
- if (latestMatch.minPendingPromise) {
2424
- await latestMatch.minPendingPromise
2425
- }
2426
- }
2427
2603
 
2428
2604
  // Actually run the loader and handle the result
2429
2605
  try {
2430
- this.loadRouteChunk(route)
2606
+ if (
2607
+ !this.isServer ||
2608
+ (this.isServer &&
2609
+ this.getMatch(matchId)!.ssr === true)
2610
+ ) {
2611
+ this.loadRouteChunk(route)
2612
+ }
2431
2613
 
2432
2614
  updateMatch(matchId, (prev) => ({
2433
2615
  ...prev,
@@ -2442,29 +2624,27 @@ export class RouterCore<
2442
2624
  this.getMatch(matchId)!,
2443
2625
  loaderData,
2444
2626
  )
2627
+ updateMatch(matchId, (prev) => ({
2628
+ ...prev,
2629
+ loaderData,
2630
+ }))
2445
2631
 
2446
2632
  // Lazy option can modify the route options,
2447
2633
  // so we need to wait for it to resolve before
2448
2634
  // we can use the options
2449
2635
  await route._lazyPromise
2450
-
2636
+ const head = await executeHead()
2451
2637
  await potentialPendingMinPromise()
2452
2638
 
2453
2639
  // Last but not least, wait for the the components
2454
2640
  // to be preloaded before we resolve the match
2455
2641
  await route._componentsPromise
2456
-
2457
2642
  updateMatch(matchId, (prev) => ({
2458
2643
  ...prev,
2459
2644
  error: undefined,
2460
2645
  status: 'success',
2461
2646
  isFetching: false,
2462
2647
  updatedAt: Date.now(),
2463
- loaderData,
2464
- }))
2465
- const head = await executeHead()
2466
- updateMatch(matchId, (prev) => ({
2467
- ...prev,
2468
2648
  ...head,
2469
2649
  }))
2470
2650
  } catch (e) {
@@ -2492,11 +2672,6 @@ export class RouterCore<
2492
2672
  ...head,
2493
2673
  }))
2494
2674
  }
2495
-
2496
- this.serverSsr?.onMatchSettled({
2497
- router: this,
2498
- match: this.getMatch(matchId)!,
2499
- })
2500
2675
  } catch (err) {
2501
2676
  const head = await executeHead()
2502
2677
 
@@ -2558,14 +2733,21 @@ export class RouterCore<
2558
2733
  loadPromise?.resolve()
2559
2734
  }
2560
2735
 
2561
- updateMatch(matchId, (prev) => ({
2562
- ...prev,
2563
- isFetching: loaderIsRunningAsync ? prev.isFetching : false,
2564
- loaderPromise: loaderIsRunningAsync
2565
- ? prev.loaderPromise
2566
- : undefined,
2567
- invalid: false,
2568
- }))
2736
+ updateMatch(matchId, (prev) => {
2737
+ clearTimeout(prev.pendingTimeout)
2738
+ return {
2739
+ ...prev,
2740
+ isFetching: loaderIsRunningAsync
2741
+ ? prev.isFetching
2742
+ : false,
2743
+ loaderPromise: loaderIsRunningAsync
2744
+ ? prev.loaderPromise
2745
+ : undefined,
2746
+ invalid: false,
2747
+ pendingTimeout: undefined,
2748
+ _dehydrated: undefined,
2749
+ }
2750
+ })
2569
2751
  return this.getMatch(matchId)!
2570
2752
  })(),
2571
2753
  )
@@ -2607,7 +2789,7 @@ export class RouterCore<
2607
2789
  return {
2608
2790
  ...d,
2609
2791
  invalid: true,
2610
- ...(d.status === 'error'
2792
+ ...(opts?.forcePending || d.status === 'error'
2611
2793
  ? ({ status: 'pending', error: undefined } as const)
2612
2794
  : {}),
2613
2795
  }
@@ -2677,7 +2859,11 @@ export class RouterCore<
2677
2859
  : (route.options.gcTime ?? this.options.defaultGcTime)) ??
2678
2860
  5 * 60 * 1000
2679
2861
 
2680
- return !(d.status !== 'error' && Date.now() - d.updatedAt < gcTime)
2862
+ const isError = d.status === 'error'
2863
+ if (isError) return true
2864
+
2865
+ const gcEligible = Date.now() - d.updatedAt >= gcTime
2866
+ return gcEligible
2681
2867
  }
2682
2868
  this.clearCache({ filter })
2683
2869
  }
@@ -2815,10 +3001,15 @@ export class RouterCore<
2815
3001
  ? this.latestLocation
2816
3002
  : this.state.resolvedLocation || this.state.location
2817
3003
 
2818
- const match = matchPathname(this.basepath, baseLocation.pathname, {
2819
- ...opts,
2820
- to: next.pathname,
2821
- }) as any
3004
+ const match = matchPathname(
3005
+ this.basepath,
3006
+ baseLocation.pathname,
3007
+ {
3008
+ ...opts,
3009
+ to: next.pathname,
3010
+ },
3011
+ this.parsePathnameCache,
3012
+ ) as any
2822
3013
 
2823
3014
  if (!match) {
2824
3015
  return false
@@ -2840,24 +3031,9 @@ export class RouterCore<
2840
3031
 
2841
3032
  ssr?: {
2842
3033
  manifest: Manifest | undefined
2843
- serializer: StartSerializer
2844
- }
2845
-
2846
- serverSsr?: {
2847
- injectedHtml: Array<InjectedHtmlEntry>
2848
- injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
2849
- injectScript: (
2850
- getScript: () => string | Promise<string>,
2851
- opts?: { logScript?: boolean },
2852
- ) => Promise<void>
2853
- streamValue: (key: string, value: any) => void
2854
- streamedKeys: Set<string>
2855
- onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
2856
3034
  }
2857
3035
 
2858
- clientSsr?: {
2859
- getStreamedValue: <T>(key: string) => T | undefined
2860
- }
3036
+ serverSsr?: ServerSsr
2861
3037
 
2862
3038
  _handleNotFound = (
2863
3039
  matches: Array<AnyRouteMatch>,
@@ -2932,6 +3108,12 @@ export class SearchParamError extends Error {}
2932
3108
 
2933
3109
  export class PathParamError extends Error {}
2934
3110
 
3111
+ const normalize = (str: string) =>
3112
+ str.endsWith('/') && str.length > 1 ? str.slice(0, -1) : str
3113
+ function comparePaths(a: string, b: string) {
3114
+ return normalize(a) === normalize(b)
3115
+ }
3116
+
2935
3117
  // A function that takes an import() argument which is a function and returns a new function that will
2936
3118
  // proxy arguments from the caller to the imported function, retaining all type
2937
3119
  // information along the way
@@ -3021,13 +3203,57 @@ interface RouteLike {
3021
3203
  }
3022
3204
  }
3023
3205
 
3206
+ export type ProcessRouteTreeResult<TRouteLike extends RouteLike> = {
3207
+ routesById: Record<string, TRouteLike>
3208
+ routesByPath: Record<string, TRouteLike>
3209
+ flatRoutes: Array<TRouteLike>
3210
+ }
3211
+
3212
+ const REQUIRED_PARAM_BASE_SCORE = 0.5
3213
+ const OPTIONAL_PARAM_BASE_SCORE = 0.4
3214
+ const WILDCARD_PARAM_BASE_SCORE = 0.25
3215
+ const BOTH_PRESENCE_BASE_SCORE = 0.05
3216
+ const PREFIX_PRESENCE_BASE_SCORE = 0.02
3217
+ const SUFFIX_PRESENCE_BASE_SCORE = 0.01
3218
+ const PREFIX_LENGTH_SCORE_MULTIPLIER = 0.0002
3219
+ const SUFFIX_LENGTH_SCORE_MULTIPLIER = 0.0001
3220
+
3221
+ function handleParam(segment: Segment, baseScore: number) {
3222
+ if (segment.prefixSegment && segment.suffixSegment) {
3223
+ return (
3224
+ baseScore +
3225
+ BOTH_PRESENCE_BASE_SCORE +
3226
+ PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length +
3227
+ SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
3228
+ )
3229
+ }
3230
+
3231
+ if (segment.prefixSegment) {
3232
+ return (
3233
+ baseScore +
3234
+ PREFIX_PRESENCE_BASE_SCORE +
3235
+ PREFIX_LENGTH_SCORE_MULTIPLIER * segment.prefixSegment.length
3236
+ )
3237
+ }
3238
+
3239
+ if (segment.suffixSegment) {
3240
+ return (
3241
+ baseScore +
3242
+ SUFFIX_PRESENCE_BASE_SCORE +
3243
+ SUFFIX_LENGTH_SCORE_MULTIPLIER * segment.suffixSegment.length
3244
+ )
3245
+ }
3246
+
3247
+ return baseScore
3248
+ }
3249
+
3024
3250
  export function processRouteTree<TRouteLike extends RouteLike>({
3025
3251
  routeTree,
3026
3252
  initRoute,
3027
3253
  }: {
3028
3254
  routeTree: TRouteLike
3029
3255
  initRoute?: (route: TRouteLike, index: number) => void
3030
- }) {
3256
+ }): ProcessRouteTreeResult<TRouteLike> {
3031
3257
  const routesById = {} as Record<string, TRouteLike>
3032
3258
  const routesByPath = {} as Record<string, TRouteLike>
3033
3259
 
@@ -3067,9 +3293,11 @@ export function processRouteTree<TRouteLike extends RouteLike>({
3067
3293
  const scoredRoutes: Array<{
3068
3294
  child: TRouteLike
3069
3295
  trimmed: string
3070
- parsed: ReturnType<typeof parsePathname>
3296
+ parsed: ReadonlyArray<Segment>
3071
3297
  index: number
3072
3298
  scores: Array<number>
3299
+ hasStaticAfter: boolean
3300
+ optionalParamCount: number
3073
3301
  }> = []
3074
3302
 
3075
3303
  const routes: Array<TRouteLike> = Object.values(routesById)
@@ -3080,81 +3308,94 @@ export function processRouteTree<TRouteLike extends RouteLike>({
3080
3308
  }
3081
3309
 
3082
3310
  const trimmed = trimPathLeft(d.fullPath)
3083
- const parsed = parsePathname(trimmed)
3311
+ let parsed = parsePathname(trimmed)
3084
3312
 
3085
3313
  // Removes the leading slash if it is not the only remaining segment
3086
- while (parsed.length > 1 && parsed[0]?.value === '/') {
3087
- parsed.shift()
3314
+ let skip = 0
3315
+ while (parsed.length > skip + 1 && parsed[skip]?.value === '/') {
3316
+ skip++
3088
3317
  }
3318
+ if (skip > 0) parsed = parsed.slice(skip)
3089
3319
 
3090
- const scores = parsed.map((segment) => {
3320
+ let optionalParamCount = 0
3321
+ let hasStaticAfter = false
3322
+ const scores = parsed.map((segment, index) => {
3091
3323
  if (segment.value === '/') {
3092
3324
  return 0.75
3093
3325
  }
3094
3326
 
3095
- if (
3096
- segment.type === 'param' &&
3097
- segment.prefixSegment &&
3098
- segment.suffixSegment
3099
- ) {
3100
- return 0.55
3101
- }
3102
-
3103
- if (segment.type === 'param' && segment.prefixSegment) {
3104
- return 0.52
3105
- }
3106
-
3107
- if (segment.type === 'param' && segment.suffixSegment) {
3108
- return 0.51
3327
+ let baseScore: number | undefined = undefined
3328
+ if (segment.type === SEGMENT_TYPE_PARAM) {
3329
+ baseScore = REQUIRED_PARAM_BASE_SCORE
3330
+ } else if (segment.type === SEGMENT_TYPE_OPTIONAL_PARAM) {
3331
+ baseScore = OPTIONAL_PARAM_BASE_SCORE
3332
+ optionalParamCount++
3333
+ } else if (segment.type === SEGMENT_TYPE_WILDCARD) {
3334
+ baseScore = WILDCARD_PARAM_BASE_SCORE
3109
3335
  }
3110
3336
 
3111
- if (segment.type === 'param') {
3112
- return 0.5
3113
- }
3114
-
3115
- if (
3116
- segment.type === 'wildcard' &&
3117
- segment.prefixSegment &&
3118
- segment.suffixSegment
3119
- ) {
3120
- return 0.3
3121
- }
3122
-
3123
- if (segment.type === 'wildcard' && segment.prefixSegment) {
3124
- return 0.27
3125
- }
3126
-
3127
- if (segment.type === 'wildcard' && segment.suffixSegment) {
3128
- return 0.26
3129
- }
3337
+ if (baseScore) {
3338
+ // if there is any static segment (that is not an index) after a required / optional param,
3339
+ // we will boost this param so it ranks higher than a required/optional param without a static segment after it
3340
+ // JUST FOR SORTING, NOT FOR MATCHING
3341
+ for (let i = index + 1; i < parsed.length; i++) {
3342
+ const nextSegment = parsed[i]!
3343
+ if (
3344
+ nextSegment.type === SEGMENT_TYPE_PATHNAME &&
3345
+ nextSegment.value !== '/'
3346
+ ) {
3347
+ hasStaticAfter = true
3348
+ return handleParam(segment, baseScore + 0.2)
3349
+ }
3350
+ }
3130
3351
 
3131
- if (segment.type === 'wildcard') {
3132
- return 0.25
3352
+ return handleParam(segment, baseScore)
3133
3353
  }
3134
3354
 
3135
3355
  return 1
3136
3356
  })
3137
3357
 
3138
- scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
3358
+ scoredRoutes.push({
3359
+ child: d,
3360
+ trimmed,
3361
+ parsed,
3362
+ index: i,
3363
+ scores,
3364
+ optionalParamCount,
3365
+ hasStaticAfter,
3366
+ })
3139
3367
  })
3140
3368
 
3141
3369
  const flatRoutes = scoredRoutes
3142
3370
  .sort((a, b) => {
3143
3371
  const minLength = Math.min(a.scores.length, b.scores.length)
3144
3372
 
3145
- // Sort by min available score
3373
+ // Sort by segment-by-segment score comparison ONLY for the common prefix
3146
3374
  for (let i = 0; i < minLength; i++) {
3147
3375
  if (a.scores[i] !== b.scores[i]) {
3148
3376
  return b.scores[i]! - a.scores[i]!
3149
3377
  }
3150
3378
  }
3151
3379
 
3152
- // Sort by length of score
3380
+ // If all common segments have equal scores, then consider length and specificity
3153
3381
  if (a.scores.length !== b.scores.length) {
3382
+ // If different number of optional parameters, fewer optional parameters wins (more specific)
3383
+ // only if both or none of the routes has static segments after the params
3384
+ if (a.optionalParamCount !== b.optionalParamCount) {
3385
+ if (a.hasStaticAfter === b.hasStaticAfter) {
3386
+ return a.optionalParamCount - b.optionalParamCount
3387
+ } else if (a.hasStaticAfter && !b.hasStaticAfter) {
3388
+ return -1
3389
+ } else if (!a.hasStaticAfter && b.hasStaticAfter) {
3390
+ return 1
3391
+ }
3392
+ }
3393
+
3394
+ // If same number of optional parameters, longer path wins (for static segments)
3154
3395
  return b.scores.length - a.scores.length
3155
3396
  }
3156
3397
 
3157
- // Sort by min available parsed value
3398
+ // Sort by min available parsed value for alphabetical ordering
3158
3399
  for (let i = 0; i < minLength; i++) {
3159
3400
  if (a.parsed[i]!.value !== b.parsed[i]!.value) {
3160
3401
  return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
@@ -3180,6 +3421,7 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3180
3421
  routesByPath,
3181
3422
  routesById,
3182
3423
  flatRoutes,
3424
+ parseCache,
3183
3425
  }: {
3184
3426
  pathname: string
3185
3427
  routePathname?: string
@@ -3188,15 +3430,22 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3188
3430
  routesByPath: Record<string, TRouteLike>
3189
3431
  routesById: Record<string, TRouteLike>
3190
3432
  flatRoutes: Array<TRouteLike>
3433
+ parseCache?: ParsePathnameCache
3191
3434
  }) {
3192
3435
  let routeParams: Record<string, string> = {}
3193
3436
  const trimmedPath = trimPathRight(pathname)
3194
3437
  const getMatchedParams = (route: TRouteLike) => {
3195
- const result = matchPathname(basepath, trimmedPath, {
3196
- to: route.fullPath,
3197
- caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3198
- fuzzy: true,
3199
- })
3438
+ const result = matchPathname(
3439
+ basepath,
3440
+ trimmedPath,
3441
+ {
3442
+ to: route.fullPath,
3443
+ caseSensitive: route.options?.caseSensitive ?? caseSensitive,
3444
+ // we need fuzzy matching for `notFoundMode: 'fuzzy'`
3445
+ fuzzy: true,
3446
+ },
3447
+ parseCache,
3448
+ )
3200
3449
  return result
3201
3450
  }
3202
3451
 
@@ -3205,16 +3454,34 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3205
3454
  if (foundRoute) {
3206
3455
  routeParams = getMatchedParams(foundRoute)!
3207
3456
  } else {
3208
- foundRoute = flatRoutes.find((route) => {
3457
+ // iterate over flatRoutes to find the best match
3458
+ // if we find a fuzzy matching route, keep looking for a perfect fit
3459
+ let fuzzyMatch:
3460
+ | { foundRoute: TRouteLike; routeParams: Record<string, string> }
3461
+ | undefined = undefined
3462
+ for (const route of flatRoutes) {
3209
3463
  const matchedParams = getMatchedParams(route)
3210
3464
 
3211
3465
  if (matchedParams) {
3212
- routeParams = matchedParams
3213
- return true
3466
+ if (
3467
+ route.path !== '/' &&
3468
+ (matchedParams as Record<string, string>)['**']
3469
+ ) {
3470
+ if (!fuzzyMatch) {
3471
+ fuzzyMatch = { foundRoute: route, routeParams: matchedParams }
3472
+ }
3473
+ } else {
3474
+ foundRoute = route
3475
+ routeParams = matchedParams
3476
+ break
3477
+ }
3214
3478
  }
3215
-
3216
- return false
3217
- })
3479
+ }
3480
+ // did not find a perfect fit, so take the fuzzy matching route if it exists
3481
+ if (!foundRoute && fuzzyMatch) {
3482
+ foundRoute = fuzzyMatch.foundRoute
3483
+ routeParams = fuzzyMatch.routeParams
3484
+ }
3218
3485
  }
3219
3486
 
3220
3487
  let routeCursor: TRouteLike = foundRoute || routesById[rootRouteId]!
@@ -3223,8 +3490,9 @@ export function getMatchedRoutes<TRouteLike extends RouteLike>({
3223
3490
 
3224
3491
  while (routeCursor.parentRoute) {
3225
3492
  routeCursor = routeCursor.parentRoute as TRouteLike
3226
- matchedRoutes.unshift(routeCursor)
3493
+ matchedRoutes.push(routeCursor)
3227
3494
  }
3495
+ matchedRoutes.reverse()
3228
3496
 
3229
3497
  return { matchedRoutes, routeParams, foundRoute }
3230
3498
  }