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