@tanstack/router-core 0.0.1-alpha.6 → 0.0.1-alpha.8
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/build/cjs/packages/router-core/src/index.js +33 -1456
- package/build/cjs/packages/router-core/src/index.js.map +1 -1
- package/build/cjs/packages/router-core/src/path.js +222 -0
- package/build/cjs/packages/router-core/src/path.js.map +1 -0
- package/build/cjs/packages/router-core/src/qss.js +1 -1
- package/build/cjs/packages/router-core/src/qss.js.map +1 -1
- package/build/cjs/packages/router-core/src/route.js +126 -0
- package/build/cjs/packages/router-core/src/route.js.map +1 -0
- package/build/cjs/packages/router-core/src/routeConfig.js +69 -0
- package/build/cjs/packages/router-core/src/routeConfig.js.map +1 -0
- package/build/cjs/packages/router-core/src/routeMatch.js +260 -0
- package/build/cjs/packages/router-core/src/routeMatch.js.map +1 -0
- package/build/cjs/packages/router-core/src/router.js +786 -0
- package/build/cjs/packages/router-core/src/router.js.map +1 -0
- package/build/cjs/packages/router-core/src/searchParams.js +70 -0
- package/build/cjs/packages/router-core/src/searchParams.js.map +1 -0
- package/build/cjs/packages/router-core/src/utils.js +118 -0
- package/build/cjs/packages/router-core/src/utils.js.map +1 -0
- package/build/esm/index.js +1306 -1246
- package/build/esm/index.js.map +1 -1
- package/build/stats-html.html +1 -1
- package/build/stats-react.json +374 -57
- package/build/types/index.d.ts +361 -336
- package/build/umd/index.development.js +1315 -1246
- package/build/umd/index.development.js.map +1 -1
- package/build/umd/index.production.js +1 -1
- package/build/umd/index.production.js.map +1 -1
- package/package.json +2 -3
- package/src/frameworks.ts +13 -0
- package/src/index.ts +15 -3060
- package/src/link.ts +289 -0
- package/src/path.ts +236 -0
- package/src/qss.ts +1 -1
- package/src/route.ts +181 -0
- package/src/routeConfig.ts +523 -0
- package/src/routeInfo.ts +228 -0
- package/src/routeMatch.ts +357 -0
- package/src/router.ts +1184 -0
- package/src/searchParams.ts +54 -0
- package/src/utils.ts +157 -0
package/src/link.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { AnyPathParams } from './routeConfig'
|
|
2
|
+
import {
|
|
3
|
+
AnyAllRouteInfo,
|
|
4
|
+
DefaultAllRouteInfo,
|
|
5
|
+
RouteInfoByPath,
|
|
6
|
+
} from './routeInfo'
|
|
7
|
+
import { Location } from './router'
|
|
8
|
+
import { NoInfer, PickAsRequired, PickRequired, Updater } from './utils'
|
|
9
|
+
|
|
10
|
+
export type LinkInfo =
|
|
11
|
+
| {
|
|
12
|
+
type: 'external'
|
|
13
|
+
href: string
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
type: 'internal'
|
|
17
|
+
next: Location
|
|
18
|
+
handleFocus: (e: any) => void
|
|
19
|
+
handleClick: (e: any) => void
|
|
20
|
+
handleEnter: (e: any) => void
|
|
21
|
+
handleLeave: (e: any) => void
|
|
22
|
+
isActive: boolean
|
|
23
|
+
disabled?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type StartsWith<A, B> = A extends `${B extends string ? B : never}${infer _}`
|
|
27
|
+
? true
|
|
28
|
+
: false
|
|
29
|
+
|
|
30
|
+
type CleanPath<T extends string> = T extends `${infer L}//${infer R}`
|
|
31
|
+
? CleanPath<`${CleanPath<L>}/${CleanPath<R>}`>
|
|
32
|
+
: T extends `${infer L}//`
|
|
33
|
+
? `${CleanPath<L>}/`
|
|
34
|
+
: T extends `//${infer L}`
|
|
35
|
+
? `/${CleanPath<L>}`
|
|
36
|
+
: T
|
|
37
|
+
|
|
38
|
+
export type Split<S, TTrailing = true> = S extends unknown
|
|
39
|
+
? string extends S
|
|
40
|
+
? string[]
|
|
41
|
+
: S extends string
|
|
42
|
+
? CleanPath<S> extends ''
|
|
43
|
+
? []
|
|
44
|
+
: TTrailing extends true
|
|
45
|
+
? CleanPath<S> extends `${infer T}/`
|
|
46
|
+
? [T, '/']
|
|
47
|
+
: CleanPath<S> extends `/${infer U}`
|
|
48
|
+
? ['/', U]
|
|
49
|
+
: CleanPath<S> extends `${infer T}/${infer U}`
|
|
50
|
+
? [T, ...Split<U>]
|
|
51
|
+
: [S]
|
|
52
|
+
: CleanPath<S> extends `${infer T}/${infer U}`
|
|
53
|
+
? [T, ...Split<U>]
|
|
54
|
+
: [S]
|
|
55
|
+
: never
|
|
56
|
+
: never
|
|
57
|
+
|
|
58
|
+
export type ParsePathParams<T extends string> = Split<T>[number] extends infer U
|
|
59
|
+
? U extends `:${infer V}`
|
|
60
|
+
? V
|
|
61
|
+
: never
|
|
62
|
+
: never
|
|
63
|
+
|
|
64
|
+
type Join<T> = T extends []
|
|
65
|
+
? ''
|
|
66
|
+
: T extends [infer L extends string]
|
|
67
|
+
? L
|
|
68
|
+
: T extends [infer L extends string, ...infer Tail extends [...string[]]]
|
|
69
|
+
? CleanPath<`${L}/${Join<Tail>}`>
|
|
70
|
+
: never
|
|
71
|
+
|
|
72
|
+
export type RelativeToPathAutoComplete<
|
|
73
|
+
AllPaths extends string,
|
|
74
|
+
TFrom extends string,
|
|
75
|
+
TTo extends string,
|
|
76
|
+
SplitPaths extends string[] = Split<AllPaths, false>,
|
|
77
|
+
> = TTo extends `..${infer _}`
|
|
78
|
+
? SplitPaths extends [
|
|
79
|
+
...Split<ResolveRelativePath<TFrom, TTo>, false>,
|
|
80
|
+
...infer TToRest,
|
|
81
|
+
]
|
|
82
|
+
? `${CleanPath<
|
|
83
|
+
Join<
|
|
84
|
+
[
|
|
85
|
+
...Split<TTo, false>,
|
|
86
|
+
...(
|
|
87
|
+
| TToRest
|
|
88
|
+
| (Split<
|
|
89
|
+
ResolveRelativePath<TFrom, TTo>,
|
|
90
|
+
false
|
|
91
|
+
>['length'] extends 1
|
|
92
|
+
? never
|
|
93
|
+
: ['../'])
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
>
|
|
97
|
+
>}`
|
|
98
|
+
: never
|
|
99
|
+
: TTo extends `./${infer RestTTo}`
|
|
100
|
+
? SplitPaths extends [
|
|
101
|
+
...Split<TFrom, false>,
|
|
102
|
+
...Split<RestTTo, false>,
|
|
103
|
+
...infer RestPath,
|
|
104
|
+
]
|
|
105
|
+
? `${TTo}${Join<RestPath>}`
|
|
106
|
+
: never
|
|
107
|
+
: './' | '../' | AllPaths
|
|
108
|
+
|
|
109
|
+
export type NavigateOptionsAbsolute<
|
|
110
|
+
TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
|
|
111
|
+
TFrom extends ValidFromPath<TAllRouteInfo> = '/',
|
|
112
|
+
TTo extends string = '.',
|
|
113
|
+
> = ToOptions<TAllRouteInfo, TFrom, TTo> & {
|
|
114
|
+
// Whether to replace the current history stack instead of pushing a new one
|
|
115
|
+
replace?: boolean
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type ToOptions<
|
|
119
|
+
TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
|
|
120
|
+
TFrom extends ValidFromPath<TAllRouteInfo> = '/',
|
|
121
|
+
TTo extends string = '.',
|
|
122
|
+
TResolvedTo = ResolveRelativePath<TFrom, NoInfer<TTo>>,
|
|
123
|
+
> = {
|
|
124
|
+
to?: ToPathOption<TAllRouteInfo, TFrom, TTo>
|
|
125
|
+
// The new has string or a function to update it
|
|
126
|
+
hash?: Updater<string>
|
|
127
|
+
// The source route path. This is automatically set when using route-level APIs, but for type-safe relative routing on the router itself, this is required
|
|
128
|
+
from?: TFrom
|
|
129
|
+
// // When using relative route paths, this option forces resolution from the current path, instead of the route API's path or `from` path
|
|
130
|
+
// fromCurrent?: boolean
|
|
131
|
+
} & CheckPath<TAllRouteInfo, NoInfer<TResolvedTo>> &
|
|
132
|
+
SearchParamOptions<TAllRouteInfo, TFrom, TResolvedTo> &
|
|
133
|
+
PathParamOptions<TAllRouteInfo, TFrom, TResolvedTo>
|
|
134
|
+
|
|
135
|
+
type SearchParamOptions<
|
|
136
|
+
TAllRouteInfo extends AnyAllRouteInfo,
|
|
137
|
+
TFrom,
|
|
138
|
+
TTo,
|
|
139
|
+
TFromSchema = RouteInfoByPath<TAllRouteInfo, TFrom>['fullSearchSchema'],
|
|
140
|
+
TToSchema = RouteInfoByPath<TAllRouteInfo, TTo>['fullSearchSchema'],
|
|
141
|
+
> = StartsWith<TFrom, TTo> extends true // If the next route search extend or cover the from route, params will be optional
|
|
142
|
+
? {
|
|
143
|
+
search?: SearchReducer<TFromSchema, TToSchema>
|
|
144
|
+
}
|
|
145
|
+
: // Optional search params? Allow it
|
|
146
|
+
keyof PickRequired<TToSchema> extends never
|
|
147
|
+
? {
|
|
148
|
+
search?: SearchReducer<TFromSchema, TToSchema>
|
|
149
|
+
}
|
|
150
|
+
: {
|
|
151
|
+
// Must have required search params, enforce it
|
|
152
|
+
search: SearchReducer<TFromSchema, TToSchema>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type SearchReducer<TFrom, TTo> =
|
|
156
|
+
| { [TKey in keyof TTo]: TTo[TKey] }
|
|
157
|
+
| ((current: TFrom) => TTo)
|
|
158
|
+
|
|
159
|
+
type PathParamOptions<
|
|
160
|
+
TAllRouteInfo extends AnyAllRouteInfo,
|
|
161
|
+
TFrom,
|
|
162
|
+
TTo,
|
|
163
|
+
TFromParams = RouteInfoByPath<TAllRouteInfo, TFrom>['allParams'],
|
|
164
|
+
TToParams = RouteInfoByPath<TAllRouteInfo, TTo>['allParams'],
|
|
165
|
+
> =
|
|
166
|
+
// If the next routes params extend or cover the from route, params will be optional
|
|
167
|
+
StartsWith<TFrom, TTo> extends true
|
|
168
|
+
? {
|
|
169
|
+
params?: ParamsReducer<TFromParams, TToParams>
|
|
170
|
+
}
|
|
171
|
+
: // If the next route doesn't have params, warn if any have been passed
|
|
172
|
+
AnyPathParams extends TToParams
|
|
173
|
+
? {
|
|
174
|
+
params?: ParamsReducer<TFromParams, Record<string, never>>
|
|
175
|
+
}
|
|
176
|
+
: // If the next route has params, enforce them
|
|
177
|
+
{
|
|
178
|
+
params: ParamsReducer<TFromParams, TToParams>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type ParamsReducer<TFrom, TTo> = TTo | ((current: TFrom) => TTo)
|
|
182
|
+
|
|
183
|
+
export type ToPathOption<
|
|
184
|
+
TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
|
|
185
|
+
TFrom extends ValidFromPath<TAllRouteInfo> = '/',
|
|
186
|
+
TTo extends string = '.',
|
|
187
|
+
> =
|
|
188
|
+
| TTo
|
|
189
|
+
| RelativeToPathAutoComplete<
|
|
190
|
+
TAllRouteInfo['routePaths'],
|
|
191
|
+
NoInfer<TFrom> extends string ? NoInfer<TFrom> : '',
|
|
192
|
+
NoInfer<TTo> & string
|
|
193
|
+
>
|
|
194
|
+
|
|
195
|
+
export type ToIdOption<
|
|
196
|
+
TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
|
|
197
|
+
TFrom extends ValidFromPath<TAllRouteInfo> = '/',
|
|
198
|
+
TTo extends string = '.',
|
|
199
|
+
> =
|
|
200
|
+
| TTo
|
|
201
|
+
| RelativeToPathAutoComplete<
|
|
202
|
+
TAllRouteInfo['routeIds'],
|
|
203
|
+
NoInfer<TFrom> extends string ? NoInfer<TFrom> : '',
|
|
204
|
+
NoInfer<TTo> & string
|
|
205
|
+
>
|
|
206
|
+
|
|
207
|
+
interface ActiveOptions {
|
|
208
|
+
exact?: boolean
|
|
209
|
+
includeHash?: boolean
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export type LinkOptions<
|
|
213
|
+
TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
|
|
214
|
+
TFrom extends ValidFromPath<TAllRouteInfo> = '/',
|
|
215
|
+
TTo extends string = '.',
|
|
216
|
+
> = NavigateOptionsAbsolute<TAllRouteInfo, TFrom, TTo> & {
|
|
217
|
+
// The standard anchor tag target attribute
|
|
218
|
+
target?: HTMLAnchorElement['target']
|
|
219
|
+
// Defaults to `{ exact: false, includeHash: false }`
|
|
220
|
+
activeOptions?: ActiveOptions
|
|
221
|
+
// If set, will preload the linked route on hover and cache it for this many milliseconds in hopes that the user will eventually navigate there.
|
|
222
|
+
preload?: false | 'intent'
|
|
223
|
+
// When preloaded and set, will cache the preloaded result for this duration in milliseconds
|
|
224
|
+
preloadMaxAge?: number
|
|
225
|
+
// Delay intent preloading by this many milliseconds. If the intent exits before this delay, the preload will be cancelled.
|
|
226
|
+
preloadDelay?: number
|
|
227
|
+
// If true, will render the link without the href attribute
|
|
228
|
+
disabled?: boolean
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export type CheckRelativePath<
|
|
232
|
+
TAllRouteInfo extends AnyAllRouteInfo,
|
|
233
|
+
TFrom,
|
|
234
|
+
TTo,
|
|
235
|
+
> = TTo extends string
|
|
236
|
+
? TFrom extends string
|
|
237
|
+
? ResolveRelativePath<TFrom, TTo> extends TAllRouteInfo['routePaths']
|
|
238
|
+
? {}
|
|
239
|
+
: {
|
|
240
|
+
Error: `${TFrom} + ${TTo} resolves to ${ResolveRelativePath<
|
|
241
|
+
TFrom,
|
|
242
|
+
TTo
|
|
243
|
+
>}, which is not a valid route path.`
|
|
244
|
+
'Valid Route Paths': TAllRouteInfo['routePaths']
|
|
245
|
+
}
|
|
246
|
+
: {}
|
|
247
|
+
: {}
|
|
248
|
+
|
|
249
|
+
export type CheckPath<TAllRouteInfo extends AnyAllRouteInfo, TPath> = Exclude<
|
|
250
|
+
TPath,
|
|
251
|
+
TAllRouteInfo['routePaths']
|
|
252
|
+
> extends never
|
|
253
|
+
? {}
|
|
254
|
+
: CheckPathError<TAllRouteInfo, Exclude<TPath, TAllRouteInfo['routePaths']>>
|
|
255
|
+
|
|
256
|
+
export type CheckPathError<TAllRouteInfo extends AnyAllRouteInfo, TInvalids> = {
|
|
257
|
+
Error: `${TInvalids extends string
|
|
258
|
+
? TInvalids
|
|
259
|
+
: never} is not a valid route path.`
|
|
260
|
+
'Valid Route Paths': TAllRouteInfo['routePaths']
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export type ResolveRelativePath<TFrom, TTo = '.'> = TFrom extends string
|
|
264
|
+
? TTo extends string
|
|
265
|
+
? TTo extends '.'
|
|
266
|
+
? TFrom
|
|
267
|
+
: TTo extends `./`
|
|
268
|
+
? Join<[TFrom, '/']>
|
|
269
|
+
: TTo extends `./${infer TRest}`
|
|
270
|
+
? ResolveRelativePath<TFrom, TRest>
|
|
271
|
+
: TTo extends `/${infer TRest}`
|
|
272
|
+
? TTo
|
|
273
|
+
: Split<TTo> extends ['..', ...infer ToRest]
|
|
274
|
+
? Split<TFrom> extends [...infer FromRest, infer FromTail]
|
|
275
|
+
? ResolveRelativePath<Join<FromRest>, Join<ToRest>>
|
|
276
|
+
: never
|
|
277
|
+
: Split<TTo> extends ['.', ...infer ToRest]
|
|
278
|
+
? ResolveRelativePath<TFrom, Join<ToRest>>
|
|
279
|
+
: CleanPath<Join<['/', ...Split<TFrom>, ...Split<TTo>]>>
|
|
280
|
+
: never
|
|
281
|
+
: never
|
|
282
|
+
|
|
283
|
+
export type ValidFromPath<
|
|
284
|
+
TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
|
|
285
|
+
> =
|
|
286
|
+
| undefined
|
|
287
|
+
| (string extends TAllRouteInfo['routePaths']
|
|
288
|
+
? string
|
|
289
|
+
: TAllRouteInfo['routePaths'])
|
package/src/path.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { AnyPathParams } from './routeConfig'
|
|
2
|
+
import { MatchLocation } from './router'
|
|
3
|
+
import { last } from './utils'
|
|
4
|
+
|
|
5
|
+
export interface Segment {
|
|
6
|
+
type: 'pathname' | 'param' | 'wildcard'
|
|
7
|
+
value: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function joinPaths(paths: (string | undefined)[]) {
|
|
11
|
+
return cleanPath(paths.filter(Boolean).join('/'))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function cleanPath(path: string) {
|
|
15
|
+
// remove double slashes
|
|
16
|
+
return path.replace(/\/{2,}/g, '/')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function trimPathLeft(path: string) {
|
|
20
|
+
return path === '/' ? path : path.replace(/^\/{1,}/, '')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function trimPathRight(path: string) {
|
|
24
|
+
return path === '/' ? path : path.replace(/\/{1,}$/, '')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function trimPath(path: string) {
|
|
28
|
+
return trimPathRight(trimPathLeft(path))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resolvePath(basepath: string, base: string, to: string) {
|
|
32
|
+
base = base.replace(new RegExp(`^${basepath}`), '/')
|
|
33
|
+
to = to.replace(new RegExp(`^${basepath}`), '/')
|
|
34
|
+
|
|
35
|
+
let baseSegments = parsePathname(base)
|
|
36
|
+
const toSegments = parsePathname(to)
|
|
37
|
+
|
|
38
|
+
toSegments.forEach((toSegment, index) => {
|
|
39
|
+
if (toSegment.value === '/') {
|
|
40
|
+
if (!index) {
|
|
41
|
+
// Leading slash
|
|
42
|
+
baseSegments = [toSegment]
|
|
43
|
+
} else if (index === toSegments.length - 1) {
|
|
44
|
+
// Trailing Slash
|
|
45
|
+
baseSegments.push(toSegment)
|
|
46
|
+
} else {
|
|
47
|
+
// ignore inter-slashes
|
|
48
|
+
}
|
|
49
|
+
} else if (toSegment.value === '..') {
|
|
50
|
+
// Extra trailing slash? pop it off
|
|
51
|
+
if (baseSegments.length > 1 && last(baseSegments)?.value === '/') {
|
|
52
|
+
baseSegments.pop()
|
|
53
|
+
}
|
|
54
|
+
baseSegments.pop()
|
|
55
|
+
} else if (toSegment.value === '.') {
|
|
56
|
+
return
|
|
57
|
+
} else {
|
|
58
|
+
baseSegments.push(toSegment)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const joined = joinPaths([basepath, ...baseSegments.map((d) => d.value)])
|
|
63
|
+
|
|
64
|
+
return cleanPath(joined)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parsePathname(pathname?: string): Segment[] {
|
|
68
|
+
if (!pathname) {
|
|
69
|
+
return []
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
pathname = cleanPath(pathname)
|
|
73
|
+
|
|
74
|
+
const segments: Segment[] = []
|
|
75
|
+
|
|
76
|
+
if (pathname.slice(0, 1) === '/') {
|
|
77
|
+
pathname = pathname.substring(1)
|
|
78
|
+
segments.push({
|
|
79
|
+
type: 'pathname',
|
|
80
|
+
value: '/',
|
|
81
|
+
})
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!pathname) {
|
|
85
|
+
return segments
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Remove empty segments and '.' segments
|
|
89
|
+
const split = pathname.split('/').filter(Boolean)
|
|
90
|
+
|
|
91
|
+
segments.push(
|
|
92
|
+
...split.map((part): Segment => {
|
|
93
|
+
if (part.startsWith('*')) {
|
|
94
|
+
return {
|
|
95
|
+
type: 'wildcard',
|
|
96
|
+
value: part,
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (part.charAt(0) === ':') {
|
|
101
|
+
return {
|
|
102
|
+
type: 'param',
|
|
103
|
+
value: part,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
type: 'pathname',
|
|
109
|
+
value: part,
|
|
110
|
+
}
|
|
111
|
+
}),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if (pathname.slice(-1) === '/') {
|
|
115
|
+
pathname = pathname.substring(1)
|
|
116
|
+
segments.push({
|
|
117
|
+
type: 'pathname',
|
|
118
|
+
value: '/',
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return segments
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function interpolatePath(
|
|
126
|
+
path: string | undefined,
|
|
127
|
+
params: any,
|
|
128
|
+
leaveWildcard?: boolean,
|
|
129
|
+
) {
|
|
130
|
+
const interpolatedPathSegments = parsePathname(path)
|
|
131
|
+
|
|
132
|
+
return joinPaths(
|
|
133
|
+
interpolatedPathSegments.map((segment) => {
|
|
134
|
+
if (segment.value === '*' && !leaveWildcard) {
|
|
135
|
+
return ''
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (segment.type === 'param') {
|
|
139
|
+
return params![segment.value.substring(1)] ?? ''
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return segment.value
|
|
143
|
+
}),
|
|
144
|
+
)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function matchPathname(
|
|
148
|
+
currentPathname: string,
|
|
149
|
+
matchLocation: Pick<MatchLocation, 'to' | 'fuzzy' | 'caseSensitive'>,
|
|
150
|
+
): AnyPathParams | undefined {
|
|
151
|
+
const pathParams = matchByPath(currentPathname, matchLocation)
|
|
152
|
+
// const searchMatched = matchBySearch(currentLocation.search, matchLocation)
|
|
153
|
+
|
|
154
|
+
if (matchLocation.to && !pathParams) {
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// if (matchLocation.search && !searchMatched) {
|
|
159
|
+
// return
|
|
160
|
+
// }
|
|
161
|
+
|
|
162
|
+
return pathParams ?? {}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function matchByPath(
|
|
166
|
+
from: string,
|
|
167
|
+
matchLocation: Pick<MatchLocation, 'to' | 'caseSensitive' | 'fuzzy'>,
|
|
168
|
+
): Record<string, string> | undefined {
|
|
169
|
+
const baseSegments = parsePathname(from)
|
|
170
|
+
const routeSegments = parsePathname(`${matchLocation.to ?? '*'}`)
|
|
171
|
+
|
|
172
|
+
const params: Record<string, string> = {}
|
|
173
|
+
|
|
174
|
+
let isMatch = (() => {
|
|
175
|
+
for (
|
|
176
|
+
let i = 0;
|
|
177
|
+
i < Math.max(baseSegments.length, routeSegments.length);
|
|
178
|
+
i++
|
|
179
|
+
) {
|
|
180
|
+
const baseSegment = baseSegments[i]
|
|
181
|
+
const routeSegment = routeSegments[i]
|
|
182
|
+
|
|
183
|
+
const isLastRouteSegment = i === routeSegments.length - 1
|
|
184
|
+
const isLastBaseSegment = i === baseSegments.length - 1
|
|
185
|
+
|
|
186
|
+
if (routeSegment) {
|
|
187
|
+
if (routeSegment.type === 'wildcard') {
|
|
188
|
+
if (baseSegment?.value) {
|
|
189
|
+
params['*'] = joinPaths(baseSegments.slice(i).map((d) => d.value))
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (routeSegment.type === 'pathname') {
|
|
196
|
+
if (routeSegment.value === '/' && !baseSegment?.value) {
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (baseSegment) {
|
|
201
|
+
if (matchLocation.caseSensitive) {
|
|
202
|
+
if (routeSegment.value !== baseSegment.value) {
|
|
203
|
+
return false
|
|
204
|
+
}
|
|
205
|
+
} else if (
|
|
206
|
+
routeSegment.value.toLowerCase() !==
|
|
207
|
+
baseSegment.value.toLowerCase()
|
|
208
|
+
) {
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!baseSegment) {
|
|
215
|
+
return false
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (routeSegment.type === 'param') {
|
|
219
|
+
if (baseSegment?.value === '/') {
|
|
220
|
+
return false
|
|
221
|
+
}
|
|
222
|
+
if (!baseSegment.value.startsWith(':')) {
|
|
223
|
+
params[routeSegment.value.substring(1)] = baseSegment.value
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (isLastRouteSegment && !isLastBaseSegment) {
|
|
229
|
+
return !!matchLocation.fuzzy
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return true
|
|
233
|
+
})()
|
|
234
|
+
|
|
235
|
+
return isMatch ? (params as Record<string, string>) : undefined
|
|
236
|
+
}
|
package/src/qss.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// qss has been slightly modified and inlined here for our use cases (and compression's sake). We've included it as a hard dependency for MIT license attribution.
|
|
4
4
|
|
|
5
5
|
export function encode(obj, pfx?: string) {
|
|
6
6
|
var k,
|
package/src/route.ts
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CheckRelativePath,
|
|
3
|
+
LinkInfo,
|
|
4
|
+
LinkOptions,
|
|
5
|
+
ResolveRelativePath,
|
|
6
|
+
ToOptions,
|
|
7
|
+
} from './link'
|
|
8
|
+
import { RouteConfig, RouteOptions } from './routeConfig'
|
|
9
|
+
import {
|
|
10
|
+
AnyAllRouteInfo,
|
|
11
|
+
AnyRouteInfo,
|
|
12
|
+
DefaultAllRouteInfo,
|
|
13
|
+
RouteInfo,
|
|
14
|
+
RouteInfoByPath,
|
|
15
|
+
} from './routeInfo'
|
|
16
|
+
import { RouteMatch } from './routeMatch'
|
|
17
|
+
import { Action, ActionState, MatchRouteOptions, Router } from './router'
|
|
18
|
+
import { NoInfer, replaceEqualDeep } from './utils'
|
|
19
|
+
|
|
20
|
+
export interface AnyRoute extends Route<any, any> {}
|
|
21
|
+
|
|
22
|
+
export interface Route<
|
|
23
|
+
TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
|
|
24
|
+
TRouteInfo extends AnyRouteInfo = RouteInfo,
|
|
25
|
+
> {
|
|
26
|
+
routeId: TRouteInfo['id']
|
|
27
|
+
routeRouteId: TRouteInfo['routeId']
|
|
28
|
+
routePath: TRouteInfo['path']
|
|
29
|
+
fullPath: TRouteInfo['fullPath']
|
|
30
|
+
parentRoute?: AnyRoute
|
|
31
|
+
childRoutes?: AnyRoute[]
|
|
32
|
+
options: RouteOptions
|
|
33
|
+
router: Router<TAllRouteInfo['routeConfig'], TAllRouteInfo>
|
|
34
|
+
buildLink: <TTo extends string = '.'>(
|
|
35
|
+
options: Omit<
|
|
36
|
+
LinkOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
|
|
37
|
+
'from'
|
|
38
|
+
>,
|
|
39
|
+
) => LinkInfo
|
|
40
|
+
matchRoute: <
|
|
41
|
+
TTo extends string = '.',
|
|
42
|
+
TResolved extends string = ResolveRelativePath<TRouteInfo['id'], TTo>,
|
|
43
|
+
>(
|
|
44
|
+
matchLocation: CheckRelativePath<
|
|
45
|
+
TAllRouteInfo,
|
|
46
|
+
TRouteInfo['fullPath'],
|
|
47
|
+
NoInfer<TTo>
|
|
48
|
+
> &
|
|
49
|
+
Omit<ToOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>, 'from'>,
|
|
50
|
+
opts?: MatchRouteOptions,
|
|
51
|
+
) => RouteInfoByPath<TAllRouteInfo, TResolved>['allParams']
|
|
52
|
+
navigate: <TTo extends string = '.'>(
|
|
53
|
+
options: Omit<LinkOptions<TAllRouteInfo, TRouteInfo['id'], TTo>, 'from'>,
|
|
54
|
+
) => Promise<void>
|
|
55
|
+
action: unknown extends TRouteInfo['actionResponse']
|
|
56
|
+
?
|
|
57
|
+
| Action<TRouteInfo['actionPayload'], TRouteInfo['actionResponse']>
|
|
58
|
+
| undefined
|
|
59
|
+
: Action<TRouteInfo['actionPayload'], TRouteInfo['actionResponse']>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createRoute<
|
|
63
|
+
TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
|
|
64
|
+
TRouteInfo extends AnyRouteInfo = RouteInfo,
|
|
65
|
+
>(
|
|
66
|
+
routeConfig: RouteConfig,
|
|
67
|
+
options: TRouteInfo['options'],
|
|
68
|
+
parent: undefined | Route<TAllRouteInfo, any>,
|
|
69
|
+
router: Router<TAllRouteInfo['routeConfig'], TAllRouteInfo>,
|
|
70
|
+
): Route<TAllRouteInfo, TRouteInfo> {
|
|
71
|
+
const { id, routeId, path: routePath, fullPath } = routeConfig
|
|
72
|
+
|
|
73
|
+
const action =
|
|
74
|
+
router.state.actions[id] ||
|
|
75
|
+
(() => {
|
|
76
|
+
router.state.actions[id] = {
|
|
77
|
+
pending: [],
|
|
78
|
+
submit: async <T, U>(
|
|
79
|
+
submission: T,
|
|
80
|
+
actionOpts?: { invalidate?: boolean },
|
|
81
|
+
) => {
|
|
82
|
+
if (!route) {
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const invalidate = actionOpts?.invalidate ?? true
|
|
87
|
+
|
|
88
|
+
const actionState: ActionState<T, U> = {
|
|
89
|
+
submittedAt: Date.now(),
|
|
90
|
+
status: 'pending',
|
|
91
|
+
submission,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
action.current = actionState
|
|
95
|
+
action.latest = actionState
|
|
96
|
+
action.pending.push(actionState)
|
|
97
|
+
|
|
98
|
+
router.state = {
|
|
99
|
+
...router.state,
|
|
100
|
+
currentAction: actionState,
|
|
101
|
+
latestAction: actionState,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
router.notify()
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const res = await route.options.action?.(submission)
|
|
108
|
+
actionState.data = res as U
|
|
109
|
+
if (invalidate) {
|
|
110
|
+
router.invalidateRoute({ to: '.', fromCurrent: true })
|
|
111
|
+
await router.reload()
|
|
112
|
+
}
|
|
113
|
+
actionState.status = 'success'
|
|
114
|
+
return res
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error(err)
|
|
117
|
+
actionState.error = err
|
|
118
|
+
actionState.status = 'error'
|
|
119
|
+
} finally {
|
|
120
|
+
action.pending = action.pending.filter((d) => d !== actionState)
|
|
121
|
+
router.removeActionQueue.push({ action, actionState })
|
|
122
|
+
router.notify()
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
return router.state.actions[id]!
|
|
127
|
+
})()
|
|
128
|
+
|
|
129
|
+
let route: Route<TAllRouteInfo, TRouteInfo> = {
|
|
130
|
+
routeId: id,
|
|
131
|
+
routeRouteId: routeId,
|
|
132
|
+
routePath,
|
|
133
|
+
fullPath,
|
|
134
|
+
options,
|
|
135
|
+
router,
|
|
136
|
+
childRoutes: undefined!,
|
|
137
|
+
parentRoute: parent,
|
|
138
|
+
action,
|
|
139
|
+
|
|
140
|
+
buildLink: (options) => {
|
|
141
|
+
return router.buildLink({
|
|
142
|
+
...options,
|
|
143
|
+
from: fullPath,
|
|
144
|
+
} as any) as any
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
navigate: (options) => {
|
|
148
|
+
return router.navigate({
|
|
149
|
+
...options,
|
|
150
|
+
from: fullPath,
|
|
151
|
+
} as any) as any
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
matchRoute: (matchLocation, opts) => {
|
|
155
|
+
return router.matchRoute(
|
|
156
|
+
{
|
|
157
|
+
...matchLocation,
|
|
158
|
+
from: fullPath,
|
|
159
|
+
} as any,
|
|
160
|
+
opts,
|
|
161
|
+
)
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
router.options.createRoute?.({ router, route })
|
|
166
|
+
|
|
167
|
+
return route
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function cascadeLoaderData(matches: RouteMatch<any, any>[]) {
|
|
171
|
+
matches.forEach((match, index) => {
|
|
172
|
+
const parent = matches[index - 1]
|
|
173
|
+
|
|
174
|
+
if (parent) {
|
|
175
|
+
match.loaderData = replaceEqualDeep(match.loaderData, {
|
|
176
|
+
...parent.loaderData,
|
|
177
|
+
...match.routeLoaderData,
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
}
|