@devp0nt/route0 1.0.0-next.3 → 1.0.0-next.30

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