@tanstack/react-router 1.114.24 → 1.114.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/router.ts CHANGED
@@ -1,93 +1,12 @@
1
- import {
2
- createBrowserHistory,
3
- createMemoryHistory,
4
- parseHref,
5
- } from '@tanstack/history'
6
- import { Store, batch } from '@tanstack/react-store'
7
- import invariant from 'tiny-invariant'
8
- import {
9
- cleanPath,
10
- createControlledPromise,
11
- deepEqual,
12
- defaultParseSearch,
13
- defaultStringifySearch,
14
- functionalUpdate,
15
- getLocationChangeInfo,
16
- interpolatePath,
17
- isNotFound,
18
- isRedirect,
19
- isResolvedRedirect,
20
- joinPaths,
21
- last,
22
- matchPathname,
23
- parsePathname,
24
- pick,
25
- replaceEqualDeep,
26
- resolvePath,
27
- rootRouteId,
28
- setupScrollRestoration,
29
- trimPath,
30
- trimPathLeft,
31
- trimPathRight,
32
- } from '@tanstack/router-core'
33
-
34
- import type * as React from 'react'
35
- import type { HistoryLocation, RouterHistory } from '@tanstack/history'
36
-
1
+ import { RouterCore } from '@tanstack/router-core'
2
+ import type { RouterHistory } from '@tanstack/history'
37
3
  import type {
38
- AnyRedirect,
39
4
  AnyRoute,
40
- AnyRouteMatch,
41
- AnyRouter,
42
- AnySchema,
43
- AnyValidator,
44
- BeforeLoadContextOptions,
45
- BuildLocationFn,
46
- BuildNextOptions,
47
- ClearCacheFn,
48
- CommitLocationFn,
49
- CommitLocationOptions,
50
- ControlledPromise,
51
- Router as CoreRouter,
52
- EmitFn,
53
- FullSearchSchema,
54
- GetMatchFn,
55
- GetMatchRoutesFn,
56
- InjectedHtmlEntry,
57
- InvalidateFn,
58
- LoadFn,
59
- LoaderFnContext,
60
- MakeRouteMatch,
61
- MakeRouteMatchUnion,
62
- Manifest,
63
- MatchRouteFn,
64
- MatchRoutesFn,
65
- MatchRoutesOpts,
66
- MatchedRoutesResult,
67
- NavigateFn,
68
- NotFoundError,
69
- ParseLocationFn,
70
- ParsedLocation,
71
- PickAsRequired,
72
- PreloadRouteFn,
73
- ResolvedRedirect,
74
- RouteContextOptions,
5
+ CreateRouterFn,
75
6
  RouterConstructorOptions,
76
- RouterEvent,
77
- RouterListener,
78
- RouterOptions,
79
- RouterState,
80
- RoutesById,
81
- RoutesByPath,
82
- SearchMiddleware,
83
- StartSerializer,
84
- StartTransitionFn,
85
- SubscribeFn,
86
7
  TrailingSlashOption,
87
- UpdateFn,
88
- UpdateMatchFn,
89
- ViewTransitionOptions,
90
8
  } from '@tanstack/router-core'
9
+
91
10
  import type {
92
11
  ErrorRouteComponent,
93
12
  NotFoundRouteComponent,
@@ -157,134 +76,23 @@ declare module '@tanstack/router-core' {
157
76
  }
158
77
  }
159
78
 
