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