@devp0nt/route0 1.0.0-next.2 → 1.0.0-next.21

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