@devp0nt/route0 1.0.0-next.2 → 1.0.0-next.21
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/index.cjs +422 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +226 -84
- package/dist/esm/index.d.ts +226 -84
- package/dist/esm/index.js +334 -71
- package/dist/esm/index.js.map +1 -1
- package/package.json +42 -15
- package/src/index.test.ts +1082 -35
- package/src/index.ts +856 -244
- package/dist/cjs/index.js +0 -158
- package/dist/cjs/index.js.map +0 -1
package/src/index.ts
CHANGED
|
@@ -1,41 +1,46 @@
|
|
|
1
|
+
// TODO: asterisk
|
|
2
|
+
// TODO: optional params
|
|
3
|
+
// TODO: required search
|
|
4
|
+
|
|
5
|
+
// TODO: .extension('.json') to not add additional / but just add some extension
|
|
6
|
+
// TODO: search input can be boolean, or even object with qs
|
|
7
|
+
// TODO: Роут0 три пусть три тоже сам генерится вероятно
|
|
8
|
+
// TODO: Роут0 три мод, тогда там все ноуты кончаются на .селф
|
|
1
9
|
// TODO: use splats in param definition "*"
|
|
2
|
-
// TODO: ? check extend for
|
|
3
|
-
// TODO: .create(route, {
|
|
10
|
+
// TODO: ? check extend for search only .extend('&x&z')
|
|
11
|
+
// TODO: .create(route, {useSearch, useParams})
|
|
4
12
|
// TODO: Из пас экзакт, из пасвизквери экзает, из чилдрен, из парент, из экзактОр
|
|
5
13
|
// TODO: isEqual, isChildren, isParent
|
|
6
|
-
// TODO: extractParams,
|
|
7
|
-
// TODO: getPathDefinition respecting definitionParamPrefix,
|
|
14
|
+
// TODO: extractParams, extractSearch
|
|
15
|
+
// TODO: getPathDefinition respecting definitionParamPrefix, definitionSearchPrefix
|
|
8
16
|
// TODO: prepend
|
|
9
17
|
// TODO: Route0.createTree({base:{self: x, children: ...})
|
|
10
18
|
// TODO: overrideTree
|
|
11
19
|
// TODO: .create(route, {baseUrl, useLocation})
|
|
12
20
|
// TODO: ? optional path params as @
|
|
13
21
|
// TODO: prependMany, extendMany, overrideMany, with types
|
|
22
|
+
// TODO: optional route params /x/:id?
|
|
14
23
|
|
|
15
|
-
export class Route0<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
> {
|
|
21
|
-
pathOriginalDefinition: TPathOriginalDefinition
|
|
22
|
-
private pathDefinition: TPathDefinition
|
|
23
|
-
paramsDefinition: TParamsDefinition
|
|
24
|
-
queryDefinition: TQueryDefinition
|
|
24
|
+
export class Route0<TDefinition extends string> {
|
|
25
|
+
readonly definition: TDefinition
|
|
26
|
+
readonly pathDefinition: _PathDefinition<TDefinition>
|
|
27
|
+
readonly paramsDefinition: _ParamsDefinition<TDefinition>
|
|
28
|
+
readonly searchDefinition: _SearchDefinition<TDefinition>
|
|
25
29
|
baseUrl: string
|
|
26
30
|
|
|
27
|
-
private constructor(definition:
|
|
28
|
-
this.
|
|
29
|
-
this.pathDefinition = Route0.
|
|
30
|
-
this.paramsDefinition = Route0.
|
|
31
|
-
this.
|
|
31
|
+
private constructor(definition: TDefinition, config: RouteConfigInput = {}) {
|
|
32
|
+
this.definition = definition
|
|
33
|
+
this.pathDefinition = Route0._getPathDefinitionBydefinition(definition)
|
|
34
|
+
this.paramsDefinition = Route0._getParamsDefinitionBydefinition(definition)
|
|
35
|
+
this.searchDefinition = Route0._getSearchDefinitionBydefinition(definition)
|
|
32
36
|
|
|
33
37
|
const { baseUrl } = config
|
|
34
38
|
if (baseUrl && typeof baseUrl === 'string' && baseUrl.length) {
|
|
35
39
|
this.baseUrl = baseUrl
|
|
36
40
|
} else {
|
|
37
41
|
const g = globalThis as unknown as { location?: { origin?: string } }
|
|
38
|
-
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
43
|
+
if (typeof g?.location?.origin === 'string' && g.location.origin.length > 0) {
|
|
39
44
|
this.baseUrl = g.location.origin
|
|
40
45
|
} else {
|
|
41
46
|
this.baseUrl = 'https://example.com'
|
|
@@ -43,185 +48,142 @@ export class Route0<
|
|
|
43
48
|
}
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
static create<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
): Route0.Callable<Route0<TPathOriginalDefinition, TPathDefinition, TParamsDefinition, TQueryDefinition>> {
|
|
55
|
-
const original = new Route0<TPathOriginalDefinition, TPathDefinition, TParamsDefinition, TQueryDefinition>(
|
|
56
|
-
definition,
|
|
57
|
-
config,
|
|
58
|
-
)
|
|
51
|
+
static create<TDefinition extends string>(
|
|
52
|
+
definition: TDefinition | AnyRoute<TDefinition>,
|
|
53
|
+
config?: RouteConfigInput,
|
|
54
|
+
): CallabelRoute<TDefinition> {
|
|
55
|
+
if (typeof definition === 'function') {
|
|
56
|
+
return definition
|
|
57
|
+
}
|
|
58
|
+
const original = typeof definition === 'object' ? definition : new Route0<TDefinition>(definition, config)
|
|
59
59
|
const callable = original.get.bind(original)
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (typeof value === 'function') {
|
|
64
|
-
return value.bind(original)
|
|
65
|
-
}
|
|
66
|
-
return value
|
|
67
|
-
},
|
|
68
|
-
set(_target, prop, value, receiver) {
|
|
69
|
-
;(original as any)[prop] = value
|
|
70
|
-
return true
|
|
71
|
-
},
|
|
72
|
-
has(_target, prop) {
|
|
73
|
-
return prop in original
|
|
74
|
-
},
|
|
60
|
+
Object.setPrototypeOf(callable, original)
|
|
61
|
+
Object.defineProperty(callable, Symbol.toStringTag, {
|
|
62
|
+
value: original.definition,
|
|
75
63
|
})
|
|
76
|
-
|
|
77
|
-
return proxy as never
|
|
64
|
+
return callable as never
|
|
78
65
|
}
|
|
79
66
|
|
|
80
|
-
private static
|
|
81
|
-
const i =
|
|
82
|
-
if (i === -1) return { pathDefinition:
|
|
67
|
+
private static _splitPathDefinitionAndSearchTailDefinition(definition: string) {
|
|
68
|
+
const i = definition.indexOf('&')
|
|
69
|
+
if (i === -1) return { pathDefinition: definition, searchTailDefinition: '' }
|
|
83
70
|
return {
|
|
84
|
-
pathDefinition:
|
|
85
|
-
|
|
71
|
+
pathDefinition: definition.slice(0, i),
|
|
72
|
+
searchTailDefinition: definition.slice(i),
|
|
86
73
|
}
|
|
87
74
|
}
|
|
88
75
|
|
|
89
|
-
private static _getAbsPath(baseUrl: string,
|
|
90
|
-
return new URL(
|
|
76
|
+
private static _getAbsPath(baseUrl: string, pathWithSearch: string) {
|
|
77
|
+
return new URL(pathWithSearch, baseUrl).toString().replace(/\/$/, '')
|
|
91
78
|
}
|
|
92
79
|
|
|
93
|
-
private static
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const { pathDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition)
|
|
97
|
-
return pathDefinition as Route0._PathDefinition<TPathOriginalDefinition>
|
|
80
|
+
private static _getPathDefinitionBydefinition<TDefinition extends string>(definition: TDefinition) {
|
|
81
|
+
const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition)
|
|
82
|
+
return pathDefinition as _PathDefinition<TDefinition>
|
|
98
83
|
}
|
|
99
84
|
|
|
100
|
-
private static
|
|
101
|
-
|
|
102
|
-
) {
|
|
103
|
-
const { pathDefinition } = Route0.
|
|
85
|
+
private static _getParamsDefinitionBydefinition<TDefinition extends string>(
|
|
86
|
+
definition: TDefinition,
|
|
87
|
+
): _ParamsDefinition<TDefinition> {
|
|
88
|
+
const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition)
|
|
104
89
|
const matches = Array.from(pathDefinition.matchAll(/:([A-Za-z0-9_]+)/g))
|
|
105
90
|
const paramsDefinition = Object.fromEntries(matches.map((m) => [m[1], true]))
|
|
106
|
-
|
|
91
|
+
const keysCount = Object.keys(paramsDefinition).length
|
|
92
|
+
if (keysCount === 0) {
|
|
93
|
+
return undefined as _ParamsDefinition<TDefinition>
|
|
94
|
+
}
|
|
95
|
+
return paramsDefinition as _ParamsDefinition<TDefinition>
|
|
107
96
|
}
|
|
108
97
|
|
|
109
|
-
private static
|
|
110
|
-
|
|
111
|
-
) {
|
|
112
|
-
const {
|
|
113
|
-
if (!
|
|
114
|
-
return
|
|
98
|
+
private static _getSearchDefinitionBydefinition<TDefinition extends string>(
|
|
99
|
+
definition: TDefinition,
|
|
100
|
+
): _SearchDefinition<TDefinition> {
|
|
101
|
+
const { searchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition)
|
|
102
|
+
if (!searchTailDefinition) {
|
|
103
|
+
return undefined as _SearchDefinition<TDefinition>
|
|
115
104
|
}
|
|
116
|
-
const keys =
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
static overrideMany<T extends Record<string, Route0<any, any, any, any>>>(
|
|
122
|
-
routes: T,
|
|
123
|
-
config: Route0.RouteConfigInput,
|
|
124
|
-
): T {
|
|
125
|
-
const result = {} as T
|
|
126
|
-
for (const [key, value] of Object.entries(routes)) {
|
|
127
|
-
;(result as any)[key] = value.clone(config)
|
|
105
|
+
const keys = searchTailDefinition.split('&').filter(Boolean)
|
|
106
|
+
const searchDefinition = Object.fromEntries(keys.map((k) => [k, true]))
|
|
107
|
+
const keysCount = Object.keys(searchDefinition).length
|
|
108
|
+
if (keysCount === 0) {
|
|
109
|
+
return undefined as _SearchDefinition<TDefinition>
|
|
128
110
|
}
|
|
129
|
-
return
|
|
111
|
+
return searchDefinition as _SearchDefinition<TDefinition>
|
|
130
112
|
}
|
|
131
113
|
|
|
132
114
|
extend<TSuffixDefinition extends string>(
|
|
133
115
|
suffixDefinition: TSuffixDefinition,
|
|
134
|
-
):
|
|
135
|
-
Route0
|
|
136
|
-
|
|
137
|
-
Route0.
|
|
138
|
-
Route0._ParamsDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>,
|
|
139
|
-
Route0._QueryDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>
|
|
140
|
-
>
|
|
141
|
-
> {
|
|
142
|
-
const { pathDefinition: parentPathDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(
|
|
143
|
-
this.pathOriginalDefinition,
|
|
144
|
-
)
|
|
145
|
-
const { pathDefinition: suffixPathDefinition, queryTailDefinition: suffixQueryTailDefinition } =
|
|
146
|
-
Route0._splitPathDefinitionAndQueryTailDefinition(suffixDefinition)
|
|
116
|
+
): CallabelRoute<PathExtended<TDefinition, TSuffixDefinition>> {
|
|
117
|
+
const { pathDefinition: parentPathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(this.definition)
|
|
118
|
+
const { pathDefinition: suffixPathDefinition, searchTailDefinition: suffixSearchTailDefinition } =
|
|
119
|
+
Route0._splitPathDefinitionAndSearchTailDefinition(suffixDefinition)
|
|
147
120
|
const pathDefinition = `${parentPathDefinition}/${suffixPathDefinition}`.replace(/\/{2,}/g, '/')
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
TPathOriginalDefinition,
|
|
151
|
-
TSuffixDefinition
|
|
152
|
-
>
|
|
153
|
-
return Route0.create<
|
|
154
|
-
Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>,
|
|
155
|
-
Route0._PathDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>,
|
|
156
|
-
Route0._ParamsDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>,
|
|
157
|
-
Route0._QueryDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>
|
|
158
|
-
>(pathOriginalDefinition, { baseUrl: this.baseUrl })
|
|
121
|
+
const definition = `${pathDefinition}${suffixSearchTailDefinition}` as PathExtended<TDefinition, TSuffixDefinition>
|
|
122
|
+
return Route0.create<PathExtended<TDefinition, TSuffixDefinition>>(definition, { baseUrl: this.baseUrl })
|
|
159
123
|
}
|
|
160
124
|
|
|
161
125
|
// has params
|
|
162
126
|
get(
|
|
163
|
-
input:
|
|
164
|
-
|
|
165
|
-
|
|
127
|
+
input: OnlyIfHasParams<
|
|
128
|
+
_ParamsDefinition<TDefinition>,
|
|
129
|
+
WithParamsInput<TDefinition, { search?: undefined; abs?: false }>
|
|
166
130
|
>,
|
|
167
|
-
):
|
|
131
|
+
): OnlyIfHasParams<_ParamsDefinition<TDefinition>, PathOnlyRouteValue<TDefinition>>
|
|
168
132
|
get(
|
|
169
|
-
input:
|
|
170
|
-
|
|
171
|
-
|
|
133
|
+
input: OnlyIfHasParams<
|
|
134
|
+
_ParamsDefinition<TDefinition>,
|
|
135
|
+
WithParamsInput<TDefinition, { search: _SearchInput<TDefinition>; abs?: false }>
|
|
172
136
|
>,
|
|
173
|
-
):
|
|
137
|
+
): OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithSearchRouteValue<TDefinition>>
|
|
174
138
|
get(
|
|
175
|
-
input:
|
|
176
|
-
|
|
177
|
-
|
|
139
|
+
input: OnlyIfHasParams<
|
|
140
|
+
_ParamsDefinition<TDefinition>,
|
|
141
|
+
WithParamsInput<TDefinition, { search?: undefined; abs: true }>
|
|
178
142
|
>,
|
|
179
|
-
):
|
|
143
|
+
): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsolutePathOnlyRouteValue<TDefinition>>
|
|
180
144
|
get(
|
|
181
|
-
input:
|
|
182
|
-
|
|
183
|
-
|
|
145
|
+
input: OnlyIfHasParams<
|
|
146
|
+
_ParamsDefinition<TDefinition>,
|
|
147
|
+
WithParamsInput<TDefinition, { search: _SearchInput<TDefinition>; abs: true }>
|
|
184
148
|
>,
|
|
185
|
-
):
|
|
149
|
+
): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsoluteWithSearchRouteValue<TDefinition>>
|
|
186
150
|
|
|
187
151
|
// no params
|
|
152
|
+
get(...args: OnlyIfNoParams<_ParamsDefinition<TDefinition>, [], [never]>): PathOnlyRouteValue<TDefinition>
|
|
188
153
|
get(
|
|
189
|
-
|
|
190
|
-
):
|
|
191
|
-
get(
|
|
192
|
-
input: Route0._OnlyIfNoParams<TParamsDefinition, { query?: undefined; abs?: false }>,
|
|
193
|
-
): Route0._OnlyIfNoParams<TParamsDefinition, Route0._PathOnlyRouteValue<TPathOriginalDefinition>>
|
|
154
|
+
input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, { search?: undefined; abs?: false }>,
|
|
155
|
+
): OnlyIfNoParams<_ParamsDefinition<TDefinition>, PathOnlyRouteValue<TDefinition>>
|
|
194
156
|
get(
|
|
195
|
-
input:
|
|
196
|
-
):
|
|
157
|
+
input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, { search: _SearchInput<TDefinition>; abs?: false }>,
|
|
158
|
+
): OnlyIfNoParams<_ParamsDefinition<TDefinition>, WithSearchRouteValue<TDefinition>>
|
|
197
159
|
get(
|
|
198
|
-
input:
|
|
199
|
-
):
|
|
160
|
+
input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, { search?: undefined; abs: true }>,
|
|
161
|
+
): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsolutePathOnlyRouteValue<TDefinition>>
|
|
200
162
|
get(
|
|
201
|
-
input:
|
|
202
|
-
):
|
|
163
|
+
input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, { search: _SearchInput<TDefinition>; abs: true }>,
|
|
164
|
+
): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsoluteWithSearchRouteValue<TDefinition>>
|
|
203
165
|
|
|
204
166
|
// implementation
|
|
205
167
|
get(...args: any[]): string {
|
|
206
|
-
const {
|
|
207
|
-
|
|
168
|
+
const { searchInput, paramsInput, absInput } = ((): {
|
|
169
|
+
searchInput: Record<string, string | number>
|
|
208
170
|
paramsInput: Record<string, string | number>
|
|
209
171
|
absInput: boolean
|
|
210
172
|
} => {
|
|
211
173
|
if (args.length === 0) {
|
|
212
|
-
return {
|
|
174
|
+
return { searchInput: {}, paramsInput: {}, absInput: false }
|
|
213
175
|
}
|
|
214
176
|
const input = args[0]
|
|
215
177
|
if (typeof input !== 'object' || input === null) {
|
|
216
178
|
// throw new Error("Invalid get route input: expected object")
|
|
217
|
-
return {
|
|
179
|
+
return { searchInput: {}, paramsInput: {}, absInput: false }
|
|
218
180
|
}
|
|
219
|
-
const {
|
|
220
|
-
return {
|
|
181
|
+
const { search, abs, ...params } = input
|
|
182
|
+
return { searchInput: search || {}, paramsInput: params, absInput: abs ?? false }
|
|
221
183
|
})()
|
|
222
184
|
|
|
223
185
|
// validate params
|
|
224
|
-
const neededParamsKeys = Object.keys(this.paramsDefinition)
|
|
186
|
+
const neededParamsKeys = this.paramsDefinition ? Object.keys(this.paramsDefinition) : []
|
|
225
187
|
const providedParamsKeys = Object.keys(paramsInput)
|
|
226
188
|
const notProvidedKeys = neededParamsKeys.filter((k) => !providedParamsKeys.includes(k))
|
|
227
189
|
if (notProvidedKeys.length) {
|
|
@@ -230,12 +192,14 @@ export class Route0<
|
|
|
230
192
|
}
|
|
231
193
|
|
|
232
194
|
// create url
|
|
233
|
-
|
|
195
|
+
|
|
196
|
+
let url = this.pathDefinition as string
|
|
234
197
|
// replace params
|
|
198
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
235
199
|
url = url.replace(/:([A-Za-z0-9_]+)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? '')))
|
|
236
|
-
//
|
|
237
|
-
const
|
|
238
|
-
url = [url, new URLSearchParams(
|
|
200
|
+
// search params
|
|
201
|
+
const searchInputStringified = Object.fromEntries(Object.entries(searchInput).map(([k, v]) => [k, String(v)]))
|
|
202
|
+
url = [url, new URLSearchParams(searchInputStringified).toString()].filter(Boolean).join('?')
|
|
239
203
|
// dedupe slashes
|
|
240
204
|
url = url.replace(/\/{2,}/g, '/')
|
|
241
205
|
// absolute
|
|
@@ -244,113 +208,761 @@ export class Route0<
|
|
|
244
208
|
return url
|
|
245
209
|
}
|
|
246
210
|
|
|
247
|
-
|
|
211
|
+
// has params
|
|
212
|
+
flat(
|
|
213
|
+
input: OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithParamsInput<TDefinition>>,
|
|
214
|
+
abs?: false,
|
|
215
|
+
): OnlyIfHasParams<_ParamsDefinition<TDefinition>, PathOnlyRouteValue<TDefinition>>
|
|
216
|
+
flat(
|
|
217
|
+
input: OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithParamsInput<TDefinition, _SearchInput<TDefinition>>>,
|
|
218
|
+
abs?: false,
|
|
219
|
+
): OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithSearchRouteValue<TDefinition>>
|
|
220
|
+
flat(
|
|
221
|
+
input: OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithParamsInput<TDefinition>>,
|
|
222
|
+
abs: true,
|
|
223
|
+
): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsolutePathOnlyRouteValue<TDefinition>>
|
|
224
|
+
flat(
|
|
225
|
+
input: OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithParamsInput<TDefinition, _SearchInput<TDefinition>>>,
|
|
226
|
+
abs: true,
|
|
227
|
+
): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsoluteWithSearchRouteValue<TDefinition>>
|
|
228
|
+
|
|
229
|
+
// no params
|
|
230
|
+
flat(...args: OnlyIfNoParams<_ParamsDefinition<TDefinition>, [], [never]>): PathOnlyRouteValue<TDefinition>
|
|
231
|
+
flat(
|
|
232
|
+
input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, Record<never, never>>,
|
|
233
|
+
abs?: false,
|
|
234
|
+
): OnlyIfNoParams<_ParamsDefinition<TDefinition>, PathOnlyRouteValue<TDefinition>>
|
|
235
|
+
flat(
|
|
236
|
+
input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, _SearchInput<TDefinition>>,
|
|
237
|
+
abs?: false,
|
|
238
|
+
): OnlyIfNoParams<_ParamsDefinition<TDefinition>, WithSearchRouteValue<TDefinition>>
|
|
239
|
+
flat(
|
|
240
|
+
input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, Record<never, never>>,
|
|
241
|
+
abs: true,
|
|
242
|
+
): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsolutePathOnlyRouteValue<TDefinition>>
|
|
243
|
+
flat(
|
|
244
|
+
input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, _SearchInput<TDefinition>>,
|
|
245
|
+
abs: true,
|
|
246
|
+
): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsoluteWithSearchRouteValue<TDefinition>>
|
|
247
|
+
|
|
248
|
+
// implementation
|
|
249
|
+
flat(...args: any[]): string {
|
|
250
|
+
const { searchInput, paramsInput, absInput } = ((): {
|
|
251
|
+
searchInput: Record<string, string | number>
|
|
252
|
+
paramsInput: Record<string, string | number>
|
|
253
|
+
absInput: boolean
|
|
254
|
+
} => {
|
|
255
|
+
if (args.length === 0) {
|
|
256
|
+
return { searchInput: {}, paramsInput: {}, absInput: false }
|
|
257
|
+
}
|
|
258
|
+
const input = args[0]
|
|
259
|
+
if (typeof input !== 'object' || input === null) {
|
|
260
|
+
// throw new Error("Invalid get route input: expected object")
|
|
261
|
+
return { searchInput: {}, paramsInput: {}, absInput: args[1] ?? false }
|
|
262
|
+
}
|
|
263
|
+
const paramsKeys = this.getParamsKeys()
|
|
264
|
+
const paramsInput = paramsKeys.reduce<Record<string, string | number>>((acc, key) => {
|
|
265
|
+
if (input[key] !== undefined) {
|
|
266
|
+
acc[key] = input[key]
|
|
267
|
+
}
|
|
268
|
+
return acc
|
|
269
|
+
}, {})
|
|
270
|
+
const searchKeys = this.getSearchKeys()
|
|
271
|
+
const searchInput = Object.keys(input)
|
|
272
|
+
.filter((k) => {
|
|
273
|
+
if (searchKeys.includes(k)) {
|
|
274
|
+
return true
|
|
275
|
+
}
|
|
276
|
+
if (paramsKeys.includes(k)) {
|
|
277
|
+
return false
|
|
278
|
+
}
|
|
279
|
+
return true
|
|
280
|
+
})
|
|
281
|
+
.reduce<Record<string, string | number>>((acc, key) => {
|
|
282
|
+
acc[key] = input[key]
|
|
283
|
+
return acc
|
|
284
|
+
}, {})
|
|
285
|
+
return { searchInput, paramsInput, absInput: args[1] ?? false }
|
|
286
|
+
})()
|
|
287
|
+
|
|
288
|
+
return this.get({ ...paramsInput, search: searchInput, abs: absInput } as never)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
getParamsKeys(): string[] {
|
|
292
|
+
return Object.keys(this.paramsDefinition || {})
|
|
293
|
+
}
|
|
294
|
+
getSearchKeys(): string[] {
|
|
295
|
+
return Object.keys(this.searchDefinition || {})
|
|
296
|
+
}
|
|
297
|
+
getFlatKeys(): string[] {
|
|
298
|
+
return [...this.getSearchKeys(), ...this.getParamsKeys()]
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
getDefinition(): string {
|
|
248
302
|
return this.pathDefinition
|
|
249
303
|
}
|
|
250
304
|
|
|
251
|
-
clone(config?:
|
|
252
|
-
return new Route0(this.
|
|
305
|
+
clone(config?: RouteConfigInput): Route0<TDefinition> {
|
|
306
|
+
return new Route0(this.definition, config)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
getRegexString(): string {
|
|
310
|
+
// Normalize the path definition (remove trailing slash except for root)
|
|
311
|
+
const def =
|
|
312
|
+
this.pathDefinition.length > 1 && this.pathDefinition.endsWith('/')
|
|
313
|
+
? this.pathDefinition.slice(0, -1)
|
|
314
|
+
: this.pathDefinition
|
|
315
|
+
|
|
316
|
+
// Replace :param with placeholders, escape regex special chars, then restore capture groups
|
|
317
|
+
const pattern = def
|
|
318
|
+
.replace(/:([A-Za-z0-9_]+)/g, '___PARAM___') // temporarily replace params with placeholder
|
|
319
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex special chars
|
|
320
|
+
.replace(/___PARAM___/g, '([^/]+)') // replace placeholder with capture group
|
|
321
|
+
|
|
322
|
+
return pattern
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
getRegex(): RegExp {
|
|
326
|
+
return new RegExp(`^${this.getRegexString()}$`)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
static getRegexString(routes: AnyRoute[] | AnyRoute): string {
|
|
330
|
+
const routesArray = Array.isArray(routes) ? routes : [routes]
|
|
331
|
+
return routesArray.map((route) => route.getRegexString()).join('|')
|
|
332
|
+
}
|
|
333
|
+
static getRegex(routes: AnyRoute[] | AnyRoute): RegExp {
|
|
334
|
+
return new RegExp(`^(${Route0.getRegexString(routes)})$`)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
static getLocation(href: `${string}://${string}`): UnknownLocation
|
|
338
|
+
static getLocation(hrefRel: `/${string}`): UnknownLocation
|
|
339
|
+
static getLocation(hrefOrHrefRel: string): UnknownLocation
|
|
340
|
+
static getLocation(location: LocationAny): UnknownLocation
|
|
341
|
+
static getLocation(hrefOrHrefRelOrLocation: string | LocationAny): UnknownLocation
|
|
342
|
+
static getLocation(hrefOrHrefRelOrLocation: string | LocationAny): UnknownLocation {
|
|
343
|
+
if (typeof hrefOrHrefRelOrLocation !== 'string') {
|
|
344
|
+
hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel
|
|
345
|
+
}
|
|
346
|
+
// Check if it's an absolute URL (starts with scheme://)
|
|
347
|
+
const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation)
|
|
348
|
+
|
|
349
|
+
// Use dummy base only if relative
|
|
350
|
+
const base = abs ? undefined : 'http://example.com'
|
|
351
|
+
const url = new URL(hrefOrHrefRelOrLocation, base)
|
|
352
|
+
|
|
353
|
+
// Extract search params
|
|
354
|
+
const searchParams = Object.fromEntries(url.searchParams.entries())
|
|
355
|
+
|
|
356
|
+
// Normalize pathname (remove trailing slash except for root)
|
|
357
|
+
let pathname = url.pathname
|
|
358
|
+
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
359
|
+
pathname = pathname.slice(0, -1)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Common derived values
|
|
363
|
+
const hrefRel = pathname + url.search + url.hash
|
|
364
|
+
|
|
365
|
+
// Build the location object consistent with _GeneralLocation
|
|
366
|
+
const location: UnknownLocation = {
|
|
367
|
+
pathname,
|
|
368
|
+
search: url.search,
|
|
369
|
+
hash: url.hash,
|
|
370
|
+
origin: abs ? url.origin : undefined,
|
|
371
|
+
href: abs ? url.href : undefined,
|
|
372
|
+
hrefRel,
|
|
373
|
+
abs,
|
|
374
|
+
|
|
375
|
+
// extra host-related fields (available even for relative with dummy base)
|
|
376
|
+
host: abs ? url.host : undefined,
|
|
377
|
+
hostname: abs ? url.hostname : undefined,
|
|
378
|
+
port: abs ? url.port || undefined : undefined,
|
|
379
|
+
|
|
380
|
+
// specific to UnknownLocation
|
|
381
|
+
searchParams,
|
|
382
|
+
params: undefined,
|
|
383
|
+
route: undefined,
|
|
384
|
+
exact: false,
|
|
385
|
+
parent: false,
|
|
386
|
+
children: false,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return location
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
getLocation(href: `${string}://${string}`): KnownLocation<TDefinition>
|
|
393
|
+
getLocation(hrefRel: `/${string}`): KnownLocation<TDefinition>
|
|
394
|
+
getLocation(hrefOrHrefRel: string): KnownLocation<TDefinition>
|
|
395
|
+
getLocation(location: LocationAny): KnownLocation<TDefinition>
|
|
396
|
+
getLocation(hrefOrHrefRelOrLocation: string | LocationAny): KnownLocation<TDefinition>
|
|
397
|
+
getLocation(hrefOrHrefRelOrLocation: string | LocationAny): KnownLocation<TDefinition> {
|
|
398
|
+
const location = Route0.getLocation(hrefOrHrefRelOrLocation) as never as KnownLocation<TDefinition>
|
|
399
|
+
location.route = this.definition as definition<TDefinition>
|
|
400
|
+
location.params = {}
|
|
401
|
+
|
|
402
|
+
const escapeRegex = (s: string) => s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
|
|
403
|
+
|
|
404
|
+
// Normalize pathname (no trailing slash except root)
|
|
405
|
+
const pathname =
|
|
406
|
+
location.pathname.length > 1 && location.pathname.endsWith('/')
|
|
407
|
+
? location.pathname.slice(0, -1)
|
|
408
|
+
: location.pathname
|
|
409
|
+
|
|
410
|
+
// Use getRegexString() to get the pattern
|
|
411
|
+
const pattern = this.getRegexString()
|
|
412
|
+
|
|
413
|
+
// Extract param names from the definition
|
|
414
|
+
const paramNames: string[] = []
|
|
415
|
+
const def =
|
|
416
|
+
this.pathDefinition.length > 1 && this.pathDefinition.endsWith('/')
|
|
417
|
+
? this.pathDefinition.slice(0, -1)
|
|
418
|
+
: this.pathDefinition
|
|
419
|
+
def.replace(/:([A-Za-z0-9_]+)/g, (_m: string, name: string) => {
|
|
420
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
|
|
421
|
+
paramNames.push(String(name))
|
|
422
|
+
return ''
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const exactRe = new RegExp(`^${pattern}$`)
|
|
426
|
+
const parentRe = new RegExp(`^${pattern}(?:/.*)?$`) // route matches the beginning of the URL (may have more)
|
|
427
|
+
const exactMatch = pathname.match(exactRe)
|
|
428
|
+
|
|
429
|
+
// Fill params only for exact match (keeps behavior predictable)
|
|
430
|
+
if (exactMatch) {
|
|
431
|
+
const values = exactMatch.slice(1)
|
|
432
|
+
const params = Object.fromEntries(paramNames.map((n, i) => [n, decodeURIComponent(values[i] ?? '')]))
|
|
433
|
+
location.params = params
|
|
434
|
+
} else {
|
|
435
|
+
location.params = {}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const exact = !!exactMatch
|
|
439
|
+
const parent = !exact && parentRe.test(pathname)
|
|
440
|
+
|
|
441
|
+
// "children": the URL is a prefix of the route definition (ignoring params' concrete values)
|
|
442
|
+
// We check if the definition starts with the URL path boundary-wise.
|
|
443
|
+
const children = !exact && new RegExp(`^${escapeRegex(pathname)}(?:/|$)`).test(def)
|
|
444
|
+
|
|
445
|
+
return {
|
|
446
|
+
...location,
|
|
447
|
+
exact,
|
|
448
|
+
parent,
|
|
449
|
+
children,
|
|
450
|
+
} as KnownLocation<TDefinition>
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
isSame(other: Route0<TDefinition>): boolean {
|
|
454
|
+
return (
|
|
455
|
+
this.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, '__PARAM__') ===
|
|
456
|
+
other.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, '__PARAM__')
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
static isSame(a: AnyRoute | string | undefined, b: AnyRoute | string | undefined): boolean {
|
|
460
|
+
if ((!a && b) || (a && !b)) return false
|
|
461
|
+
if (!a || !b) return true
|
|
462
|
+
return Route0.create(a).isSame(Route0.create(b))
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
isChildren(other: Route0<TDefinition>): boolean {
|
|
466
|
+
return (
|
|
467
|
+
this.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, '__PARAM__') ===
|
|
468
|
+
other.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, '__PARAM__')
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
isParent(other: Route0<TDefinition>): boolean {
|
|
473
|
+
return (
|
|
474
|
+
other.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, '__PARAM__') ===
|
|
475
|
+
this.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, '__PARAM__')
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
isConflict(other: Route0<any>): boolean {
|
|
480
|
+
const getParts = (path: string) => {
|
|
481
|
+
if (path === '/') return ['/']
|
|
482
|
+
return path.split('/').filter(Boolean)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const thisParts = getParts(this.pathDefinition)
|
|
486
|
+
const otherParts = getParts(other.pathDefinition)
|
|
487
|
+
|
|
488
|
+
// Different lengths = no conflict (one is deeper than the other)
|
|
489
|
+
if (thisParts.length !== otherParts.length) {
|
|
490
|
+
return false
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Check if all segments could match
|
|
494
|
+
for (let i = 0; i < thisParts.length; i++) {
|
|
495
|
+
const thisPart = thisParts[i]
|
|
496
|
+
const otherPart = otherParts[i]
|
|
497
|
+
|
|
498
|
+
// Both params = always match
|
|
499
|
+
if (thisPart.startsWith(':') && otherPart.startsWith(':')) {
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// One is param = can match
|
|
504
|
+
if (thisPart.startsWith(':') || otherPart.startsWith(':')) {
|
|
505
|
+
continue
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Both static = must be same
|
|
509
|
+
if (thisPart !== otherPart) {
|
|
510
|
+
return false
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return true
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
isMoreSpecificThan(other: Route0<any>): boolean {
|
|
518
|
+
// More specific = should come earlier when conflicted
|
|
519
|
+
// Static segments beat param segments at the same position
|
|
520
|
+
const getParts = (path: string) => {
|
|
521
|
+
if (path === '/') return ['/']
|
|
522
|
+
return path.split('/').filter(Boolean)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const thisParts = getParts(this.pathDefinition)
|
|
526
|
+
const otherParts = getParts(other.pathDefinition)
|
|
527
|
+
|
|
528
|
+
// Compare segment by segment
|
|
529
|
+
for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
|
|
530
|
+
const thisIsStatic = !thisParts[i].startsWith(':')
|
|
531
|
+
const otherIsStatic = !otherParts[i].startsWith(':')
|
|
532
|
+
|
|
533
|
+
if (thisIsStatic && !otherIsStatic) return true
|
|
534
|
+
if (!thisIsStatic && otherIsStatic) return false
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// All equal, use lexicographic
|
|
538
|
+
return this.pathDefinition < other.pathDefinition
|
|
253
539
|
}
|
|
254
540
|
}
|
|
255
541
|
|
|
256
|
-
export
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
542
|
+
export class Routes<const T extends RoutesRecord = RoutesRecord> {
|
|
543
|
+
readonly routes: RoutesRecordHydrated<T>
|
|
544
|
+
readonly pathsOrdering: string[]
|
|
545
|
+
readonly keysOrdering: string[]
|
|
546
|
+
readonly ordered: CallabelRoute[]
|
|
547
|
+
|
|
548
|
+
private constructor({
|
|
549
|
+
routes,
|
|
550
|
+
isHydrated = false,
|
|
551
|
+
pathsOrdering,
|
|
552
|
+
keysOrdering,
|
|
553
|
+
ordered,
|
|
554
|
+
}: {
|
|
555
|
+
routes: RoutesRecordHydrated<T> | T
|
|
556
|
+
isHydrated?: boolean
|
|
557
|
+
pathsOrdering?: string[]
|
|
558
|
+
keysOrdering?: string[]
|
|
559
|
+
ordered?: CallabelRoute[]
|
|
560
|
+
}) {
|
|
561
|
+
this.routes = (
|
|
562
|
+
isHydrated ? (routes as RoutesRecordHydrated<T>) : Routes._hydrate(routes)
|
|
563
|
+
) as RoutesRecordHydrated<T>
|
|
564
|
+
if (!pathsOrdering || !keysOrdering || !ordered) {
|
|
565
|
+
const ordering = Routes._makeOrdering(this.routes)
|
|
566
|
+
this.pathsOrdering = ordering.pathsOrdering
|
|
567
|
+
this.keysOrdering = ordering.keysOrdering
|
|
568
|
+
this.ordered = this.keysOrdering.map((key) => this.routes[key])
|
|
569
|
+
} else {
|
|
570
|
+
this.pathsOrdering = pathsOrdering
|
|
571
|
+
this.keysOrdering = keysOrdering
|
|
572
|
+
this.ordered = ordered
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
static create<const T extends RoutesRecord>(routes: T): RoutesPretty<T> {
|
|
577
|
+
const instance = new Routes({ routes })
|
|
578
|
+
return Routes._prettify(instance)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
static _prettify<const T extends RoutesRecord>(instance: Routes<T>): RoutesPretty<T> {
|
|
582
|
+
Object.setPrototypeOf(instance, Routes.prototype)
|
|
583
|
+
Object.defineProperty(instance, Symbol.toStringTag, {
|
|
584
|
+
value: 'Routes',
|
|
585
|
+
})
|
|
586
|
+
Object.assign(instance, {
|
|
587
|
+
override: instance.override.bind(instance),
|
|
588
|
+
})
|
|
589
|
+
Object.assign(instance, instance.routes)
|
|
590
|
+
return instance as unknown as RoutesPretty<T>
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
static _hydrate<const T extends RoutesRecord>(routes: T): RoutesRecordHydrated<T> {
|
|
594
|
+
const result = {} as RoutesRecordHydrated<T>
|
|
595
|
+
for (const key in routes) {
|
|
596
|
+
if (Object.prototype.hasOwnProperty.call(routes, key)) {
|
|
597
|
+
const value = routes[key]
|
|
598
|
+
result[key] = (typeof value === 'string' ? Route0.create(value) : value) as CallabelRoute<T[typeof key]>
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return result
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
getLocation(href: `${string}://${string}`): UnknownLocation | ExactLocation
|
|
605
|
+
getLocation(hrefRel: `/${string}`): UnknownLocation | ExactLocation
|
|
606
|
+
getLocation(hrefOrHrefRel: string): UnknownLocation | ExactLocation
|
|
607
|
+
getLocation(location: LocationAny): UnknownLocation | ExactLocation
|
|
608
|
+
getLocation(hrefOrHrefRelOrLocation: string | LocationAny): UnknownLocation | ExactLocation
|
|
609
|
+
getLocation(hrefOrHrefRelOrLocation: string | LocationAny): UnknownLocation | ExactLocation {
|
|
610
|
+
// Find the route that exactly matches the given location
|
|
611
|
+
for (const route of this.ordered) {
|
|
612
|
+
const loc = route.getLocation(hrefOrHrefRelOrLocation)
|
|
613
|
+
if (loc.exact) {
|
|
614
|
+
return loc
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// No exact match found, return UnknownLocation
|
|
618
|
+
return Route0.getLocation(hrefOrHrefRelOrLocation)
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
static _makeOrdering(routes: RoutesRecord): { pathsOrdering: string[]; keysOrdering: string[] } {
|
|
622
|
+
const hydrated = Routes._hydrate(routes)
|
|
623
|
+
const entries = Object.entries(hydrated)
|
|
624
|
+
|
|
625
|
+
const getParts = (path: string) => {
|
|
626
|
+
if (path === '/') return ['/']
|
|
627
|
+
return path.split('/').filter(Boolean)
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Sort: shorter paths first, then by specificity, then alphabetically
|
|
631
|
+
entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
|
|
632
|
+
const partsA = getParts(routeA.pathDefinition)
|
|
633
|
+
const partsB = getParts(routeB.pathDefinition)
|
|
634
|
+
|
|
635
|
+
// 1. Shorter paths first (by segment count)
|
|
636
|
+
if (partsA.length !== partsB.length) {
|
|
637
|
+
return partsA.length - partsB.length
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// 2. Same length: check if they conflict
|
|
641
|
+
if (routeA.isConflict(routeB)) {
|
|
642
|
+
// Conflicting routes: more specific first
|
|
643
|
+
if (routeA.isMoreSpecificThan(routeB)) return -1
|
|
644
|
+
if (routeB.isMoreSpecificThan(routeA)) return 1
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// 3. Same length, not conflicting or equal specificity: alphabetically
|
|
648
|
+
return routeA.pathDefinition.localeCompare(routeB.pathDefinition)
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
const pathsOrdering = entries.map(([_key, route]) => route.definition)
|
|
652
|
+
const keysOrdering = entries.map(([_key, route]) => _key)
|
|
653
|
+
return { pathsOrdering, keysOrdering }
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
override(config: RouteConfigInput): RoutesPretty<T> {
|
|
657
|
+
const newRoutes = {} as RoutesRecordHydrated<T>
|
|
658
|
+
for (const key in this.routes) {
|
|
659
|
+
if (Object.prototype.hasOwnProperty.call(this.routes, key)) {
|
|
660
|
+
newRoutes[key] = this.routes[key].clone(config) as CallabelRoute<T[typeof key]>
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const instance = new Routes({
|
|
664
|
+
routes: newRoutes,
|
|
665
|
+
isHydrated: true,
|
|
666
|
+
pathsOrdering: this.pathsOrdering,
|
|
667
|
+
keysOrdering: this.keysOrdering,
|
|
668
|
+
ordered: this.keysOrdering.map((key) => newRoutes[key]),
|
|
669
|
+
})
|
|
670
|
+
return Routes._prettify(instance)
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// main
|
|
675
|
+
|
|
676
|
+
export type AnyRoute<T extends Route0<string> | string = string> = T extends string ? Route0<T> : T
|
|
677
|
+
export type CallabelRoute<T extends Route0<string> | string = string> = AnyRoute<T> & AnyRoute<T>['get']
|
|
678
|
+
export type AnyRouteOrDefinition<T extends string> = AnyRoute<T> | T
|
|
679
|
+
export type RouteConfigInput = {
|
|
680
|
+
baseUrl?: string
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// collection
|
|
684
|
+
|
|
685
|
+
export type RoutesRecord = Record<string, AnyRoute | string>
|
|
686
|
+
export type RoutesRecordHydrated<TRoutesRecord extends RoutesRecord = RoutesRecord> = {
|
|
687
|
+
[K in keyof TRoutesRecord]: CallabelRoute<TRoutesRecord[K]>
|
|
688
|
+
}
|
|
689
|
+
export type RoutesPretty<TRoutesRecord extends RoutesRecord = RoutesRecord> = RoutesRecordHydrated<TRoutesRecord> &
|
|
690
|
+
Routes<TRoutesRecord>
|
|
691
|
+
export type ExtractRoutesKeys<TRoutes extends RoutesPretty | RoutesRecord> = TRoutes extends RoutesPretty
|
|
692
|
+
? keyof TRoutes['routes']
|
|
693
|
+
: TRoutes extends RoutesRecord
|
|
694
|
+
? keyof TRoutes
|
|
279
695
|
: never
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
696
|
+
export type ExtractRoute<
|
|
697
|
+
TRoutes extends RoutesPretty | RoutesRecord,
|
|
698
|
+
TKey extends keyof ExtractRoutesKeys<TRoutes>,
|
|
699
|
+
> = TKey extends keyof TRoutes ? TRoutes[TKey] : never
|
|
700
|
+
|
|
701
|
+
// public utils
|
|
702
|
+
|
|
703
|
+
export type definition<T extends AnyRoute | string> = T extends AnyRoute
|
|
704
|
+
? T['definition']
|
|
705
|
+
: T extends string
|
|
706
|
+
? T
|
|
707
|
+
: never
|
|
708
|
+
export type PathDefinition<T extends AnyRoute | string> = T extends AnyRoute
|
|
709
|
+
? T['pathDefinition']
|
|
710
|
+
: T extends string
|
|
711
|
+
? _PathDefinition<T>
|
|
712
|
+
: never
|
|
713
|
+
export type ParamsDefinition<T extends AnyRoute | string> = T extends AnyRoute
|
|
714
|
+
? T['paramsDefinition']
|
|
715
|
+
: T extends string
|
|
716
|
+
? _ParamsDefinition<T>
|
|
717
|
+
: undefined
|
|
718
|
+
export type SearchDefinition<T extends AnyRoute | string> = T extends AnyRoute
|
|
719
|
+
? T['searchDefinition']
|
|
720
|
+
: T extends string
|
|
721
|
+
? _SearchDefinition<T>
|
|
722
|
+
: undefined
|
|
723
|
+
|
|
724
|
+
export type Extended<T extends AnyRoute | string | undefined, TSuffixDefinition extends string> = T extends AnyRoute
|
|
725
|
+
? Route0<PathExtended<T['definition'], TSuffixDefinition>>
|
|
726
|
+
: T extends string
|
|
727
|
+
? Route0<PathExtended<T, TSuffixDefinition>>
|
|
728
|
+
: T extends undefined
|
|
729
|
+
? Route0<TSuffixDefinition>
|
|
304
730
|
: never
|
|
305
|
-
>
|
|
306
731
|
|
|
307
|
-
|
|
308
|
-
|
|
732
|
+
export type IsParent<T extends AnyRoute | string, TParent extends AnyRoute | string> = _IsParent<
|
|
733
|
+
PathDefinition<T>,
|
|
734
|
+
PathDefinition<TParent>
|
|
735
|
+
>
|
|
736
|
+
export type IsChildren<T extends AnyRoute | string, TChildren extends AnyRoute | string> = _IsChildren<
|
|
737
|
+
PathDefinition<T>,
|
|
738
|
+
PathDefinition<TChildren>
|
|
739
|
+
>
|
|
740
|
+
export type IsSame<T extends AnyRoute | string, TExact extends AnyRoute | string> = _IsSame<
|
|
741
|
+
PathDefinition<T>,
|
|
742
|
+
PathDefinition<TExact>
|
|
743
|
+
>
|
|
744
|
+
export type IsSameParams<T1 extends AnyRoute | string, T2 extends AnyRoute | string> = _IsSameParams<
|
|
745
|
+
ParamsDefinition<T1>,
|
|
746
|
+
ParamsDefinition<T2>
|
|
747
|
+
>
|
|
309
748
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
749
|
+
export type HasParams<T extends AnyRoute | string> =
|
|
750
|
+
ExtractPathParams<PathDefinition<T>> extends infer U ? ([U] extends [never] ? false : true) : false
|
|
751
|
+
export type HasSearch<T extends AnyRoute | string> =
|
|
752
|
+
NonEmpty<SearchTailDefinitionWithoutFirstAmp<definition<T>>> extends infer Tail extends string
|
|
753
|
+
? AmpSplit<Tail> extends infer U
|
|
754
|
+
? [U] extends [never]
|
|
755
|
+
? false
|
|
756
|
+
: true
|
|
757
|
+
: false
|
|
758
|
+
: false
|
|
759
|
+
|
|
760
|
+
export type ParamsOutput<T extends AnyRoute | string> = {
|
|
761
|
+
[K in keyof ParamsDefinition<T>]: string
|
|
762
|
+
}
|
|
763
|
+
export type SearchOutput<T extends AnyRoute | string = string> = Partial<
|
|
764
|
+
{
|
|
765
|
+
[K in keyof SearchDefinition<T>]?: string
|
|
766
|
+
} & Record<string, string | undefined>
|
|
767
|
+
>
|
|
768
|
+
export type StrictSearchOutput<T extends AnyRoute | string> = Partial<{
|
|
769
|
+
[K in keyof SearchDefinition<T>]?: string | undefined
|
|
770
|
+
}>
|
|
771
|
+
export type ParamsInput<T extends AnyRoute | string = string> = _ParamsInput<PathDefinition<T>>
|
|
772
|
+
export type SearchInput<T extends AnyRoute | string = string> = _SearchInput<definition<T>>
|
|
773
|
+
export type StrictSearchInput<T extends AnyRoute | string> = _StrictSearchInput<definition<T>>
|
|
774
|
+
|
|
775
|
+
// location
|
|
776
|
+
|
|
777
|
+
export type LocationParams<TDefinition extends string> = {
|
|
778
|
+
[K in keyof _ParamsDefinition<TDefinition>]: string
|
|
779
|
+
}
|
|
780
|
+
export type LocationSearch<TDefinition extends string = string> = {
|
|
781
|
+
[K in keyof _SearchDefinition<TDefinition>]: string | undefined
|
|
782
|
+
} & Record<string, string | undefined>
|
|
783
|
+
|
|
784
|
+
export type _GeneralLocation = {
|
|
785
|
+
pathname: string
|
|
786
|
+
search: string
|
|
787
|
+
hash: string
|
|
788
|
+
origin?: string
|
|
789
|
+
href?: string
|
|
790
|
+
hrefRel: string
|
|
791
|
+
abs: boolean
|
|
792
|
+
port?: string
|
|
793
|
+
host?: string
|
|
794
|
+
hostname?: string
|
|
795
|
+
}
|
|
796
|
+
export type UnknownLocation = _GeneralLocation & {
|
|
797
|
+
params: undefined
|
|
798
|
+
searchParams: SearchOutput
|
|
799
|
+
route: undefined
|
|
800
|
+
exact: false
|
|
801
|
+
parent: false
|
|
802
|
+
children: false
|
|
803
|
+
}
|
|
804
|
+
export type UnmatchedLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation & {
|
|
805
|
+
params: Record<never, never>
|
|
806
|
+
searchParams: SearchOutput<TRoute>
|
|
807
|
+
route: definition<TRoute>
|
|
808
|
+
exact: false
|
|
809
|
+
parent: false
|
|
810
|
+
children: false
|
|
811
|
+
}
|
|
812
|
+
export type ExactLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation & {
|
|
813
|
+
params: ParamsOutput<TRoute>
|
|
814
|
+
searchParams: SearchOutput<TRoute>
|
|
815
|
+
route: definition<TRoute>
|
|
816
|
+
exact: true
|
|
817
|
+
parent: false
|
|
818
|
+
children: false
|
|
819
|
+
}
|
|
820
|
+
export type ParentLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation & {
|
|
821
|
+
params: Partial<ParamsOutput<TRoute>> // in fact maybe there will be whole params object, but does not matter now
|
|
822
|
+
searchParams: SearchOutput<TRoute>
|
|
823
|
+
route: definition<TRoute>
|
|
824
|
+
exact: false
|
|
825
|
+
parent: true
|
|
826
|
+
children: false
|
|
827
|
+
}
|
|
828
|
+
export type ChildrenLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation & {
|
|
829
|
+
params: ParamsOutput<TRoute>
|
|
830
|
+
searchParams: SearchOutput<TRoute>
|
|
831
|
+
route: definition<TRoute>
|
|
832
|
+
exact: false
|
|
833
|
+
parent: false
|
|
834
|
+
children: true
|
|
835
|
+
}
|
|
836
|
+
export type KnownLocation<TRoute extends AnyRoute | string = AnyRoute | string> =
|
|
837
|
+
| UnmatchedLocation<TRoute>
|
|
838
|
+
| ExactLocation<TRoute>
|
|
839
|
+
| ParentLocation<TRoute>
|
|
840
|
+
| ChildrenLocation<TRoute>
|
|
841
|
+
export type LocationAny<TRoute extends AnyRoute = AnyRoute> = UnknownLocation | KnownLocation<TRoute>
|
|
842
|
+
|
|
843
|
+
// internal utils
|
|
844
|
+
|
|
845
|
+
export type _PathDefinition<T extends string> = T extends string ? TrimSearchTailDefinition<T> : never
|
|
846
|
+
export type _ParamsDefinition<TDefinition extends string> =
|
|
847
|
+
ExtractPathParams<PathDefinition<TDefinition>> extends infer U
|
|
315
848
|
? [U] extends [never]
|
|
316
|
-
?
|
|
849
|
+
? undefined
|
|
317
850
|
: { [K in Extract<U, string>]: true }
|
|
318
|
-
:
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
? _AmpSplit<Tail> extends infer U
|
|
851
|
+
: undefined
|
|
852
|
+
export type _SearchDefinition<TDefinition extends string> =
|
|
853
|
+
NonEmpty<SearchTailDefinitionWithoutFirstAmp<TDefinition>> extends infer Tail extends string
|
|
854
|
+
? AmpSplit<Tail> extends infer U
|
|
323
855
|
? [U] extends [never]
|
|
324
|
-
?
|
|
856
|
+
? undefined
|
|
325
857
|
: { [K in Extract<U, string>]: true }
|
|
326
|
-
:
|
|
327
|
-
:
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
858
|
+
: undefined
|
|
859
|
+
: undefined
|
|
860
|
+
|
|
861
|
+
export type _ParamsInput<TDefinition extends string> =
|
|
862
|
+
_ParamsDefinition<TDefinition> extends undefined
|
|
863
|
+
? Record<never, never>
|
|
864
|
+
: {
|
|
865
|
+
[K in keyof _ParamsDefinition<TDefinition>]: string | number
|
|
866
|
+
}
|
|
867
|
+
export type _SearchInput<TDefinition extends string> =
|
|
868
|
+
_SearchDefinition<TDefinition> extends undefined
|
|
869
|
+
? Record<string, string | number>
|
|
870
|
+
: Partial<{
|
|
871
|
+
[K in keyof _SearchDefinition<TDefinition>]: string | number
|
|
872
|
+
}> &
|
|
873
|
+
Record<string, string | number>
|
|
874
|
+
export type _StrictSearchInput<TDefinition extends string> = Partial<{
|
|
875
|
+
[K in keyof _SearchDefinition<TDefinition>]: string | number
|
|
876
|
+
}>
|
|
877
|
+
|
|
878
|
+
export type TrimSearchTailDefinition<S extends string> = S extends `${infer P}&${string}` ? P : S
|
|
879
|
+
export type SearchTailDefinitionWithoutFirstAmp<S extends string> = S extends `${string}&${infer T}` ? T : ''
|
|
880
|
+
export type SearchTailDefinitionWithFirstAmp<S extends string> = S extends `${string}&${infer T}` ? `&${T}` : ''
|
|
881
|
+
export type AmpSplit<S extends string> = S extends `${infer A}&${infer B}` ? A | AmpSplit<B> : S
|
|
882
|
+
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
883
|
+
export type NonEmpty<T> = [T] extends ['' | never] ? never : T
|
|
884
|
+
export type ExtractPathParams<S extends string> = S extends `${string}:${infer After}`
|
|
885
|
+
? After extends `${infer Name}/${infer Rest}`
|
|
886
|
+
? Name | ExtractPathParams<`/${Rest}`>
|
|
887
|
+
: After
|
|
888
|
+
: never
|
|
889
|
+
export type ReplacePathParams<S extends string> = S extends `${infer Head}:${infer Tail}`
|
|
890
|
+
? // eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
891
|
+
Tail extends `${infer _Param}/${infer Rest}`
|
|
892
|
+
? ReplacePathParams<`${Head}${string}/${Rest}`>
|
|
893
|
+
: `${Head}${string}`
|
|
894
|
+
: S
|
|
895
|
+
export type DedupeSlashes<S extends string> = S extends `${infer A}//${infer B}` ? DedupeSlashes<`${A}/${B}`> : S
|
|
896
|
+
export type EmptyRecord = Record<never, never>
|
|
897
|
+
export type JoinPath<Parent extends string, Suffix extends string> = DedupeSlashes<
|
|
898
|
+
PathDefinition<Parent> extends infer A extends string
|
|
899
|
+
? PathDefinition<Suffix> extends infer B extends string
|
|
900
|
+
? A extends ''
|
|
901
|
+
? B extends ''
|
|
902
|
+
? ''
|
|
903
|
+
: B extends `/${string}`
|
|
904
|
+
? B
|
|
905
|
+
: `/${B}`
|
|
906
|
+
: B extends ''
|
|
907
|
+
? A
|
|
908
|
+
: A extends `${string}/`
|
|
909
|
+
? `${A}${B}`
|
|
910
|
+
: B extends `/${string}`
|
|
911
|
+
? `${A}${B}`
|
|
912
|
+
: `${A}/${B}`
|
|
913
|
+
: never
|
|
914
|
+
: never
|
|
915
|
+
>
|
|
916
|
+
|
|
917
|
+
export type OnlyIfNoParams<TParams extends object | undefined, Yes, No = never> = TParams extends undefined ? Yes : No
|
|
918
|
+
export type OnlyIfHasParams<TParams extends object | undefined, Yes, No = never> = TParams extends undefined ? No : Yes
|
|
919
|
+
|
|
920
|
+
export type PathOnlyRouteValue<TDefinition extends string> = `${ReplacePathParams<PathDefinition<TDefinition>>}`
|
|
921
|
+
export type WithSearchRouteValue<TDefinition extends string> =
|
|
922
|
+
`${ReplacePathParams<PathDefinition<TDefinition>>}?${string}`
|
|
923
|
+
export type AbsolutePathOnlyRouteValue<TDefinition extends string> =
|
|
924
|
+
PathOnlyRouteValue<TDefinition> extends '/' ? string : `${string}${PathOnlyRouteValue<TDefinition>}`
|
|
925
|
+
export type AbsoluteWithSearchRouteValue<TDefinition extends string> = `${string}${WithSearchRouteValue<TDefinition>}`
|
|
926
|
+
|
|
927
|
+
export type PathExtended<
|
|
928
|
+
TSourcedefinitionDefinition extends string,
|
|
929
|
+
TSuffixdefinitionDefinition extends string,
|
|
930
|
+
> = `${JoinPath<TSourcedefinitionDefinition, TSuffixdefinitionDefinition>}${SearchTailDefinitionWithFirstAmp<TSuffixdefinitionDefinition>}`
|
|
931
|
+
|
|
932
|
+
export type WithParamsInput<
|
|
933
|
+
TDefinition extends string,
|
|
934
|
+
T extends
|
|
935
|
+
| {
|
|
936
|
+
search?: _SearchInput<any>
|
|
937
|
+
abs?: boolean
|
|
938
|
+
}
|
|
939
|
+
| undefined = undefined,
|
|
940
|
+
> = _ParamsInput<TDefinition> & (T extends undefined ? Record<never, never> : T)
|
|
941
|
+
|
|
942
|
+
export type _IsSameParams<T1 extends object | undefined, T2 extends object | undefined> = T1 extends undefined
|
|
943
|
+
? T2 extends undefined
|
|
944
|
+
? true
|
|
945
|
+
: false
|
|
946
|
+
: T2 extends undefined
|
|
947
|
+
? false
|
|
948
|
+
: T1 extends T2
|
|
949
|
+
? T2 extends T1
|
|
950
|
+
? true
|
|
951
|
+
: false
|
|
952
|
+
: false
|
|
953
|
+
|
|
954
|
+
export type _IsParent<T extends string, TParent extends string> = T extends TParent
|
|
955
|
+
? false
|
|
956
|
+
: T extends `${TParent}${string}`
|
|
957
|
+
? true
|
|
958
|
+
: false
|
|
959
|
+
export type _IsChildren<T extends string, TChildren extends string> = TChildren extends T
|
|
960
|
+
? false
|
|
961
|
+
: TChildren extends `${T}${string}`
|
|
962
|
+
? true
|
|
963
|
+
: false
|
|
964
|
+
export type _IsSame<T extends string, TExact extends string> = T extends TExact
|
|
965
|
+
? TExact extends T
|
|
966
|
+
? true
|
|
967
|
+
: false
|
|
968
|
+
: false
|