@devp0nt/route0 1.0.0-next.6 → 1.0.0-next.60

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,235 +1,342 @@
1
+ // TODO: asterisk
2
+ // TODO: when asterisk then query params will be extended also after extend
3
+ // TODO: optional params
4
+ // TODO: required search
1
5
 
2
-
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: route0 if ens with "...&" then can be any query, else only provided type of queries
9
+ // TODO: Роут0 три мод, тогда там все ноуты кончаются на .селф
3
10
  // TODO: use splats in param definition "*"
4
- // TODO: ? check extend for query only .extend('&x&z')
5
- // TODO: .create(route, {useQuery, useParams})
11
+ // TODO: ? check extend for search only .extend('&x&z')
12
+ // TODO: .create(route, {useSearch, useParams})
6
13
  // TODO: Из пас экзакт, из пасвизквери экзает, из чилдрен, из парент, из экзактОр
7
- // TODO: isEqual, isChildren, isParent
8
- // TODO: extractParams, extractQuery
9
- // TODO: getPathDefinition respecting definitionParamPrefix, definitionQueryPrefix
14
+ // TODO: isEqual, isDescendant, isAncestor
15
+ // TODO: extractParams, extractSearch
16
+ // TODO: getPathDefinition respecting definitionParamPrefix, definitionSearchPrefix
10
17
  // TODO: prepend
11
- // TODO: Route0.createTree({base:{self: x, children: ...})
18
+ // TODO: ?? Route0.createTree({base:{self: x, children: ...})
19
+ // TODO: ? Routes.create({base:{self: x, children: ...}).attach('section', Routes.create({...}))
12
20
  // TODO: overrideTree
13
- // TODO: .create(route, {baseUrl, useLocation})
21
+ // TODO: .create(route, {baseurl, useLocation})
14
22
  // TODO: ? optional path params as @
15
23
  // TODO: prependMany, extendMany, overrideMany, with types