160
- export const componentTypes = [
161
- 'component',
162
- 'errorComponent',
163
- 'pendingComponent',
164
- 'notFoundComponent',
165
- ] as const
166
-
167
- function routeNeedsPreload(route: AnyRoute) {
168
- for (const componentType of componentTypes) {
169
- if ((route.options[componentType] as any)?.preload) {
170
- return true
171
- }
172
- }
173
- return false
79
+ export const createRouter: CreateRouterFn = (options) => {
80
+ return new Router(options)
174
81
  }
175
82
 
176
- function validateSearch(validateSearch: AnyValidator, input: unknown): unknown {
177
- if (validateSearch == null) return {}
178
-
179
- if ('~standard' in validateSearch) {
180
- const result = validateSearch['~standard'].validate(input)
181
-
182
- if (result instanceof Promise)
183
- throw new SearchParamError('Async validation not supported')
184
-
185
- if (result.issues)
186
- throw new SearchParamError(JSON.stringify(result.issues, undefined, 2), {
187
- cause: result,
188
- })
189
-
190
- return result.value
191
- }
192
-
193
- if ('parse' in validateSearch) {
194
- return validateSearch.parse(input)
195
- }
196
-
197
- if (typeof validateSearch === 'function') {
198
- return validateSearch(input)
199
- }
200
-
201
- return {}
202
- }
203
-
204
- export function createRouter<
205
- TRouteTree extends AnyRoute,
206
- TTrailingSlashOption extends TrailingSlashOption,
207
- TDefaultStructuralSharingOption extends boolean,
208
- TRouterHistory extends RouterHistory = RouterHistory,
209
- TDehydrated extends Record<string, any> = Record<string, any>,
210
- >(
211
- options: undefined extends number
212
- ? 'strictNullChecks must be enabled in tsconfig.json'
213
- : RouterConstructorOptions<
214
- TRouteTree,
215
- TTrailingSlashOption,
216
- TDefaultStructuralSharingOption,
217
- TRouterHistory,
218
- TDehydrated
219
- >,
220
- ): CoreRouter<
83
+ export class Router<
84
+ in out TRouteTree extends AnyRoute,
85
+ in out TTrailingSlashOption extends TrailingSlashOption = 'never',
86
+ in out TDefaultStructuralSharingOption extends boolean = false,
87
+ in out TRouterHistory extends RouterHistory = RouterHistory,
88
+ in out TDehydrated extends Record<string, any> = Record<string, any>,
89
+ > extends RouterCore<
221
90
  TRouteTree,
222
91
  TTrailingSlashOption,
223
92
  TDefaultStructuralSharingOption,
224
93
  TRouterHistory,
225
94
  TDehydrated
226
95
  > {
227
- return new Router<
228
- TRouteTree,
229
- TTrailingSlashOption,
230
- TDefaultStructuralSharingOption,
231
- TRouterHistory,
232
- TDehydrated
233
- >(options)
234
- }
235
-
236
- export class Router<
237
- in out TRouteTree extends AnyRoute,
238
- in out TTrailingSlashOption extends TrailingSlashOption,
239
- in out TDefaultStructuralSharingOption extends boolean,
240
- in out TRouterHistory extends RouterHistory = RouterHistory,
241
- in out TDehydrated extends Record<string, any> = Record<string, any>,
242
- > implements
243
- CoreRouter<
244
- TRouteTree,
245
- TTrailingSlashOption,
246
- TDefaultStructuralSharingOption,
247
- TRouterHistory,
248
- TDehydrated
249
- >
250
- {
251
- // Option-independent properties
252
- tempLocationKey: string | undefined = `${Math.round(
253
- Math.random() * 10000000,
254
- )}`
255
- resetNextScroll = true
256
- shouldViewTransition?: boolean | ViewTransitionOptions = undefined
257
- isViewTransitionTypesSupported?: boolean = undefined
258
- subscribers = new Set<RouterListener<RouterEvent>>()
259
- viewTransitionPromise?: ControlledPromise<true>
260
- isScrollRestoring = false
261
- isScrollRestorationSetup = false
262
-
263
- // Must build in constructor
264
- __store!: Store<RouterState<TRouteTree>>
265
- options!: PickAsRequired<
266
- RouterOptions<
267
- TRouteTree,
268
- TTrailingSlashOption,
269
- TDefaultStructuralSharingOption,
270
- TRouterHistory,
271
- TDehydrated
272
- >,
273
- 'stringifySearch' | 'parseSearch' | 'context'
274
- >
275
- history!: TRouterHistory
276
- latestLocation!: ParsedLocation<FullSearchSchema<TRouteTree>>
277
- basepath!: string
278
- routeTree!: TRouteTree
279
- routesById!: RoutesById<TRouteTree>
280
- routesByPath!: RoutesByPath<TRouteTree>
281
- flatRoutes!: Array<AnyRoute>
282
- isServer!: boolean
283
- pathParamsDecodeCharMap?: Map<string, string>
284
-
285
- /**
286
- * @deprecated Use the `createRouter` function instead
287
- */
288
96
  constructor(
289
97
  options: RouterConstructorOptions<
290
98
  TRouteTree,
@@ -294,2335 +102,6 @@ export class Router<
294
102
  TDehydrated
295
103
  >,
296
104
  ) {
297
- this.update({
298
- defaultPreloadDelay: 50,
299
- defaultPendingMs: 1000,
300
- defaultPendingMinMs: 500,
301
- context: undefined!,
302
- ...options,
303
- caseSensitive: options.caseSensitive ?? false,
304
- notFoundMode: options.notFoundMode ?? 'fuzzy',
305
- stringifySearch: options.stringifySearch ?? defaultStringifySearch,
306
- parseSearch: options.parseSearch ?? defaultParseSearch,
307
- })
308
-
309
- if (typeof document !== 'undefined') {
310
- ;(window as any).__TSR_ROUTER__ = this
311
- }
312
- }
313
-
314
- // These are default implementations that can optionally be overridden
315
- // by the router provider once rendered. We provide these so that the
316
- // router can be used in a non-react environment if necessary
317
- startTransition: StartTransitionFn = (fn) => fn()
318
-
319
- update: UpdateFn<
320
- TRouteTree,
321
- TTrailingSlashOption,
322
- TDefaultStructuralSharingOption,
323
- TRouterHistory,
324
- TDehydrated
325
- > = (newOptions) => {
326
- if (newOptions.notFoundRoute) {
327
- console.warn(
328
- 'The notFoundRoute API is deprecated and will be removed in the next major version. See https://tanstack.com/router/v1/docs/framework/react/guide/not-found-errors#migrating-from-notfoundroute for more info.',
329
- )
330
- }
331
-
332
- const previousOptions = this.options
333
- this.options = {
334
- ...this.options,
335
- ...newOptions,
336
- }
337
-
338
- this.isServer = this.options.isServer ?? typeof document === 'undefined'
339
-
340
- this.pathParamsDecodeCharMap = this.options.pathParamsAllowedCharacters
341
- ? new Map(
342
- this.options.pathParamsAllowedCharacters.map((char) => [
343
- encodeURIComponent(char),
344
- char,
345
- ]),
346
- )
347
- : undefined
348
-
349
- if (
350
- !this.basepath ||
351
- (newOptions.basepath && newOptions.basepath !== previousOptions.basepath)
352
- ) {
353
- if (
354
- newOptions.basepath === undefined ||
355
- newOptions.basepath === '' ||
356
- newOptions.basepath === '/'
357
- ) {
358
- this.basepath = '/'
359
- } else {
360
- this.basepath = `/${trimPath(newOptions.basepath)}`
361
- }
362
- }
363
-
364
- if (
365
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
366
- !this.history ||
367
- (this.options.history && this.options.history !== this.history)
368
- ) {
369
- this.history =
370
- this.options.history ??
371
- ((this.isServer
372
- ? createMemoryHistory({
373
- initialEntries: [this.basepath || '/'],
374
- })
375
- : createBrowserHistory()) as TRouterHistory)
376
- this.latestLocation = this.parseLocation()
377
- }
378
-
379
- if (this.options.routeTree !== this.routeTree) {
380
- this.routeTree = this.options.routeTree as TRouteTree
381
- this.buildRouteTree()
382
- }
383
-
384
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
385
- if (!this.__store) {
386
- this.__store = new Store(getInitialRouterState(this.latestLocation), {
387
- onUpdate: () => {
388
- this.__store.state = {
389
- ...this.state,
390
- cachedMatches: this.state.cachedMatches.filter(
391
- (d) => !['redirected'].includes(d.status),
392
- ),
393
- }
394
- },
395
- })
396
-
397
- setupScrollRestoration(this)
398
- }
399
-
400
- if (
401
- typeof window !== 'undefined' &&
402
- 'CSS' in window &&
403
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
404
- typeof window.CSS?.supports === 'function'
405
- ) {
406
- this.isViewTransitionTypesSupported = window.CSS.supports(
407
- 'selector(:active-view-transition-type(a)',
408
- )
409
- }
410
- }
411
-
412
- get state() {
413
- return this.__store.state
414
- }
415
-
416
- buildRouteTree = () => {
417
- this.routesById = {} as RoutesById<TRouteTree>
418
- this.routesByPath = {} as RoutesByPath<TRouteTree>
419
-
420
- const notFoundRoute = this.options.notFoundRoute
421
- if (notFoundRoute) {
422
- notFoundRoute.init({
423
- originalIndex: 99999999999,
424
- defaultSsr: this.options.defaultSsr,
425
- })
426
- ;(this.routesById as any)[notFoundRoute.id] = notFoundRoute
427
- }
428
-
429
- const recurseRoutes = (childRoutes: Array<AnyRoute>) => {
430
- childRoutes.forEach((childRoute, i) => {
431
- childRoute.init({
432
- originalIndex: i,
433
- defaultSsr: this.options.defaultSsr,
434
- })
435
-
436
- const existingRoute = (this.routesById as any)[childRoute.id]
437
-
438
- invariant(
439
- !existingRoute,
440
- `Duplicate routes found with id: ${String(childRoute.id)}`,
441
- )
442
- ;(this.routesById as any)[childRoute.id] = childRoute
443
-
444
- if (!childRoute.isRoot && childRoute.path) {
445
- const trimmedFullPath = trimPathRight(childRoute.fullPath)
446
- if (
447
- !(this.routesByPath as any)[trimmedFullPath] ||
448
- childRoute.fullPath.endsWith('/')
449
- ) {
450
- ;(this.routesByPath as any)[trimmedFullPath] = childRoute
451
- }
452
- }
453
-
454
- const children = childRoute.children
455
-
456
- if (children?.length) {
457
- recurseRoutes(children)
458
- }
459
- })
460
- }
461
-
462
- recurseRoutes([this.routeTree])
463
-
464
- const scoredRoutes: Array<{
465
- child: AnyRoute
466
- trimmed: string
467
- parsed: ReturnType<typeof parsePathname>
468
- index: number
469
- scores: Array<number>
470
- }> = []
471
-
472
- const routes: Array<AnyRoute> = Object.values(this.routesById)
473
-
474
- routes.forEach((d, i) => {
475
- if (d.isRoot || !d.path) {
476
- return
477
- }
478
-
479
- const trimmed = trimPathLeft(d.fullPath)
480
- const parsed = parsePathname(trimmed)
481
-
482
- while (parsed.length > 1 && parsed[0]?.value === '/') {
483
- parsed.shift()
484
- }
485
-
486
- const scores = parsed.map((segment) => {
487
- if (segment.value === '/') {
488
- return 0.75
489
- }
490
-
491
- if (segment.type === 'param') {
492
- return 0.5
493
- }
494
-
495
- if (segment.type === 'wildcard') {
496
- return 0.25
497
- }
498
-
499
- return 1
500
- })
501
-
502
- scoredRoutes.push({ child: d, trimmed, parsed, index: i, scores })
503
- })
504
-
505
- this.flatRoutes = scoredRoutes
506
- .sort((a, b) => {
507
- const minLength = Math.min(a.scores.length, b.scores.length)
508
-
509
- // Sort by min available score
510
- for (let i = 0; i < minLength; i++) {
511
- if (a.scores[i] !== b.scores[i]) {
512
- return b.scores[i]! - a.scores[i]!
513
- }
514
- }
515
-
516
- // Sort by length of score
517
- if (a.scores.length !== b.scores.length) {
518
- return b.scores.length - a.scores.length
519
- }
520
-
521
- // Sort by min available parsed value
522
- for (let i = 0; i < minLength; i++) {
523
- if (a.parsed[i]!.value !== b.parsed[i]!.value) {
524
- return a.parsed[i]!.value > b.parsed[i]!.value ? 1 : -1
525
- }
526
- }
527
-
528
- // Sort by original index
529
- return a.index - b.index
530
- })
531
- .map((d, i) => {
532
- d.child.rank = i
533
- return d.child
534
- })
535
- }
536
-
537
- subscribe: SubscribeFn = (eventType, fn) => {
538
- const listener: RouterListener<any> = {
539
- eventType,
540
- fn,
541
- }
542
-
543
- this.subscribers.add(listener)
544
-
545
- return () => {
546
- this.subscribers.delete(listener)
547
- }
548
- }
549
-
550
- emit: EmitFn = (routerEvent) => {
551
- this.subscribers.forEach((listener) => {
552
- if (listener.eventType === routerEvent.type) {
553
- listener.fn(routerEvent)
554
- }
555
- })
556
- }
557
-
558
- parseLocation: ParseLocationFn<TRouteTree> = (
559
- previousLocation,
560
- locationToParse,
561
- ) => {
562
- const parse = ({
563
- pathname,
564
- search,
565
- hash,
566
- state,
567
- }: HistoryLocation): ParsedLocation<FullSearchSchema<TRouteTree>> => {
568
- const parsedSearch = this.options.parseSearch(search)
569
- const searchStr = this.options.stringifySearch(parsedSearch)
570
-
571
- return {
572
- pathname,
573
- searchStr,
574
- search: replaceEqualDeep(previousLocation?.search, parsedSearch) as any,
575
- hash: hash.split('#').reverse()[0] ?? '',
576
- href: `${pathname}${searchStr}${hash}`,
577
- state: replaceEqualDeep(previousLocation?.state, state),
578
- }
579
- }
580
-
581
- const location = parse(locationToParse ?? this.history.location)
582
-
583
- const { __tempLocation, __tempKey } = location.state
584
-
585
- if (__tempLocation && (!__tempKey || __tempKey === this.tempLocationKey)) {
586
- // Sync up the location keys
587
- const parsedTempLocation = parse(__tempLocation) as any
588
- parsedTempLocation.state.key = location.state.key
589
-
590
- delete parsedTempLocation.state.__tempLocation
591
-
592
- return {
593
- ...parsedTempLocation,
594
- maskedLocation: location,
595
- }
596
- }
597
-
598
- return location
599
- }
600
-
601
- resolvePathWithBase = (from: string, path: string) => {
602
- const resolvedPath = resolvePath({
603
- basepath: this.basepath,
604
- base: from,
605
- to: cleanPath(path),
606
- trailingSlash: this.options.trailingSlash,
607
- caseSensitive: this.options.caseSensitive,
608
- })
609
- return resolvedPath
610
- }
611
-
612
- get looseRoutesById() {
613
- return this.routesById as Record<string, AnyRoute>
614
- }
615
-
616
- /**
617
- @deprecated use the following signature instead
618
- ```ts
619
- matchRoutes (
620
- next: ParsedLocation,
621
- opts?: { preload?: boolean; throwOnError?: boolean },
622
- ): Array<AnyRouteMatch>;
623
- ```
624
- */
625
- matchRoutes: MatchRoutesFn = (
626
- pathnameOrNext: string | ParsedLocation,
627
- locationSearchOrOpts?: AnySchema | MatchRoutesOpts,
628
- opts?: MatchRoutesOpts,
629
- ) => {
630
- if (typeof pathnameOrNext === 'string') {
631
- return this.matchRoutesInternal(
632
- {
633
- pathname: pathnameOrNext,
634
- search: locationSearchOrOpts,
635
- } as ParsedLocation,
636
- opts,
637
- )
638
- } else {
639
- return this.matchRoutesInternal(pathnameOrNext, locationSearchOrOpts)
640
- }
641
- }
642
-
643
- private matchRoutesInternal(
644
- next: ParsedLocation,
645
- opts?: MatchRoutesOpts,
646
- ): Array<AnyRouteMatch> {
647
- const { foundRoute, matchedRoutes, routeParams } = this.getMatchedRoutes(
648
- next,
649
- opts?.dest,
650
- )
651
- let isGlobalNotFound = false
652
-
653
- // Check to see if the route needs a 404 entry
654
- if (
655
- // If we found a route, and it's not an index route and we have left over path
656
- foundRoute
657
- ? foundRoute.path !== '/' && routeParams['**']
658
- : // Or if we didn't find a route and we have left over path
659
- trimPathRight(next.pathname)
660
- ) {
661
- // If the user has defined an (old) 404 route, use it
662
- if (this.options.notFoundRoute) {
663
- matchedRoutes.push(this.options.notFoundRoute)
664
- } else {
665
- // If there is no routes found during path matching
666
- isGlobalNotFound = true
667
- }
668
- }
669
-
670
- const globalNotFoundRouteId = (() => {
671
- if (!isGlobalNotFound) {
672
- return undefined
673
- }
674
-
675
- if (this.options.notFoundMode !== 'root') {
676
- for (let i = matchedRoutes.length - 1; i >= 0; i--) {
677
- const route = matchedRoutes[i]!
678
- if (route.children) {
679
- return route.id
680
- }
681
- }
682
- }
683
-
684
- return rootRouteId
685
- })()
686
-
687
- const parseErrors = matchedRoutes.map((route) => {
688
- let parsedParamsError
689
-
690
- const parseParams =
691
- route.options.params?.parse ?? route.options.parseParams
692
-
693
- if (parseParams) {
694
- try {
695
- const parsedParams = parseParams(routeParams)
696
- // Add the parsed params to the accumulated params bag
697
- Object.assign(routeParams, parsedParams)
698
- } catch (err: any) {
699
- parsedParamsError = new PathParamError(err.message, {
700
- cause: err,
701
- })
702
-
703
- if (opts?.throwOnError) {
704
- throw parsedParamsError
705
- }
706
-
707
- return parsedParamsError
708
- }
709
- }
710
-
711
- return
712
- })
713
-
714
- const matches: Array<AnyRouteMatch> = []
715
-
716
- const getParentContext = (parentMatch?: AnyRouteMatch) => {
717
- const parentMatchId = parentMatch?.id
718
-
719
- const parentContext = !parentMatchId
720
- ? ((this.options.context as any) ?? {})
721
- : (parentMatch.context ?? this.options.context ?? {})
722
-
723
- return parentContext
724
- }
725
-
726
- matchedRoutes.forEach((route, index) => {
727
- // Take each matched route and resolve + validate its search params
728
- // This has to happen serially because each route's search params
729
- // can depend on the parent route's search params
730
- // It must also happen before we create the match so that we can
731
- // pass the search params to the route's potential key function
732
- // which is used to uniquely identify the route match in state
733
-
734
- const parentMatch = matches[index - 1]
735
-
736
- const [preMatchSearch, strictMatchSearch, searchError]: [
737
- Record<string, any>,
738
- Record<string, any>,
739
- any,
740
- ] = (() => {
741
- // Validate the search params and stabilize them
742
- const parentSearch = parentMatch?.search ?? next.search
743
- const parentStrictSearch = parentMatch?._strictSearch ?? {}
744
-
745
- try {
746
- const strictSearch =
747
- validateSearch(route.options.validateSearch, { ...parentSearch }) ??
748
- {}
749
-
750
- return [
751
- {
752
- ...parentSearch,
753
- ...strictSearch,
754
- },
755
- { ...parentStrictSearch, ...strictSearch },
756
- undefined,
757
- ]
758
- } catch (err: any) {
759
- let searchParamError = err
760
- if (!(err instanceof SearchParamError)) {
761
- searchParamError = new SearchParamError(err.message, {
762
- cause: err,
763
- })
764
- }
765
-
766
- if (opts?.throwOnError) {
767
- throw searchParamError
768
- }
769
-
770
- return [parentSearch, {}, searchParamError]
771
- }
772
- })()
773
-
774
- // This is where we need to call route.options.loaderDeps() to get any additional
775
- // deps that the route's loader function might need to run. We need to do this
776
- // before we create the match so that we can pass the deps to the route's
777
- // potential key function which is used to uniquely identify the route match in state
778
-
779
- const loaderDeps =
780
- route.options.loaderDeps?.({
781
- search: preMatchSearch,
782
- }) ?? ''
783
-
784
- const loaderDepsHash = loaderDeps ? JSON.stringify(loaderDeps) : ''
785
-
786
- const { usedParams, interpolatedPath } = interpolatePath({
787
- path: route.fullPath,
788
- params: routeParams,
789
- decodeCharMap: this.pathParamsDecodeCharMap,
790
- })
791
-
792
- const matchId =
793
- interpolatePath({
794
- path: route.id,
795
- params: routeParams,
796
- leaveWildcards: true,
797
- decodeCharMap: this.pathParamsDecodeCharMap,
798
- }).interpolatedPath + loaderDepsHash
799
-
800
- // Waste not, want not. If we already have a match for this route,
801
- // reuse it. This is important for layout routes, which might stick
802
- // around between navigation actions that only change leaf routes.
803
-
804
- // Existing matches are matches that are already loaded along with
805
- // pending matches that are still loading
806
- const existingMatch = this.getMatch(matchId)
807
-
808
- const previousMatch = this.state.matches.find(
809
- (d) => d.routeId === route.id,
810
- )
811
-
812
- const cause = previousMatch ? 'stay' : 'enter'
813
-
814
- let match: AnyRouteMatch
815
-
816
- if (existingMatch) {
817
- match = {
818
- ...existingMatch,
819
- cause,
820
- params: previousMatch
821
- ? replaceEqualDeep(previousMatch.params, routeParams)
822
- : routeParams,
823
- _strictParams: usedParams,
824
- search: previousMatch
825
- ? replaceEqualDeep(previousMatch.search, preMatchSearch)
826
- : replaceEqualDeep(existingMatch.search, preMatchSearch),
827
- _strictSearch: strictMatchSearch,
828
- }
829
- } else {
830
- const status =
831
- route.options.loader ||
832
- route.options.beforeLoad ||
833
- route.lazyFn ||
834
- routeNeedsPreload(route)
835
- ? 'pending'
836
- : 'success'
837
-
838
- match = {
839
- id: matchId,
840
- index,
841
- routeId: route.id,
842
- params: previousMatch
843
- ? replaceEqualDeep(previousMatch.params, routeParams)
844
- : routeParams,
845
- _strictParams: usedParams,
846
- pathname: joinPaths([this.basepath, interpolatedPath]),
847
- updatedAt: Date.now(),
848
- search: previousMatch
849
- ? replaceEqualDeep(previousMatch.search, preMatchSearch)
850
- : preMatchSearch,
851
- _strictSearch: strictMatchSearch,
852
- searchError: undefined,
853
- status,
854
- isFetching: false,
855
- error: undefined,
856
- paramsError: parseErrors[index],
857
- __routeContext: {},
858
- __beforeLoadContext: {},
859
- context: {},
860
- abortController: new AbortController(),
861
- fetchCount: 0,
862
- cause,
863
- loaderDeps: previousMatch
864
- ? replaceEqualDeep(previousMatch.loaderDeps, loaderDeps)
865
- : loaderDeps,
866
- invalid: false,
867
- preload: false,
868
- links: undefined,
869
- scripts: undefined,
870
- headScripts: undefined,
871
- meta: undefined,
872
- staticData: route.options.staticData || {},
873
- loadPromise: createControlledPromise(),
874
- fullPath: route.fullPath,
875
- }
876
- }
877
-
878
- if (!opts?.preload) {
879
- // If we have a global not found, mark the right match as global not found
880
- match.globalNotFound = globalNotFoundRouteId === route.id
881
- }
882
-
883
- // update the searchError if there is one
884
- match.searchError = searchError
885
-
886
- const parentContext = getParentContext(parentMatch)
887
-
888
- match.context = {
889
- ...parentContext,
890
- ...match.__routeContext,
891
- ...match.__beforeLoadContext,
892
- }
893
-
894
- matches.push(match)
895
- })
896
-
897
- matches.forEach((match, index) => {
898
- const route = this.looseRoutesById[match.routeId]!
899
- const existingMatch = this.getMatch(match.id)
900
-
901
- // only execute `context` if we are not just building a location
902
- if (!existingMatch && opts?._buildLocation !== true) {
903
- const parentMatch = matches[index - 1]
904
- const parentContext = getParentContext(parentMatch)
905
-
906
- // Update the match's context
907
- const contextFnContext: RouteContextOptions<any, any, any, any> = {
908
- deps: match.loaderDeps,
909
- params: match.params,
910
- context: parentContext,
911
- location: next,
912
- navigate: (opts: any) =>
913
- this.navigate({ ...opts, _fromLocation: next }),
914
- buildLocation: this.buildLocation,
915
- cause: match.cause,
916
- abortController: match.abortController,
917
- preload: !!match.preload,
918
- matches,
919
- }
920
-
921
- // Get the route context
922
- match.__routeContext = route.options.context?.(contextFnContext) ?? {}
923
-
924
- match.context = {
925
- ...parentContext,
926
- ...match.__routeContext,
927
- ...match.__beforeLoadContext,
928
- }
929
- }
930
-
931
- // If it's already a success, update headers and head content
932
- // These may get updated again if the match is refreshed
933
- // due to being stale
934
- if (match.status === 'success') {
935
- match.headers = route.options.headers?.({
936
- loaderData: match.loaderData,
937
- })
938
- const assetContext = {
939
- matches,
940
- match,
941
- params: match.params,
942
- loaderData: match.loaderData,
943
- }
944
- const headFnContent = route.options.head?.(assetContext)
945
- match.links = headFnContent?.links
946
- match.headScripts = headFnContent?.scripts
947
- match.meta = headFnContent?.meta
948
- match.scripts = route.options.scripts?.(assetContext)
949
- }
950
- })
951
-
952
- return matches
953
- }
954
-
955
- getMatchedRoutes: GetMatchRoutesFn = (next, dest) => {
956
- let routeParams: Record<string, string> = {}
957
- const trimmedPath = trimPathRight(next.pathname)
958
- const getMatchedParams = (route: AnyRoute) => {
959
- const result = matchPathname(this.basepath, trimmedPath, {
960
- to: route.fullPath,
961
- caseSensitive:
962
- route.options.caseSensitive ?? this.options.caseSensitive,
963
- fuzzy: true,
964
- })
965
- return result
966
- }
967
-
968
- let foundRoute: AnyRoute | undefined =
969
- dest?.to !== undefined ? this.routesByPath[dest.to!] : undefined
970
- if (foundRoute) {
971
- routeParams = getMatchedParams(foundRoute)!
972
- } else {
973
- foundRoute = this.flatRoutes.find((route) => {
974
- const matchedParams = getMatchedParams(route)
975
-
976
- if (matchedParams) {
977
- routeParams = matchedParams
978
- return true
979
- }
980
-
981
- return false
982
- })
983
- }
984
-
985
- let routeCursor: AnyRoute =
986
- foundRoute || (this.routesById as any)[rootRouteId]
987
-
988
- const matchedRoutes: Array<AnyRoute> = [routeCursor]
989
-
990
- while (routeCursor.parentRoute) {
991
- routeCursor = routeCursor.parentRoute
992
- matchedRoutes.unshift(routeCursor)
993
- }
994
-
995
- return { matchedRoutes, routeParams, foundRoute }
996
- }
997
-
998
- cancelMatch = (id: string) => {
999
- const match = this.getMatch(id)
1000
-
1001
- if (!match) return
1002
-
1003
- match.abortController.abort()
1004
- clearTimeout(match.pendingTimeout)
1005
- }
1006
-
1007
- cancelMatches = () => {
1008
- this.state.pendingMatches?.forEach((match) => {
1009
- this.cancelMatch(match.id)
1010
- })
1011
- }
1012
-
1013
- buildLocation: BuildLocationFn = (opts) => {
1014
- const build = (
1015
- dest: BuildNextOptions & {
1016
- unmaskOnReload?: boolean
1017
- } = {},
1018
- matchedRoutesResult?: MatchedRoutesResult,
1019
- ): ParsedLocation => {
1020
- const fromMatches = dest._fromLocation
1021
- ? this.matchRoutes(dest._fromLocation, { _buildLocation: true })
1022
- : this.state.matches
1023
-
1024
- const fromMatch =
1025
- dest.from != null
1026
- ? fromMatches.find((d) =>
1027
- matchPathname(this.basepath, trimPathRight(d.pathname), {
1028
- to: dest.from,
1029
- caseSensitive: false,
1030
- fuzzy: false,
1031
- }),
1032
- )
1033
- : undefined
1034
-
1035
- const fromPath = fromMatch?.pathname || this.latestLocation.pathname
1036
-
1037
- invariant(
1038
- dest.from == null || fromMatch != null,
1039
- 'Could not find match for from: ' + dest.from,
1040
- )
1041
-
1042
- const fromSearch = this.state.pendingMatches?.length
1043
- ? last(this.state.pendingMatches)?.search
1044
- : last(fromMatches)?.search || this.latestLocation.search
1045
-
1046
- const stayingMatches = matchedRoutesResult?.matchedRoutes.filter((d) =>
1047
- fromMatches.find((e) => e.routeId === d.id),
1048
- )
1049
- let pathname: string
1050
- if (dest.to) {
1051
- const resolvePathTo =
1052
- fromMatch?.fullPath ||
1053
- last(fromMatches)?.fullPath ||
1054
- this.latestLocation.pathname
1055
- pathname = this.resolvePathWithBase(resolvePathTo, `${dest.to}`)
1056
- } else {
1057
- const fromRouteByFromPathRouteId =
1058
- this.routesById[
1059
- stayingMatches?.find((route) => {
1060
- const interpolatedPath = interpolatePath({
1061
- path: route.fullPath,
1062
- params: matchedRoutesResult?.routeParams ?? {},
1063
- decodeCharMap: this.pathParamsDecodeCharMap,
1064
- }).interpolatedPath
1065
- const pathname = joinPaths([this.basepath, interpolatedPath])
1066
- return pathname === fromPath
1067
- })?.id as keyof this['routesById']
1068
- ]
1069
- pathname = this.resolvePathWithBase(
1070
- fromPath,
1071
- fromRouteByFromPathRouteId?.to ?? fromPath,
1072
- )
1073
- }
1074
-
1075
- const prevParams = { ...last(fromMatches)?.params }
1076
-
1077
- let nextParams =
1078
- (dest.params ?? true) === true
1079
- ? prevParams
1080
- : {
1081
- ...prevParams,
1082
- ...functionalUpdate(dest.params as any, prevParams),
1083
- }
1084
-
1085
- if (Object.keys(nextParams).length > 0) {
1086
- matchedRoutesResult?.matchedRoutes
1087
- .map((route) => {
1088
- return (
1089
- route.options.params?.stringify ?? route.options.stringifyParams
1090
- )
1091
- })
1092
- .filter(Boolean)
1093
- .forEach((fn) => {
1094
- nextParams = { ...nextParams!, ...fn!(nextParams) }
1095
- })
1096
- }
1097
-
1098
- pathname = interpolatePath({
1099
- path: pathname,
1100
- params: nextParams ?? {},
1101
- leaveWildcards: false,
1102
- leaveParams: opts.leaveParams,
1103
- decodeCharMap: this.pathParamsDecodeCharMap,
1104
- }).interpolatedPath
1105
-
1106
- let search = fromSearch
1107
- if (opts._includeValidateSearch && this.options.search?.strict) {
1108
- let validatedSearch = {}
1109
- matchedRoutesResult?.matchedRoutes.forEach((route) => {
1110
- try {
1111
- if (route.options.validateSearch) {
1112
- validatedSearch = {
1113
- ...validatedSearch,
1114
- ...(validateSearch(route.options.validateSearch, {
1115
- ...validatedSearch,
1116
- ...search,
1117
- }) ?? {}),
1118
- }
1119
- }
1120
- } catch {
1121
- // ignore errors here because they are already handled in matchRoutes
1122
- }
1123
- })
1124
- search = validatedSearch
1125
- }
1126
-
1127
- const applyMiddlewares = (search: any) => {
1128
- const allMiddlewares =
1129
- matchedRoutesResult?.matchedRoutes.reduce(
1130
- (acc, route) => {
1131
- const middlewares: Array<SearchMiddleware<any>> = []
1132
- if ('search' in route.options) {
1133
- if (route.options.search?.middlewares) {
1134
- middlewares.push(...route.options.search.middlewares)
1135
- }
1136
- }
1137
- // TODO remove preSearchFilters and postSearchFilters in v2
1138
- else if (
1139
- route.options.preSearchFilters ||
1140
- route.options.postSearchFilters
1141
- ) {
1142
- const legacyMiddleware: SearchMiddleware<any> = ({
1143
- search,
1144
- next,
1145
- }) => {
1146
- let nextSearch = search
1147
- if (
1148
- 'preSearchFilters' in route.options &&
1149
- route.options.preSearchFilters
1150
- ) {
1151
- nextSearch = route.options.preSearchFilters.reduce(
1152
- (prev, next) => next(prev),
1153
- search,
1154
- )
1155
- }
1156
- const result = next(nextSearch)
1157
- if (
1158
- 'postSearchFilters' in route.options &&
1159
- route.options.postSearchFilters
1160
- ) {
1161
- return route.options.postSearchFilters.reduce(
1162
- (prev, next) => next(prev),
1163
- result,
1164
- )
1165
- }
1166
- return result
1167
- }
1168
- middlewares.push(legacyMiddleware)
1169
- }
1170
- if (opts._includeValidateSearch && route.options.validateSearch) {
1171
- const validate: SearchMiddleware<any> = ({ search, next }) => {
1172
- const result = next(search)
1173
- try {
1174
- const validatedSearch = {
1175
- ...result,
1176
- ...(validateSearch(
1177
- route.options.validateSearch,
1178
- result,
1179
- ) ?? {}),
1180
- }
1181
- return validatedSearch
1182
- } catch {
1183
- // ignore errors here because they are already handled in matchRoutes
1184
- return result
1185
- }
1186
- }
1187
- middlewares.push(validate)
1188
- }
1189
- return acc.concat(middlewares)
1190
- },
1191
- [] as Array<SearchMiddleware<any>>,
1192
- ) ?? []
1193
-
1194
- // the chain ends here since `next` is not called
1195
- const final: SearchMiddleware<any> = ({ search }) => {
1196
- if (!dest.search) {
1197
- return {}
1198
- }
1199
- if (dest.search === true) {
1200
- return search
1201
- }
1202
- return functionalUpdate(dest.search, search)
1203
- }
1204
- allMiddlewares.push(final)
1205
-
1206
- const applyNext = (index: number, currentSearch: any): any => {
1207
- // no more middlewares left, return the current search
1208
- if (index >= allMiddlewares.length) {
1209
- return currentSearch
1210
- }
1211
-
1212
- const middleware = allMiddlewares[index]!
1213
-
1214
- const next = (newSearch: any): any => {
1215
- return applyNext(index + 1, newSearch)
1216
- }
1217
-
1218
- return middleware({ search: currentSearch, next })
1219
- }
1220
-
1221
- // Start applying middlewares
1222
- return applyNext(0, search)
1223
- }
1224
-
1225
- search = applyMiddlewares(search)
1226
-
1227
- search = replaceEqualDeep(fromSearch, search)
1228
- const searchStr = this.options.stringifySearch(search)
1229
-
1230
- const hash =
1231
- dest.hash === true
1232
- ? this.latestLocation.hash
1233
- : dest.hash
1234
- ? functionalUpdate(dest.hash, this.latestLocation.hash)
1235
- : undefined
1236
-
1237
- const hashStr = hash ? `#${hash}` : ''
1238
-
1239
- let nextState =
1240
- dest.state === true
1241
- ? this.latestLocation.state
1242
- : dest.state
1243
- ? functionalUpdate(dest.state, this.latestLocation.state)
1244
- : {}
1245
-
1246
- nextState = replaceEqualDeep(this.latestLocation.state, nextState)
1247
-
1248
- return {
1249
- pathname,
1250
- search,
1251
- searchStr,
1252
- state: nextState as any,
1253
- hash: hash ?? '',
1254
- href: `${pathname}${searchStr}${hashStr}`,
1255
- unmaskOnReload: dest.unmaskOnReload,
1256
- }
1257
- }
1258
-
1259
- const buildWithMatches = (
1260
- dest: BuildNextOptions = {},
1261
- maskedDest?: BuildNextOptions,
1262
- ) => {
1263
- const next = build(dest)
1264
- let maskedNext = maskedDest ? build(maskedDest) : undefined
1265
-
1266
- if (!maskedNext) {
1267
- let params = {}
1268
-
1269
- const foundMask = this.options.routeMasks?.find((d) => {
1270
- const match = matchPathname(this.basepath, next.pathname, {
1271
- to: d.from,
1272
- caseSensitive: false,
1273
- fuzzy: false,
1274
- })
1275
-
1276
- if (match) {
1277
- params = match
1278
- return true
1279
- }
1280
-
1281
- return false
1282
- })
1283
-
1284
- if (foundMask) {
1285
- const { from: _from, ...maskProps } = foundMask
1286
- maskedDest = {
1287
- ...pick(opts, ['from']),
1288
- ...maskProps,
1289
- params,
1290
- }
1291
- maskedNext = build(maskedDest)
1292
- }
1293
- }
1294
-
1295
- const nextMatches = this.getMatchedRoutes(next, dest)
1296
- const final = build(dest, nextMatches)
1297
-
1298
- if (maskedNext) {
1299
- const maskedMatches = this.getMatchedRoutes(maskedNext, maskedDest)
1300
- const maskedFinal = build(maskedDest, maskedMatches)
1301
- final.maskedLocation = maskedFinal
1302
- }
1303
-
1304
- return final
1305
- }
1306
-
1307
- if (opts.mask) {
1308
- return buildWithMatches(opts, {
1309
- ...pick(opts, ['from']),
1310
- ...opts.mask,
1311
- })
1312
- }
1313
-
1314
- return buildWithMatches(opts)
1315
- }
1316
-
1317
- commitLocationPromise: undefined | ControlledPromise<void>
1318
-
1319
- commitLocation: CommitLocationFn = ({
1320
- viewTransition,
1321
- ignoreBlocker,
1322
- ...next
1323
- }) => {
1324
- const isSameState = () => {
1325
- // the following props are ignored but may still be provided when navigating,
1326
- // temporarily add the previous values to the next state so they don't affect
1327
- // the comparison
1328
- const ignoredProps = [
1329
- 'key',
1330
- '__TSR_index',
1331
- '__hashScrollIntoViewOptions',
1332
- ] as const
1333
- ignoredProps.forEach((prop) => {
1334
- ;(next.state as any)[prop] = this.latestLocation.state[prop]
1335
- })
1336
- const isEqual = deepEqual(next.state, this.latestLocation.state)
1337
- ignoredProps.forEach((prop) => {
1338
- delete next.state[prop]
1339
- })
1340
- return isEqual
1341
- }
1342
-
1343
- const isSameUrl = this.latestLocation.href === next.href
1344
-
1345
- const previousCommitPromise = this.commitLocationPromise
1346
- this.commitLocationPromise = createControlledPromise<void>(() => {
1347
- previousCommitPromise?.resolve()
1348
- })
1349
-
1350
- // Don't commit to history if nothing changed
1351
- if (isSameUrl && isSameState()) {
1352
- this.load()
1353
- } else {
1354
- // eslint-disable-next-line prefer-const
1355
- let { maskedLocation, hashScrollIntoView, ...nextHistory } = next
1356
-
1357
- if (maskedLocation) {
1358
- nextHistory = {
1359
- ...maskedLocation,
1360
- state: {
1361
- ...maskedLocation.state,
1362
- __tempKey: undefined,
1363
- __tempLocation: {
1364
- ...nextHistory,
1365
- search: nextHistory.searchStr,
1366
- state: {
1367
- ...nextHistory.state,
1368
- __tempKey: undefined!,
1369
- __tempLocation: undefined!,
1370
- key: undefined!,
1371
- },
1372
- },
1373
- },
1374
- }
1375
-
1376
- if (
1377
- nextHistory.unmaskOnReload ??
1378
- this.options.unmaskOnReload ??
1379
- false
1380
- ) {
1381
- nextHistory.state.__tempKey = this.tempLocationKey
1382
- }
1383
- }
1384
-
1385
- nextHistory.state.__hashScrollIntoViewOptions =
1386
- hashScrollIntoView ?? this.options.defaultHashScrollIntoView ?? true
1387
-
1388
- this.shouldViewTransition = viewTransition
1389
-
1390
- this.history[next.replace ? 'replace' : 'push'](
1391
- nextHistory.href,
1392
- nextHistory.state,
1393
- { ignoreBlocker },
1394
- )
1395
- }
1396
-
1397
- this.resetNextScroll = next.resetScroll ?? true
1398
-
1399
- if (!this.history.subscribers.size) {
1400
- this.load()
1401
- }
1402
-
1403
- return this.commitLocationPromise
1404
- }
1405
-
1406
- buildAndCommitLocation = ({
1407
- replace,
1408
- resetScroll,
1409
- hashScrollIntoView,
1410
- viewTransition,
1411
- ignoreBlocker,
1412
- href,
1413
- ...rest
1414
- }: BuildNextOptions & CommitLocationOptions = {}) => {
1415
- if (href) {
1416
- const currentIndex = this.history.location.state.__TSR_index
1417
- const parsed = parseHref(href, {
1418
- __TSR_index: replace ? currentIndex : currentIndex + 1,
1419
- })
1420
- rest.to = parsed.pathname
1421
- rest.search = this.options.parseSearch(parsed.search)
1422
- // remove the leading `#` from the hash
1423
- rest.hash = parsed.hash.slice(1)
1424
- }
1425
-
1426
- const location = this.buildLocation({
1427
- ...(rest as any),
1428
- _includeValidateSearch: true,
1429
- })
1430
- return this.commitLocation({
1431
- ...location,
1432
- viewTransition,
1433
- replace,
1434
- resetScroll,
1435
- hashScrollIntoView,
1436
- ignoreBlocker,
1437
- })
1438
- }
1439
-
1440
- navigate: NavigateFn = ({ to, reloadDocument, href, ...rest }) => {
1441
- if (reloadDocument) {
1442
- if (!href) {
1443
- const location = this.buildLocation({ to, ...rest } as any)
1444
- href = this.history.createHref(location.href)
1445
- }
1446
- if (rest.replace) {
1447
- window.location.replace(href)
1448
- } else {
1449
- window.location.href = href
1450
- }
1451
- return
1452
- }
1453
-
1454
- return this.buildAndCommitLocation({
1455
- ...rest,
1456
- href,
1457
- to: to as string,
1458
- })
1459
- }
1460
-
1461
- latestLoadPromise: undefined | Promise<void>
1462
-
1463
- load: LoadFn = async (opts?: { sync?: boolean }): Promise<void> => {
1464
- this.latestLocation = this.parseLocation(this.latestLocation)
1465
-
1466
- let redirect: ResolvedRedirect | undefined
1467
- let notFound: NotFoundError | undefined
1468
-
1469
- let loadPromise: Promise<void>
1470
-
1471
- // eslint-disable-next-line prefer-const
1472
- loadPromise = new Promise<void>((resolve) => {
1473
- this.startTransition(async () => {
1474
- try {
1475
- const next = this.latestLocation
1476
- const prevLocation = this.state.resolvedLocation
1477
-
1478
- // Cancel any pending matches
1479
- this.cancelMatches()
1480
-
1481
- let pendingMatches!: Array<AnyRouteMatch>
1482
-
1483
- batch(() => {
1484
- // this call breaks a route context of destination route after a redirect
1485
- // we should be fine not eagerly calling this since we call it later
1486
- // this.clearExpiredCache()
1487
-
1488
- // Match the routes
1489
- pendingMatches = this.matchRoutes(next)
1490
-
1491
- // Ingest the new matches
1492
- this.__store.setState((s) => ({
1493
- ...s,
1494
- status: 'pending',
1495
- isLoading: true,
1496
- location: next,
1497
- pendingMatches,
1498
- // If a cached moved to pendingMatches, remove it from cachedMatches
1499
- cachedMatches: s.cachedMatches.filter((d) => {
1500
- return !pendingMatches.find((e) => e.id === d.id)
1501
- }),
1502
- }))
1503
- })
1504
-
1505
- if (!this.state.redirect) {
1506
- this.emit({
1507
- type: 'onBeforeNavigate',
1508
- ...getLocationChangeInfo({
1509
- resolvedLocation: prevLocation,
1510
- location: next,
1511
- }),
1512
- })
1513
- }
1514
-
1515
- this.emit({
1516
- type: 'onBeforeLoad',
1517
- ...getLocationChangeInfo({
1518
- resolvedLocation: prevLocation,
1519
- location: next,
1520
- }),
1521
- })
1522
-
1523
- await this.loadMatches({
1524
- sync: opts?.sync,
1525
- matches: pendingMatches,
1526
- location: next,
1527
- // eslint-disable-next-line @typescript-eslint/require-await
1528
- onReady: async () => {
1529
- // eslint-disable-next-line @typescript-eslint/require-await
1530
- this.startViewTransition(async () => {
1531
- // this.viewTransitionPromise = createControlledPromise<true>()
1532
-
1533
- // Commit the pending matches. If a previous match was
1534
- // removed, place it in the cachedMatches
1535
- let exitingMatches!: Array<AnyRouteMatch>
1536
- let enteringMatches!: Array<AnyRouteMatch>
1537
- let stayingMatches!: Array<AnyRouteMatch>
1538
-
1539
- batch(() => {
1540
- this.__store.setState((s) => {
1541
- const previousMatches = s.matches
1542
- const newMatches = s.pendingMatches || s.matches
1543
-
1544
- exitingMatches = previousMatches.filter(
1545
- (match) => !newMatches.find((d) => d.id === match.id),
1546
- )
1547
- enteringMatches = newMatches.filter(
1548
- (match) =>
1549
- !previousMatches.find((d) => d.id === match.id),
1550
- )
1551
- stayingMatches = previousMatches.filter((match) =>
1552
- newMatches.find((d) => d.id === match.id),
1553
- )
1554
-
1555
- return {
1556
- ...s,
1557
- isLoading: false,
1558
- loadedAt: Date.now(),
1559
- matches: newMatches,
1560
- pendingMatches: undefined,
1561
- cachedMatches: [
1562
- ...s.cachedMatches,
1563
- ...exitingMatches.filter((d) => d.status !== 'error'),
1564
- ],
1565
- }
1566
- })
1567
- this.clearExpiredCache()
1568
- })
1569
-
1570
- //
1571
- ;(
1572
- [
1573
- [exitingMatches, 'onLeave'],
1574
- [enteringMatches, 'onEnter'],
1575
- [stayingMatches, 'onStay'],
1576
- ] as const
1577
- ).forEach(([matches, hook]) => {
1578
- matches.forEach((match) => {
1579
- this.looseRoutesById[match.routeId]!.options[hook]?.(match)
1580
- })
1581
- })
1582
- })
1583
- },
1584
- })
1585
- } catch (err) {
1586
- if (isResolvedRedirect(err)) {
1587
- redirect = err
1588
- if (!this.isServer) {
1589
- this.navigate({
1590
- ...redirect,
1591
- replace: true,
1592
- ignoreBlocker: true,
1593
- })
1594
- }
1595
- } else if (isNotFound(err)) {
1596
- notFound = err
1597
- }
1598
-
1599
- this.__store.setState((s) => ({
1600
- ...s,
1601
- statusCode: redirect
1602
- ? redirect.statusCode
1603
- : notFound
1604
- ? 404
1605
- : s.matches.some((d) => d.status === 'error')
1606
- ? 500
1607
- : 200,
1608
- redirect,
1609
- }))
1610
- }
1611
-
1612
- if (this.latestLoadPromise === loadPromise) {
1613
- this.commitLocationPromise?.resolve()
1614
- this.latestLoadPromise = undefined
1615
- this.commitLocationPromise = undefined
1616
- }
1617
- resolve()
1618
- })
1619
- })
1620
-
1621
- this.latestLoadPromise = loadPromise
1622
-
1623
- await loadPromise
1624
-
1625
- while (
1626
- (this.latestLoadPromise as any) &&
1627
- loadPromise !== this.latestLoadPromise
1628
- ) {
1629
- await this.latestLoadPromise
1630
- }
1631
-
1632
- if (this.hasNotFoundMatch()) {
1633
- this.__store.setState((s) => ({
1634
- ...s,
1635
- statusCode: 404,
1636
- }))
1637
- }
1638
- }
1639
-
1640
- startViewTransition = (fn: () => Promise<void>) => {
1641
- // Determine if we should start a view transition from the navigation
1642
- // or from the router default
1643
- const shouldViewTransition =
1644
- this.shouldViewTransition ?? this.options.defaultViewTransition
1645
-
1646
- // Reset the view transition flag
1647
- delete this.shouldViewTransition
1648
- // Attempt to start a view transition (or just apply the changes if we can't)
1649
- if (
1650
- shouldViewTransition &&
1651
- typeof document !== 'undefined' &&
1652
- 'startViewTransition' in document &&
1653
- typeof document.startViewTransition === 'function'
1654
- ) {
1655
- // lib.dom.ts doesn't support viewTransition types variant yet.
1656
- // TODO: Fix this when dom types are updated
1657
- let startViewTransitionParams: any
1658
-
1659
- if (
1660
- typeof shouldViewTransition === 'object' &&
1661
- this.isViewTransitionTypesSupported
1662
- ) {
1663
- startViewTransitionParams = {
1664
- update: fn,
1665
- types: shouldViewTransition.types,
1666
- }
1667
- } else {
1668
- startViewTransitionParams = fn
1669
- }
1670
-
1671
- document.startViewTransition(startViewTransitionParams)
1672
- } else {
1673
- fn()
1674
- }
1675
- }
1676
-
1677
- updateMatch: UpdateMatchFn = (id, updater) => {
1678
- let updated!: AnyRouteMatch
1679
- const isPending = this.state.pendingMatches?.find((d) => d.id === id)
1680
- const isMatched = this.state.matches.find((d) => d.id === id)
1681
- const isCached = this.state.cachedMatches.find((d) => d.id === id)
1682
-
1683
- const matchesKey = isPending
1684
- ? 'pendingMatches'
1685
- : isMatched
1686
- ? 'matches'
1687
- : isCached
1688
- ? 'cachedMatches'
1689
- : ''
1690
-
1691
- if (matchesKey) {
1692
- this.__store.setState((s) => ({
1693
- ...s,
1694
- [matchesKey]: s[matchesKey]?.map((d) =>
1695
- d.id === id ? (updated = updater(d)) : d,
1696
- ),
1697
- }))
1698
- }
1699
-
1700
- return updated
1701
- }
1702
-
1703
- getMatch: GetMatchFn = (matchId: string) => {
1704
- return [
1705
- ...this.state.cachedMatches,
1706
- ...(this.state.pendingMatches ?? []),
1707
- ...this.state.matches,
1708
- ].find((d) => d.id === matchId)
1709
- }
1710
-
1711
- loadMatches = async ({
1712
- location,
1713
- matches,
1714
- preload: allPreload,
1715
- onReady,
1716
- updateMatch = this.updateMatch,
1717
- sync,
1718
- }: {
1719
- location: ParsedLocation
1720
- matches: Array<AnyRouteMatch>
1721
- preload?: boolean
1722
- onReady?: () => Promise<void>
1723
- updateMatch?: (
1724
- id: string,
1725
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
1726
- ) => void
1727
- getMatch?: (matchId: string) => AnyRouteMatch | undefined
1728
- sync?: boolean
1729
- }): Promise<Array<MakeRouteMatch>> => {
1730
- let firstBadMatchIndex: number | undefined
1731
- let rendered = false
1732
-
1733
- const triggerOnReady = async () => {
1734
- if (!rendered) {
1735
- rendered = true
1736
- await onReady?.()
1737
- }
1738
- }
1739
-
1740
- const resolvePreload = (matchId: string) => {
1741
- return !!(allPreload && !this.state.matches.find((d) => d.id === matchId))
1742
- }
1743
-
1744
- if (!this.isServer && !this.state.matches.length) {
1745
- triggerOnReady()
1746
- }
1747
-
1748
- const handleRedirectAndNotFound = (match: AnyRouteMatch, err: any) => {
1749
- if (isResolvedRedirect(err)) {
1750
- if (!err.reloadDocument) {
1751
- throw err
1752
- }
1753
- }
1754
-
1755
- if (isRedirect(err) || isNotFound(err)) {
1756
- updateMatch(match.id, (prev) => ({
1757
- ...prev,
1758
- status: isRedirect(err)
1759
- ? 'redirected'
1760
- : isNotFound(err)
1761
- ? 'notFound'
1762
- : 'error',
1763
- isFetching: false,
1764
- error: err,
1765
- beforeLoadPromise: undefined,
1766
- loaderPromise: undefined,
1767
- }))
1768
-
1769
- if (!(err as any).routeId) {
1770
- ;(err as any).routeId = match.routeId
1771
- }
1772
-
1773
- match.beforeLoadPromise?.resolve()
1774
- match.loaderPromise?.resolve()
1775
- match.loadPromise?.resolve()
1776
-
1777
- if (isRedirect(err)) {
1778
- rendered = true
1779
- err = this.resolveRedirect({ ...err, _fromLocation: location })
1780
- throw err
1781
- } else if (isNotFound(err)) {
1782
- this._handleNotFound(matches, err, {
1783
- updateMatch,
1784
- })
1785
- this.serverSsr?.onMatchSettled({
1786
- router: this,
1787
- match: this.getMatch(match.id)!,
1788
- })
1789
- throw err
1790
- }
1791
- }
1792
- }
1793
-
1794
- try {
1795
- await new Promise<void>((resolveAll, rejectAll) => {
1796
- ;(async () => {
1797
- try {
1798
- const handleSerialError = (
1799
- index: number,
1800
- err: any,
1801
- routerCode: string,
1802
- ) => {
1803
- const { id: matchId, routeId } = matches[index]!
1804
- const route = this.looseRoutesById[routeId]!
1805
-
1806
- // Much like suspense, we use a promise here to know if
1807
- // we've been outdated by a new loadMatches call and
1808
- // should abort the current async operation
1809
- if (err instanceof Promise) {
1810
- throw err
1811
- }
1812
-
1813
- err.routerCode = routerCode
1814
- firstBadMatchIndex = firstBadMatchIndex ?? index
1815
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
1816
-
1817
- try {
1818
- route.options.onError?.(err)
1819
- } catch (errorHandlerErr) {
1820
- err = errorHandlerErr
1821
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
1822
- }
1823
-
1824
- updateMatch(matchId, (prev) => {
1825
- prev.beforeLoadPromise?.resolve()
1826
- prev.loadPromise?.resolve()
1827
-
1828
- return {
1829
- ...prev,
1830
- error: err,
1831
- status: 'error',
1832
- isFetching: false,
1833
- updatedAt: Date.now(),
1834
- abortController: new AbortController(),
1835
- beforeLoadPromise: undefined,
1836
- }
1837
- })
1838
- }
1839
-
1840
- for (const [index, { id: matchId, routeId }] of matches.entries()) {
1841
- const existingMatch = this.getMatch(matchId)!
1842
- const parentMatchId = matches[index - 1]?.id
1843
-
1844
- const route = this.looseRoutesById[routeId]!
1845
-
1846
- const pendingMs =
1847
- route.options.pendingMs ?? this.options.defaultPendingMs
1848
-
1849
- const shouldPending = !!(
1850
- onReady &&
1851
- !this.isServer &&
1852
- !resolvePreload(matchId) &&
1853
- (route.options.loader || route.options.beforeLoad) &&
1854
- typeof pendingMs === 'number' &&
1855
- pendingMs !== Infinity &&
1856
- (route.options.pendingComponent ??
1857
- this.options.defaultPendingComponent)
1858
- )
1859
-
1860
- let executeBeforeLoad = true
1861
- if (
1862
- // If we are in the middle of a load, either of these will be present
1863
- // (not to be confused with `loadPromise`, which is always defined)
1864
- existingMatch.beforeLoadPromise ||
1865
- existingMatch.loaderPromise
1866
- ) {
1867
- if (shouldPending) {
1868
- setTimeout(() => {
1869
- try {
1870
- // Update the match and prematurely resolve the loadMatches promise so that
1871
- // the pending component can start rendering
1872
- triggerOnReady()
1873
- } catch {}
1874
- }, pendingMs)
1875
- }
1876
-
1877
- // Wait for the beforeLoad to resolve before we continue
1878
- await existingMatch.beforeLoadPromise
1879
- executeBeforeLoad = this.getMatch(matchId)!.status !== 'success'
1880
- }
1881
- if (executeBeforeLoad) {
1882
- // If we are not in the middle of a load OR the previous load failed, start it
1883
- try {
1884
- updateMatch(matchId, (prev) => {
1885
- // explicitly capture the previous loadPromise
1886
- const prevLoadPromise = prev.loadPromise
1887
- return {
1888
- ...prev,
1889
- loadPromise: createControlledPromise<void>(() => {
1890
- prevLoadPromise?.resolve()
1891
- }),
1892
- beforeLoadPromise: createControlledPromise<void>(),
1893
- }
1894
- })
1895
- const abortController = new AbortController()
1896
-
1897
- let pendingTimeout: ReturnType<typeof setTimeout>
1898
-
1899
- if (shouldPending) {
1900
- // If we might show a pending component, we need to wait for the
1901
- // pending promise to resolve before we start showing that state
1902
- pendingTimeout = setTimeout(() => {
1903
- try {
1904
- // Update the match and prematurely resolve the loadMatches promise so that
1905
- // the pending component can start rendering
1906
- triggerOnReady()
1907
- } catch {}
1908
- }, pendingMs)
1909
- }
1910
-
1911
- const { paramsError, searchError } = this.getMatch(matchId)!
1912
-
1913
- if (paramsError) {
1914
- handleSerialError(index, paramsError, 'PARSE_PARAMS')
1915
- }
1916
-
1917
- if (searchError) {
1918
- handleSerialError(index, searchError, 'VALIDATE_SEARCH')
1919
- }
1920
-
1921
- const getParentMatchContext = () =>
1922
- parentMatchId
1923
- ? this.getMatch(parentMatchId)!.context
1924
- : (this.options.context ?? {})
1925
-
1926
- updateMatch(matchId, (prev) => ({
1927
- ...prev,
1928
- isFetching: 'beforeLoad',
1929
- fetchCount: prev.fetchCount + 1,
1930
- abortController,
1931
- pendingTimeout,
1932
- context: {
1933
- ...getParentMatchContext(),
1934
- ...prev.__routeContext,
1935
- },
1936
- }))
1937
-
1938
- const { search, params, context, cause } =
1939
- this.getMatch(matchId)!
1940
-
1941
- const preload = resolvePreload(matchId)
1942
-
1943
- const beforeLoadFnContext: BeforeLoadContextOptions<
1944
- any,
1945
- any,
1946
- any,
1947
- any,
1948
- any
1949
- > = {
1950
- search,
1951
- abortController,
1952
- params,
1953
- preload,
1954
- context,
1955
- location,
1956
- navigate: (opts: any) =>
1957
- this.navigate({ ...opts, _fromLocation: location }),
1958
- buildLocation: this.buildLocation,
1959
- cause: preload ? 'preload' : cause,
1960
- matches,
1961
- }
1962
-
1963
- const beforeLoadContext =
1964
- (await route.options.beforeLoad?.(beforeLoadFnContext)) ??
1965
- {}
1966
-
1967
- if (
1968
- isRedirect(beforeLoadContext) ||
1969
- isNotFound(beforeLoadContext)
1970
- ) {
1971
- handleSerialError(index, beforeLoadContext, 'BEFORE_LOAD')
1972
- }
1973
-
1974
- updateMatch(matchId, (prev) => {
1975
- return {
1976
- ...prev,
1977
- __beforeLoadContext: beforeLoadContext,
1978
- context: {
1979
- ...getParentMatchContext(),
1980
- ...prev.__routeContext,
1981
- ...beforeLoadContext,
1982
- },
1983
- abortController,
1984
- }
1985
- })
1986
- } catch (err) {
1987
- handleSerialError(index, err, 'BEFORE_LOAD')
1988
- }
1989
-
1990
- updateMatch(matchId, (prev) => {
1991
- prev.beforeLoadPromise?.resolve()
1992
-
1993
- return {
1994
- ...prev,
1995
- beforeLoadPromise: undefined,
1996
- isFetching: false,
1997
- }
1998
- })
1999
- }
2000
- }
2001
-
2002
- const validResolvedMatches = matches.slice(0, firstBadMatchIndex)
2003
- const matchPromises: Array<Promise<AnyRouteMatch>> = []
2004
-
2005
- validResolvedMatches.forEach(({ id: matchId, routeId }, index) => {
2006
- matchPromises.push(
2007
- (async () => {
2008
- const { loaderPromise: prevLoaderPromise } =
2009
- this.getMatch(matchId)!
2010
-
2011
- let loaderShouldRunAsync = false
2012
- let loaderIsRunningAsync = false
2013
-
2014
- if (prevLoaderPromise) {
2015
- await prevLoaderPromise
2016
- const match = this.getMatch(matchId)!
2017
- if (match.error) {
2018
- handleRedirectAndNotFound(match, match.error)
2019
- }
2020
- } else {
2021
- const parentMatchPromise = matchPromises[index - 1] as any
2022
- const route = this.looseRoutesById[routeId]!
2023
-
2024
- const getLoaderContext = (): LoaderFnContext => {
2025
- const {
2026
- params,
2027
- loaderDeps,
2028
- abortController,
2029
- context,
2030
- cause,
2031
- } = this.getMatch(matchId)!
2032
-
2033
- const preload = resolvePreload(matchId)
2034
-
2035
- return {
2036
- params,
2037
- deps: loaderDeps,
2038
- preload: !!preload,
2039
- parentMatchPromise,
2040
- abortController: abortController,
2041
- context,
2042
- location,
2043
- navigate: (opts) =>
2044
- this.navigate({ ...opts, _fromLocation: location }),
2045
- cause: preload ? 'preload' : cause,
2046
- route,
2047
- }
2048
- }
2049
-
2050
- // This is where all of the stale-while-revalidate magic happens
2051
- const age = Date.now() - this.getMatch(matchId)!.updatedAt
2052
-
2053
- const preload = resolvePreload(matchId)
2054
-
2055
- const staleAge = preload
2056
- ? (route.options.preloadStaleTime ??
2057
- this.options.defaultPreloadStaleTime ??
2058
- 30_000) // 30 seconds for preloads by default
2059
- : (route.options.staleTime ??
2060
- this.options.defaultStaleTime ??
2061
- 0)
2062
-
2063
- const shouldReloadOption = route.options.shouldReload
2064
-
2065
- // Default to reloading the route all the time
2066
- // Allow shouldReload to get the last say,
2067
- // if provided.
2068
- const shouldReload =
2069
- typeof shouldReloadOption === 'function'
2070
- ? shouldReloadOption(getLoaderContext())
2071
- : shouldReloadOption
2072
-
2073
- updateMatch(matchId, (prev) => ({
2074
- ...prev,
2075
- loaderPromise: createControlledPromise<void>(),
2076
- preload:
2077
- !!preload &&
2078
- !this.state.matches.find((d) => d.id === matchId),
2079
- }))
2080
-
2081
- const runLoader = async () => {
2082
- try {
2083
- // If the Matches component rendered
2084
- // the pending component and needs to show it for
2085
- // a minimum duration, we''ll wait for it to resolve
2086
- // before committing to the match and resolving
2087
- // the loadPromise
2088
- const potentialPendingMinPromise = async () => {
2089
- const latestMatch = this.getMatch(matchId)!
2090
-
2091
- if (latestMatch.minPendingPromise) {
2092
- await latestMatch.minPendingPromise
2093
- }
2094
- }
2095
-
2096
- // Actually run the loader and handle the result
2097
- try {
2098
- this.loadRouteChunk(route)
2099
-
2100
- updateMatch(matchId, (prev) => ({
2101
- ...prev,
2102
- isFetching: 'loader',
2103
- }))
2104
-
2105
- // Kick off the loader!
2106
- const loaderData =
2107
- await route.options.loader?.(getLoaderContext())
2108
-
2109
- handleRedirectAndNotFound(
2110
- this.getMatch(matchId)!,
2111
- loaderData,
2112
- )
2113
-
2114
- // Lazy option can modify the route options,
2115
- // so we need to wait for it to resolve before
2116
- // we can use the options
2117
- await route._lazyPromise
2118
-
2119
- await potentialPendingMinPromise()
2120
-
2121
- const assetContext = {
2122
- matches,
2123
- match: this.getMatch(matchId)!,
2124
- params: this.getMatch(matchId)!.params,
2125
- loaderData,
2126
- }
2127
- const headFnContent =
2128
- route.options.head?.(assetContext)
2129
- const meta = headFnContent?.meta
2130
- const links = headFnContent?.links
2131
- const headScripts = headFnContent?.scripts
2132
-
2133
- const scripts = route.options.scripts?.(assetContext)
2134
- const headers = route.options.headers?.({
2135
- loaderData,
2136
- })
2137
-
2138
- updateMatch(matchId, (prev) => ({
2139
- ...prev,
2140
- error: undefined,
2141
- status: 'success',
2142
- isFetching: false,
2143
- updatedAt: Date.now(),
2144
- loaderData,
2145
- meta,
2146
- links,
2147
- headScripts,
2148
- headers,
2149
- scripts,
2150
- }))
2151
- } catch (e) {
2152
- let error = e
2153
-
2154
- await potentialPendingMinPromise()
2155
-
2156
- handleRedirectAndNotFound(this.getMatch(matchId)!, e)
2157
-
2158
- try {
2159
- route.options.onError?.(e)
2160
- } catch (onErrorError) {
2161
- error = onErrorError
2162
- handleRedirectAndNotFound(
2163
- this.getMatch(matchId)!,
2164
- onErrorError,
2165
- )
2166
- }
2167
-
2168
- updateMatch(matchId, (prev) => ({
2169
- ...prev,
2170
- error,
2171
- status: 'error',
2172
- isFetching: false,
2173
- }))
2174
- }
2175
-
2176
- this.serverSsr?.onMatchSettled({
2177
- router: this,
2178
- match: this.getMatch(matchId)!,
2179
- })
2180
-
2181
- // Last but not least, wait for the the components
2182
- // to be preloaded before we resolve the match
2183
- await route._componentsPromise
2184
- } catch (err) {
2185
- updateMatch(matchId, (prev) => ({
2186
- ...prev,
2187
- loaderPromise: undefined,
2188
- }))
2189
- handleRedirectAndNotFound(this.getMatch(matchId)!, err)
2190
- }
2191
- }
2192
-
2193
- // If the route is successful and still fresh, just resolve
2194
- const { status, invalid } = this.getMatch(matchId)!
2195
- loaderShouldRunAsync =
2196
- status === 'success' &&
2197
- (invalid || (shouldReload ?? age > staleAge))
2198
- if (preload && route.options.preload === false) {
2199
- // Do nothing
2200
- } else if (loaderShouldRunAsync && !sync) {
2201
- loaderIsRunningAsync = true
2202
- ;(async () => {
2203
- try {
2204
- await runLoader()
2205
- const { loaderPromise, loadPromise } =
2206
- this.getMatch(matchId)!
2207
- loaderPromise?.resolve()
2208
- loadPromise?.resolve()
2209
- updateMatch(matchId, (prev) => ({
2210
- ...prev,
2211
- loaderPromise: undefined,
2212
- }))
2213
- } catch (err) {
2214
- if (isResolvedRedirect(err)) {
2215
- await this.navigate(err)
2216
- }
2217
- }
2218
- })()
2219
- } else if (
2220
- status !== 'success' ||
2221
- (loaderShouldRunAsync && sync)
2222
- ) {
2223
- await runLoader()
2224
- }
2225
- }
2226
- if (!loaderIsRunningAsync) {
2227
- const { loaderPromise, loadPromise } =
2228
- this.getMatch(matchId)!
2229
- loaderPromise?.resolve()
2230
- loadPromise?.resolve()
2231
- }
2232
-
2233
- updateMatch(matchId, (prev) => ({
2234
- ...prev,
2235
- isFetching: loaderIsRunningAsync ? prev.isFetching : false,
2236
- loaderPromise: loaderIsRunningAsync
2237
- ? prev.loaderPromise
2238
- : undefined,
2239
- invalid: false,
2240
- }))
2241
- return this.getMatch(matchId)!
2242
- })(),
2243
- )
2244
- })
2245
-
2246
- await Promise.all(matchPromises)
2247
-
2248
- resolveAll()
2249
- } catch (err) {
2250
- rejectAll(err)
2251
- }
2252
- })()
2253
- })
2254
- await triggerOnReady()
2255
- } catch (err) {
2256
- if (isRedirect(err) || isNotFound(err)) {
2257
- if (isNotFound(err) && !allPreload) {
2258
- await triggerOnReady()
2259
- }
2260
-
2261
- throw err
2262
- }
2263
- }
2264
-
2265
- return matches
2266
- }
2267
-
2268
- invalidate: InvalidateFn<
2269
- Router<
2270
- TRouteTree,
2271
- TTrailingSlashOption,
2272
- TDefaultStructuralSharingOption,
2273
- TRouterHistory,
2274
- TDehydrated
2275
- >
2276
- > = (opts) => {
2277
- const invalidate = (d: MakeRouteMatch<TRouteTree>) => {
2278
- if (opts?.filter?.(d as MakeRouteMatchUnion<this>) ?? true) {
2279
- return {
2280
- ...d,
2281
- invalid: true,
2282
- ...(d.status === 'error'
2283
- ? ({ status: 'pending', error: undefined } as const)
2284
- : {}),
2285
- }
2286
- }
2287
- return d
2288
- }
2289
-
2290
- this.__store.setState((s) => ({
2291
- ...s,
2292
- matches: s.matches.map(invalidate),
2293
- cachedMatches: s.cachedMatches.map(invalidate),
2294
- pendingMatches: s.pendingMatches?.map(invalidate),
2295
- }))
2296
-
2297
- return this.load({ sync: opts?.sync })
2298
- }
2299
-
2300
- resolveRedirect = (err: AnyRedirect): ResolvedRedirect => {
2301
- const redirect = err as ResolvedRedirect
2302
-
2303
- if (!redirect.href) {
2304
- redirect.href = this.buildLocation(redirect as any).href
2305
- }
2306
-
2307
- return redirect
2308
- }
2309
-
2310
- clearCache: ClearCacheFn<this> = (opts) => {
2311
- const filter = opts?.filter
2312
- if (filter !== undefined) {
2313
- this.__store.setState((s) => {
2314
- return {
2315
- ...s,
2316
- cachedMatches: s.cachedMatches.filter(
2317
- (m) => !filter(m as MakeRouteMatchUnion<this>),
2318
- ),
2319
- }
2320
- })
2321
- } else {
2322
- this.__store.setState((s) => {
2323
- return {
2324
- ...s,
2325
- cachedMatches: [],
2326
- }
2327
- })
2328
- }
2329
- }
2330
-
2331
- clearExpiredCache = () => {
2332
- // This is where all of the garbage collection magic happens
2333
- const filter = (d: MakeRouteMatch<TRouteTree>) => {
2334
- const route = this.looseRoutesById[d.routeId]!
2335
-
2336
- if (!route.options.loader) {
2337
- return true
2338
- }
2339
-
2340
- // If the route was preloaded, use the preloadGcTime
2341
- // otherwise, use the gcTime
2342
- const gcTime =
2343
- (d.preload
2344
- ? (route.options.preloadGcTime ?? this.options.defaultPreloadGcTime)
2345
- : (route.options.gcTime ?? this.options.defaultGcTime)) ??
2346
- 5 * 60 * 1000
2347
-
2348
- return !(d.status !== 'error' && Date.now() - d.updatedAt < gcTime)
2349
- }
2350
- this.clearCache({ filter })
2351
- }
2352
-
2353
- loadRouteChunk = (route: AnyRoute) => {
2354
- if (route._lazyPromise === undefined) {
2355
- if (route.lazyFn) {
2356
- route._lazyPromise = route.lazyFn().then((lazyRoute) => {
2357
- // explicitly don't copy over the lazy route's id
2358
- const { id: _id, ...options } = lazyRoute.options
2359
- Object.assign(route.options, options)
2360
- })
2361
- } else {
2362
- route._lazyPromise = Promise.resolve()
2363
- }
2364
- }
2365
-
2366
- // If for some reason lazy resolves more lazy components...
2367
- // We'll wait for that before pre attempt to preload any
2368
- // components themselves.
2369
- if (route._componentsPromise === undefined) {
2370
- route._componentsPromise = route._lazyPromise.then(() =>
2371
- Promise.all(
2372
- componentTypes.map(async (type) => {
2373
- const component = route.options[type]
2374
- if ((component as any)?.preload) {
2375
- await (component as any).preload()
2376
- }
2377
- }),
2378
- ),
2379
- )
2380
- }
2381
- return route._componentsPromise
2382
- }
2383
-
2384
- preloadRoute: PreloadRouteFn<
2385
- TRouteTree,
2386
- TTrailingSlashOption,
2387
- TDefaultStructuralSharingOption,
2388
- TRouterHistory
2389
- > = async (opts) => {
2390
- const next = this.buildLocation(opts as any)
2391
-
2392
- let matches = this.matchRoutes(next, {
2393
- throwOnError: true,
2394
- preload: true,
2395
- dest: opts,
2396
- })
2397
-
2398
- const activeMatchIds = new Set(
2399
- [...this.state.matches, ...(this.state.pendingMatches ?? [])].map(
2400
- (d) => d.id,
2401
- ),
2402
- )
2403
-
2404
- const loadedMatchIds = new Set([
2405
- ...activeMatchIds,
2406
- ...this.state.cachedMatches.map((d) => d.id),
2407
- ])
2408
-
2409
- // If the matches are already loaded, we need to add them to the cachedMatches
2410
- batch(() => {
2411
- matches.forEach((match) => {
2412
- if (!loadedMatchIds.has(match.id)) {
2413
- this.__store.setState((s) => ({
2414
- ...s,
2415
- cachedMatches: [...(s.cachedMatches as any), match],
2416
- }))
2417
- }
2418
- })
2419
- })
2420
-
2421
- try {
2422
- matches = await this.loadMatches({
2423
- matches,
2424
- location: next,
2425
- preload: true,
2426
- updateMatch: (id, updater) => {
2427
- // Don't update the match if it's currently loaded
2428
- if (activeMatchIds.has(id)) {
2429
- matches = matches.map((d) => (d.id === id ? updater(d) : d))
2430
- } else {
2431
- this.updateMatch(id, updater)
2432
- }
2433
- },
2434
- })
2435
-
2436
- return matches
2437
- } catch (err) {
2438
- if (isRedirect(err)) {
2439
- if (err.reloadDocument) {
2440
- return undefined
2441
- }
2442
- return await this.preloadRoute({
2443
- ...(err as any),
2444
- _fromLocation: next,
2445
- })
2446
- }
2447
- if (!isNotFound(err)) {
2448
- // Preload errors are not fatal, but we should still log them
2449
- console.error(err)
2450
- }
2451
- return undefined
2452
- }
2453
- }
2454
-
2455
- matchRoute: MatchRouteFn<
2456
- TRouteTree,
2457
- TTrailingSlashOption,
2458
- TDefaultStructuralSharingOption,
2459
- TRouterHistory
2460
- > = (location, opts) => {
2461
- const matchLocation = {
2462
- ...location,
2463
- to: location.to
2464
- ? this.resolvePathWithBase(
2465
- (location.from || '') as string,
2466
- location.to as string,
2467
- )
2468
- : undefined,
2469
- params: location.params || {},
2470
- leaveParams: true,
2471
- }
2472
- const next = this.buildLocation(matchLocation as any)
2473
-
2474
- if (opts?.pending && this.state.status !== 'pending') {
2475
- return false
2476
- }
2477
-
2478
- const pending =
2479
- opts?.pending === undefined ? !this.state.isLoading : opts.pending
2480
-
2481
- const baseLocation = pending
2482
- ? this.latestLocation
2483
- : this.state.resolvedLocation || this.state.location
2484
-
2485
- const match = matchPathname(this.basepath, baseLocation.pathname, {
2486
- ...opts,
2487
- to: next.pathname,
2488
- }) as any
2489
-
2490
- if (!match) {
2491
- return false
2492
- }
2493
- if (location.params) {
2494
- if (!deepEqual(match, location.params, { partial: true })) {
2495
- return false
2496
- }
2497
- }
2498
-
2499
- if (match && (opts?.includeSearch ?? true)) {
2500
- return deepEqual(baseLocation.search, next.search, { partial: true })
2501
- ? match
2502
- : false
2503
- }
2504
-
2505
- return match
2506
- }
2507
-
2508
- ssr?: {
2509
- manifest: Manifest | undefined
2510
- serializer: StartSerializer
2511
- }
2512
-
2513
- serverSsr?: {
2514
- injectedHtml: Array<InjectedHtmlEntry>
2515
- injectHtml: (getHtml: () => string | Promise<string>) => Promise<void>
2516
- injectScript: (
2517
- getScript: () => string | Promise<string>,
2518
- opts?: { logScript?: boolean },
2519
- ) => Promise<void>
2520
- streamValue: (key: string, value: any) => void
2521
- streamedKeys: Set<string>
2522
- onMatchSettled: (opts: { router: AnyRouter; match: AnyRouteMatch }) => any
2523
- }
2524
-
2525
- clientSsr?: {
2526
- getStreamedValue: <T>(key: string) => T | undefined
2527
- }
2528
-
2529
- _handleNotFound = (
2530
- matches: Array<AnyRouteMatch>,
2531
- err: NotFoundError,
2532
- {
2533
- updateMatch = this.updateMatch,
2534
- }: {
2535
- updateMatch?: (
2536
- id: string,
2537
- updater: (match: AnyRouteMatch) => AnyRouteMatch,
2538
- ) => void
2539
- } = {},
2540
- ) => {
2541
- const matchesByRouteId = Object.fromEntries(
2542
- matches.map((match) => [match.routeId, match]),
2543
- ) as Record<string, AnyRouteMatch>
2544
-
2545
- // Start at the route that errored or default to the root route
2546
- let routeCursor =
2547
- (err.global
2548
- ? this.looseRoutesById[rootRouteId]
2549
- : this.looseRoutesById[err.routeId]) ||
2550
- this.looseRoutesById[rootRouteId]!
2551
-
2552
- // Go up the tree until we find a route with a notFoundComponent or we hit the root
2553
- while (
2554
- !routeCursor.options.notFoundComponent &&
2555
- !this.options.defaultNotFoundComponent &&
2556
- routeCursor.id !== rootRouteId
2557
- ) {
2558
- routeCursor = routeCursor.parentRoute
2559
-
2560
- invariant(
2561
- routeCursor,
2562
- 'Found invalid route tree while trying to find not-found handler.',
2563
- )
2564
- }
2565
-
2566
- const match = matchesByRouteId[routeCursor.id]
2567
-
2568
- invariant(match, 'Could not find match for route: ' + routeCursor.id)
2569
-
2570
- // Assign the error to the match
2571
-
2572
- updateMatch(match.id, (prev) => ({
2573
- ...prev,
2574
- status: 'notFound',
2575
- error: err,
2576
- isFetching: false,
2577
- }))
2578
-
2579
- if ((err as any).routerCode === 'BEFORE_LOAD' && routeCursor.parentRoute) {
2580
- err.routeId = routeCursor.parentRoute.id
2581
- this._handleNotFound(matches, err, {
2582
- updateMatch,
2583
- })
2584
- }
2585
- }
2586
-
2587
- hasNotFoundMatch = () => {
2588
- return this.__store.state.matches.some(
2589
- (d) => d.status === 'notFound' || d.globalNotFound,
2590
- )
2591
- }
2592
- }
2593
-
2594
- // A function that takes an import() argument which is a function and returns a new function that will
2595
- // proxy arguments from the caller to the imported function, retaining all type
2596
- // information along the way
2597
- export function lazyFn<
2598
- T extends Record<string, (...args: Array<any>) => any>,
2599
- TKey extends keyof T = 'default',
2600
- >(fn: () => Promise<T>, key?: TKey) {
2601
- return async (
2602
- ...args: Parameters<T[TKey]>
2603
- ): Promise<Awaited<ReturnType<T[TKey]>>> => {
2604
- const imported = await fn()
2605
- return imported[key || 'default'](...args)
2606
- }
2607
- }
2608
-
2609
- export class SearchParamError extends Error {}
2610
-
2611
- export class PathParamError extends Error {}
2612
-
2613
- export function getInitialRouterState(
2614
- location: ParsedLocation,
2615
- ): RouterState<any> {
2616
- return {
2617
- loadedAt: 0,
2618
- isLoading: false,
2619
- isTransitioning: false,
2620
- status: 'idle',
2621
- resolvedLocation: undefined,
2622
- location,
2623
- matches: [],
2624
- pendingMatches: [],
2625
- cachedMatches: [],
2626
- statusCode: 200,
105
+ super(options)
2627
106
  }
2628
107
  }