@tanstack/solid-router 1.114.24 → 1.114.26

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