24
+ // TODO: optional route params /x/:id?
25
+ // TODO: fix CallableRoute<CallableRoute<>> in RoutesPretty type, it should be just CallableRoute<>
26
+
27
+ /**
28
+ * Strongly typed route descriptor and URL builder.
29
+ *
30
+ * A route definition uses:
31
+ * - path params: `/users/:id`
32
+ * - named search keys: `/users&tab&sort`
33
+ * - loose search mode: trailing `&`, e.g. `/users&`
34
+ *
35
+ * Instances are callable (same as `.get()`), so `route(input)` and
36
+ * `route.get(input)` are equivalent.
37
+ */
38
+ export class Route0<TDefinition extends string> {
39
+ readonly definition: TDefinition
40
+ readonly pathDefinition: _PathDefinition<TDefinition>
41
+ readonly paramsDefinition: _ParamsDefinition<TDefinition>
42
+ readonly searchDefinition: _SearchDefinition<TDefinition>
43
+ readonly hasLooseSearch: HasLooseSearch<TDefinition>
44
+ private _baseurl: string | undefined
45
+
46
+ /** Base URL used when generating absolute URLs (`abs: true`). */
47
+ get baseurl(): string {
48
+ if (!this._baseurl) {
49
+ throw new Error(
50
+ 'baseurl for route ' +
51
+ this.definition +
52
+ ' is not set, please provide it like Route0.create(route, {baseurl: "https://example.com"}) in config or set via overrides like routes._.override({baseurl: "https://example.com"})',
53
+ )
54
+ }
55
+ return this._baseurl
56
+ }
57
+ set baseurl(baseurl: string) {
58
+ this._baseurl = baseurl
59
+ }
60
+
61
+ private constructor(definition: TDefinition, config: RouteConfigInput = {}) {
62
+ this.definition = definition
63
+ this.pathDefinition = Route0._getPathDefinitionBydefinition(definition)
64
+ this.paramsDefinition = Route0._getParamsDefinitionBydefinition(definition)
65
+ this.searchDefinition = Route0._getSearchDefinitionBydefinition(definition)
66
+ this.hasLooseSearch = Route0._hasLooseSearch(definition)
16
67
 
17
- // page0
18
- // TODO: Сделать чисто фронтовую штуку, которая сама вызывает лоадер, сама вызывает нужные мета и title, и отдаёт в компонент нужные штуки
19
-
20
- // ssr0
21
- // TODO: ССР работает просто поверх любого роутера, который поддерживает асинхронную загрузку страниц
22
-
23
- export class Route0<
24
- TPathOriginalDefinition extends string,
25
- TPathDefinition extends Route0._PathDefinition<TPathOriginalDefinition>,
26
- TParamsDefinition extends Route0._ParamsDefinition<TPathOriginalDefinition>,
27
- TQueryDefinition extends Route0._QueryDefinition<TPathOriginalDefinition>,
28
- > {
29
- pathOriginalDefinition: TPathOriginalDefinition
30
- private pathDefinition: TPathDefinition
31
- paramsDefinition: TParamsDefinition
32
- queryDefinition: TQueryDefinition
33
- baseUrl: string
34
-
35
- private constructor(definition: TPathOriginalDefinition, config: Route0.RouteConfigInput = {}) {
36
- this.pathOriginalDefinition = definition as TPathOriginalDefinition
37
- this.pathDefinition = Route0._getPathDefinitionByOriginalDefinition(definition) as TPathDefinition
38
- this.paramsDefinition = Route0._getParamsDefinitionByRouteDefinition(definition) as TParamsDefinition
39
- this.queryDefinition = Route0._getQueryDefinitionByRouteDefinition(definition) as TQueryDefinition
40
-
41
- const { baseUrl } = config
42
- if (baseUrl && typeof baseUrl === 'string' && baseUrl.length) {
43
- this.baseUrl = baseUrl
68
+ const { baseurl } = config
69
+ if (baseurl && typeof baseurl === 'string' && baseurl.length) {
70
+ this._baseurl = baseurl
44
71
  } else {
45
72
  const g = globalThis as unknown as { location?: { origin?: string } }
46
- if (g?.location?.origin) {
47
- this.baseUrl = g.location.origin
73
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
74
+ if (typeof g?.location?.origin === 'string' && g.location.origin.length > 0) {
75
+ this._baseurl = g.location.origin
48
76
  } else {
49
- this.baseUrl = 'https://example.com'
77
+ this._baseurl = undefined
50
78
  }
51
79
  }
52
80
  }
53
81
 
54
- static create<
55
- TPathOriginalDefinition extends string,
56
- TPathDefinition extends Route0._PathDefinition<TPathOriginalDefinition>,
57
- TParamsDefinition extends Route0._ParamsDefinition<TPathOriginalDefinition>,
58
- TQueryDefinition extends Route0._QueryDefinition<TPathOriginalDefinition>,
59
- >(
60
- definition: TPathOriginalDefinition,
61
- config?: Route0.RouteConfigInput,
62
- ): Route0.Callable<Route0<TPathOriginalDefinition, TPathDefinition, TParamsDefinition, TQueryDefinition>> {
63
- const original = new Route0<TPathOriginalDefinition, TPathDefinition, TParamsDefinition, TQueryDefinition>(
64
- definition,
65
- config,
66
- )
82
+ /**
83
+ * Creates a callable route instance.
84
+ *
85
+ * If an existing route/callable route is provided, it is cloned.
86
+ */
87
+ static create<TDefinition extends string>(
88
+ definition: TDefinition | AnyRoute<TDefinition> | CallableRoute<TDefinition>,
89
+ config?: RouteConfigInput,
90
+ ): CallableRoute<TDefinition> {
91
+ if (typeof definition === 'function') {
92
+ return definition.clone(config) as CallableRoute<TDefinition>
93
+ }
94
+ if (typeof definition === 'object') {
95
+ return definition.clone(config) as CallableRoute<TDefinition>
96
+ }
97
+ const original = new Route0<TDefinition>(definition, config)
67
98
  const callable = original.get.bind(original)
68
- const proxy = new Proxy(callable, {
69
- get(_target, prop, receiver) {
70
- const value = (original as any)[prop]
71
- if (typeof value === 'function') {
72
- return value.bind(original)
73
- }
74
- return value
75
- },
76
- set(_target, prop, value, receiver) {
77
- ;(original as any)[prop] = value
78
- return true
79
- },
80
- has(_target, prop) {
81
- return prop in original
82
- },
99
+ Object.setPrototypeOf(callable, original)
100
+ Object.defineProperty(callable, Symbol.toStringTag, {
101
+ value: original.definition,
102
+ })
103
+ return callable as never
104
+ }
105
+
106
+ /**
107
+ * Normalizes a definition/route into a callable route.
108
+ *
109
+ * Unlike `create`, passing a callable route returns the same instance.
110
+ */
111
+ static from<TDefinition extends string>(
112
+ definition: TDefinition | AnyRoute<TDefinition> | CallableRoute<TDefinition>,
113
+ ): CallableRoute<TDefinition> {
114
+ if (typeof definition === 'function') {
115
+ return definition
116
+ }
117
+ const original = typeof definition === 'object' ? definition : new Route0<TDefinition>(definition)
118
+ const callable = original.get.bind(original)
119
+ Object.setPrototypeOf(callable, original)
120
+ Object.defineProperty(callable, Symbol.toStringTag, {
121
+ value: original.definition,
83
122
  })
84
- Object.setPrototypeOf(proxy, Route0.prototype)
85
- return proxy as never
123
+ return callable as never
86
124
  }
87
125
 
88
- private static _splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition: string) {
89
- const i = pathOriginalDefinition.indexOf('&')
90
- if (i === -1) return { pathDefinition: pathOriginalDefinition, queryTailDefinition: '' }
126
+ private static _splitPathDefinitionAndSearchTailDefinition(definition: string) {
127
+ const i = definition.indexOf('&')
128
+ if (i === -1) return { pathDefinition: definition, searchTailDefinition: '' }
91
129
  return {
92
- pathDefinition: pathOriginalDefinition.slice(0, i),
93
- queryTailDefinition: pathOriginalDefinition.slice(i),
130
+ pathDefinition: definition.slice(0, i),
131
+ searchTailDefinition: definition.slice(i),
94
132
  }
95
133
  }
96
134
 
97
- private static _getAbsPath(baseUrl: string, pathWithQuery: string) {
98
- return new URL(pathWithQuery, baseUrl).toString().replace(/\/$/, '')
135
+ private static _getAbsPath(baseurl: string, pathWithSearch: string) {
136
+ return new URL(pathWithSearch, baseurl).toString().replace(/\/$/, '')
99
137
  }
100
138
 
101
- private static _getPathDefinitionByOriginalDefinition<TPathOriginalDefinition extends string>(
102
- pathOriginalDefinition: TPathOriginalDefinition,
103
- ) {
104
- const { pathDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition)
105
- return pathDefinition as Route0._PathDefinition<TPathOriginalDefinition>
139
+ private static _getPathDefinitionBydefinition<TDefinition extends string>(definition: TDefinition) {
140
+ const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition)
141
+ return pathDefinition as _PathDefinition<TDefinition>
106
142
  }
107
143
 
108
- private static _getParamsDefinitionByRouteDefinition<TPathOriginalDefinition extends string>(
109
- pathOriginalDefinition: TPathOriginalDefinition,
110
- ) {
111
- const { pathDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition)
144
+ private static _getParamsDefinitionBydefinition<TDefinition extends string>(
145
+ definition: TDefinition,
146
+ ): _ParamsDefinition<TDefinition> {
147
+ const { pathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition)
112
148
  const matches = Array.from(pathDefinition.matchAll(/:([A-Za-z0-9_]+)/g))
113
149
  const paramsDefinition = Object.fromEntries(matches.map((m) => [m[1], true]))
114
- return paramsDefinition as Route0._ParamsDefinition<TPathOriginalDefinition>
150
+ const keysCount = Object.keys(paramsDefinition).length
151
+ if (keysCount === 0) {
152
+ return undefined as _ParamsDefinition<TDefinition>
153
+ }
154
+ return paramsDefinition as _ParamsDefinition<TDefinition>
115
155
  }
116
156
 
117
- private static _getQueryDefinitionByRouteDefinition<TPathOriginalDefinition extends string>(
118
- pathOriginalDefinition: TPathOriginalDefinition,
119
- ) {
120
- const { queryTailDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(pathOriginalDefinition)
121
- if (!queryTailDefinition) {
122
- return {} as Route0._QueryDefinition<TPathOriginalDefinition>
157
+ private static _getSearchDefinitionBydefinition<TDefinition extends string>(
158
+ definition: TDefinition,
159
+ ): _SearchDefinition<TDefinition> {
160
+ const { searchTailDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(definition)
161
+ if (!searchTailDefinition) {
162
+ return undefined as _SearchDefinition<TDefinition>
163
+ }
164
+ const keys = searchTailDefinition.split('&').filter(Boolean)
165
+ const searchDefinition = Object.fromEntries(keys.map((k) => [k, true]))
166
+ const keysCount = Object.keys(searchDefinition).length
167
+ if (keysCount === 0) {
168
+ return undefined as _SearchDefinition<TDefinition>
123
169
  }
124
- const keys = queryTailDefinition.split('&').map(Boolean)
125
- const queryDefinition = Object.fromEntries(keys.map((k) => [k, true]))
126
- return queryDefinition as Route0._QueryDefinition<TPathOriginalDefinition>
170
+ return searchDefinition as _SearchDefinition<TDefinition>
127
171
  }
128
172
 
129
- static overrideMany<T extends Record<string, Route0<any, any, any, any>>>(
130
- routes: T,
131
- config: Route0.RouteConfigInput,
132
- ): T {
133
- const result = {} as T
134
- for (const [key, value] of Object.entries(routes)) {
135
- ;(result as any)[key] = value.clone(config)
136
- }
137
- return result
173
+ private static _hasLooseSearch<TDefinition extends string>(definition: TDefinition): HasLooseSearch<TDefinition> {
174
+ // ends with &
175
+ // eslint-disable-next-line @typescript-eslint/prefer-string-starts-ends-with
176
+ return /&$/.test(definition) as HasLooseSearch<TDefinition>
138
177
  }
139
178
 
179
+ /** Extends the current route definition by appending a suffix route. */
140
180
  extend<TSuffixDefinition extends string>(
141
181
  suffixDefinition: TSuffixDefinition,
142
- ): Route0.Callable<
143
- Route0<
144
- Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>,
145
- Route0._PathDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>,
146
- Route0._ParamsDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>,
147
- Route0._QueryDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>
148
- >
149
- > {
150
- const { pathDefinition: parentPathDefinition } = Route0._splitPathDefinitionAndQueryTailDefinition(
151
- this.pathOriginalDefinition,
152
- )
153
- const { pathDefinition: suffixPathDefinition, queryTailDefinition: suffixQueryTailDefinition } =
154
- Route0._splitPathDefinitionAndQueryTailDefinition(suffixDefinition)
182
+ ): CallableRoute<PathExtended<TDefinition, TSuffixDefinition>> {
183
+ const { pathDefinition: parentPathDefinition } = Route0._splitPathDefinitionAndSearchTailDefinition(this.definition)
184
+ const { pathDefinition: suffixPathDefinition, searchTailDefinition: suffixSearchTailDefinition } =
185
+ Route0._splitPathDefinitionAndSearchTailDefinition(suffixDefinition)
155
186
  const pathDefinition = `${parentPathDefinition}/${suffixPathDefinition}`.replace(/\/{2,}/g, '/')
156
- const pathOriginalDefinition =
157
- `${pathDefinition}${suffixQueryTailDefinition}` as Route0._RoutePathOriginalDefinitionExtended<
158
- TPathOriginalDefinition,
159
- TSuffixDefinition
160
- >
161
- return Route0.create<
162
- Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>,
163
- Route0._PathDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>,
164
- Route0._ParamsDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>,
165
- Route0._QueryDefinition<Route0._RoutePathOriginalDefinitionExtended<TPathOriginalDefinition, TSuffixDefinition>>
166
- >(pathOriginalDefinition, { baseUrl: this.baseUrl })
187
+ const definition = `${pathDefinition}${suffixSearchTailDefinition}` as PathExtended<TDefinition, TSuffixDefinition>
188
+ return Route0.create<PathExtended<TDefinition, TSuffixDefinition>>(definition, { baseurl: this._baseurl })
167
189
  }
168
190
 
169
191
  // has params
192
+ // get(
193
+ // input: OnlyIfHasParams<
194
+ // _ParamsDefinition<TDefinition>,
195
+ // WithParamsInput<TDefinition, { search?: undefined; abs?: false; hash?: string | number }>
196
+ // >,
197
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, PathOnlyRouteValue<TDefinition>>
198
+ // get(
199
+ // input: OnlyIfHasParams<
200
+ // _ParamsDefinition<TDefinition>,
201
+ // WithParamsInput<TDefinition, { search: _SearchInput<TDefinition>; abs?: false; hash?: string | number }>
202
+ // >,
203
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithSearchRouteValue<TDefinition>>
204
+ // get(
205
+ // input: OnlyIfHasParams<
206
+ // _ParamsDefinition<TDefinition>,
207
+ // WithParamsInput<TDefinition, { search?: undefined; abs: true; hash?: string | number }>
208
+ // >,
209
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsolutePathOnlyRouteValue<TDefinition>>
210
+ // get(
211
+ // input: OnlyIfHasParams<
212
+ // _ParamsDefinition<TDefinition>,
213
+ // WithParamsInput<TDefinition, { search: _SearchInput<TDefinition>; abs: true; hash?: string | number }>
214
+ // >,
215
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsoluteWithSearchRouteValue<TDefinition>>
216
+
217
+ // get(
218
+ // input: OnlyIfHasParams<
219
+ // _ParamsDefinition<TDefinition>,
220
+ // WithParamsInput<TDefinition, { search?: _SearchInput<TDefinition>; abs?: false; hash?: string | number }>
221
+ // >,
222
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, PathRouteValue<TDefinition>>
223
+ // get(
224
+ // input: OnlyIfHasParams<
225
+ // _ParamsDefinition<TDefinition>,
226
+ // WithParamsInput<TDefinition, { search: _SearchInput<TDefinition>; abs: true; hash?: string | number }>
227
+ // >,
228
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsolutePathRouteValue<TDefinition>>
229
+
230
+ /**
231
+ * Builds a URL string from typed params/search input.
232
+ *
233
+ * - `abs: true` returns absolute URL using `baseurl`
234
+ * - `hash` appends URL fragment
235
+ * - `search` accepts named/loose search input based on definition
236
+ */
170
237
  get(
171
- input: Route0._OnlyIfHasParams<
172
- TParamsDefinition,
173
- Route0._WithParamsInput<TParamsDefinition, { query?: undefined; abs?: false }>
174
- >,
175
- ): Route0._OnlyIfHasParams<TParamsDefinition, Route0._PathOnlyRouteValue<TPathOriginalDefinition>>
176
- get(
177
- input: Route0._OnlyIfHasParams<
178
- TParamsDefinition,
179
- Route0._WithParamsInput<TParamsDefinition, { query: Route0._QueryInput<TQueryDefinition>; abs?: false }>
180
- >,
181
- ): Route0._OnlyIfHasParams<TParamsDefinition, Route0._WithQueryRouteValue<TPathOriginalDefinition>>
182
- get(
183
- input: Route0._OnlyIfHasParams<
184
- TParamsDefinition,
185
- Route0._WithParamsInput<TParamsDefinition, { query?: undefined; abs: true }>
186
- >,
187
- ): Route0._OnlyIfHasParams<TParamsDefinition, Route0._AbsolutePathOnlyRouteValue<TPathOriginalDefinition>>
188
- get(
189
- input: Route0._OnlyIfHasParams<
190
- TParamsDefinition,
191
- Route0._WithParamsInput<TParamsDefinition, { query: Route0._QueryInput<TQueryDefinition>; abs: true }>
238
+ input: OnlyIfHasParams<
239
+ TDefinition,
240
+ WithParamsInput<
241
+ TDefinition,
242
+ {
243
+ search?: _LooseSearchInput<TDefinition>
244
+ abs?: boolean
245
+ hash?: string | number
246
+ }
247
+ >
192
248
  >,
193
- ): Route0._OnlyIfHasParams<TParamsDefinition, Route0._AbsoluteWithQueryRouteValue<TPathOriginalDefinition>>
249
+ ): OnlyIfHasParams<TDefinition, string>
194
250
 
195
251
  // no params
252
+ // get(...args: OnlyIfNoParams<_ParamsDefinition<TDefinition>, [], [never]>): PathOnlyRouteValue<TDefinition>
253
+ // get(
254
+ // input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, { search?: undefined; abs?: false; hash?: string | number }>,
255
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, PathOnlyRouteValue<TDefinition>>
256
+ // get(
257
+ // input: OnlyIfNoParams<
258
+ // _ParamsDefinition<TDefinition>,
259
+ // { search: _SearchInput<TDefinition>; abs?: false; hash?: string | number }
260
+ // >,
261
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, WithSearchRouteValue<TDefinition>>
262
+ // get(
263
+ // input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, { search?: undefined; abs: true; hash?: string | number }>,
264
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsolutePathOnlyRouteValue<TDefinition>>
265
+ // get(
266
+ // input: OnlyIfNoParams<
267
+ // _ParamsDefinition<TDefinition>,
268
+ // { search: _SearchInput<TDefinition>; abs: true; hash?: string | number }
269
+ // >,
270
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsoluteWithSearchRouteValue<TDefinition>>
271
+
272
+ // get(...args: OnlyIfNoParams<_ParamsDefinition<TDefinition>, [], [never]>): PathRouteValue<TDefinition>
273
+ // get(
274
+ // input: OnlyIfNoParams<
275
+ // _ParamsDefinition<TDefinition>,
276
+ // { search?: _SearchInput<TDefinition>; abs?: false; hash?: string | number }
277
+ // >,
278
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, PathRouteValue<TDefinition>>
279
+ // get(
280
+ // input: OnlyIfNoParams<
281
+ // _ParamsDefinition<TDefinition>,
282
+ // { search?: _SearchInput<TDefinition>; abs: true; hash?: string | number }
283
+ // >,
284
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsolutePathRouteValue<TDefinition>>
285
+
286
+ get(...args: OnlyIfNoParams<TDefinition, [], [never]>): string
196
287
  get(
197
- ...args: Route0._OnlyIfNoParams<TParamsDefinition, [], [never]>
198
- ): Route0._PathOnlyRouteValue<TPathOriginalDefinition>
199
- get(
200
- input: Route0._OnlyIfNoParams<TParamsDefinition, { query?: undefined; abs?: false }>,
201
- ): Route0._OnlyIfNoParams<TParamsDefinition, Route0._PathOnlyRouteValue<TPathOriginalDefinition>>
202
- get(
203
- input: Route0._OnlyIfNoParams<TParamsDefinition, { query: Route0._QueryInput<TQueryDefinition>; abs?: false }>,
204
- ): Route0._OnlyIfNoParams<TParamsDefinition, Route0._WithQueryRouteValue<TPathOriginalDefinition>>
205
- get(
206
- input: Route0._OnlyIfNoParams<TParamsDefinition, { query?: undefined; abs: true }>,
207
- ): Route0._OnlyIfNoParams<TParamsDefinition, Route0._AbsolutePathOnlyRouteValue<TPathOriginalDefinition>>
208
- get(
209
- input: Route0._OnlyIfNoParams<TParamsDefinition, { query: Route0._QueryInput<TQueryDefinition>; abs: true }>,
210
- ): Route0._OnlyIfNoParams<TParamsDefinition, Route0._AbsoluteWithQueryRouteValue<TPathOriginalDefinition>>
288
+ input: OnlyIfNoParams<
289
+ TDefinition,
290
+ {
291
+ search?: _LooseSearchInput<TDefinition>
292
+ abs?: boolean
293
+ hash?: string | number
294
+ }
295
+ >,
296
+ ): OnlyIfNoParams<TDefinition, string>
211
297
 
212
298
  // implementation
213
- get(...args: any[]): string {
214
- const { queryInput, paramsInput, absInput } = ((): {
215
- queryInput: Record<string, string | number>
299
+ get(...args: unknown[]): string {
300
+ const { searchInput, paramsInput, absInput, hashInput } = ((): {
301
+ searchInput: Record<string, string | number>
216
302
  paramsInput: Record<string, string | number>
217
303
  absInput: boolean
304
+ hashInput: string | undefined
218
305
  } => {
219
306
  if (args.length === 0) {
220
- return { queryInput: {}, paramsInput: {}, absInput: false }
307
+ return {
308
+ searchInput: {},
309
+ paramsInput: {},
310
+ absInput: false,
311
+ hashInput: undefined,
312
+ }
221
313
  }
222
314
  const input = args[0]
223
315
  if (typeof input !== 'object' || input === null) {
224
316
  // throw new Error("Invalid get route input: expected object")
225
- return { queryInput: {}, paramsInput: {}, absInput: false }
317
+ return {
318
+ searchInput: {},
319
+ paramsInput: {},
320
+ absInput: false,
321
+ hashInput: undefined,
322
+ }
323
+ }
324
+ const { search, abs, hash, ...params } = input as Record<string, string | number> & {
325
+ search: Record<string, string | number>
326
+ abs: boolean
327
+ hash: string | undefined
328
+ [key: string]: unknown
329
+ }
330
+ return {
331
+ searchInput: search || {},
332
+ paramsInput: params,
333
+ absInput: abs ?? false,
334
+ hashInput: hash,
226
335
  }
227
- const { query, abs, ...params } = input
228
- return { queryInput: query || {}, paramsInput: params, absInput: abs ?? false }
229
336
  })()
230
337
 
231
338
  // validate params
232
- const neededParamsKeys = Object.keys(this.paramsDefinition)
339
+ const neededParamsKeys = this.paramsDefinition ? Object.keys(this.paramsDefinition) : []
233
340
  const providedParamsKeys = Object.keys(paramsInput)
234
341
  const notProvidedKeys = neededParamsKeys.filter((k) => !providedParamsKeys.includes(k))
235
342
  if (notProvidedKeys.length) {
@@ -238,127 +345,1428 @@ export class Route0<
238
345
  }
239
346
 
240
347
  // create url
241
- let url = String(this.pathDefinition)
348
+
349
+ let url = this.pathDefinition as string
242
350
  // replace params
351
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
243
352
  url = url.replace(/:([A-Za-z0-9_]+)/g, (_m, k) => encodeURIComponent(String(paramsInput?.[k] ?? '')))
244
- // query params
245
- const queryInputStringified = Object.fromEntries(Object.entries(queryInput).map(([k, v]) => [k, String(v)]))
246
- url = [url, new URLSearchParams(queryInputStringified).toString()].filter(Boolean).join('?')
353
+ // search params
354
+ const searchInputStringified = Object.fromEntries(Object.entries(searchInput).map(([k, v]) => [k, String(v)]))
355
+ url = [url, new URLSearchParams(searchInputStringified).toString()].filter(Boolean).join('?')
247
356
  // dedupe slashes
248
357
  url = url.replace(/\/{2,}/g, '/')
249
358
  // absolute
250
- url = absInput ? Route0._getAbsPath(this.baseUrl, url) : url
359
+ url = absInput ? Route0._getAbsPath(this.baseurl, url) : url
360
+ // hash
361
+ if (hashInput !== undefined) {
362
+ url = `${url}#${hashInput}`
363
+ }
251
364
 
252
365
  return url
253
366
  }
254
367
 
255
- getDefinition() {
368
+ // has params
369
+ // flat(
370
+ // input: OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithParamsInput<TDefinition, { hash?: string | number }>>,
371
+ // abs?: false,
372
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, PathOnlyRouteValue<TDefinition>>
373
+ // flat(
374
+ // input: OnlyIfHasParams<
375
+ // _ParamsDefinition<TDefinition>,
376
+ // WithParamsInput<TDefinition, _SearchInput<TDefinition> & { hash?: string | number }>
377
+ // >,
378
+ // abs?: false,
379
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithSearchRouteValue<TDefinition>>
380
+ // flat(
381
+ // input: OnlyIfHasParams<_ParamsDefinition<TDefinition>, WithParamsInput<TDefinition, { hash?: string | number }>>,
382
+ // abs: true,
383
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsolutePathOnlyRouteValue<TDefinition>>
384
+ // flat(
385
+ // input: OnlyIfHasParams<
386
+ // _ParamsDefinition<TDefinition>,
387
+ // WithParamsInput<TDefinition, _SearchInput<TDefinition> & { hash?: string | number }>
388
+ // >,
389
+ // abs: true,
390
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsoluteWithSearchRouteValue<TDefinition>>
391
+
392
+ // flat(
393
+ // input: OnlyIfHasParams<
394
+ // _ParamsDefinition<TDefinition>,
395
+ // WithParamsInput<TDefinition, _SearchInput<TDefinition> & { hash?: string | number }>
396
+ // >,
397
+ // abs?: false,
398
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, PathRouteValue<TDefinition>>
399
+ // flat(
400
+ // input: OnlyIfHasParams<
401
+ // _ParamsDefinition<TDefinition>,
402
+ // WithParamsInput<TDefinition, _SearchInput<TDefinition> & { hash?: string | number }>
403
+ // >,
404
+ // abs: true,
405
+ // ): OnlyIfHasParams<_ParamsDefinition<TDefinition>, AbsolutePathRouteValue<TDefinition>>
406
+
407
+ /**
408
+ * Flat input variant of `get()`, where path params + search keys
409
+ * are provided in a single object.
410
+ */
411
+ flat<TLoose extends boolean = HasLooseSearch<TDefinition>>(
412
+ input: OnlyIfHasParams<
413
+ TDefinition,
414
+ WithParamsInput<TDefinition, FlatInput<TDefinition, TLoose> & { hash?: string | number }>
415
+ >,
416
+ abs?: boolean,
417
+ loose?: TLoose,
418
+ ): OnlyIfHasParams<TDefinition, string>
419
+
420
+ // no params
421
+ // flat(...args: OnlyIfNoParams<_ParamsDefinition<TDefinition>, [], [never]>): PathOnlyRouteValue<TDefinition>
422
+ // flat(
423
+ // input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, { hash?: string | number }>,
424
+ // abs?: false,
425
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, PathOnlyRouteValue<TDefinition>>
426
+ // flat(
427
+ // input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, _SearchInput<TDefinition> & { hash?: string | number }>,
428
+ // abs?: false,
429
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, WithSearchRouteValue<TDefinition>>
430
+ // flat(
431
+ // input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, { hash?: string | number }>,
432
+ // abs: true,
433
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsolutePathOnlyRouteValue<TDefinition>>
434
+ // flat(
435
+ // input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, _SearchInput<TDefinition> & { hash?: string | number }>,
436
+ // abs: true,
437
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsoluteWithSearchRouteValue<TDefinition>>
438
+
439
+ // flat(...args: OnlyIfNoParams<_ParamsDefinition<TDefinition>, [], [never]>): PathRouteValue<TDefinition>
440
+ // flat(
441
+ // input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, _SearchInput<TDefinition> & { hash?: string | number }>,
442
+ // abs?: false,
443
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, PathRouteValue<TDefinition>>
444
+ // flat(
445
+ // input: OnlyIfNoParams<_ParamsDefinition<TDefinition>, _SearchInput<TDefinition> & { hash?: string | number }>,
446
+ // abs: true,
447
+ // ): OnlyIfNoParams<_ParamsDefinition<TDefinition>, AbsolutePathRouteValue<TDefinition>>
448
+
449
+ flat(...args: OnlyIfNoParams<TDefinition, [], [never]>): string
450
+ flat<TLoose extends boolean = HasLooseSearch<TDefinition>>(
451
+ input: OnlyIfNoParams<TDefinition, FlatInput<TDefinition, TLoose> & { hash?: string | number }>,
452
+ abs?: boolean,
453
+ loose?: TLoose,
454
+ ): OnlyIfNoParams<TDefinition, string>
455
+
456
+ // implementation
457
+ flat(...args: unknown[]): string {
458
+ const { searchInput, paramsInput, absInput, hashInput } = ((): {
459
+ searchInput: Record<string, string | number>
460
+ paramsInput: Record<string, string | number>
461
+ absInput: boolean
462
+ hashInput: string | undefined
463
+ } => {
464
+ if (args.length === 0) {
465
+ return {
466
+ searchInput: {},
467
+ paramsInput: {},
468
+ absInput: false,
469
+ hashInput: undefined,
470
+ }
471
+ }
472
+ const input = args[0] as Record<string, string | number> | undefined
473
+ if (typeof input !== 'object' || input === null) {
474
+ // throw new Error("Invalid get route input: expected object")
475
+ return {
476
+ searchInput: {},
477
+ paramsInput: {},
478
+ absInput: (args[1] as boolean | undefined) ?? false,
479
+ hashInput: undefined,
480
+ }
481
+ }
482
+ const loose = (args[2] as boolean | undefined) ?? this.hasLooseSearch
483
+ const paramsKeys = this.getParamsKeys()
484
+ const paramsInput = paramsKeys.reduce<Record<string, string | number>>((acc, key) => {
485
+ if (input[key] !== undefined) {
486
+ acc[key] = input[key]
487
+ }
488
+ return acc
489
+ }, {})
490
+ const searchKeys = this.getSearchKeys()
491
+ const searchInput = Object.keys(input)
492
+ .filter((k) => {
493
+ if (k === 'hash') {
494
+ return false
495
+ }
496
+ if (searchKeys.includes(k)) {
497
+ return true
498
+ }
499
+ if (paramsKeys.includes(k)) {
500
+ return false
501
+ }
502
+ return !!loose
503
+ })
504
+ .reduce<Record<string, string | number>>((acc, key) => {
505
+ acc[key] = input[key]
506
+ return acc
507
+ }, {})
508
+ const hashInput = input.hash
509
+ return {
510
+ searchInput,
511
+ paramsInput,
512
+ absInput: (args[1] as boolean | undefined) ?? false,
513
+ hashInput: hashInput as string | undefined,
514
+ }
515
+ })()
516
+
517
+ return this.get({
518
+ ...paramsInput,
519
+ search: searchInput,
520
+ abs: absInput,
521
+ hash: hashInput,
522
+ } as never)
523
+ }
524
+
525
+ /** Same as `flat()`, but always accepts loose search keys. */
526
+ flatLoose(
527
+ input: OnlyIfHasParams<
528
+ TDefinition,
529
+ WithParamsInput<TDefinition, LooseFlatInput<TDefinition> & { hash?: string | number }>
530
+ >,
531
+ abs?: boolean,
532
+ ): OnlyIfHasParams<TDefinition, string>
533
+ flatLoose(...args: OnlyIfNoParams<TDefinition, [], [never]>): string
534
+ flatLoose(
535
+ input: OnlyIfNoParams<TDefinition, LooseFlatInput<TDefinition> & { hash?: string | number }>,
536
+ abs?: boolean,
537
+ ): OnlyIfNoParams<TDefinition, string>
538
+ flatLoose(...args: unknown[]): string {
539
+ return this.flat(args[0] as never, args[1] as never, true)
540
+ }
541
+
542
+ /** Same as `flat()`, but only allows declared search keys. */
543
+ flatStrict(
544
+ input: OnlyIfHasParams<
545
+ TDefinition,
546
+ WithParamsInput<TDefinition, StrictFlatInput<TDefinition> & { hash?: string | number }>
547
+ >,
548
+ abs?: boolean,
549
+ ): OnlyIfHasParams<TDefinition, string>
550
+ flatStrict(...args: OnlyIfNoParams<TDefinition, [], [never]>): string
551
+ flatStrict(
552
+ input: OnlyIfNoParams<TDefinition, StrictFlatInput<TDefinition> & { hash?: string | number }>,
553
+ abs?: boolean,
554
+ ): OnlyIfNoParams<TDefinition, string>
555
+ flatStrict(...args: unknown[]): string {
556
+ return this.flat(args[0] as never, args[1] as never, false)
557
+ }
558
+
559
+ /** Returns path param keys extracted from route definition. */
560
+ getParamsKeys(): string[] {
561
+ return Object.keys(this.paramsDefinition || {})
562
+ }
563
+ /** Returns named search keys extracted from route definition. */
564
+ getSearchKeys(): string[] {
565
+ return Object.keys(this.searchDefinition || {})
566
+ }
567
+ /** Returns all flat input keys (`search + params`). */
568
+ getFlatKeys(): string[] {
569
+ return [...this.getSearchKeys(), ...this.getParamsKeys()]
570
+ }
571
+
572
+ getDefinition(): string {
573
+ return this.pathDefinition
574
+ }
575
+
576
+ /** Clones route with optional config override. */
577
+ clone(config?: RouteConfigInput): CallableRoute<TDefinition> {
578
+ return Route0.create(this.definition, config)
579
+ }
580
+
581
+ getRegexBaseStrictString(): string {
256
582
  return this.pathDefinition
583
+ .replace(/:(\w+)/g, '___PARAM___') // temporarily replace params with placeholder
584
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // escape regex special chars
585
+ .replace(/___PARAM___/g, '([^/]+)')
586
+ }
587
+
588
+ getRegexBaseString(): string {
589
+ return this.getRegexBaseStrictString().replace(/\/+$/, '') + '/?' // remove trailing slashes and add optional slash
590
+ }
591
+
592
+ getRegexStrictString(): string {
593
+ return `^${this.getRegexBaseStrictString()}$`
594
+ }
595
+
596
+ getRegexString(): string {
597
+ return `^${this.getRegexBaseString()}$`
598
+ }
599
+
600
+ getRegexStrict(): RegExp {
601
+ return new RegExp(this.getRegexStrictString())
602
+ }
603
+
604
+ getRegex(): RegExp {
605
+ return new RegExp(this.getRegexString())
606
+ }
607
+
608
+ /** Creates a grouped strict regex pattern string from many routes. */
609
+ static getRegexStrictStringGroup(routes: AnyRoute[]): string {
610
+ const patterns = routes.map((route) => route.getRegexStrictString()).join('|')
611
+ return `(${patterns})`
612
+ }
613
+
614
+ /** Creates a strict grouped regex from many routes. */
615
+ static getRegexStrictGroup(routes: AnyRoute[]): RegExp {
616
+ const patterns = Route0.getRegexStrictStringGroup(routes)
617
+ return new RegExp(`^(${patterns})$`)
618
+ }
619
+
620
+ /** Creates a grouped regex pattern string from many routes. */
621
+ static getRegexStringGroup(routes: AnyRoute[]): string {
622
+ const patterns = routes.map((route) => route.getRegexString()).join('|')
623
+ return `(${patterns})`
624
+ }
625
+
626
+ /** Creates a grouped regex from many routes. */
627
+ static getRegexGroup(routes: AnyRoute[]): RegExp {
628
+ const patterns = Route0.getRegexStringGroup(routes)
629
+ return new RegExp(`^(${patterns})$`)
630
+ }
631
+
632
+ /** Converts any location shape to relative form (removes host/origin fields). */
633
+ static toRelLocation<TLocation extends AnyLocation>(location: TLocation): TLocation {
634
+ return {
635
+ ...location,
636
+ abs: false,
637
+ origin: undefined,
638
+ href: undefined,
639
+ port: undefined,
640
+ host: undefined,
641
+ hostname: undefined,
642
+ }
643
+ }
644
+
645
+ /** Converts a location to absolute form using provided base URL. */
646
+ static toAbsLocation<TLocation extends AnyLocation>(location: TLocation, baseurl: string): TLocation {
647
+ const relLoc = Route0.toRelLocation(location)
648
+ const url = new URL(relLoc.hrefRel, baseurl)
649
+ return {
650
+ ...location,
651
+ abs: true,
652
+ origin: url.origin,
653
+ href: url.href,
654
+ port: url.port,
655
+ host: url.host,
656
+ hostname: url.hostname,
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Parses a URL-like input into raw location object (without route knowledge).
662
+ *
663
+ * Result is always `UnknownLocation` because no route matching is applied.
664
+ */
665
+ static getLocation(href: `${string}://${string}`): UnknownLocation
666
+ static getLocation(hrefRel: `/${string}`): UnknownLocation
667
+ static getLocation(hrefOrHrefRel: string): UnknownLocation
668
+ static getLocation(location: AnyLocation): UnknownLocation
669
+ static getLocation(url: URL): UnknownLocation
670
+ static getLocation(hrefOrHrefRelOrLocation: string | AnyLocation | URL): UnknownLocation
671
+ static getLocation(hrefOrHrefRelOrLocation: string | AnyLocation | URL): UnknownLocation {
672
+ if (hrefOrHrefRelOrLocation instanceof URL) {
673
+ return Route0.getLocation(hrefOrHrefRelOrLocation.href)
674
+ }
675
+ if (typeof hrefOrHrefRelOrLocation !== 'string') {
676
+ hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel
677
+ }
678
+ // Check if it's an absolute URL (starts with scheme://)
679
+ const abs = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(hrefOrHrefRelOrLocation)
680
+
681
+ // Use dummy base only if relative
682
+ const base = abs ? undefined : 'http://example.com'
683
+ const url = new URL(hrefOrHrefRelOrLocation, base)
684
+
685
+ // Extract search params
686
+ const searchParams = Object.fromEntries(url.searchParams.entries())
687
+
688
+ // Normalize pathname (remove trailing slash except for root)
689
+ let pathname = url.pathname
690
+ if (pathname.length > 1 && pathname.endsWith('/')) {
691
+ pathname = pathname.slice(0, -1)
692
+ }
693
+
694
+ // Common derived values
695
+ const hrefRel = pathname + url.search + url.hash
696
+
697
+ // Build the location object consistent with _GeneralLocation
698
+ const location: UnknownLocation = {
699
+ pathname,
700
+ search: url.search,
701
+ hash: url.hash,
702
+ origin: abs ? url.origin : undefined,
703
+ href: abs ? url.href : undefined,
704
+ hrefRel,
705
+ abs,
706
+
707
+ // extra host-related fields (available even for relative with dummy base)
708
+ host: abs ? url.host : undefined,
709
+ hostname: abs ? url.hostname : undefined,
710
+ port: abs ? url.port || undefined : undefined,
711
+
712
+ // specific to UnknownLocation
713
+ searchParams,
714
+ params: undefined,
715
+ route: undefined,
716
+ known: false,
717
+ exact: false,
718
+ ancestor: false,
719
+ descendant: false,
720
+ unmatched: false,
721
+ }
722
+
723
+ return location
724
+ }
725
+
726
+ /**
727
+ * Parses input and matches it against this route definition.
728
+ *
729
+ * Result includes relation flags:
730
+ * - `exact`
731
+ * - `ancestor`
732
+ * - `descendant`
733
+ * - `unmatched`
734
+ */
735
+ getLocation(href: `${string}://${string}`): KnownLocation<TDefinition>
736
+ getLocation(hrefRel: `/${string}`): KnownLocation<TDefinition>
737
+ getLocation(hrefOrHrefRel: string): KnownLocation<TDefinition>
738
+ getLocation(location: AnyLocation): KnownLocation<TDefinition>
739
+ getLocation(url: AnyLocation): KnownLocation<TDefinition>
740
+ getLocation(hrefOrHrefRelOrLocation: string | AnyLocation | URL): KnownLocation<TDefinition>
741
+ getLocation(hrefOrHrefRelOrLocation: string | AnyLocation | URL): KnownLocation<TDefinition> {
742
+ if (hrefOrHrefRelOrLocation instanceof URL) {
743
+ return this.getLocation(hrefOrHrefRelOrLocation.href)
744
+ }
745
+ if (typeof hrefOrHrefRelOrLocation !== 'string') {
746
+ hrefOrHrefRelOrLocation = hrefOrHrefRelOrLocation.href || hrefOrHrefRelOrLocation.hrefRel
747
+ }
748
+ const location = Route0.getLocation(hrefOrHrefRelOrLocation) as never as KnownLocation<TDefinition>
749
+ location.route = this.definition as Definition<TDefinition>
750
+ location.params = {}
751
+
752
+ // Normalize pathname (no trailing slash except root)
753
+ const pathname =
754
+ location.pathname.length > 1 && location.pathname.endsWith('/')
755
+ ? location.pathname.slice(0, -1)
756
+ : location.pathname
757
+
758
+ // Extract param names from the definition
759
+ const paramNames: string[] = []
760
+ const def =
761
+ this.pathDefinition.length > 1 && this.pathDefinition.endsWith('/')
762
+ ? this.pathDefinition.slice(0, -1)
763
+ : this.pathDefinition
764
+ def.replace(/:([A-Za-z0-9_]+)/g, (_m: string, name: string) => {
765
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-conversion
766
+ paramNames.push(String(name))
767
+ return ''
768
+ })
769
+
770
+ const exactRe = new RegExp(`^${this.getRegexBaseString()}$`)
771
+ const ancestorRe = new RegExp(`^${this.getRegexBaseString()}(?:/.*)?$`) // route matches the beginning of the URL (may have more)
772
+ const exactMatch = pathname.match(exactRe)
773
+ const ancestorMatch = pathname.match(ancestorRe)
774
+ const exact = !!exactMatch
775
+ const ancestor = !exact && !!ancestorMatch
776
+
777
+ // Parse params for exact and ancestor matches.
778
+ const paramsMatch = exactMatch || (ancestor ? ancestorMatch : null)
779
+ if (paramsMatch) {
780
+ const values = paramsMatch.slice(1, 1 + paramNames.length)
781
+ const params = Object.fromEntries(paramNames.map((n, i) => [n, decodeURIComponent(values[i] ?? '')]))
782
+ location.params = params
783
+ } else {
784
+ location.params = {}
785
+ }
786
+
787
+ // "descendant": the URL is a prefix of the route definition (params match any single segment)
788
+ const getParts = (path: string) => (path === '/' ? ['/'] : path.split('/').filter(Boolean))
789
+ const defParts = getParts(def)
790
+ const pathParts = getParts(pathname)
791
+
792
+ let isPrefix = true
793
+ if (pathParts.length > defParts.length) {
794
+ isPrefix = false
795
+ } else {
796
+ for (let i = 0; i < pathParts.length; i++) {
797
+ const defPart = defParts[i]
798
+ const pathPart = pathParts[i]
799
+ if (!defPart) {
800
+ isPrefix = false
801
+ break
802
+ }
803
+ if (defPart.startsWith(':')) continue
804
+ if (defPart !== pathPart) {
805
+ isPrefix = false
806
+ break
807
+ }
808
+ }
809
+ }
810
+ const descendant = !exact && isPrefix
811
+ const unmatched = !exact && !ancestor && !descendant
812
+
813
+ // For descendant matches, include only params that are already determined
814
+ // by the current (shorter) pathname prefix.
815
+ if (descendant) {
816
+ const descendantParams: Record<string, string> = {}
817
+ for (let i = 0; i < pathParts.length; i++) {
818
+ const defPart = defParts[i]
819
+ const pathPart = pathParts[i]
820
+ if (!defPart || !pathPart) continue
821
+ if (defPart.startsWith(':')) {
822
+ descendantParams[defPart.slice(1)] = decodeURIComponent(pathPart)
823
+ }
824
+ }
825
+ location.params = descendantParams
826
+ }
827
+
828
+ return {
829
+ ...location,
830
+ known: true,
831
+ exact,
832
+ ancestor,
833
+ descendant,
834
+ unmatched,
835
+ } as KnownLocation<TDefinition>
836
+ }
837
+
838
+ /**
839
+ * Safe parser for flat input objects.
840
+ *
841
+ * Returns structured success/error result instead of throwing.
842
+ */
843
+ safeParseFlatInput<TLoose extends boolean = HasLooseSearch<TDefinition>>(
844
+ input: unknown,
845
+ loose?: TLoose,
846
+ ): TLoose extends true ? SafeParseInputLooseResult<TDefinition> : SafeParseInputStrictResult<TDefinition> {
847
+ loose ??= this.hasLooseSearch as TLoose
848
+ const paramsKeys = this.getParamsKeys()
849
+ if (input === undefined) {
850
+ if (paramsKeys.length) {
851
+ return {
852
+ success: false,
853
+ data: undefined,
854
+ error: new Error(`Missing params: ${paramsKeys.map((k) => `"${k}"`).join(', ')}`),
855
+ }
856
+ }
857
+ input = {}
858
+ }
859
+ if (typeof input !== 'object' || input === null) {
860
+ return {
861
+ success: false,
862
+ data: undefined,
863
+ error: new Error('Invalid input: expected object'),
864
+ }
865
+ }
866
+ const inputKeys = Object.keys(input)
867
+ const notDefinedKeys = paramsKeys.filter((k) => !inputKeys.includes(k))
868
+ if (notDefinedKeys.length) {
869
+ return {
870
+ success: false,
871
+ data: undefined,
872
+ error: new Error(`Missing params: ${notDefinedKeys.map((k) => `"${k}"`).join(', ')}`),
873
+ }
874
+ }
875
+ const data: Record<string, string> = {}
876
+ const filterKeys = !loose ? [...paramsKeys, ...this.getSearchKeys()] : false
877
+ for (const [k, v] of Object.entries(input)) {
878
+ if (filterKeys && !filterKeys.includes(k)) {
879
+ continue
880
+ }
881
+ if (typeof v === 'string') {
882
+ data[k] = v
883
+ } else if (typeof v === 'number') {
884
+ data[k] = String(v)
885
+ } else {
886
+ const isParamKey = paramsKeys.includes(k)
887
+ return {
888
+ success: false,
889
+ data: undefined,
890
+ error: new Error(
891
+ `Invalid input: expected string, number,${!isParamKey ? ' or undefined,' : ''} got ${typeof v} for "${k}"`,
892
+ ),
893
+ }
894
+ }
895
+ }
896
+ return {
897
+ success: true,
898
+ data: data as LooseFlatOutputWithHash<TDefinition>,
899
+ error: undefined,
900
+ }
901
+ }
902
+
903
+ /** Throwing variant of `safeParseFlatInput()`. */
904
+ parseFlatInput<TLoose extends boolean = HasLooseSearch<TDefinition>>(
905
+ input: unknown,
906
+ loose?: TLoose,
907
+ ): TLoose extends true ? LooseFlatOutput<TDefinition> : StrictFlatOutput<TDefinition> {
908
+ loose ??= this.hasLooseSearch as TLoose
909
+ const result = this.safeParseFlatInput(input, loose)
910
+ if (result.error) {
911
+ throw result.error
912
+ }
913
+ return result.data as TLoose extends true ? LooseFlatOutput<TDefinition> : StrictFlatOutput<TDefinition>
914
+ }
915
+
916
+ /** True when path structure is equal (param names are ignored). */
917
+ isSame(other: AnyRoute): boolean {
918
+ return (
919
+ this.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, '__PARAM__') ===
920
+ other.pathDefinition.replace(/:([A-Za-z0-9_]+)/g, '__PARAM__')
921
+ )
922
+ }
923
+ /** Static convenience wrapper for `isSame`. */
924
+ static isSame(a: AnyRoute | string | undefined, b: AnyRoute | string | undefined): boolean {
925
+ if (!a) {
926
+ if (!b) return true
927
+ return false
928
+ }
929
+ if (!b) {
930
+ return false
931
+ }
932
+ return Route0.create(a).isSame(Route0.create(b))
933
+ }
934
+
935
+ /** True when current route is more specific/deeper than `other`. */
936
+ isDescendant(other: AnyRoute | string | undefined): boolean {
937
+ if (!other) return false
938
+ other = Route0.create(other)
939
+ // this is a descendant of other if:
940
+ // - paths are not exactly the same
941
+ // - other's path is a prefix of this path, matching params as wildcards
942
+ const getParts = (path: string) => (path === '/' ? ['/'] : path.split('/').filter(Boolean))
943
+ // Root is ancestor of any non-root; thus any non-root is a descendant of root
944
+ if (other.pathDefinition === '/' && this.pathDefinition !== '/') {
945
+ return true
946
+ }
947
+ const thisParts = getParts(this.pathDefinition)
948
+ const otherParts = getParts(other.pathDefinition)
949
+
950
+ // A descendant must be deeper
951
+ if (thisParts.length <= otherParts.length) return false
952
+
953
+ for (let i = 0; i < otherParts.length; i++) {
954
+ const otherPart = otherParts[i]
955
+ const thisPart = thisParts[i]
956
+ if (otherPart.startsWith(':')) continue
957
+ if (otherPart !== thisPart) return false
958
+ }
959
+ // Not equal (depth already ensures not equal)
960
+ return true
961
+ }
962
+
963
+ /** True when current route is broader/shallower than `other`. */
964
+ isAncestor(other: AnyRoute | string | undefined): boolean {
965
+ if (!other) return false
966
+ other = Route0.create(other)
967
+ // this is an ancestor of other if:
968
+ // - paths are not exactly the same
969
+ // - this path is a prefix of other path, matching params as wildcards
970
+ const getParts = (path: string) => (path === '/' ? ['/'] : path.split('/').filter(Boolean))
971
+ // Root is ancestor of any non-root path
972
+ if (this.pathDefinition === '/' && other.pathDefinition !== '/') {
973
+ return true
974
+ }
975
+ const thisParts = getParts(this.pathDefinition)
976
+ const otherParts = getParts(other.pathDefinition)
977
+
978
+ // An ancestor must be shallower
979
+ if (thisParts.length >= otherParts.length) return false
980
+
981
+ for (let i = 0; i < thisParts.length; i++) {
982
+ const thisPart = thisParts[i]
983
+ const otherPart = otherParts[i]
984
+ if (thisPart.startsWith(':')) continue
985
+ if (thisPart !== otherPart) return false
986
+ }
987
+ // Not equal (depth already ensures not equal)
988
+ return true
989
+ }
990
+
991
+ /** True when two route patterns can match the same concrete URL. */
992
+ isConflict(other: AnyRoute | string | undefined): boolean {
993
+ if (!other) return false
994
+ other = Route0.create(other)
995
+ const getParts = (path: string) => {
996
+ if (path === '/') return ['/']
997
+ return path.split('/').filter(Boolean)
998
+ }
999
+
1000
+ const thisParts = getParts(this.pathDefinition)
1001
+ const otherParts = getParts(other.pathDefinition)
1002
+
1003
+ // Different lengths = no conflict (one is deeper than the other)
1004
+ if (thisParts.length !== otherParts.length) {
1005
+ return false
1006
+ }
1007
+
1008
+ // Check if all segments could match
1009
+ for (let i = 0; i < thisParts.length; i++) {
1010
+ const thisPart = thisParts[i]
1011
+ const otherPart = otherParts[i]
1012
+
1013
+ // Both params = always match
1014
+ if (thisPart.startsWith(':') && otherPart.startsWith(':')) {
1015
+ continue
1016
+ }
1017
+
1018
+ // One is param = can match
1019
+ if (thisPart.startsWith(':') || otherPart.startsWith(':')) {
1020
+ continue
1021
+ }
1022
+
1023
+ // Both static = must be same
1024
+ if (thisPart !== otherPart) {
1025
+ return false
1026
+ }
1027
+ }
1028
+
1029
+ return true
257
1030
  }
258
1031
 
259
- clone(config?: Route0.RouteConfigInput) {
260
- return new Route0(this.pathOriginalDefinition, config)
1032
+ /** Specificity comparator used for deterministic route ordering. */
1033
+ isMoreSpecificThan(other: AnyRoute | string | undefined): boolean {
1034
+ if (!other) return false
1035
+ other = Route0.create(other)
1036
+ // More specific = should come earlier when conflicted
1037
+ // Static segments beat param segments at the same position
1038
+ const getParts = (path: string) => {
1039
+ if (path === '/') return ['/']
1040
+ return path.split('/').filter(Boolean)
1041
+ }
1042
+
1043
+ const thisParts = getParts(this.pathDefinition)
1044
+ const otherParts = getParts(other.pathDefinition)
1045
+
1046
+ // Compare segment by segment
1047
+ for (let i = 0; i < Math.min(thisParts.length, otherParts.length); i++) {
1048
+ const thisIsStatic = !thisParts[i].startsWith(':')
1049
+ const otherIsStatic = !otherParts[i].startsWith(':')
1050
+
1051
+ if (thisIsStatic && !otherIsStatic) return true
1052
+ if (!thisIsStatic && otherIsStatic) return false
1053
+ }
1054
+
1055
+ // All equal, use lexicographic
1056
+ return this.pathDefinition < other.pathDefinition
261
1057
  }
262
1058
  }
263
1059
 
264
- export namespace Route0 {
265
- export type Callable<T extends Route0<any, any, any, any>> = T & T['get']
266
- export type RouteConfigInput = {
267
- baseUrl?: string
1060
+ /**
1061
+ * Typed route collection with deterministic matching order.
1062
+ *
1063
+ * `Routes.create()` accepts either plain string definitions or route objects
1064
+ * and returns a "pretty" object with direct route access + helper methods under `._`.
1065
+ */
1066
+
1067
+ // biome-ignore lint/suspicious/noExplicitAny: ok
1068
+ export class Routes<const T extends RoutesRecord = any> {
1069
+ _routes: RoutesRecordHydrated<T>
1070
+ _pathsOrdering: string[]
1071
+ _keysOrdering: string[]
1072
+ _ordered: CallableRoute[]
1073
+
1074
+ _: {
1075
+ routes: Routes<T>['_routes']
1076
+ getLocation: Routes<T>['_getLocation']
1077
+ override: Routes<T>['_override']
1078
+ pathsOrdering: Routes<T>['_pathsOrdering']
1079
+ keysOrdering: Routes<T>['_keysOrdering']
1080
+ ordered: Routes<T>['_ordered']
268
1081
  }
269
- export type Params<TRoute0 extends Route0<any, any, any, any>> = {
270
- [K in keyof TRoute0['paramsDefinition']]: string
1082
+
1083
+ private constructor({
1084
+ routes,
1085
+ isHydrated = false,
1086
+ pathsOrdering,
1087
+ keysOrdering,
1088
+ ordered,
1089
+ }: {
1090
+ routes: RoutesRecordHydrated<T> | T
1091
+ isHydrated?: boolean
1092
+ pathsOrdering?: string[]
1093
+ keysOrdering?: string[]
1094
+ ordered?: CallableRoute[]
1095
+ }) {
1096
+ this._routes = (
1097
+ isHydrated ? (routes as RoutesRecordHydrated<T>) : Routes.hydrate(routes)
1098
+ ) as RoutesRecordHydrated<T>
1099
+ if (!pathsOrdering || !keysOrdering || !ordered) {
1100
+ const ordering = Routes.makeOrdering(this._routes)
1101
+ this._pathsOrdering = ordering.pathsOrdering
1102
+ this._keysOrdering = ordering.keysOrdering
1103
+ this._ordered = this._keysOrdering.map((key) => this._routes[key])
1104
+ } else {
1105
+ this._pathsOrdering = pathsOrdering
1106
+ this._keysOrdering = keysOrdering
1107
+ this._ordered = ordered
1108
+ }
1109
+ this._ = {
1110
+ routes: this._routes,
1111
+ getLocation: this._getLocation.bind(this),
1112
+ override: this._override.bind(this),
1113
+ pathsOrdering: this._pathsOrdering,
1114
+ keysOrdering: this._keysOrdering,
1115
+ ordered: this._ordered,
1116
+ }
1117
+ }
1118
+
1119
+ /** Creates and hydrates a typed routes collection. */
1120
+ static create<const T extends RoutesRecord>(routes: T, override?: RouteConfigInput): RoutesPretty<T> {
1121
+ const result = Routes.prettify(new Routes({ routes }))
1122
+ if (!override) {
1123
+ return result
1124
+ }
1125
+ return result._.override(override)
1126
+ }
1127
+
1128
+ private static prettify<const T extends RoutesRecord>(instance: Routes<T>): RoutesPretty<T> {
1129
+ Object.setPrototypeOf(instance, Routes.prototype)
1130
+ Object.defineProperty(instance, Symbol.toStringTag, {
1131
+ value: 'Routes',
1132
+ })
1133
+ Object.assign(instance, {
1134
+ override: instance._override.bind(instance),
1135
+ })
1136
+ Object.assign(instance, instance._routes)
1137
+ return instance as unknown as RoutesPretty<T>
271
1138
  }
272
- export type Query<TRoute0 extends Route0<any, any, any, any>> = Partial<
273
- {
274
- [K in keyof TRoute0['queryDefinition']]: string | undefined
275
- } & Record<string, string | undefined>
1139
+
1140
+ private static hydrate<const T extends RoutesRecord>(routes: T): RoutesRecordHydrated<T> {
1141
+ const result = {} as RoutesRecordHydrated<T>
1142
+ for (const key in routes) {
1143
+ if (Object.hasOwn(routes, key)) {
1144
+ const value = routes[key]
1145
+ result[key] = (typeof value === 'string' ? Route0.create(value) : value) as CallableRoute<T[typeof key]>
1146
+ }
1147
+ }
1148
+ return result
1149
+ }
1150
+
1151
+ /**
1152
+ * Matches an input URL against collection routes.
1153
+ *
1154
+ * Returns first exact match according to precomputed ordering,
1155
+ * otherwise returns `UnknownLocation`.
1156
+ */
1157
+ _getLocation(href: `${string}://${string}`): UnknownLocation | ExactLocation
1158
+ _getLocation(hrefRel: `/${string}`): UnknownLocation | ExactLocation
1159
+ _getLocation(hrefOrHrefRel: string): UnknownLocation | ExactLocation
1160
+ _getLocation(location: AnyLocation): UnknownLocation | ExactLocation
1161
+ _getLocation(url: URL): UnknownLocation | ExactLocation
1162
+ _getLocation(hrefOrHrefRelOrLocation: string | AnyLocation | URL): UnknownLocation | ExactLocation
1163
+ _getLocation(hrefOrHrefRelOrLocation: string | AnyLocation | URL): UnknownLocation | ExactLocation {
1164
+ // Find the route that exactly matches the given location
1165
+ const input = hrefOrHrefRelOrLocation
1166
+ for (const route of this._ordered) {
1167
+ const loc = route.getLocation(hrefOrHrefRelOrLocation)
1168
+ if (loc.exact) {
1169
+ return loc
1170
+ }
1171
+ }
1172
+ // No exact match found, return UnknownLocation
1173
+ return typeof input === 'string' ? Route0.getLocation(input) : Route0.getLocation(input)
1174
+ }
1175
+
1176
+ private static makeOrdering(routes: RoutesRecord): {
1177
+ pathsOrdering: string[]
1178
+ keysOrdering: string[]
1179
+ } {
1180
+ const hydrated = Routes.hydrate(routes)
1181
+ const entries = Object.entries(hydrated)
1182
+
1183
+ const getParts = (path: string) => {
1184
+ if (path === '/') return ['/']
1185
+ return path.split('/').filter(Boolean)
1186
+ }
1187
+
1188
+ // Sort: shorter paths first, then by specificity, then alphabetically
1189
+ entries.sort(([_keyA, routeA], [_keyB, routeB]) => {
1190
+ const partsA = getParts(routeA.pathDefinition)
1191
+ const partsB = getParts(routeB.pathDefinition)
1192
+
1193
+ // 1. Shorter paths first (by segment count)
1194
+ if (partsA.length !== partsB.length) {
1195
+ return partsA.length - partsB.length
1196
+ }
1197
+
1198
+ // 2. Same length: check if they conflict
1199
+ if (routeA.isConflict(routeB)) {
1200
+ // Conflicting routes: more specific first
1201
+ if (routeA.isMoreSpecificThan(routeB)) return -1
1202
+ if (routeB.isMoreSpecificThan(routeA)) return 1
1203
+ }
1204
+
1205
+ // 3. Same length, not conflicting or equal specificity: alphabetically
1206
+ return routeA.pathDefinition.localeCompare(routeB.pathDefinition)
1207
+ })
1208
+
1209
+ const pathsOrdering = entries.map(([_key, route]) => route.definition)
1210
+ const keysOrdering = entries.map(([_key]) => _key)
1211
+ return { pathsOrdering, keysOrdering }
1212
+ }
1213
+
1214
+ /** Returns a cloned routes collection with config applied to each route. */
1215
+ _override(config: RouteConfigInput): RoutesPretty<T> {
1216
+ const newRoutes = {} as RoutesRecordHydrated<T>
1217
+ for (const key in this._routes) {
1218
+ if (Object.hasOwn(this._routes, key)) {
1219
+ newRoutes[key] = this._routes[key].clone(config) as CallableRoute<T[typeof key]>
1220
+ }
1221
+ }
1222
+ const instance = new Routes({
1223
+ routes: newRoutes,
1224
+ isHydrated: true,
1225
+ pathsOrdering: this._pathsOrdering,
1226
+ keysOrdering: this._keysOrdering,
1227
+ ordered: this._keysOrdering.map((key) => newRoutes[key]),
1228
+ })
1229
+ return Routes.prettify(instance)
1230
+ }
1231
+
1232
+ static _ = {
1233
+ prettify: Routes.prettify.bind(Routes),
1234
+ hydrate: Routes.hydrate.bind(Routes),
1235
+ makeOrdering: Routes.makeOrdering.bind(Routes),
1236
+ }
1237
+ }
1238
+
1239
+ // main
1240
+
1241
+ /** Any route instance shape, preserving literal path type when known. */
1242
+ export type AnyRoute<T extends Route0<string> | string = string> = T extends string ? Route0<T> : T
1243
+ /** Callable route (`route(input)`) plus route instance methods/properties. */
1244
+ export type CallableRoute<T extends Route0<string> | string = string> = AnyRoute<T> & AnyRoute<T>['get']
1245
+ /** Route input accepted by most APIs: definition string or route object/callable. */
1246
+ export type AnyRouteOrDefinition<T extends string = string> = AnyRoute<T> | CallableRoute<T> | T
1247
+ /** Route-level runtime configuration. */
1248
+ export type RouteConfigInput = {
1249
+ baseurl?: string
1250
+ }
1251
+
1252
+ // collection
1253
+
1254
+ /** User-provided routes map (plain definitions or route instances). */
1255
+ export type RoutesRecord = Record<string, AnyRoute | string>
1256
+ /** Same as `RoutesRecord` but all values normalized to callable routes. */
1257
+ // biome-ignore lint/suspicious/noExplicitAny: ok
1258
+ export type RoutesRecordHydrated<TRoutesRecord extends RoutesRecord = any> = {
1259
+ [K in keyof TRoutesRecord]: CallableRoute<TRoutesRecord[K]>
1260
+ }
1261
+ /** Public shape returned by `Routes.create()`. Default `any` so `satisfies RoutesPretty` accepts any created routes. */
1262
+ // biome-ignore lint/suspicious/noExplicitAny: ok
1263
+ export type RoutesPretty<TRoutesRecord extends RoutesRecord = any> = RoutesRecordHydrated<TRoutesRecord> &
1264
+ Omit<
1265
+ Routes<TRoutesRecord>,
1266
+ '_routes' | '_getLocation' | '_override' | '_pathsOrdering' | '_keysOrdering' | '_ordered'
276
1267
  >
1268
+ export type ExtractRoutesKeys<TRoutes extends RoutesPretty | RoutesRecord> = TRoutes extends RoutesPretty
1269
+ ? Extract<keyof TRoutes['_']['routes'], string>
1270
+ : TRoutes extends RoutesRecord
1271
+ ? Extract<keyof TRoutes, string>
1272
+ : never
1273
+ export type ExtractRoute<
1274
+ TRoutes extends RoutesPretty | RoutesRecord,
1275
+ TKey extends ExtractRoutesKeys<TRoutes>,
1276
+ > = TRoutes extends RoutesPretty ? TRoutes['_']['routes'][TKey] : TRoutes extends RoutesRecord ? TRoutes[TKey] : never
277
1277
 
278
- export type _TrimQueryTailDefinition<S extends string> = S extends `${infer P}&${string}` ? P : S
279
- export type _QueryTailDefinitionWithoutFirstAmp<S extends string> = S extends `${string}&${infer T}` ? T : ''
280
- export type _QueryTailDefinitionWithFirstAmp<S extends string> = S extends `${string}&${infer T}` ? `&${T}` : ''
281
- export type _AmpSplit<S extends string> = S extends `${infer A}&${infer B}` ? A | _AmpSplit<B> : S
282
- export type _NonEmpty<T> = [T] extends ['' | never] ? never : T
283
- export type _ExtractPathParams<S extends string> = S extends `${string}:${infer After}`
284
- ? After extends `${infer Name}/${infer Rest}`
285
- ? Name | _ExtractPathParams<`/${Rest}`>
286
- : After
1278
+ // public utils
1279
+
1280
+ export type Definition<T extends AnyRoute | string> = T extends AnyRoute
1281
+ ? T['definition']
1282
+ : T extends string
1283
+ ? T
287
1284
  : never
288
- export type _ReplacePathParams<S extends string> = S extends `${infer Head}:${infer Tail}`
289
- ? Tail extends `${infer _Param}/${infer Rest}`
290
- ? _ReplacePathParams<`${Head}${string}/${Rest}`>
291
- : `${Head}${string}`
292
- : S
293
- export type _DedupeSlashes<S extends string> = S extends `${infer A}//${infer B}` ? _DedupeSlashes<`${A}/${B}`> : S
294
- export type _EmptyRecord = Record<never, never>
295
- export type _JoinPath<Parent extends string, Suffix extends string> = _DedupeSlashes<
296
- Route0._PathDefinition<Parent> extends infer A extends string
297
- ? _PathDefinition<Suffix> extends infer B extends string
298
- ? A extends ''
299
- ? B extends ''
300
- ? ''
301
- : B extends `/${string}`
302
- ? B
303
- : `/${B}`
304
- : B extends ''
305
- ? A
306
- : A extends `${string}/`
307
- ? `${A}${B}`
308
- : B extends `/${string}`
309
- ? `${A}${B}`
310
- : `${A}/${B}`
311
- : never
1285
+ export type PathDefinition<T extends AnyRoute | string> = T extends AnyRoute
1286
+ ? T['pathDefinition']
1287
+ : T extends string
1288
+ ? _PathDefinition<T>
1289
+ : never
1290
+ export type ParamsDefinition<T extends AnyRoute | string> = T extends AnyRoute
1291
+ ? T['paramsDefinition']
1292
+ : T extends string
1293
+ ? _ParamsDefinition<T>
1294
+ : undefined
1295
+ export type SearchDefinition<T extends AnyRoute | string> = T extends AnyRoute
1296
+ ? T['searchDefinition']
1297
+ : T extends string
1298
+ ? _SearchDefinition<T>
1299
+ : undefined
1300
+
1301
+ export type Extended<T extends AnyRoute | string | undefined, TSuffixDefinition extends string> = T extends AnyRoute
1302
+ ? Route0<PathExtended<T['definition'], TSuffixDefinition>>
1303
+ : T extends string
1304
+ ? Route0<PathExtended<T, TSuffixDefinition>>
1305
+ : T extends undefined
1306
+ ? Route0<TSuffixDefinition>
312
1307
  : never
313
- >
314
1308
 
315
- export type _OnlyIfNoParams<TParams extends object, Yes, No = never> = keyof TParams extends never ? Yes : No
316
- export type _OnlyIfHasParams<TParams extends object, Yes, No = never> = keyof TParams extends never ? No : Yes
1309
+ export type IsAncestor<T extends AnyRoute | string, TAncestor extends AnyRoute | string> = _IsAncestor<
1310
+ PathDefinition<T>,
1311
+ PathDefinition<TAncestor>
1312
+ >
1313
+ export type IsDescendant<T extends AnyRoute | string, TDescendant extends AnyRoute | string> = _IsDescendant<
1314
+ PathDefinition<T>,
1315
+ PathDefinition<TDescendant>
1316
+ >
1317
+ export type IsSame<T extends AnyRoute | string, TExact extends AnyRoute | string> = _IsSame<
1318
+ PathDefinition<T>,
1319
+ PathDefinition<TExact>
1320
+ >
1321
+ export type IsSameParams<T1 extends AnyRoute | string, T2 extends AnyRoute | string> = _IsSameParams<
1322
+ ParamsDefinition<T1>,
1323
+ ParamsDefinition<T2>
1324
+ >
1325
+
1326
+ export type HasParams<T extends AnyRoute | string> =
1327
+ ExtractPathParams<PathDefinition<T>> extends infer U ? ([U] extends [never] ? false : true) : false
1328
+ export type HasSearch<T extends AnyRoute | string> = Definition<T> extends `${string}&${string}` ? true : false
1329
+ export type HasNamedSearch<T extends AnyRoute | string> = // Definition<T> extends `${string}&${string}` ? true : false
1330
+ SearchTailDefinitionWithoutFirstAndLastAmp<Definition<T>> extends '' ? false : true
1331
+ export type HasLooseSearch<T extends AnyRoute | string> = Definition<T> extends `${string}&` ? true : false
1332
+
1333
+ export type ParamsOutput<T extends AnyRoute | string> = {
1334
+ [K in keyof ParamsDefinition<T>]: string
1335
+ }
1336
+ export type LooseSearchOutput<T extends AnyRoute | string = string> = Partial<
1337
+ {
1338
+ [K in keyof SearchDefinition<T>]?: string
1339
+ } & Record<string, string | undefined>
1340
+ >
1341
+ export type StrictSearchOutput<T extends AnyRoute | string> = Partial<{
1342
+ [K in keyof SearchDefinition<T>]?: string | undefined
1343
+ }>
1344
+ export type LooseFlatOutput<T extends AnyRoute | string = string> =
1345
+ HasParams<Definition<T>> extends true ? ParamsOutput<T> & LooseSearchOutput<T> : LooseSearchOutput<T>
1346
+ export type StrictFlatOutput<T extends AnyRoute | string> =
1347
+ HasParams<Definition<T>> extends true ? ParamsOutput<T> & StrictSearchOutput<T> : StrictSearchOutput<T>
1348
+ export type FlatOutput<T extends AnyRoute | string, TLoose extends boolean = HasLooseSearch<T>> = TLoose extends true
1349
+ ? LooseFlatOutput<T>
1350
+ : StrictFlatOutput<T>
1351
+ export type LooseFlatOutputWithHash<T extends AnyRoute | string = string> = LooseFlatOutput<T> & {
1352
+ hash?: string | undefined
1353
+ }
1354
+ export type StrictFlatOutputWithHash<T extends AnyRoute | string> = StrictFlatOutput<T> & { hash?: string | undefined }
1355
+ export type FlatOutputWithHash<T extends AnyRoute | string, TLoose extends boolean = HasLooseSearch<T>> = FlatOutput<
1356
+ T,
1357
+ TLoose
1358
+ > & { hash?: string | undefined }
1359
+ export type ParamsInput<T extends AnyRoute | string = string> = _ParamsInput<PathDefinition<T>>
1360
+ export type LooseSearchInput<T extends AnyRoute | string = string> = _LooseSearchInput<Definition<T>>
1361
+ export type StrictSearchInput<T extends AnyRoute | string> = _StrictSearchInput<Definition<T>>
1362
+ export type LooseFlatInput<T extends AnyRoute | string> = _LooseFlatInput<Definition<T>>
1363
+ export type StrictFlatInput<T extends AnyRoute | string> = _StrictFlatInput<Definition<T>>
1364
+ export type FlatInput<T extends AnyRoute | string, TLoose extends boolean = HasLooseSearch<T>> = TLoose extends true
1365
+ ? LooseFlatInput<T>
1366
+ : StrictFlatInput<T>
1367
+ export type LooseFlatInputWithHash<T extends AnyRoute | string> = LooseFlatInput<T> & {
1368
+ hash?: string | number
1369
+ }
1370
+ export type StrictFlatInputWithHash<T extends AnyRoute | string> = StrictFlatInput<T> & {
1371
+ hash?: string | number
1372
+ }
1373
+ export type FlatInputWithHash<T extends AnyRoute | string, TLoose extends boolean = HasLooseSearch<T>> = FlatInput<
1374
+ T,
1375
+ TLoose
1376
+ > & { hash?: string | number }
1377
+ export type CanInputBeEmpty<T extends AnyRoute | string> = HasParams<Definition<T>> extends true ? false : true
1378
+
1379
+ export type ParamsInputStringOnly<T extends AnyRoute | string = string> = _ParamsInputStringOnly<PathDefinition<T>>
1380
+ export type LooseSearchInputStringOnly<T extends AnyRoute | string = string> = _LooseSearchInputStringOnly<
1381
+ Definition<T>
1382
+ >
1383
+ export type StrictSearchInputStringOnly<T extends AnyRoute | string> = _StrictSearchInputStringOnly<Definition<T>>
1384
+ export type LooseFlatInputStringOnly<T extends AnyRoute | string> = _LooseFlatInputStringOnly<Definition<T>>
1385
+ export type StrictFlatInputStringOnly<T extends AnyRoute | string> = _StrictFlatInputStringOnly<Definition<T>>
1386
+ export type FlatInputStringOnly<
1387
+ T extends AnyRoute | string,
1388
+ TLoose extends boolean = HasLooseSearch<T>,
1389
+ > = TLoose extends true ? LooseFlatInputStringOnly<T> : StrictFlatInputStringOnly<T>
1390
+
1391
+ // location
317
1392
 
318
- export type _PathDefinition<TPathOriginalDefinition extends string> =
319
- _TrimQueryTailDefinition<TPathOriginalDefinition>
320
- export type _ParamsDefinition<TPathOriginalDefinition extends string> = _ExtractPathParams<
321
- _PathDefinition<TPathOriginalDefinition>
322
- > extends infer U
1393
+ export type LocationParams<TDefinition extends string> = {
1394
+ [K in keyof _ParamsDefinition<TDefinition>]: string
1395
+ }
1396
+ export type LocationSearch<TDefinition extends string = string> = {
1397
+ [K in keyof _SearchDefinition<TDefinition>]: string | undefined
1398
+ } & Record<string, string | undefined>
1399
+
1400
+ /**
1401
+ * URL location primitives independent from route-matching state.
1402
+ *
1403
+ * `hrefRel` is relative href and includes `pathname + search + hash`.
1404
+ */
1405
+ export type _GeneralLocation = {
1406
+ /**
1407
+ * Path without search/hash (normalized for trailing slash).
1408
+ *
1409
+ * Example:
1410
+ * - input: `https://example.com/users/42?tab=posts#section`
1411
+ * - pathname: `/users/42`
1412
+ */
1413
+ pathname: string
1414
+ /**
1415
+ * Raw query string with leading `?`, if present.
1416
+ *
1417
+ * Example:
1418
+ * - `?tab=posts&sort=desc`
1419
+ */
1420
+ search: string
1421
+ /**
1422
+ * Parsed query map (first value per key).
1423
+ *
1424
+ * Example:
1425
+ * - search: `?tab=posts&sort=desc`
1426
+ * - searchParams: `{ tab: 'posts', sort: 'desc' }`
1427
+ */
1428
+ searchParams: Record<string, string | undefined>
1429
+ /**
1430
+ * Raw hash with leading `#`, if present.
1431
+ *
1432
+ * Example:
1433
+ * - `#section`
1434
+ */
1435
+ hash: string
1436
+ /**
1437
+ * URL origin for absolute inputs.
1438
+ *
1439
+ * Example:
1440
+ * - href: `https://example.com/users/42`
1441
+ * - origin: `https://example.com`
1442
+ */
1443
+ origin?: string
1444
+ /**
1445
+ * Full absolute href for absolute inputs.
1446
+ *
1447
+ * Example:
1448
+ * - `https://example.com/users/42?tab=posts#section`
1449
+ */
1450
+ href?: string
1451
+ /**
1452
+ * Relative href (`pathname + search + hash`).
1453
+ *
1454
+ * Example:
1455
+ * - pathname: `/users/42`
1456
+ * - search: `?tab=posts`
1457
+ * - hash: `#section`
1458
+ * - hrefRel: `/users/42?tab=posts#section`
1459
+ */
1460
+ hrefRel: string
1461
+ /**
1462
+ * Whether input was absolute URL.
1463
+ *
1464
+ * Examples:
1465
+ * - `https://example.com/users/42` -> `true`
1466
+ * - `/users/42` -> `false`
1467
+ */
1468
+ abs: boolean
1469
+ port?: string
1470
+ host?: string
1471
+ hostname?: string
1472
+ }
1473
+ /** Location state before matching against a concrete route. */
1474
+ export type UnknownLocationState = {
1475
+ known: false
1476
+ route: undefined
1477
+ params: undefined
1478
+ searchParams: LooseSearchOutput
1479
+ exact: false
1480
+ ancestor: false
1481
+ descendant: false
1482
+ unmatched: false
1483
+ }
1484
+ export type UnknownLocation = _GeneralLocation & UnknownLocationState
1485
+
1486
+ /** Known route context, but no exact/ancestor/descendant relation matched. */
1487
+ export type UnmatchedLocationState<TRoute extends AnyRoute | string = AnyRoute | string> = {
1488
+ known: true
1489
+ route: Definition<TRoute>
1490
+ params: Record<never, never>
1491
+ searchParams: Record<string, string | undefined>
1492
+ exact: false
1493
+ ancestor: false
1494
+ descendant: false
1495
+ unmatched: true
1496
+ }
1497
+ export type UnmatchedLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation &
1498
+ UnmatchedLocationState<TRoute>
1499
+
1500
+ /** Exact match state for a known route. */
1501
+ export type ExactLocationState<TRoute extends AnyRoute | string = AnyRoute | string> = {
1502
+ known: true
1503
+ route: Definition<TRoute>
1504
+ params: ParamsOutput<TRoute>
1505
+ searchParams: LooseSearchOutput<TRoute>
1506
+ exact: true
1507
+ ancestor: false
1508
+ descendant: false
1509
+ unmatched: false
1510
+ }
1511
+ export type ExactLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation &
1512
+ ExactLocationState<TRoute>
1513
+
1514
+ /** Input URL is a descendant of route definition (route is ancestor). */
1515
+ export type AncestorLocationState<TRoute extends AnyRoute | string = AnyRoute | string> = {
1516
+ known: true
1517
+ route: Definition<TRoute>
1518
+ params: ParamsOutput<TRoute>
1519
+ searchParams: LooseSearchOutput<TRoute>
1520
+ exact: false
1521
+ ancestor: true
1522
+ descendant: false
1523
+ unmatched: false
1524
+ }
1525
+ export type AncestorLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation &
1526
+ AncestorLocationState<TRoute>
1527
+
1528
+ /** It is when route not match at all, but params match. */
1529
+ export type WeakAncestorLocationState<TRoute extends AnyRoute | string = AnyRoute | string> = {
1530
+ known: true
1531
+ route: Definition<TRoute>
1532
+ params: ParamsOutput<TRoute>
1533
+ searchParams: LooseSearchOutput<TRoute>
1534
+ exact: false
1535
+ ancestor: true
1536
+ descendant: false
1537
+ unmatched: false
1538
+ }
1539
+ export type WeakAncestorLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation &
1540
+ WeakAncestorLocationState<TRoute>
1541
+
1542
+ /** Input URL is an ancestor prefix of route definition (route is descendant). */
1543
+ export type DescendantLocationState<TRoute extends AnyRoute | string = AnyRoute | string> = {
1544
+ known: true
1545
+ route: Definition<TRoute>
1546
+ params: Partial<ParamsOutput<TRoute>>
1547
+ searchParams: LooseSearchOutput<TRoute>
1548
+ exact: false
1549
+ ancestor: false
1550
+ descendant: true
1551
+ unmatched: false
1552
+ }
1553
+ export type DescendantLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation &
1554
+ DescendantLocationState<TRoute>
1555
+
1556
+ /** It is when route not match at all, but params partially match. */
1557
+ export type WeakDescendantLocationState<TRoute extends AnyRoute | string = AnyRoute | string> = {
1558
+ known: true
1559
+ route: Definition<TRoute>
1560
+ params: Partial<ParamsOutput<TRoute>>
1561
+ searchParams: LooseSearchOutput<TRoute>
1562
+ exact: false
1563
+ ancestor: false
1564
+ descendant: true
1565
+ unmatched: false
1566
+ }
1567
+ export type WeakDescendantLocation<TRoute extends AnyRoute | string = AnyRoute | string> = _GeneralLocation &
1568
+ WeakDescendantLocationState<TRoute>
1569
+ export type KnownLocation<TRoute extends AnyRoute | string = AnyRoute | string> =
1570
+ | UnmatchedLocation<TRoute>
1571
+ | ExactLocation<TRoute>
1572
+ | AncestorLocation<TRoute>
1573
+ | WeakAncestorLocation<TRoute>
1574
+ | DescendantLocation<TRoute>
1575
+ | WeakDescendantLocation<TRoute>
1576
+ export type AnyLocation<TRoute extends AnyRoute | string = AnyRoute | string> = UnknownLocation | KnownLocation<TRoute>
1577
+
1578
+ // internal utils
1579
+
1580
+ export type _PathDefinition<T extends string> = T extends string ? TrimSearchTailDefinition<T> : never
1581
+ export type _ParamsDefinition<TDefinition extends string> =
1582
+ ExtractPathParams<PathDefinition<TDefinition>> extends infer U
323
1583
  ? [U] extends [never]
324
- ? _EmptyRecord
1584
+ ? undefined
325
1585
  : { [K in Extract<U, string>]: true }
326
- : _EmptyRecord
327
- export type _QueryDefinition<TPathOriginalDefinition extends string> = _NonEmpty<
328
- _QueryTailDefinitionWithoutFirstAmp<TPathOriginalDefinition>
329
- > extends infer Tail extends string
330
- ? _AmpSplit<Tail> extends infer U
1586
+ : undefined
1587
+ export type _SearchDefinition<TDefinition extends string> =
1588
+ NonEmpty<SearchTailDefinitionWithoutFirstAndLastAmp<TDefinition>> extends infer Tail extends string
1589
+ ? AmpSplit<Tail> extends infer U
331
1590
  ? [U] extends [never]
332
- ? _EmptyRecord
1591
+ ? undefined
333
1592
  : { [K in Extract<U, string>]: true }
334
- : _EmptyRecord
335
- : _EmptyRecord
336
- export type _RoutePathOriginalDefinitionExtended<
337
- TSourcePathOriginalDefinition extends string,
338
- TSuffixPathOriginalDefinition extends string,
339
- > = `${_JoinPath<TSourcePathOriginalDefinition, TSuffixPathOriginalDefinition>}${_QueryTailDefinitionWithFirstAmp<TSuffixPathOriginalDefinition>}`
340
-
341
- export type _ParamsInput<TParamsDefinition extends object> = {
342
- [K in keyof TParamsDefinition]: string | number
343
- }
344
- export type _QueryInput<TQueryDefinition extends object> = Partial<{
345
- [K in keyof TQueryDefinition]: string | number
346
- }> &
347
- Record<string, string | number>
348
- export type _WithParamsInput<
349
- TParamsDefinition extends object,
350
- T extends {
351
- query?: _QueryInput<any>
352
- abs?: boolean
353
- },
354
- > = _ParamsInput<TParamsDefinition> & T
355
-
356
- export type _PathOnlyRouteValue<TPathOriginalDefinition extends string> =
357
- `${_ReplacePathParams<_PathDefinition<TPathOriginalDefinition>>}`
358
- export type _WithQueryRouteValue<TPathOriginalDefinition extends string> =
359
- `${_ReplacePathParams<_PathDefinition<TPathOriginalDefinition>>}?${string}`
360
- export type _AbsolutePathOnlyRouteValue<TPathOriginalDefinition extends string> =
361
- `${string}${_PathOnlyRouteValue<TPathOriginalDefinition>}`
362
- export type _AbsoluteWithQueryRouteValue<TPathOriginalDefinition extends string> =
363
- `${string}${_WithQueryRouteValue<TPathOriginalDefinition>}`
364
- }
1593
+ : undefined
1594
+ : undefined
1595
+
1596
+ export type _ParamsInput<TDefinition extends string> =
1597
+ _ParamsDefinition<TDefinition> extends undefined
1598
+ ? Record<never, never>
1599
+ : {
1600
+ [K in keyof _ParamsDefinition<TDefinition>]: string | number
1601
+ }
1602
+ export type _LooseSearchInput<TDefinition extends string> =
1603
+ _SearchDefinition<TDefinition> extends undefined
1604
+ ? Record<string, string | number>
1605
+ : Partial<{
1606
+ [K in keyof _SearchDefinition<TDefinition>]: string | number
1607
+ }> &
1608
+ Record<string, string | number>
1609
+ export type _StrictSearchInput<TDefinition extends string> = Partial<{
1610
+ [K in keyof _SearchDefinition<TDefinition>]: string | number
1611
+ }>
1612
+ export type _LooseFlatInput<TDefinition extends string> =
1613
+ HasParams<TDefinition> extends true
1614
+ ? _ParamsInput<TDefinition> & _LooseSearchInput<TDefinition>
1615
+ : _LooseSearchInput<TDefinition>
1616
+ export type _StrictFlatInput<TDefinition extends string> =
1617
+ HasParams<TDefinition> extends true
1618
+ ? HasNamedSearch<TDefinition> extends true
1619
+ ? _StrictSearchInput<TDefinition> & _ParamsInput<TDefinition>
1620
+ : _ParamsInput<TDefinition>
1621
+ : HasNamedSearch<TDefinition> extends true
1622
+ ? _StrictSearchInput<TDefinition>
1623
+ : Record<never, never>
1624
+
1625
+ export type _ParamsInputStringOnly<TDefinition extends string> =
1626
+ _ParamsDefinition<TDefinition> extends undefined
1627
+ ? Record<never, never>
1628
+ : {
1629
+ [K in keyof _ParamsDefinition<TDefinition>]: string
1630
+ }
1631
+ export type _LooseSearchInputStringOnly<TDefinition extends string> =
1632
+ _SearchDefinition<TDefinition> extends undefined
1633
+ ? Record<string, string>
1634
+ : Partial<{
1635
+ [K in keyof _SearchDefinition<TDefinition>]: string
1636
+ }> &
1637
+ Record<string, string>
1638
+ export type _StrictSearchInputStringOnly<TDefinition extends string> = Partial<{
1639
+ [K in keyof _SearchDefinition<TDefinition>]: string
1640
+ }>
1641
+ export type _LooseFlatInputStringOnly<TDefinition extends string> =
1642
+ HasParams<TDefinition> extends true
1643
+ ? _ParamsInputStringOnly<TDefinition> & _LooseSearchInputStringOnly<TDefinition>
1644
+ : _LooseSearchInputStringOnly<TDefinition>
1645
+ export type _StrictFlatInputStringOnly<TDefinition extends string> =
1646
+ HasParams<TDefinition> extends true
1647
+ ? HasNamedSearch<TDefinition> extends true
1648
+ ? _StrictSearchInputStringOnly<TDefinition> & _ParamsInputStringOnly<TDefinition>
1649
+ : _ParamsInputStringOnly<TDefinition>
1650
+ : HasNamedSearch<TDefinition> extends true
1651
+ ? _StrictSearchInputStringOnly<TDefinition>
1652
+ : Record<never, never>
1653
+
1654
+ export type TrimSearchTailDefinition<S extends string> = S extends `${infer P}&${string}` ? P : S
1655
+ export type SearchTailDefinitionWithoutFirstAmp<S extends string> = S extends `${string}&${infer T}` ? T : ''
1656
+ export type SearchTailDefinitionWithoutFirstAndLastAmp<S extends string> = S extends `${string}&${infer T}&`
1657
+ ? T
1658
+ : S extends `${string}&${infer T}`
1659
+ ? T
1660
+ : ''
1661
+ export type SearchTailDefinitionWithFirstAmp<S extends string> = S extends `${string}&${infer T}` ? `&${T}` : ''
1662
+ export type AmpSplit<S extends string> = S extends `${infer A}&${infer B}` ? A | AmpSplit<B> : S
1663
+ // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
1664
+ export type NonEmpty<T> = [T] extends ['' | never] ? never : T
1665
+ export type ExtractPathParams<S extends string> = S extends `${string}:${infer After}`
1666
+ ? After extends `${infer Name}/${infer Rest}`
1667
+ ? Name | ExtractPathParams<`/${Rest}`>
1668
+ : After
1669
+ : never
1670
+ export type ReplacePathParams<S extends string> = S extends `${infer Head}:${infer Tail}`
1671
+ ? // eslint-disable-next-line @typescript-eslint/no-unused-vars
1672
+ Tail extends `${infer _Param}/${infer Rest}`
1673
+ ? ReplacePathParams<`${Head}${string}/${Rest}`>
1674
+ : `${Head}${string}`
1675
+ : S
1676
+ export type DedupeSlashes<S extends string> = S extends `${infer A}//${infer B}` ? DedupeSlashes<`${A}/${B}`> : S
1677
+ export type EmptyRecord = Record<never, never>
1678
+ export type JoinPath<Parent extends string, Suffix extends string> = DedupeSlashes<
1679
+ PathDefinition<Parent> extends infer A extends string
1680
+ ? PathDefinition<Suffix> extends infer B extends string
1681
+ ? A extends ''
1682
+ ? B extends ''
1683
+ ? ''
1684
+ : B extends `/${string}`
1685
+ ? B
1686
+ : `/${B}`
1687
+ : B extends ''
1688
+ ? A
1689
+ : A extends `${string}/`
1690
+ ? `${A}${B}`
1691
+ : B extends `/${string}`
1692
+ ? `${A}${B}`
1693
+ : `${A}/${B}`
1694
+ : never
1695
+ : never
1696
+ >
1697
+
1698
+ export type OnlyIfNoParams<TRoute extends AnyRoute | string, Yes, No = never> =
1699
+ HasParams<TRoute> extends false ? Yes : No
1700
+ export type OnlyIfHasParams<TRoute extends AnyRoute | string, Yes, No = never> =
1701
+ HasParams<TRoute> extends true ? Yes : No
1702
+
1703
+ // export type PathRouteValue<TDefinition extends string> = `${ReplacePathParams<PathDefinition<TDefinition>>}`
1704
+ // export type PathOnlyRouteValue<TDefinition extends string> = `${ReplacePathParams<PathDefinition<TDefinition>>}`
1705
+ // export type WithSearchRouteValue<TDefinition extends string> =
1706
+ // `${ReplacePathParams<PathDefinition<TDefinition>>}?${string}`
1707
+ // export type AbsolutePathRouteValue<TDefinition extends string> =
1708
+ // PathRouteValue<TDefinition> extends '/' ? string : `${string}${PathRouteValue<TDefinition>}`
1709
+ // export type AbsolutePathOnlyRouteValue<TDefinition extends string> =
1710
+ // PathOnlyRouteValue<TDefinition> extends '/' ? string : `${string}${PathOnlyRouteValue<TDefinition>}`
1711
+ // export type AbsoluteWithSearchRouteValue<TDefinition extends string> = `${string}${WithSearchRouteValue<TDefinition>}`
1712
+
1713
+ export type PathExtended<
1714
+ TSourcedefinitionDefinition extends string,
1715
+ TSuffixdefinitionDefinition extends string,
1716
+ > = `${JoinPath<TSourcedefinitionDefinition, TSuffixdefinitionDefinition>}${SearchTailDefinitionWithFirstAmp<TSuffixdefinitionDefinition>}`
1717
+
1718
+ export type WithParamsInput<
1719
+ TDefinition extends string,
1720
+ T extends
1721
+ | {
1722
+ // biome-ignore lint/suspicious/noExplicitAny: ok
1723
+ search?: _LooseSearchInput<any>
1724
+ abs?: boolean
1725
+ hash?: string | number
1726
+ }
1727
+ | undefined = undefined,
1728
+ > = _ParamsInput<TDefinition> & (T extends undefined ? Record<never, never> : T)
1729
+
1730
+ export type _IsSameParams<T1 extends object | undefined, T2 extends object | undefined> = T1 extends undefined
1731
+ ? T2 extends undefined
1732
+ ? true
1733
+ : false
1734
+ : T2 extends undefined
1735
+ ? false
1736
+ : T1 extends T2
1737
+ ? T2 extends T1
1738
+ ? true
1739
+ : false
1740
+ : false
1741
+
1742
+ export type _IsAncestor<T extends string, TAncestor extends string> = T extends TAncestor
1743
+ ? false
1744
+ : T extends `${TAncestor}${string}`
1745
+ ? true
1746
+ : false
1747
+ export type _IsDescendant<T extends string, TDescendant extends string> = TDescendant extends T
1748
+ ? false
1749
+ : TDescendant extends `${T}${string}`
1750
+ ? true
1751
+ : false
1752
+ export type _IsSame<T extends string, TExact extends string> = T extends TExact
1753
+ ? TExact extends T
1754
+ ? true
1755
+ : false
1756
+ : false
1757
+
1758
+ export type _SafeParseInputResult<TInputParsed extends Record<string, unknown>> =
1759
+ | {
1760
+ success: true
1761
+ data: TInputParsed
1762
+ error: undefined
1763
+ }
1764
+ | {
1765
+ success: false
1766
+ data: undefined
1767
+ error: Error
1768
+ }
1769
+ export type SafeParseInputStrictResult<TDefinition extends string> = _SafeParseInputResult<
1770
+ StrictFlatOutput<TDefinition>
1771
+ >
1772
+ export type SafeParseInputLooseResult<TDefinition extends string> = _SafeParseInputResult<LooseFlatOutput<TDefinition>>