@tanstack/react-router 0.0.1-beta.22 → 0.0.1-beta.220
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/LICENSE +21 -0
- package/build/cjs/CatchBoundary.js +126 -0
- package/build/cjs/CatchBoundary.js.map +1 -0
- package/build/cjs/Matches.js +235 -0
- package/build/cjs/Matches.js.map +1 -0
- package/build/cjs/RouterProvider.js +1085 -0
- package/build/cjs/RouterProvider.js.map +1 -0
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js +1 -19
- package/build/cjs/_virtual/_rollupPluginBabelHelpers.js.map +1 -1
- package/build/cjs/awaited.js +45 -0
- package/build/cjs/awaited.js.map +1 -0
- package/build/cjs/defer.js +39 -0
- package/build/cjs/defer.js.map +1 -0
- package/build/cjs/fileRoute.js +29 -0
- package/build/cjs/fileRoute.js.map +1 -0
- package/build/cjs/index.js +135 -0
- package/build/cjs/index.js.map +1 -0
- package/build/cjs/lazyRouteComponent.js +57 -0
- package/build/cjs/lazyRouteComponent.js.map +1 -0
- package/build/cjs/link.js +151 -0
- package/build/cjs/link.js.map +1 -0
- package/build/cjs/path.js +211 -0
- package/build/cjs/path.js.map +1 -0
- package/build/cjs/qss.js +65 -0
- package/build/cjs/qss.js.map +1 -0
- package/build/cjs/redirects.js +27 -0
- package/build/cjs/redirects.js.map +1 -0
- package/build/cjs/route.js +139 -0
- package/build/cjs/route.js.map +1 -0
- package/build/cjs/router.js +160 -0
- package/build/cjs/router.js.map +1 -0
- package/build/cjs/scroll-restoration.js +186 -0
- package/build/cjs/scroll-restoration.js.map +1 -0
- package/build/cjs/searchParams.js +83 -0
- package/build/cjs/searchParams.js.map +1 -0
- package/build/cjs/useBlocker.js +64 -0
- package/build/cjs/useBlocker.js.map +1 -0
- package/build/cjs/useNavigate.js +78 -0
- package/build/cjs/useNavigate.js.map +1 -0
- package/build/cjs/useParams.js +28 -0
- package/build/cjs/useParams.js.map +1 -0
- package/build/cjs/useSearch.js +27 -0
- package/build/cjs/useSearch.js.map +1 -0
- package/build/cjs/utils.js +236 -0
- package/build/cjs/utils.js.map +1 -0
- package/build/esm/index.js +2136 -2564
- package/build/esm/index.js.map +1 -1
- package/build/stats-html.html +59 -49
- package/build/stats-react.json +922 -43
- package/build/types/CatchBoundary.d.ts +33 -0
- package/build/types/Matches.d.ts +34 -0
- package/build/types/RouterProvider.d.ts +87 -0
- package/build/types/awaited.d.ts +8 -0
- package/build/types/defer.d.ts +19 -0
- package/build/types/fileRoute.d.ts +32 -0
- package/build/types/history.d.ts +7 -0
- package/build/types/index.d.ts +27 -104
- package/build/types/injectHtml.d.ts +0 -0
- package/build/types/lazyRouteComponent.d.ts +2 -0
- package/build/types/link.d.ts +105 -0
- package/build/types/location.d.ts +14 -0
- package/build/types/path.d.ts +16 -0
- package/build/types/qss.d.ts +2 -0
- package/build/types/redirects.d.ts +10 -0
- package/build/types/route.d.ts +270 -0
- package/build/types/routeInfo.d.ts +22 -0
- package/build/types/router.d.ts +123 -0
- package/build/types/scroll-restoration.d.ts +6 -0
- package/build/types/searchParams.d.ts +7 -0
- package/build/types/useBlocker.d.ts +8 -0
- package/build/types/useNavigate.d.ts +20 -0
- package/build/types/useParams.d.ts +7 -0
- package/build/types/useSearch.d.ts +7 -0
- package/build/types/utils.d.ts +66 -0
- package/build/umd/index.development.js +2399 -2484
- package/build/umd/index.development.js.map +1 -1
- package/build/umd/index.production.js +4 -4
- package/build/umd/index.production.js.map +1 -1
- package/package.json +9 -10
- package/src/CatchBoundary.tsx +98 -0
- package/src/Matches.tsx +345 -0
- package/src/RouterProvider.tsx +1575 -0
- package/src/awaited.tsx +40 -0
- package/src/defer.ts +55 -0
- package/src/fileRoute.ts +153 -0
- package/src/history.ts +8 -0
- package/src/index.tsx +28 -693
- package/src/injectHtml.ts +28 -0
- package/src/lazyRouteComponent.tsx +33 -0
- package/src/link.tsx +508 -0
- package/src/location.ts +15 -0
- package/src/path.ts +256 -0
- package/src/qss.ts +53 -0
- package/src/redirects.ts +31 -0
- package/src/route.ts +837 -0
- package/src/routeInfo.ts +68 -0
- package/src/router.ts +330 -0
- package/src/scroll-restoration.tsx +192 -0
- package/src/searchParams.ts +79 -0
- package/src/useBlocker.tsx +34 -0
- package/src/useNavigate.tsx +109 -0
- package/src/useParams.tsx +25 -0
- package/src/useSearch.tsx +25 -0
- package/src/utils.ts +350 -0
- package/build/cjs/react-router/src/index.js +0 -466
- package/build/cjs/react-router/src/index.js.map +0 -1
- package/build/cjs/router-core/build/esm/index.js +0 -2523
- package/build/cjs/router-core/build/esm/index.js.map +0 -1
package/src/routeInfo.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AnyRoute, Route } from './route'
|
|
2
|
+
import { Expand, UnionToIntersection } from './utils'
|
|
3
|
+
|
|
4
|
+
export type ParseRoute<TRouteTree extends AnyRoute> =
|
|
5
|
+
| TRouteTree
|
|
6
|
+
| ParseRouteChildren<TRouteTree>
|
|
7
|
+
|
|
8
|
+
export type ParseRouteChildren<TRouteTree extends AnyRoute> =
|
|
9
|
+
TRouteTree extends Route<
|
|
10
|
+
any,
|
|
11
|
+
any,
|
|
12
|
+
any,
|
|
13
|
+
any,
|
|
14
|
+
any,
|
|
15
|
+
any,
|
|
16
|
+
any,
|
|
17
|
+
any,
|
|
18
|
+
any,
|
|
19
|
+
any,
|
|
20
|
+
any,
|
|
21
|
+
any,
|
|
22
|
+
infer TChildren,
|
|
23
|
+
any
|
|
24
|
+
>
|
|
25
|
+
? unknown extends TChildren
|
|
26
|
+
? never
|
|
27
|
+
: TChildren extends AnyRoute[]
|
|
28
|
+
? {
|
|
29
|
+
[TId in TChildren[number]['id'] as string]: ParseRoute<
|
|
30
|
+
TChildren[number]
|
|
31
|
+
>
|
|
32
|
+
}[string]
|
|
33
|
+
: never
|
|
34
|
+
: never
|
|
35
|
+
|
|
36
|
+
export type RoutesById<TRouteTree extends AnyRoute> = {
|
|
37
|
+
[K in ParseRoute<TRouteTree> as K['id']]: K
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type RouteById<TRouteTree extends AnyRoute, TId> = Extract<
|
|
41
|
+
ParseRoute<TRouteTree>,
|
|
42
|
+
{ id: TId }
|
|
43
|
+
>
|
|
44
|
+
|
|
45
|
+
export type RouteIds<TRouteTree extends AnyRoute> = ParseRoute<TRouteTree>['id']
|
|
46
|
+
|
|
47
|
+
export type RoutesByPath<TRouteTree extends AnyRoute> = {
|
|
48
|
+
[K in ParseRoute<TRouteTree> as K['fullPath']]: K
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type RouteByPath<TRouteTree extends AnyRoute, TPath> = Extract<
|
|
52
|
+
ParseRoute<TRouteTree>,
|
|
53
|
+
{ fullPath: TPath }
|
|
54
|
+
>
|
|
55
|
+
|
|
56
|
+
export type RoutePaths<TRouteTree extends AnyRoute> =
|
|
57
|
+
| ParseRoute<TRouteTree>['fullPath']
|
|
58
|
+
| '/'
|
|
59
|
+
|
|
60
|
+
export type FullSearchSchema<TRouteTree extends AnyRoute> = Partial<
|
|
61
|
+
Expand<
|
|
62
|
+
UnionToIntersection<ParseRoute<TRouteTree>['types']['fullSearchSchema']>
|
|
63
|
+
>
|
|
64
|
+
>
|
|
65
|
+
|
|
66
|
+
export type AllParams<TRouteTree extends AnyRoute> = Expand<
|
|
67
|
+
UnionToIntersection<ParseRoute<TRouteTree>['types']['allParams']>
|
|
68
|
+
>
|
package/src/router.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { RouterHistory } from '@tanstack/history'
|
|
2
|
+
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
AnySearchSchema,
|
|
7
|
+
AnyRoute,
|
|
8
|
+
AnyContext,
|
|
9
|
+
AnyPathParams,
|
|
10
|
+
RouteMask,
|
|
11
|
+
} from './route'
|
|
12
|
+
import { FullSearchSchema } from './routeInfo'
|
|
13
|
+
import { defaultParseSearch, defaultStringifySearch } from './searchParams'
|
|
14
|
+
import { PickAsRequired, Updater, NonNullableUpdater } from './utils'
|
|
15
|
+
import {
|
|
16
|
+
ErrorRouteComponent,
|
|
17
|
+
PendingRouteComponent,
|
|
18
|
+
RouteComponent,
|
|
19
|
+
} from './route'
|
|
20
|
+
import { RouteMatch } from './RouterProvider'
|
|
21
|
+
import { ParsedLocation } from './location'
|
|
22
|
+
import { LocationState } from './location'
|
|
23
|
+
import { SearchSerializer, SearchParser } from './searchParams'
|
|
24
|
+
import { RouterContext } from './RouterProvider'
|
|
25
|
+
|
|
26
|
+
//
|
|
27
|
+
|
|
28
|
+
declare global {
|
|
29
|
+
interface Window {
|
|
30
|
+
__TSR_DEHYDRATED__?: HydrationCtx
|
|
31
|
+
__TSR_ROUTER_CONTEXT__?: React.Context<RouterContext<any>>
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Register {
|
|
36
|
+
// router: Router
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type AnyRouter = Router<AnyRoute, any>
|
|
40
|
+
|
|
41
|
+
export type RegisteredRouter = Register extends {
|
|
42
|
+
router: infer TRouter extends AnyRouter
|
|
43
|
+
}
|
|
44
|
+
? TRouter
|
|
45
|
+
: AnyRouter
|
|
46
|
+
|
|
47
|
+
export type HydrationCtx = {
|
|
48
|
+
router: DehydratedRouter
|
|
49
|
+
payload: Record<string, any>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type RouterContextOptions<TRouteTree extends AnyRoute> =
|
|
53
|
+
AnyContext extends TRouteTree['types']['routerContext']
|
|
54
|
+
? {
|
|
55
|
+
context?: TRouteTree['types']['routerContext']
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
context: TRouteTree['types']['routerContext']
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RouterOptions<
|
|
62
|
+
TRouteTree extends AnyRoute,
|
|
63
|
+
TDehydrated extends Record<string, any> = Record<string, any>,
|
|
64
|
+
> {
|
|
65
|
+
history?: RouterHistory
|
|
66
|
+
stringifySearch?: SearchSerializer
|
|
67
|
+
parseSearch?: SearchParser
|
|
68
|
+
defaultPreload?: false | 'intent'
|
|
69
|
+
defaultPreloadDelay?: number
|
|
70
|
+
defaultComponent?: RouteComponent<AnySearchSchema, AnyPathParams, AnyContext>
|
|
71
|
+
defaultErrorComponent?: ErrorRouteComponent<
|
|
72
|
+
AnySearchSchema,
|
|
73
|
+
AnyPathParams,
|
|
74
|
+
AnyContext
|
|
75
|
+
>
|
|
76
|
+
defaultPendingComponent?: PendingRouteComponent<
|
|
77
|
+
AnySearchSchema,
|
|
78
|
+
AnyPathParams,
|
|
79
|
+
AnyContext
|
|
80
|
+
>
|
|
81
|
+
defaultMaxAge?: number
|
|
82
|
+
defaultGcMaxAge?: number
|
|
83
|
+
defaultPreloadMaxAge?: number
|
|
84
|
+
caseSensitive?: boolean
|
|
85
|
+
routeTree?: TRouteTree
|
|
86
|
+
basepath?: string
|
|
87
|
+
createRoute?: (opts: { route: AnyRoute; router: AnyRouter }) => void
|
|
88
|
+
context?: TRouteTree['types']['routerContext']
|
|
89
|
+
// dehydrate?: () => TDehydrated
|
|
90
|
+
// hydrate?: (dehydrated: TDehydrated) => void
|
|
91
|
+
routeMasks?: RouteMask<TRouteTree>[]
|
|
92
|
+
unmaskOnReload?: boolean
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface RouterState<TRouteTree extends AnyRoute = AnyRoute> {
|
|
96
|
+
status: 'pending' | 'idle'
|
|
97
|
+
matches: RouteMatch<TRouteTree>[]
|
|
98
|
+
pendingMatches: RouteMatch<TRouteTree>[]
|
|
99
|
+
location: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
100
|
+
resolvedLocation: ParsedLocation<FullSearchSchema<TRouteTree>>
|
|
101
|
+
lastUpdated: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export type ListenerFn<TEvent extends RouterEvent> = (event: TEvent) => void
|
|
105
|
+
|
|
106
|
+
export interface BuildNextOptions {
|
|
107
|
+
to?: string | number | null
|
|
108
|
+
params?: true | Updater<unknown>
|
|
109
|
+
search?: true | Updater<unknown>
|
|
110
|
+
hash?: true | Updater<string>
|
|
111
|
+
state?: true | NonNullableUpdater<LocationState>
|
|
112
|
+
mask?: {
|
|
113
|
+
to?: string | number | null
|
|
114
|
+
params?: true | Updater<unknown>
|
|
115
|
+
search?: true | Updater<unknown>
|
|
116
|
+
hash?: true | Updater<string>
|
|
117
|
+
state?: true | NonNullableUpdater<LocationState>
|
|
118
|
+
unmaskOnReload?: boolean
|
|
119
|
+
}
|
|
120
|
+
from?: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface DehydratedRouterState {
|
|
124
|
+
dehydratedMatches: DehydratedRouteMatch[]
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export type DehydratedRouteMatch = Pick<
|
|
128
|
+
RouteMatch,
|
|
129
|
+
'fetchedAt' | 'invalid' | 'id' | 'status' | 'updatedAt'
|
|
130
|
+
>
|
|
131
|
+
|
|
132
|
+
export interface DehydratedRouter {
|
|
133
|
+
state: DehydratedRouterState
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export type RouterConstructorOptions<
|
|
137
|
+
TRouteTree extends AnyRoute,
|
|
138
|
+
TDehydrated extends Record<string, any>,
|
|
139
|
+
> = Omit<RouterOptions<TRouteTree, TDehydrated>, 'context'> &
|
|
140
|
+
RouterContextOptions<TRouteTree>
|
|
141
|
+
|
|
142
|
+
export const componentTypes = [
|
|
143
|
+
'component',
|
|
144
|
+
'errorComponent',
|
|
145
|
+
'pendingComponent',
|
|
146
|
+
] as const
|
|
147
|
+
|
|
148
|
+
export type RouterEvents = {
|
|
149
|
+
onBeforeLoad: {
|
|
150
|
+
type: 'onBeforeLoad'
|
|
151
|
+
fromLocation: ParsedLocation
|
|
152
|
+
toLocation: ParsedLocation
|
|
153
|
+
pathChanged: boolean
|
|
154
|
+
}
|
|
155
|
+
onLoad: {
|
|
156
|
+
type: 'onLoad'
|
|
157
|
+
fromLocation: ParsedLocation
|
|
158
|
+
toLocation: ParsedLocation
|
|
159
|
+
pathChanged: boolean
|
|
160
|
+
}
|
|
161
|
+
onResolved: {
|
|
162
|
+
type: 'onResolved'
|
|
163
|
+
fromLocation: ParsedLocation
|
|
164
|
+
toLocation: ParsedLocation
|
|
165
|
+
pathChanged: boolean
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export type RouterEvent = RouterEvents[keyof RouterEvents]
|
|
170
|
+
|
|
171
|
+
export type RouterListener<TRouterEvent extends RouterEvent> = {
|
|
172
|
+
eventType: TRouterEvent['type']
|
|
173
|
+
fn: ListenerFn<TRouterEvent>
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export class Router<
|
|
177
|
+
TRouteTree extends AnyRoute = AnyRoute,
|
|
178
|
+
TDehydrated extends Record<string, any> = Record<string, any>,
|
|
179
|
+
> {
|
|
180
|
+
options: PickAsRequired<
|
|
181
|
+
RouterOptions<TRouteTree, TDehydrated>,
|
|
182
|
+
'stringifySearch' | 'parseSearch' | 'context'
|
|
183
|
+
>
|
|
184
|
+
routeTree: TRouteTree
|
|
185
|
+
// dehydratedData?: TDehydrated
|
|
186
|
+
// resetNextScroll = false
|
|
187
|
+
// tempLocationKey = `${Math.round(Math.random() * 10000000)}`
|
|
188
|
+
|
|
189
|
+
constructor(options: RouterConstructorOptions<TRouteTree, TDehydrated>) {
|
|
190
|
+
this.options = {
|
|
191
|
+
defaultPreloadDelay: 50,
|
|
192
|
+
context: undefined!,
|
|
193
|
+
...options,
|
|
194
|
+
stringifySearch: options?.stringifySearch ?? defaultStringifySearch,
|
|
195
|
+
parseSearch: options?.parseSearch ?? defaultParseSearch,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
this.routeTree = this.options.routeTree as TRouteTree
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
subscribers = new Set<RouterListener<RouterEvent>>()
|
|
202
|
+
|
|
203
|
+
subscribe = <TType extends keyof RouterEvents>(
|
|
204
|
+
eventType: TType,
|
|
205
|
+
fn: ListenerFn<RouterEvents[TType]>,
|
|
206
|
+
) => {
|
|
207
|
+
const listener: RouterListener<any> = {
|
|
208
|
+
eventType,
|
|
209
|
+
fn,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
this.subscribers.add(listener)
|
|
213
|
+
|
|
214
|
+
return () => {
|
|
215
|
+
this.subscribers.delete(listener)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
emit = (routerEvent: RouterEvent) => {
|
|
220
|
+
this.subscribers.forEach((listener) => {
|
|
221
|
+
if (listener.eventType === routerEvent.type) {
|
|
222
|
+
listener.fn(routerEvent)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// dehydrate = (): DehydratedRouter => {
|
|
228
|
+
// return {
|
|
229
|
+
// state: {
|
|
230
|
+
// dehydratedMatches: state.matches.map((d) =>
|
|
231
|
+
// pick(d, ['fetchedAt', 'invalid', 'id', 'status', 'updatedAt']),
|
|
232
|
+
// ),
|
|
233
|
+
// },
|
|
234
|
+
// }
|
|
235
|
+
// }
|
|
236
|
+
|
|
237
|
+
// hydrate = async (__do_not_use_server_ctx?: HydrationCtx) => {
|
|
238
|
+
// let _ctx = __do_not_use_server_ctx
|
|
239
|
+
// // Client hydrates from window
|
|
240
|
+
// if (typeof document !== 'undefined') {
|
|
241
|
+
// _ctx = window.__TSR_DEHYDRATED__
|
|
242
|
+
// }
|
|
243
|
+
|
|
244
|
+
// invariant(
|
|
245
|
+
// _ctx,
|
|
246
|
+
// 'Expected to find a __TSR_DEHYDRATED__ property on window... but we did not. Did you forget to render <DehydrateRouter /> in your app?',
|
|
247
|
+
// )
|
|
248
|
+
|
|
249
|
+
// const ctx = _ctx
|
|
250
|
+
// this.dehydratedData = ctx.payload as any
|
|
251
|
+
// this.options.hydrate?.(ctx.payload as any)
|
|
252
|
+
// const dehydratedState = ctx.router.state
|
|
253
|
+
|
|
254
|
+
// let matches = this.matchRoutes(
|
|
255
|
+
// state.location.pathname,
|
|
256
|
+
// state.location.search,
|
|
257
|
+
// ).map((match) => {
|
|
258
|
+
// const dehydratedMatch = dehydratedState.dehydratedMatches.find(
|
|
259
|
+
// (d) => d.id === match.id,
|
|
260
|
+
// )
|
|
261
|
+
|
|
262
|
+
// invariant(
|
|
263
|
+
// dehydratedMatch,
|
|
264
|
+
// `Could not find a client-side match for dehydrated match with id: ${match.id}!`,
|
|
265
|
+
// )
|
|
266
|
+
|
|
267
|
+
// if (dehydratedMatch) {
|
|
268
|
+
// return {
|
|
269
|
+
// ...match,
|
|
270
|
+
// ...dehydratedMatch,
|
|
271
|
+
// }
|
|
272
|
+
// }
|
|
273
|
+
// return match
|
|
274
|
+
// })
|
|
275
|
+
|
|
276
|
+
// this.setState((s) => {
|
|
277
|
+
// return {
|
|
278
|
+
// ...s,
|
|
279
|
+
// matches: dehydratedState.dehydratedMatches as any,
|
|
280
|
+
// }
|
|
281
|
+
// })
|
|
282
|
+
// }
|
|
283
|
+
|
|
284
|
+
// resolveMatchPromise = (matchId: string, key: string, value: any) => {
|
|
285
|
+
// state.matches
|
|
286
|
+
// .find((d) => d.id === matchId)
|
|
287
|
+
// ?.__promisesByKey[key]?.resolve(value)
|
|
288
|
+
// }
|
|
289
|
+
|
|
290
|
+
// setRouteMatch = (
|
|
291
|
+
// id: string,
|
|
292
|
+
// pending: boolean,
|
|
293
|
+
// updater: NonNullableUpdater<RouteMatch<TRouteTree>>,
|
|
294
|
+
// ) => {
|
|
295
|
+
// const key = pending ? 'pendingMatches' : 'matches'
|
|
296
|
+
|
|
297
|
+
// this.setState((prev) => {
|
|
298
|
+
// return {
|
|
299
|
+
// ...prev,
|
|
300
|
+
// [key]: prev[key].map((d) => {
|
|
301
|
+
// if (d.id === id) {
|
|
302
|
+
// return functionalUpdate(updater, d)
|
|
303
|
+
// }
|
|
304
|
+
|
|
305
|
+
// return d
|
|
306
|
+
// }),
|
|
307
|
+
// }
|
|
308
|
+
// })
|
|
309
|
+
// }
|
|
310
|
+
|
|
311
|
+
// setPendingRouteMatch = (
|
|
312
|
+
// id: string,
|
|
313
|
+
// updater: NonNullableUpdater<RouteMatch<TRouteTree>>,
|
|
314
|
+
// ) => {
|
|
315
|
+
// this.setRouteMatch(id, true, updater)
|
|
316
|
+
// }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// A function that takes an import() argument which is a function and returns a new function that will
|
|
320
|
+
// proxy arguments from the caller to the imported function, retaining all type
|
|
321
|
+
// information along the way
|
|
322
|
+
export function lazyFn<
|
|
323
|
+
T extends Record<string, (...args: any[]) => any>,
|
|
324
|
+
TKey extends keyof T = 'default',
|
|
325
|
+
>(fn: () => Promise<T>, key?: TKey) {
|
|
326
|
+
return async (...args: Parameters<T[TKey]>): Promise<ReturnType<T[TKey]>> => {
|
|
327
|
+
const imported = await fn()
|
|
328
|
+
return imported[key || 'default'](...args)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
const useLayoutEffect =
|
|
4
|
+
typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect
|
|
5
|
+
|
|
6
|
+
import { ParsedLocation } from './location'
|
|
7
|
+
import { useRouter } from './RouterProvider'
|
|
8
|
+
import { NonNullableUpdater, functionalUpdate } from './utils'
|
|
9
|
+
|
|
10
|
+
const windowKey = 'window'
|
|
11
|
+
const delimiter = '___'
|
|
12
|
+
|
|
13
|
+
let weakScrolledElements = new WeakSet<any>()
|
|
14
|
+
|
|
15
|
+
type CacheValue = Record<string, { scrollX: number; scrollY: number }>
|
|
16
|
+
type CacheState = {
|
|
17
|
+
cached: CacheValue
|
|
18
|
+
next: CacheValue
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Cache = {
|
|
22
|
+
state: CacheState
|
|
23
|
+
set: (updater: NonNullableUpdater<CacheState>) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let cache: Cache
|
|
27
|
+
|
|
28
|
+
const sessionsStorage = typeof window !== 'undefined' && window.sessionStorage
|
|
29
|
+
|
|
30
|
+
export type ScrollRestorationOptions = {
|
|
31
|
+
getKey?: (location: ParsedLocation) => string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const defaultGetKey = (location: ParsedLocation) => location.state.key!
|
|
35
|
+
|
|
36
|
+
export function useScrollRestoration(options?: ScrollRestorationOptions) {
|
|
37
|
+
const { state, subscribe, resetNextScrollRef } = useRouter()
|
|
38
|
+
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
const getKey = options?.getKey || defaultGetKey
|
|
41
|
+
|
|
42
|
+
if (sessionsStorage) {
|
|
43
|
+
if (!cache) {
|
|
44
|
+
cache = (() => {
|
|
45
|
+
const storageKey = 'tsr-scroll-restoration-v2'
|
|
46
|
+
|
|
47
|
+
const state: CacheState = JSON.parse(
|
|
48
|
+
window.sessionStorage.getItem(storageKey) || 'null',
|
|
49
|
+
) || { cached: {}, next: {} }
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
state,
|
|
53
|
+
set: (updater) => {
|
|
54
|
+
cache.state = functionalUpdate(updater, cache.state)
|
|
55
|
+
window.sessionStorage.setItem(
|
|
56
|
+
storageKey,
|
|
57
|
+
JSON.stringify(cache.state),
|
|
58
|
+
)
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
})()
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { history } = window
|
|
66
|
+
if (history.scrollRestoration) {
|
|
67
|
+
history.scrollRestoration = 'manual'
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const onScroll = (event: Event) => {
|
|
71
|
+
if (weakScrolledElements.has(event.target)) return
|
|
72
|
+
weakScrolledElements.add(event.target)
|
|
73
|
+
|
|
74
|
+
const elementSelector =
|
|
75
|
+
event.target === document || event.target === window
|
|
76
|
+
? windowKey
|
|
77
|
+
: getCssSelector(event.target)
|
|
78
|
+
|
|
79
|
+
if (!cache.state.next[elementSelector]) {
|
|
80
|
+
cache.set((c) => ({
|
|
81
|
+
...c,
|
|
82
|
+
next: {
|
|
83
|
+
...c.next,
|
|
84
|
+
[elementSelector]: {
|
|
85
|
+
scrollX: NaN,
|
|
86
|
+
scrollY: NaN,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
}))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const getCssSelector = (el: any): string => {
|
|
94
|
+
let path = [],
|
|
95
|
+
parent
|
|
96
|
+
while ((parent = el.parentNode)) {
|
|
97
|
+
path.unshift(
|
|
98
|
+
`${el.tagName}:nth-child(${
|
|
99
|
+
([].indexOf as any).call(parent.children, el) + 1
|
|
100
|
+
})`,
|
|
101
|
+
)
|
|
102
|
+
el = parent
|
|
103
|
+
}
|
|
104
|
+
return `${path.join(' > ')}`.toLowerCase()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof document !== 'undefined') {
|
|
108
|
+
document.addEventListener('scroll', onScroll, true)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const unsubOnBeforeLoad = subscribe('onBeforeLoad', (event) => {
|
|
112
|
+
if (event.pathChanged) {
|
|
113
|
+
const restoreKey = getKey(event.fromLocation)
|
|
114
|
+
for (const elementSelector in cache.state.next) {
|
|
115
|
+
const entry = cache.state.next[elementSelector]!
|
|
116
|
+
if (elementSelector === windowKey) {
|
|
117
|
+
entry.scrollX = window.scrollX || 0
|
|
118
|
+
entry.scrollY = window.scrollY || 0
|
|
119
|
+
} else if (elementSelector) {
|
|
120
|
+
const element = document.querySelector(elementSelector)
|
|
121
|
+
entry.scrollX = element?.scrollLeft || 0
|
|
122
|
+
entry.scrollY = element?.scrollTop || 0
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
cache.set((c) => {
|
|
126
|
+
const next = { ...c.next }
|
|
127
|
+
delete next[elementSelector]
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
...c,
|
|
131
|
+
next,
|
|
132
|
+
cached: {
|
|
133
|
+
...c.cached,
|
|
134
|
+
[[restoreKey, elementSelector].join(delimiter)]: entry,
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const unsubOnResolved = subscribe('onResolved', (event) => {
|
|
143
|
+
if (event.pathChanged) {
|
|
144
|
+
if (!resetNextScrollRef.current) {
|
|
145
|
+
return
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
resetNextScrollRef.current = true
|
|
149
|
+
|
|
150
|
+
const getKey = options?.getKey || defaultGetKey
|
|
151
|
+
|
|
152
|
+
const restoreKey = getKey(event.toLocation)
|
|
153
|
+
let windowRestored = false
|
|
154
|
+
|
|
155
|
+
for (const cacheKey in cache.state.cached) {
|
|
156
|
+
const entry = cache.state.cached[cacheKey]!
|
|
157
|
+
const [key, elementSelector] = cacheKey.split(delimiter)
|
|
158
|
+
if (key === restoreKey) {
|
|
159
|
+
if (elementSelector === windowKey) {
|
|
160
|
+
windowRestored = true
|
|
161
|
+
window.scrollTo(entry.scrollX, entry.scrollY)
|
|
162
|
+
} else if (elementSelector) {
|
|
163
|
+
const element = document.querySelector(elementSelector)
|
|
164
|
+
if (element) {
|
|
165
|
+
element.scrollLeft = entry.scrollX
|
|
166
|
+
element.scrollTop = entry.scrollY
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!windowRestored) {
|
|
173
|
+
window.scrollTo(0, 0)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
cache.set((c) => ({ ...c, next: {} }))
|
|
177
|
+
weakScrolledElements = new WeakSet<any>()
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
document.removeEventListener('scroll', onScroll)
|
|
183
|
+
unsubOnBeforeLoad()
|
|
184
|
+
unsubOnResolved()
|
|
185
|
+
}
|
|
186
|
+
}, [])
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function ScrollRestoration(props: ScrollRestorationOptions) {
|
|
190
|
+
useScrollRestoration(props)
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { decode, encode } from './qss'
|
|
2
|
+
import { AnySearchSchema } from './route'
|
|
3
|
+
|
|
4
|
+
export const defaultParseSearch = parseSearchWith(JSON.parse)
|
|
5
|
+
export const defaultStringifySearch = stringifySearchWith(
|
|
6
|
+
JSON.stringify,
|
|
7
|
+
JSON.parse,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
export function parseSearchWith(parser: (str: string) => any) {
|
|
11
|
+
return (searchStr: string): AnySearchSchema => {
|
|
12
|
+
if (searchStr.substring(0, 1) === '?') {
|
|
13
|
+
searchStr = searchStr.substring(1)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let query: Record<string, unknown> = decode(searchStr)
|
|
17
|
+
|
|
18
|
+
// Try to parse any query params that might be json
|
|
19
|
+
for (let key in query) {
|
|
20
|
+
const value = query[key]
|
|
21
|
+
if (typeof value === 'string') {
|
|
22
|
+
try {
|
|
23
|
+
query[key] = parser(value)
|
|
24
|
+
} catch (err) {
|
|
25
|
+
//
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return query
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function stringifySearchWith(
|
|
35
|
+
stringify: (search: any) => string,
|
|
36
|
+
parser?: (str: string) => any,
|
|
37
|
+
) {
|
|
38
|
+
function stringifyValue(val: any) {
|
|
39
|
+
if (typeof val === 'object' && val !== null) {
|
|
40
|
+
try {
|
|
41
|
+
return stringify(val)
|
|
42
|
+
} catch (err) {
|
|
43
|
+
// silent
|
|
44
|
+
}
|
|
45
|
+
} else if (typeof val === 'string' && typeof parser === 'function') {
|
|
46
|
+
try {
|
|
47
|
+
// Check if it's a valid parseable string.
|
|
48
|
+
// If it is, then stringify it again.
|
|
49
|
+
parser(val)
|
|
50
|
+
return stringify(val)
|
|
51
|
+
} catch (err) {
|
|
52
|
+
// silent
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return val
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (search: Record<string, any>) => {
|
|
59
|
+
search = { ...search }
|
|
60
|
+
|
|
61
|
+
if (search) {
|
|
62
|
+
Object.keys(search).forEach((key) => {
|
|
63
|
+
const val = search[key]
|
|
64
|
+
if (typeof val === 'undefined' || val === undefined) {
|
|
65
|
+
delete search[key]
|
|
66
|
+
} else {
|
|
67
|
+
search[key] = stringifyValue(val)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const searchStr = encode(search as Record<string, string>).toString()
|
|
73
|
+
|
|
74
|
+
return searchStr ? `?${searchStr}` : ''
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type SearchSerializer = (searchObj: Record<string, any>) => string
|
|
79
|
+
export type SearchParser = (searchStr: string) => Record<string, any>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { ReactNode } from './route'
|
|
3
|
+
import { useRouter } from './RouterProvider'
|
|
4
|
+
|
|
5
|
+
export function useBlocker(
|
|
6
|
+
message: string,
|
|
7
|
+
condition: boolean | any = true,
|
|
8
|
+
): void {
|
|
9
|
+
const { history } = useRouter()
|
|
10
|
+
|
|
11
|
+
React.useEffect(() => {
|
|
12
|
+
if (!condition) return
|
|
13
|
+
|
|
14
|
+
let unblock = history.block((retry, cancel) => {
|
|
15
|
+
if (window.confirm(message)) {
|
|
16
|
+
unblock()
|
|
17
|
+
retry()
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return unblock
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function Block({ message, condition, children }: PromptProps) {
|
|
26
|
+
useBlocker(message, condition)
|
|
27
|
+
return (children ?? null) as ReactNode
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type PromptProps = {
|
|
31
|
+
message: string
|
|
32
|
+
condition?: boolean | any
|
|
33
|
+
children?: ReactNode
|
|
34
|
+
}
|