@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/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,883 @@ 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
+ // 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 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
807
+ export type Definition<T extends AnyRoute | string> = T extends AnyRoute
808
+ ? T['definition']
809
+ : T extends string
810
+ ? T
279
811
  : 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
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
- 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
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
- export type _PathDefinition<TPathOriginalDefinition extends string> =
311
- _TrimQueryTailDefinition<TPathOriginalDefinition>
312
- export type _ParamsDefinition<TPathOriginalDefinition extends string> = _ExtractPathParams<
313
- _PathDefinition<TPathOriginalDefinition>
314
- > extends infer U
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
- ? _EmptyRecord
960
+ ? undefined
317
961
  : { [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
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
- ? _EmptyRecord
967
+ ? undefined
325
968
  : { [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
- }
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