@devp0nt/route0 1.0.0-next.67 → 1.0.0-next.68

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.test.ts CHANGED
@@ -5,40 +5,21 @@ import type {
5
5
  AnyRoute,
6
6
  AnyRouteOrDefinition,
7
7
  CallableRoute,
8
- CanInputBeEmpty,
8
+ IsParamsOptional,
9
9
  ExactLocation,
10
10
  Extended,
11
11
  ExtractRoute,
12
12
  ExtractRoutesKeys,
13
- HasLooseSearch,
14
- HasNamedSearch,
15
13
  HasParams,
16
- HasSearch,
17
14
  IsAncestor,
18
15
  IsDescendant,
19
16
  IsSame,
20
17
  IsSameParams,
21
18
  KnownLocation,
22
- LooseFlatInput,
23
- LooseFlatInputStringOnly,
24
- LooseFlatInputWithHash,
25
- LooseFlatOutput,
26
- LooseFlatOutputWithHash,
27
- LooseSearchInput,
28
- LooseSearchInputStringOnly,
29
- LooseSearchOutput,
30
19
  ParamsInput,
31
20
  ParamsInputStringOnly,
32
21
  ParamsOutput,
33
22
  RoutesPretty,
34
- StrictFlatInput,
35
- StrictFlatInputStringOnly,
36
- StrictFlatInputWithHash,
37
- StrictFlatOutput,
38
- StrictFlatOutputWithHash,
39
- StrictSearchInput,
40
- StrictSearchInputStringOnly,
41
- StrictSearchOutput,
42
23
  UnknownLocation,
43
24
  WeakAncestorLocation,
44
25
  WeakDescendantLocation,
@@ -49,124 +30,212 @@ describe('Route0', () => {
49
30
  it('simple', () => {
50
31
  const route0 = Route0.create('/')
51
32
  const path = route0.get()
52
- const pathHash = route0.get({ hash: 'zxc' })
33
+ const pathHash = route0.get({ '#': 'zxc' })
53
34
  expect(route0).toBeInstanceOf(Route0)
54
- // expectTypeOf<typeof path>().toEqualTypeOf<'/'>()
55
35
  expect(path).toBe('/')
56
36
  expectTypeOf<HasParams<typeof route0>>().toEqualTypeOf<false>()
57
- expect(path).toBe(route0.flat())
58
37
  expect(pathHash).toBe('/#zxc')
59
- expect(pathHash).toBe(route0.flat({ hash: 'zxc' }))
60
38
  })
61
39
 
62
40
  it('simple, callable', () => {
63
41
  const route0 = Route0.create('/')
64
42
  const path = route0()
65
- const pathHash = route0({ hash: 'zxc' })
43
+ const pathHash = route0({ '#': 'zxc' })
66
44
  expect(route0).toBeInstanceOf(Route0)
67
- // expectTypeOf<typeof path>().toEqualTypeOf<'/'>()
68
45
  expect(path).toBe('/')
69
- expect(path).toBe(route0.flat())
70
46
  expect(pathHash).toBe('/#zxc')
71
- expect(pathHash).toBe(route0.flat({ hash: 'zxc' }))
72
47
  })
73
48
 
74
- it('simple any search', () => {
49
+ it('search', () => {
75
50
  const route0 = Route0.create('/')
76
- const path = route0.get({ search: { q: '1' } })
77
- const pathHash = route0.get({ search: { q: '1' }, hash: 'zxc' })
78
- // expectTypeOf<typeof path>().toEqualTypeOf<`/?${string}`>()
51
+ const path = route0.get({ '?': { q: '1' } })
52
+ const pathHash = route0.get({ '?': { q: '1' }, '#': 'zxc' })
79
53
  expect(path).toBe('/?q=1')
80
- expect(path).toBe(route0.flat({ q: '1' }, false, true))
81
- expect(path).toBe(route0.flatLoose({ q: '1' }))
82
54
  expect(pathHash).toBe('/?q=1#zxc')
83
- expect(pathHash).toBe(route0.flat({ q: '1', hash: 'zxc' }, false, true))
84
- expect(pathHash).toBe(route0.flatLoose({ q: '1', hash: 'zxc' }))
85
-
86
- const route1 = Route0.create('/&')
87
- expect(path).toBe(route1.flat({ q: '1' }))
88
- expect(pathHash).toBe(route1.flat({ q: '1', hash: 'zxc' }))
55
+ expect(route0({ '?': { q: '1' } })).toBe(path)
56
+ expect(route0({ '?': { q: '1' }, '#': 'zxc' })).toBe(pathHash)
57
+ expectTypeOf<(typeof route0)['Infer']['SearchInput']>().toEqualTypeOf<Record<string, unknown>>()
58
+ })
59
+
60
+ it('typed search input', () => {
61
+ const route0 = Route0.create('/').search<{ q: string }>()
62
+ const path = route0.get({ '?': { q: '1' } })
63
+ const pathHash = route0.get({ '?': { q: '1' }, '#': 'zxc' })
64
+ const pathNoQuery = route0.get({ '#': 'zxc' })
65
+ // @ts-expect-error invalid search param type
66
+ const pathInvalidQueryType = route0.get({ '?': { q: 1 } })
67
+ // @ts-expect-error invalid search param key
68
+ const pathInvalidQueryKey = route0.get({ '?': { x: '1' } })
69
+ expect(path).toBe('/?q=1')
70
+ expect(pathHash).toBe('/?q=1#zxc')
71
+ expect(pathNoQuery).toBe('/#zxc')
72
+ expect(pathInvalidQueryType).toBe('/?q=1')
73
+ expect(pathInvalidQueryKey).toBe('/?x=1')
74
+ expectTypeOf<(typeof route0)['Infer']['SearchInput']>().toEqualTypeOf<{ q: string }>()
75
+ expect(route0({ '?': { q: '1' } })).toBe(path)
76
+ expect(route0({ '?': { q: '1' }, '#': 'zxc' })).toBe(pathHash)
77
+ expect(route0({ '#': 'zxc' })).toBe(pathNoQuery)
78
+ // @ts-expect-error invalid search param type
79
+ expect(route0({ '?': { q: 1 } })).toBe(pathInvalidQueryType)
80
+ // @ts-expect-error invalid search param key
81
+ expect(route0({ '?': { x: '1' } })).toBe(pathInvalidQueryKey)
89
82
  })
90
83
 
91
84
  it('params', () => {
92
85
  const route0 = Route0.create('/prefix/:x/some/:y/:z')
93
86
  const path = route0.get({ x: '1', y: 2, z: '3' })
94
- const pathHash = route0.get({ x: '1', y: 2, z: '3', hash: 'zxc' })
95
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}`>()
87
+ const pathHash = route0.get({ x: '1', y: 2, z: '3', '#': 'zxc' })
96
88
  expect(path).toBe('/prefix/1/some/2/3')
97
89
  expectTypeOf<HasParams<typeof route0>>().toEqualTypeOf<true>()
98
- expect(path).toBe(route0.flat({ x: '1', y: 2, z: '3' }))
99
90
  expect(pathHash).toBe('/prefix/1/some/2/3#zxc')
100
- expect(pathHash).toBe(route0.flat({ x: '1', y: 2, z: '3', hash: 'zxc' }))
91
+ expectTypeOf<(typeof route0)['Infer']['ParamsInput']>().toEqualTypeOf<{
92
+ x: string | number
93
+ y: string | number
94
+ z: string | number
95
+ }>()
96
+ expectTypeOf<(typeof route0)['Infer']['ParamsOutput']>().toEqualTypeOf<{ x: string; y: string; z: string }>()
97
+ expect(route0({ x: '1', y: 2, z: '3' })).toBe(path)
98
+ expect(route0({ x: '1', y: 2, z: '3', '#': 'zxc' })).toBe(pathHash)
101
99
  })
102
100
 
103
- it('params and any search', () => {
101
+ it('params and search', () => {
104
102
  const route0 = Route0.create('/prefix/:x/some/:y/:z')
105
- const path = route0.get({ x: '1', y: 2, z: '3', search: { q: '1' } })
106
- const pathHash = route0.get({ x: '1', y: 2, z: '3', search: { q: '1' }, hash: 'zxc' })
107
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
103
+ const path = route0.get({ x: '1', y: 2, z: '3', '?': { q: '1' } })
104
+ const pathHash = route0.get({ x: '1', y: 2, z: '3', '?': { q: '1' }, '#': 'zxc' })
108
105
  expect(path).toBe('/prefix/1/some/2/3?q=1')
109
- expect(path).toBe(route0.flat({ x: '1', y: 2, z: '3', q: '1' }, false, true))
110
106
  expect(pathHash).toBe('/prefix/1/some/2/3?q=1#zxc')
111
- expect(pathHash).toBe(route0.flat({ x: '1', y: 2, z: '3', q: '1', hash: 'zxc' }, false, true))
112
- const route1 = Route0.create('/prefix/:x/some/:y/:z&')
113
- expect(path).toBe(route1.flat({ x: '1', y: 2, z: '3', q: '1' }))
114
- expect(pathHash).toBe(route1.flat({ x: '1', y: 2, z: '3', q: '1', hash: 'zxc' }))
107
+ expect(route0({ x: '1', y: 2, z: '3', '?': { q: '1' } })).toBe(path)
108
+ expect(route0({ x: '1', y: 2, z: '3', '?': { q: '1' }, '#': 'zxc' })).toBe(pathHash)
109
+ })
110
+
111
+ it('optional named params', () => {
112
+ const route0 = Route0.create('/prefix/:x?/:y')
113
+ expect(route0.get({ y: '2' })).toBe('/prefix/2')
114
+ expect(route0.get({ x: '1', y: '2' })).toBe('/prefix/1/2')
115
+ expectTypeOf<(typeof route0)['Infer']['ParamsDefinition']>().toEqualTypeOf<{ x: false; y: true }>()
116
+ expectTypeOf<(typeof route0)['Infer']['ParamsInput']>().toEqualTypeOf<{
117
+ y: string | number
118
+ x?: string | number | undefined
119
+ }>()
120
+ expectTypeOf<(typeof route0)['Infer']['ParamsOutput']>().toEqualTypeOf<{
121
+ y: string
122
+ x: string | undefined
123
+ }>()
115
124
  })
116
125
 
117
- it('search', () => {
118
- const route0 = Route0.create('/prefix&y&z')
119
- expectTypeOf<(typeof route0)['searchDefinition']>().toEqualTypeOf<{ y: true; z: true }>()
120
- const path = route0.get({ search: { y: '1', z: '2' } })
121
- const pathHash = route0.get({ search: { y: '1', z: '2' }, hash: 'zxc' })
122
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix?${string}`>()
123
- expect(path).toBe('/prefix?y=1&z=2')
124
- expect(path).toBe(route0.flat({ y: '1', z: '2' }))
125
- expect(pathHash).toBe('/prefix?y=1&z=2#zxc')
126
- expect(pathHash).toBe(route0.flat({ y: '1', z: '2', hash: 'zxc' }))
126
+ it('wildcards and optional wildcards', () => {
127
+ const routeWildcard = Route0.create('/app*')
128
+ const routeOptionalWildcard = Route0.create('/orders/*?')
129
+ expect(routeWildcard.get({ '*': '' })).toBe('/app')
130
+ expect(routeWildcard.get({ '*': '/home' })).toBe('/app/home')
131
+ expect(routeWildcard.get({ '*': '-1' })).toBe('/app-1')
132
+ expect(routeWildcard.getLocation('/app').exact).toBe(true)
133
+ expect(routeWildcard.getLocation('/app/home').exact).toBe(true)
134
+ expect(routeOptionalWildcard.get()).toBe('/orders')
135
+ expect(routeOptionalWildcard.get({ '*': 'completed/list' })).toBe('/orders/completed/list')
136
+ expect(routeOptionalWildcard.getLocation('/orders').exact).toBe(true)
137
+ expect(routeOptionalWildcard.getLocation('/orders/').exact).toBe(true)
138
+ expect(routeOptionalWildcard.getLocation('/orders/completed/list').exact).toBe(true)
139
+ expectTypeOf<(typeof routeWildcard)['Infer']['ParamsDefinition']>().toEqualTypeOf<{ '*': true }>()
140
+ expectTypeOf<(typeof routeOptionalWildcard)['Infer']['ParamsDefinition']>().toEqualTypeOf<{ '*': false }>()
141
+ expectTypeOf<(typeof routeWildcard)['Infer']['ParamsOutput']>().toEqualTypeOf<{ '*': string }>()
142
+ expectTypeOf<(typeof routeOptionalWildcard)['Infer']['ParamsOutput']>().toEqualTypeOf<{ '*': string | undefined }>()
143
+ })
144
+
145
+ it('difference: /path/x* vs /path/x/* matching', () => {
146
+ const inlineWildcard = Route0.create('/path/x*')
147
+ const segmentWildcard = Route0.create('/path/x/*')
148
+
149
+ // /path/x*:
150
+ // - matches '/path/x'
151
+ // - matches '/path/x123' (same segment continuation)
152
+ // - matches '/path/x/123' (slash continuation)
153
+ expect(inlineWildcard.getLocation('/path/x').exact).toBe(true)
154
+ expect(inlineWildcard.getLocation('/path/x123').exact).toBe(true)
155
+ expect(inlineWildcard.getLocation('/path/x/123').exact).toBe(true)
156
+
157
+ // /path/x/*:
158
+ // - matches '/path/x' and '/path/x/...'
159
+ // - does NOT match '/path/x123' (because 'x' is a full segment here)
160
+ expect(segmentWildcard.getLocation('/path/x').exact).toBe(true)
161
+ expect(segmentWildcard.getLocation('/path/x/123').exact).toBe(true)
162
+ expect(segmentWildcard.getLocation('/path/x123').exact).toBe(false)
163
+ })
164
+
165
+ it('difference: /path/x* vs /path/x/* URL building', () => {
166
+ const inlineWildcard = Route0.create('/path/x*')
167
+ const segmentWildcard = Route0.create('/path/x/*')
168
+
169
+ // Inline wildcard appends directly to the "x" prefix.
170
+ expect(inlineWildcard.get({ '*': '123' })).toBe('/path/x123')
171
+ expect(inlineWildcard.get({ '*': '/123' })).toBe('/path/x/123')
172
+
173
+ // Segment wildcard appends as a new segment after '/path/x/'.
174
+ expect(segmentWildcard.get({ '*': '123' })).toBe('/path/x/123')
175
+ expect(segmentWildcard.get({ '*': '/123' })).toBe('/path/x/123')
176
+ })
177
+
178
+ it('mixed required and optional named params combinations', () => {
179
+ const route = Route0.create('/org/:orgId/user/:userId?/:tab')
180
+ expect(route.get({ orgId: 'acme', tab: 'settings' })).toBe('/org/acme/user/settings')
181
+ expect(route.get({ orgId: 'acme', userId: '42', tab: 'settings' })).toBe('/org/acme/user/42/settings')
182
+
183
+ const locNoOptional = route.getLocation('/org/acme/user/settings')
184
+ expect(locNoOptional.exact).toBe(true)
185
+ if (locNoOptional.exact) {
186
+ expect(locNoOptional.params).toMatchObject({
187
+ orgId: 'acme',
188
+ userId: undefined,
189
+ tab: 'settings',
190
+ })
191
+ }
192
+
193
+ const locWithOptional = route.getLocation('/org/acme/user/42/settings')
194
+ expect(locWithOptional.exact).toBe(true)
195
+ if (locWithOptional.exact) {
196
+ expect(locWithOptional.params).toMatchObject({
197
+ orgId: 'acme',
198
+ userId: '42',
199
+ tab: 'settings',
200
+ })
201
+ }
127
202
  })
128
203
 
129
- it('params and search', () => {
130
- const route0 = Route0.create('/prefix/:x/some/:y/:z&z&c')
131
- const path = route0.get({ x: '1', y: '2', z: '3', search: { z: '4', c: '5' } })
132
- const pathHash = route0.get({ x: '1', y: '2', z: '3', search: { z: '4', c: '5' }, hash: 'zxc' })
133
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
134
- expect(path).toBe('/prefix/1/some/2/3?z=4&c=5')
135
- expect(pathHash).toBe('/prefix/1/some/2/3?z=4&c=5#zxc')
136
- // very strange case
137
- expect(route0.flat({ x: '1', y: '2', z: '4', c: '5' })).toBe('/prefix/1/some/2/4?z=4&c=5')
138
- expect(route0.flat({ x: '1', y: '2', z: '4', c: '5', hash: 'zxc' })).toBe('/prefix/1/some/2/4?z=4&c=5#zxc')
139
- })
140
-
141
- it('params and search and any search', () => {
142
- const route0 = Route0.create('/prefix/:x/some/:y/:z&z&c')
143
- const path = route0.get({ x: '1', y: '2', z: '3', search: { z: '4', c: '5', o: '6' } })
144
- const pathHash = route0.get({ x: '1', y: '2', z: '3', search: { z: '4', c: '5', o: '6' }, hash: 'zxc' })
145
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/some/${string}/${string}?${string}`>()
146
- expect(path).toBe('/prefix/1/some/2/3?z=4&c=5&o=6')
147
- expect(pathHash).toBe('/prefix/1/some/2/3?z=4&c=5&o=6#zxc')
148
- // very strange case
149
- expect(route0.flat({ x: '1', y: '2', z: '4', c: '5', o: '6' }, false, true)).toBe('/prefix/1/some/2/4?z=4&c=5&o=6')
150
- expect(route0.flat({ x: '1', y: '2', z: '4', c: '5', o: '6', hash: 'zxc' }, false, true)).toBe(
151
- '/prefix/1/some/2/4?z=4&c=5&o=6#zxc',
152
- )
153
- const route1 = Route0.create('/prefix/:x/some/:y/:z&z&c&')
154
- expect(route1.flat({ x: '1', y: '2', z: '4', c: '5', o: '6' })).toBe('/prefix/1/some/2/4?z=4&c=5&o=6')
155
- expect(route1.flat({ x: '1', y: '2', z: '4', c: '5', o: '6', hash: 'zxc' })).toBe(
156
- '/prefix/1/some/2/4?z=4&c=5&o=6#zxc',
157
- )
204
+ it('optional wildcard before required static segment', () => {
205
+ const route = Route0.create('/orders/*?/details')
206
+ expect(route.get()).toBe('/orders/details')
207
+ expect(route.get({ '*': 'completed/list' })).toBe('/orders/completed/list/details')
208
+ expect(route.getLocation('/orders/details').exact).toBe(true)
209
+ expect(route.getLocation('/orders/completed/list/details').exact).toBe(true)
210
+ })
211
+
212
+ it('paramsSchema accepts optional-only and mixed params', () => {
213
+ const optionalOnly = Route0.create('/x/:id?')
214
+ expect(optionalOnly.paramsSchema.safeParse(undefined)).toMatchObject({
215
+ success: true,
216
+ data: { id: undefined },
217
+ error: undefined,
218
+ })
219
+
220
+ const mixed = Route0.create('/x/:id/:slug?')
221
+ expect(mixed.paramsSchema.safeParse({ id: '1' })).toMatchObject({
222
+ success: true,
223
+ data: { id: '1', slug: undefined },
224
+ error: undefined,
225
+ })
226
+ expect(mixed.paramsSchema.safeParse({ slug: 'x' }).success).toBe(false)
158
227
  })
159
228
 
160
229
  it('simple extend', () => {
161
230
  const route0 = Route0.create('/prefix')
162
231
  const route1 = route0.extend('/suffix')
163
232
  const path = route1.get()
164
- const pathHash = route1.get({ hash: 'zxc' })
233
+ const pathHash = route1.get({ '#': 'zxc' })
165
234
  // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/suffix`>()
166
235
  expect(path).toBe('/prefix/suffix')
167
- expect(path).toBe(route1.flat())
168
236
  expect(pathHash).toBe('/prefix/suffix#zxc')
169
- expect(pathHash).toBe(route1.flat({ hash: 'zxc' }))
237
+ expect(route1()).toBe(path)
238
+ expect(route1({ '#': 'zxc' })).toBe(pathHash)
170
239
  })
171
240
 
172
241
  it('simple extend double slash', () => {
@@ -176,12 +245,10 @@ describe('Route0', () => {
176
245
  expect(route1.get()).toBe('/suffix1/')
177
246
  const route2 = route1.extend('/suffix2')
178
247
  const path = route2.get()
179
- const pathHash = route2.get({ hash: 'zxc' })
248
+ const pathHash = route2.get({ '#': 'zxc' })
180
249
  // expectTypeOf<typeof path>().toEqualTypeOf<`/suffix1/suffix2`>()
181
250
  expect(path).toBe('/suffix1/suffix2')
182
- expect(path).toBe(route2.flat())
183
251
  expect(pathHash).toBe('/suffix1/suffix2#zxc')
184
- expect(pathHash).toBe(route2.flat({ hash: 'zxc' }))
185
252
  })
186
253
 
187
254
  it('simple extend no slash', () => {
@@ -189,12 +256,10 @@ describe('Route0', () => {
189
256
  const route1 = route0.extend('suffix1')
190
257
  const route2 = route1.extend('suffix2')
191
258
  const path = route2.get()
192
- const pathHash = route2.get({ hash: 'zxc' })
259
+ const pathHash = route2.get({ '#': 'zxc' })
193
260
  // expectTypeOf<typeof path>().toEqualTypeOf<`/suffix1/suffix2`>()
194
261
  expect(path).toBe('/suffix1/suffix2')
195
- expect(path).toBe(route2.flat())
196
262
  expect(pathHash).toBe('/suffix1/suffix2#zxc')
197
- expect(pathHash).toBe(route2.flat({ hash: 'zxc' }))
198
263
  })
199
264
 
200
265
  it('simple extend no slash chaos', () => {
@@ -231,143 +296,124 @@ describe('Route0', () => {
231
296
  const route0 = Route0.create('/prefix/:x')
232
297
  const route1 = route0.extend('/suffix/:y')
233
298
  const path = route1.get({ x: '1', y: '2' })
234
- const pathHash = route1.get({ x: '1', y: '2', hash: 'zxc' })
235
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/suffix/${string}`>()
299
+ const pathHash = route1.get({ x: '1', y: '2', '#': 'zxc' })
236
300
  expect(path).toBe('/prefix/1/suffix/2')
237
- expect(path).toBe(route1.flat({ x: '1', y: '2' }))
238
301
  expect(pathHash).toBe('/prefix/1/suffix/2#zxc')
239
- expect(pathHash).toBe(route1.flat({ x: '1', y: '2', hash: 'zxc' }))
240
302
  })
241
303
 
242
- it('extend with search params', () => {
243
- const route0 = Route0.create('/prefix&y&z')
244
- const route1 = route0.extend('/suffix&z&c')
245
- const path = route1.get({ search: { y: '2', c: '3', a: '4' } })
246
- expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
247
- z: true
248
- c: true
304
+ it('extend with typed search', () => {
305
+ const route0 = Route0.create('/prefix').search<{ y: string; z: string }>()
306
+ const route1 = route0.extend('/suffix')
307
+ const path = route1.get({ '?': { y: '2', z: '3' } })
308
+ expectTypeOf<(typeof route1)['Infer']['SearchInput']>().toEqualTypeOf<{
309
+ z: string
310
+ y: string
249
311
  }>()
250
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/suffix?${string}`>()
251
- expect(path).toBe('/prefix/suffix?y=2&c=3&a=4')
312
+ expect(path).toBe('/prefix/suffix?y=2&z=3')
252
313
  const path1 = route1.get()
253
- const pathHash1 = route1.get({ hash: 'zxc' })
254
- // expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/suffix`>()
314
+ const pathHash1 = route1.get({ '#': 'zxc' })
255
315
  expect(path1).toBe('/prefix/suffix')
256
- expect(path1).toBe(route1.flat())
257
316
  expect(pathHash1).toBe('/prefix/suffix#zxc')
258
- expect(pathHash1).toBe(route1.flat({ hash: 'zxc' }))
259
317
  })
260
318
 
261
- it('extend with params and search', () => {
262
- const route0 = Route0.create('/prefix/:id&y&z')
263
- const route1 = route0.extend('/:sn/suffix&z&c')
264
- const path = route1.get({ id: 'myid', sn: 'mysn', search: { y: '2', c: '3', a: '4' } })
265
- expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
266
- z: true
267
- c: true
319
+ it('extend with params and typed search', () => {
320
+ const route0 = Route0.create('/prefix/:id').search<{ y: string; z: string }>()
321
+ const route1 = route0.extend('/:sn/suffix')
322
+ const path = route1.get({ id: 'myid', sn: 'mysn', '?': { y: '2', z: '3' } })
323
+ expectTypeOf<(typeof route1)['Infer']['SearchInput']>().toEqualTypeOf<{
324
+ z: string
325
+ y: string
268
326
  }>()
269
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/${string}/suffix?${string}`>()
270
- expect(path).toBe('/prefix/myid/mysn/suffix?y=2&c=3&a=4')
327
+ expect(path).toBe('/prefix/myid/mysn/suffix?y=2&z=3')
271
328
  const path1 = route1.get({ id: 'myid', sn: 'mysn' })
272
- const pathHash1 = route1.get({ id: 'myid', sn: 'mysn', hash: 'zxc' })
273
- // expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/${string}/${string}/suffix`>()
329
+ const pathHash1 = route1.get({ id: 'myid', sn: 'mysn', '#': 'zxc' })
274
330
  expect(path1).toBe('/prefix/myid/mysn/suffix')
275
- expect(path1).toBe(route1.flat({ id: 'myid', sn: 'mysn' }))
276
331
  expect(pathHash1).toBe('/prefix/myid/mysn/suffix#zxc')
277
- expect(pathHash1).toBe(route1.flat({ id: 'myid', sn: 'mysn', hash: 'zxc' }))
278
332
  })
279
333
 
280
- it('extend with params and search, callable', () => {
281
- const route0 = Route0.create('/prefix/:id&y&z')
282
- const route1 = route0.extend('/:sn/suffix&z&c')
283
- const path = route1({ id: 'myid', sn: 'mysn', search: { y: '2', c: '3', a: '4' } })
284
- expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
285
- z: true
286
- c: true
287
- }>()
288
- // expectTypeOf<typeof path>().toEqualTypeOf<`/prefix/${string}/${string}/suffix?${string}`>()
289
- expect(path).toBe('/prefix/myid/mysn/suffix?y=2&c=3&a=4')
290
- const path1 = route1({ id: 'myid', sn: 'mysn' })
291
- // expectTypeOf<typeof path1>().toEqualTypeOf<`/prefix/${string}/${string}/suffix`>()
292
- expect(path1).toBe('/prefix/myid/mysn/suffix')
293
- expect(path1).toBe(route1.flat({ id: 'myid', sn: 'mysn' }))
294
- const pathHash1 = route1({ id: 'myid', sn: 'mysn', hash: 'zxc' })
295
- expect(pathHash1).toBe('/prefix/myid/mysn/suffix#zxc')
296
- expect(pathHash1).toBe(route1.flat({ id: 'myid', sn: 'mysn', hash: 'zxc' }))
334
+ it('extend with params and typed search, callable', () => {
335
+ // const route0 = Route0.create('/prefix/:id&y&z')
336
+ // const route1 = route0.extend('/:sn/suffix&z&c')
337
+ // const path = route1({ id: 'myid', sn: 'mysn', '?': { y: '2', c: '3', a: '4' } })
338
+ // expectTypeOf<(typeof route1)['searchDefinition']>().toEqualTypeOf<{
339
+ // z: true
340
+ // c: true
341
+ // }>()
342
+ // expect(path).toBe('/prefix/myid/mysn/suffix?y=2&c=3&a=4')
343
+ // const path1 = route1({ id: 'myid', sn: 'mysn' })
344
+ // expect(path1).toBe('/prefix/myid/mysn/suffix')
345
+ // const pathHash1 = route1({ id: 'myid', sn: 'mysn', '#': 'zxc' })
346
+ // expect(pathHash1).toBe('/prefix/myid/mysn/suffix#zxc')
297
347
  })
298
348
 
299
349
  it('abs default throw if no window.location.origin', () => {
300
350
  const route0 = Route0.create('/path')
301
- expect(() => route0.get({ abs: true })).toThrow()
351
+ expect(() => route0.get(undefined, true)).toThrow()
302
352
  // const route0 = Route0.create('/path')
303
353
  // const path = route0.get({ abs: true })
304
- // const pathHash = route0.get({ abs: true, hash: 'zxc' })
354
+ // const pathHash = route0.get({ abs: true, '#': 'zxc' })
305
355
  // // expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
306
356
  // expect(path).toBe('https://example.com/path')
307
357
  // expect(path).toBe(route0.flat({}, true))
308
358
  // expect(pathHash).toBe('https://example.com/path#zxc')
309
- // expect(pathHash).toBe(route0.flat({ hash: 'zxc' }, true))
359
+ // expect(pathHash).toBe(route0.flat({ '#': 'zxc' }, true))
310
360
  })
311
361
 
312
362
  it('abs as string not throw if no window.location.origin', () => {
313
363
  const route0 = Route0.create('/path')
314
- const path = route0.get({ abs: 'https://example.com' })
364
+ const path = route0.get('https://example.com')
315
365
  expect(path).toBe('https://example.com/path')
316
366
  })
317
367
 
318
368
  it('abs as string not throw if no window.location.origin and not used additional path', () => {
319
369
  const route0 = Route0.create('/path')
320
- const path = route0.get({ abs: 'https://example.com/x' })
370
+ const path = route0.get('https://example.com/x')
321
371
  expect(path).toBe('https://example.com/path')
322
372
  })
323
373
 
324
374
  it('abs default set window.location.origin', () => {
325
375
  ;(globalThis as unknown as { location?: { origin?: string } }).location = { origin: 'https://example.com' }
326
376
  const route0 = Route0.create('/path')
327
- const path = route0.get({ abs: true })
328
- const pathHash = route0.get({ abs: true, hash: 'zxc' })
329
- // expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
377
+ const path = route0.get(true)
378
+ const pathHash = route0.get({ '#': 'zxc' }, true)
330
379
  expect(path).toBe('https://example.com/path')
331
- expect(path).toBe(route0.flat({}, true))
380
+ expect(path).toBe(route0.get({}, true))
332
381
  expect(pathHash).toBe('https://example.com/path#zxc')
333
- expect(pathHash).toBe(route0.flat({ hash: 'zxc' }, true))
382
+ expect(pathHash).toBe(route0.get({ '#': 'zxc' }, true))
334
383
  delete (globalThis as unknown as { location?: { origin?: string } }).location?.origin
335
384
  })
336
385
 
337
386
  it('abs set', () => {
338
387
  const route0 = Route0.create('/path', { origin: 'https://x.com' })
339
- const path = route0.get({ abs: true })
340
- const pathHash = route0.get({ abs: true, hash: 'zxc' })
341
- // expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
388
+ const path = route0.get(true)
389
+ const pathHash = route0.get({ '#': 'zxc' }, true)
342
390
  expect(path).toBe('https://x.com/path')
343
- expect(path).toBe(route0.flat({}, true))
391
+ expect(path).toBe(route0.get({}, true))
344
392
  expect(pathHash).toBe('https://x.com/path#zxc')
345
- expect(pathHash).toBe(route0.flat({ hash: 'zxc' }, true))
393
+ expect(pathHash).toBe(route0.get({ '#': 'zxc' }, true))
346
394
  })
347
395
 
348
396
  it('abs override', () => {
349
397
  const route0 = Route0.create('/path', { origin: 'https://x.com' })
350
398
  route0.origin = 'https://y.com'
351
- const path = route0.get({ abs: true })
352
- const pasthHash = route0.get({ abs: true, hash: 'zxc' })
353
- // expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path`>()
399
+ const path = route0.get(true)
400
+ const pasthHash = route0.get({ '#': 'zxc' }, true)
354
401
  expect(path).toBe('https://y.com/path')
355
- expect(path).toBe(route0.flat({}, true))
402
+ expect(path).toBe(route0.get({}, true))
356
403
  expect(pasthHash).toBe('https://y.com/path#zxc')
357
- expect(pasthHash).toBe(route0.flat({ hash: 'zxc' }, true))
404
+ expect(pasthHash).toBe(route0.get({ '#': 'zxc' }, true))
358
405
  })
359
406
 
360
407
  it('abs override extend', () => {
361
408
  const route0 = Route0.create('/path', { origin: 'https://x.com' })
362
409
  route0.origin = 'https://y.com'
363
410
  const route1 = route0.extend('/suffix')
364
- const path = route1.get({ abs: true })
365
- const pathHash = route1.get({ abs: true, hash: 'zxc' })
366
- // expectTypeOf<typeof path>().toEqualTypeOf<`${string}/path/suffix`>()
411
+ const path = route1.get(true)
412
+ const pathHash = route1.get({ '#': 'zxc' }, true)
367
413
  expect(path).toBe('https://y.com/path/suffix')
368
- expect(path).toBe(route1.flat({}, true))
414
+ expect(path).toBe(route1.get({}, true))
369
415
  expect(pathHash).toBe('https://y.com/path/suffix#zxc')
370
- expect(pathHash).toBe(route1.flat({ hash: 'zxc' }, true))
416
+ expect(pathHash).toBe(route1.get({ '#': 'zxc' }, true))
371
417
  })
372
418
 
373
419
  // it('abs override many', () => {
@@ -387,53 +433,40 @@ describe('Route0', () => {
387
433
  const rWith = Route0.create('/a/:id', { origin: 'https://example.com' })
388
434
  // @ts-expect-error missing required path params
389
435
  expect(rWith.get()).toBe('/a/undefined')
390
- // @ts-expect-error missing required path params
391
- expect(rWith.flat()).toBe('/a/undefined')
392
436
 
393
437
  // @ts-expect-error missing required path params
394
438
  expect(rWith.get({})).toBe('/a/undefined')
395
- // @ts-expect-error missing required path params
396
- expect(rWith.flat({})).toBe('/a/undefined')
397
- // @ts-expect-error missing required path params (object form abs)
398
- expect(rWith.get({ abs: true })).toBe('https://example.com/a/undefined')
399
439
  // @ts-expect-error missing required path params (object form abs)
400
- expect(rWith.flat({}, true)).toBe('https://example.com/a/undefined')
440
+ expect(rWith.get(true)).toBe('https://example.com/a/undefined')
401
441
  // @ts-expect-error missing required path params (object form search)
402
- expect(rWith.get({ search: { q: '1' } })).toBe('/a/undefined?q=1')
403
- // @ts-expect-error missing required path params (object form search), and loose search not allowed
404
- expect(rWith.flat({ q: '1' })).toBe('/a/undefined')
442
+ expect(rWith.get({ '?': { q: '1' } })).toBe('/a/undefined?q=1')
405
443
 
406
444
  // @ts-expect-error params can not be sent as object value it should be argument
407
445
  rWith.get({ params: { id: '1' } }) // not throw becouse this will not used
408
- expect(rWith.flat({ id: '1' })).toBe('/a/1')
446
+ expect(rWith.get({ id: '1' })).toBe('/a/1')
409
447
 
410
448
  const rNo = Route0.create('/b')
411
449
  // @ts-expect-error no path params allowed for this route (shorthand)
412
450
  expect(rNo.get({ id: '1' })).toBe('/b')
413
- // @ts-expect-error loose search not allowed
414
- expect(rNo.flat({ id: '1' })).toBe('/b')
415
- // @ts-expect-error loose search not allowed
416
- expect(rNo.flatStrict({ id: '1' })).toBe('/b')
417
- expect(rNo.flat({ id: '1' }, false, true)).toBe('/b?id=1')
418
- expect(Route0.create('/b&').flat({ id: '1' })).toBe('/b?id=1')
419
- expect(Route0.create('/b').flatLoose({ id: '1' })).toBe('/b?id=1')
420
451
  })
421
452
 
422
453
  it('really any route assignable to AnyRoute', () => {
423
454
  expectTypeOf<Route0<string>>().toExtend<AnyRoute>()
455
+ expectTypeOf<Route0<string, { x: string }>>().toExtend<AnyRoute>()
424
456
  expectTypeOf<Route0<string>>().toExtend<AnyRouteOrDefinition>()
457
+ expectTypeOf<Route0<string, { x: string }>>().toExtend<AnyRouteOrDefinition>()
425
458
  expectTypeOf<Route0<'/path'>>().toExtend<AnyRoute>()
426
- expectTypeOf<Route0<'/path'>>().toExtend<AnyRouteOrDefinition>()
459
+ expectTypeOf<Route0<'/path', { x: string }>>().toExtend<AnyRouteOrDefinition>()
427
460
  expectTypeOf<Route0<'/path/:id'>>().toExtend<AnyRoute>()
428
- expectTypeOf<Route0<'/path/:id'>>().toExtend<AnyRouteOrDefinition>()
429
- expectTypeOf<Route0<'/path/:id&x'>>().toExtend<AnyRoute>()
461
+ expectTypeOf<Route0<'/path/:id', { x?: string }>>().toExtend<AnyRouteOrDefinition>()
430
462
  expectTypeOf<CallableRoute<'/path'>>().toExtend<AnyRouteOrDefinition>()
463
+ expectTypeOf<CallableRoute<'/path', { x: string }>>().toExtend<AnyRouteOrDefinition>()
431
464
  expectTypeOf<CallableRoute<'/path'>>().toExtend<AnyRoute>()
432
- expectTypeOf<CallableRoute<'/path'>>().toExtend<AnyRouteOrDefinition>()
465
+ expectTypeOf<CallableRoute<'/path', { x: string }>>().toExtend<AnyRoute>()
433
466
  expectTypeOf<CallableRoute<'/path/:id'>>().toExtend<AnyRoute>()
467
+ expectTypeOf<CallableRoute<'/path/:id', { x?: string }>>().toExtend<AnyRoute>()
434
468
  expectTypeOf<CallableRoute<'/path/:id'>>().toExtend<AnyRouteOrDefinition>()
435
- expectTypeOf<CallableRoute<'/path/:id&x'>>().toExtend<AnyRoute>()
436
- expectTypeOf<CallableRoute<'/path/:id&x'>>().toExtend<AnyRouteOrDefinition>()
469
+ expectTypeOf<CallableRoute<'/path/:id', { x?: string }>>().toExtend<AnyRouteOrDefinition>()
437
470
  expectTypeOf<CallableRoute>().toExtend<AnyRoute>()
438
471
  expectTypeOf<CallableRoute>().toExtend<AnyRouteOrDefinition>()
439
472
 
@@ -446,6 +479,11 @@ describe('Route0', () => {
446
479
  expectTypeOf<typeof route2>().toExtend<AnyRoute>()
447
480
  expectTypeOf<typeof route2>().toExtend<AnyRouteOrDefinition>()
448
481
 
482
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
483
+ const route3 = route.extend('/path3').search<{ x: string }>()
484
+ expectTypeOf<typeof route3>().toExtend<AnyRoute>()
485
+ expectTypeOf<typeof route3>().toExtend<AnyRouteOrDefinition>()
486
+
449
487
  // Test that specific CallableRoute with literal path IS assignable to AnyRouteOrDefinition
450
488
  expectTypeOf<CallableRoute<'/ideas/best'>>().toExtend<AnyRouteOrDefinition>()
451
489
 
@@ -505,39 +543,6 @@ describe('type utilities', () => {
505
543
  expectTypeOf<HasParams<Route0<'/path/:id'>>>().toEqualTypeOf<true>()
506
544
  })
507
545
 
508
- it('HasSearch', () => {
509
- expectTypeOf<HasSearch<'/path'>>().toEqualTypeOf<false>()
510
- expectTypeOf<HasSearch<'/path&x'>>().toEqualTypeOf<true>()
511
- expectTypeOf<HasSearch<'/path&x&y'>>().toEqualTypeOf<true>()
512
- expectTypeOf<HasSearch<'/path&'>>().toEqualTypeOf<true>()
513
-
514
- expectTypeOf<HasSearch<Route0<'/path'>>>().toEqualTypeOf<false>()
515
- expectTypeOf<HasSearch<Route0<'/path&x&y'>>>().toEqualTypeOf<true>()
516
- expectTypeOf<HasSearch<Route0<'/path&'>>>().toEqualTypeOf<true>()
517
- })
518
-
519
- it('HasNamedSearch', () => {
520
- expectTypeOf<HasNamedSearch<'/path'>>().toEqualTypeOf<false>()
521
- expectTypeOf<HasNamedSearch<'/path&x'>>().toEqualTypeOf<true>()
522
- expectTypeOf<HasNamedSearch<'/path&x&y'>>().toEqualTypeOf<true>()
523
- expectTypeOf<HasNamedSearch<'/path&'>>().toEqualTypeOf<false>()
524
-
525
- expectTypeOf<HasNamedSearch<Route0<'/path'>>>().toEqualTypeOf<false>()
526
- expectTypeOf<HasNamedSearch<Route0<'/path&x&y'>>>().toEqualTypeOf<true>()
527
- expectTypeOf<HasNamedSearch<Route0<'/path&'>>>().toEqualTypeOf<false>()
528
- })
529
-
530
- it('HasLooseSearch', () => {
531
- expectTypeOf<HasLooseSearch<'/path'>>().toEqualTypeOf<false>()
532
- expectTypeOf<HasLooseSearch<'/path&x'>>().toEqualTypeOf<false>()
533
- expectTypeOf<HasLooseSearch<'/path&x&y'>>().toEqualTypeOf<false>()
534
- expectTypeOf<HasLooseSearch<'/path&'>>().toEqualTypeOf<true>()
535
-
536
- expectTypeOf<HasLooseSearch<Route0<'/path'>>>().toEqualTypeOf<false>()
537
- expectTypeOf<HasLooseSearch<Route0<'/path&x&y'>>>().toEqualTypeOf<false>()
538
- expectTypeOf<HasLooseSearch<Route0<'/path&'>>>().toEqualTypeOf<true>()
539
- })
540
-
541
546
  it('ParamsInput', () => {
542
547
  expectTypeOf<ParamsInput<'/path'>>().toEqualTypeOf<Record<never, never>>()
543
548
  expectTypeOf<ParamsInput<'/path/:id'>>().toEqualTypeOf<{ id: string | number }>()
@@ -557,186 +562,6 @@ describe('type utilities', () => {
557
562
  expectTypeOf<ParamsOutput<typeof route>>().toEqualTypeOf<{ id: string; name: string }>()
558
563
  })
559
564
 
560
- it('LooseSearchInput', () => {
561
- type T1 = LooseSearchInput<'/path'>
562
- expectTypeOf<T1>().toEqualTypeOf<Record<string, string | number>>()
563
-
564
- type T2 = LooseSearchInput<'/path&x&y'>
565
- expectTypeOf<T2>().toEqualTypeOf<
566
- Partial<{
567
- x: string | number
568
- y: string | number
569
- }> &
570
- Record<string, string | number>
571
- >()
572
- })
573
-
574
- it('LooseSearchOutput', () => {
575
- type T1 = LooseSearchOutput<'/path'>
576
- expectTypeOf<T1>().toEqualTypeOf<{
577
- [key: string]: string | undefined
578
- }>()
579
-
580
- type T2 = LooseSearchOutput<'/path&x&y'>
581
- expectTypeOf<T2>().toEqualTypeOf<{
582
- [key: string]: string | undefined
583
- x?: string | undefined
584
- y?: string | undefined
585
- }>()
586
- })
587
-
588
- it('StrictSearchInput', () => {
589
- type T1 = StrictSearchInput<'/path&x&y'>
590
- expectTypeOf<T1>().toEqualTypeOf<{ x?: string | number; y?: string | number }>()
591
- })
592
-
593
- it('StrictSearchOutput', () => {
594
- type T1 = StrictSearchOutput<'/path&x&y'>
595
- expectTypeOf<T1>().toEqualTypeOf<{ x?: string | undefined; y?: string | undefined }>()
596
- })
597
-
598
- it('LooseFlatInput', () => {
599
- type T1 = LooseFlatInput<'/path&x&y'>
600
- expectTypeOf<T1>().toEqualTypeOf<
601
- Partial<{
602
- x: string | number
603
- y: string | number
604
- }> &
605
- Record<string, string | number>
606
- >()
607
-
608
- type T2 = LooseFlatInput<'/path/:id&x&y'>
609
- expectTypeOf<T2>().toEqualTypeOf<
610
- {
611
- id: string | number
612
- } & Partial<{
613
- x: string | number
614
- y: string | number
615
- }> &
616
- Record<string, string | number>
617
- >()
618
- })
619
- it('StrictFlatInput', () => {
620
- type T1 = StrictFlatInput<'/path&x&y'>
621
- expectTypeOf<T1>().toEqualTypeOf<{ x?: string | number; y?: string | number }>()
622
- type T2 = StrictFlatInput<'/path/:id&x&y'>
623
- expectTypeOf<T2>().toEqualTypeOf<
624
- Partial<{
625
- x: string | number
626
- y: string | number
627
- }> & {
628
- id: string | number
629
- }
630
- >()
631
- })
632
-
633
- it('LooseFlatOutput', () => {
634
- type T1 = LooseFlatOutput<'/path&x&y'>
635
- expectTypeOf<T1>().toEqualTypeOf<{
636
- [x: string]: string | undefined
637
- x?: string | undefined
638
- y?: string | undefined
639
- }>()
640
-
641
- type T2 = LooseFlatOutput<'/path/:id&x&y'>
642
- expectTypeOf<T2>().toEqualTypeOf<
643
- {
644
- id: string
645
- } & {
646
- [x: string]: string | undefined
647
- x?: string | undefined
648
- y?: string | undefined
649
- }
650
- >()
651
- })
652
- it('StrictLooseFlatOutput', () => {
653
- type T1 = StrictFlatOutput<'/path&x&y'>
654
- expectTypeOf<T1>().toEqualTypeOf<{ x?: string | undefined; y?: string | undefined }>()
655
- type T2 = StrictFlatOutput<'/path/:id&x&y'>
656
- expectTypeOf<T2>().toEqualTypeOf<
657
- { id: string } & Partial<{
658
- x?: string | undefined
659
- y?: string | undefined
660
- }>
661
- >()
662
- })
663
-
664
- it('LooseFlatInputWithHash', () => {
665
- type T1 = LooseFlatInputWithHash<'/path&x&y'>
666
- expectTypeOf<T1>().toEqualTypeOf<
667
- { hash?: string | number } & Partial<{
668
- x: string | number
669
- y: string | number
670
- }> &
671
- Record<string, string | number>
672
- >()
673
-
674
- type T2 = LooseFlatInputWithHash<'/path/:id&x&y'>
675
- expectTypeOf<T2>().toEqualTypeOf<
676
- { hash?: string | number } & {
677
- id: string | number
678
- } & Partial<{
679
- x: string | number
680
- y: string | number
681
- }> &
682
- Record<string, string | number>
683
- >()
684
- })
685
- it('StrictFlatInputWithHash', () => {
686
- type T1 = StrictFlatInputWithHash<'/path&x&y'>
687
- expectTypeOf<T1>().toEqualTypeOf<{ hash?: string | number } & { x?: string | number; y?: string | number }>()
688
- type T2 = StrictFlatInputWithHash<'/path/:id&x&y'>
689
- expectTypeOf<T2>().toEqualTypeOf<
690
- { hash?: string | number } & Partial<{
691
- x: string | number
692
- y: string | number
693
- }> & {
694
- id: string | number
695
- }
696
- >()
697
- })
698
-
699
- it('LooseFlatOutputWithHash', () => {
700
- type T1 = LooseFlatOutputWithHash<'/path&x&y'>
701
- expectTypeOf<T1>().toEqualTypeOf<
702
- {
703
- [x: string]: string | undefined
704
- x?: string | undefined
705
- y?: string | undefined
706
- } & {
707
- hash?: string | undefined
708
- }
709
- >()
710
-
711
- type T2 = LooseFlatOutputWithHash<'/path/:id&x&y'>
712
- expectTypeOf<T2>().toEqualTypeOf<
713
- {
714
- id: string
715
- } & {
716
- [x: string]: string | undefined
717
- x?: string | undefined
718
- y?: string | undefined
719
- } & {
720
- hash?: string | undefined
721
- }
722
- >()
723
- })
724
- it('StrictFlatOutputWithHash', () => {
725
- type T1 = StrictFlatOutputWithHash<'/path&x&y'>
726
- expectTypeOf<T1>().toEqualTypeOf<
727
- { x?: string | undefined; y?: string | undefined } & { hash?: string | undefined }
728
- >()
729
- type T2 = StrictFlatOutputWithHash<'/path/:id&x&y'>
730
- expectTypeOf<T2>().toEqualTypeOf<
731
- { id: string } & Partial<{
732
- x?: string | undefined
733
- y?: string | undefined
734
- }> & {
735
- hash?: string | undefined
736
- }
737
- >()
738
- })
739
-
740
565
  it('ParamsInputStringOnly', () => {
741
566
  expectTypeOf<ParamsInputStringOnly<'/path'>>().toEqualTypeOf<Record<never, never>>()
742
567
  expectTypeOf<ParamsInputStringOnly<'/path/:id'>>().toEqualTypeOf<{ id: string }>()
@@ -747,69 +572,21 @@ describe('type utilities', () => {
747
572
  expectTypeOf<ParamsInputStringOnly<typeof route>>().toEqualTypeOf<{ id: string; name: string }>()
748
573
  })
749
574
 
750
- it('LooseSearchInputStringOnly', () => {
751
- type T1 = LooseSearchInputStringOnly<'/path'>
752
- expectTypeOf<T1>().toEqualTypeOf<Record<string, string>>()
753
-
754
- type T2 = LooseSearchInputStringOnly<'/path&x&y'>
755
- expectTypeOf<T2>().toEqualTypeOf<
756
- Partial<{
757
- x: string
758
- y: string
759
- }> &
760
- Record<string, string>
761
- >()
762
- })
763
-
764
- it('StrictSearchInputStringOnly', () => {
765
- type T1 = StrictSearchInputStringOnly<'/path&x&y'>
766
- expectTypeOf<T1>().toEqualTypeOf<{ x?: string; y?: string }>()
767
- })
768
-
769
- it('LooseFlatInputStringOnly', () => {
770
- type T1 = LooseFlatInputStringOnly<'/path&x&y'>
771
- expectTypeOf<T1>().toEqualTypeOf<
772
- Partial<{
773
- x: string
774
- y: string
775
- }> &
776
- Record<string, string>
777
- >()
778
-
779
- type T2 = LooseFlatInputStringOnly<'/path/:id&x&y'>
780
- expectTypeOf<T2>().toEqualTypeOf<
781
- {
782
- id: string
783
- } & Partial<{
784
- x: string
785
- y: string
786
- }> &
787
- Record<string, string>
788
- >()
789
- })
790
- it('StrictFlatInputStringOnly', () => {
791
- type T1 = StrictFlatInputStringOnly<'/path&x&y'>
792
- expectTypeOf<T1>().toEqualTypeOf<{ x?: string; y?: string }>()
793
- type T2 = StrictFlatInputStringOnly<'/path/:id&x&y'>
794
- expectTypeOf<T2>().toEqualTypeOf<
795
- Partial<{
796
- x: string
797
- y: string
798
- }> & {
799
- id: string
800
- }
801
- >()
802
- })
803
-
804
- it('CanInputBeEmpty', () => {
805
- type T1 = CanInputBeEmpty<'/path'>
575
+ it('IsParamsOptional', () => {
576
+ type T1 = IsParamsOptional<'/path'>
806
577
  expectTypeOf<T1>().toEqualTypeOf<true>()
807
- type T2 = CanInputBeEmpty<'/path/:id'>
578
+ type T2 = IsParamsOptional<'/path/:id'>
808
579
  expectTypeOf<T2>().toEqualTypeOf<false>()
809
- type T3 = CanInputBeEmpty<'/path&x&y'>
580
+ type T3 = IsParamsOptional<'/path'>
810
581
  expectTypeOf<T3>().toEqualTypeOf<true>()
811
- type T4 = CanInputBeEmpty<'/path/:id&x&y'>
582
+ type T4 = IsParamsOptional<'/path/:id'>
812
583
  expectTypeOf<T4>().toEqualTypeOf<false>()
584
+ type T5 = IsParamsOptional<'/path/:id?'>
585
+ expectTypeOf<T5>().toEqualTypeOf<true>()
586
+ type T6 = IsParamsOptional<'/path*'>
587
+ expectTypeOf<T6>().toEqualTypeOf<false>()
588
+ type T7 = IsParamsOptional<'/path*?'>
589
+ expectTypeOf<T7>().toEqualTypeOf<true>()
813
590
  })
814
591
 
815
592
  it('IsAncestor', () => {
@@ -857,9 +634,13 @@ describe('type utilities', () => {
857
634
  it('Extended', () => {
858
635
  expectTypeOf<Extended<'/path', '/child'>>().toEqualTypeOf<Route0<'/path/child'>>()
859
636
  expectTypeOf<Extended<'/path', '/:id'>>().toEqualTypeOf<Route0<'/path/:id'>>()
860
- expectTypeOf<Extended<'/path', '&x&y'>>().toEqualTypeOf<Route0<'/path&x&y'>>()
861
- expectTypeOf<Extended<'/path/:id', '/child&x'>>().toEqualTypeOf<Route0<'/path/:id/child&x'>>()
862
- expectTypeOf<Extended<undefined, '/path'>>().toEqualTypeOf<Route0<'/path'>>()
637
+ expectTypeOf<Extended<'/path', '', { x: string; y: string }>>().toEqualTypeOf<
638
+ Route0<'/path', { x: string; y: string }>
639
+ >()
640
+ expectTypeOf<Extended<'/path/:id', '/child', { x: string }>>().toEqualTypeOf<
641
+ Route0<'/path/:id/child', { x: string }>
642
+ >()
643
+ expectTypeOf<Extended<undefined, '/path', { x: string }>>().toEqualTypeOf<Route0<'/path', { x: string }>>()
863
644
 
864
645
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
865
646
  const ancestor = Route0.create('/path')
@@ -1181,7 +962,7 @@ describe('getLocation', () => {
1181
962
 
1182
963
  it('with search params', () => {
1183
964
  const routes = Routes.create({
1184
- search: '/search&q&filter',
965
+ search: '/search',
1185
966
  users: '/users',
1186
967
  })
1187
968
 
@@ -1265,7 +1046,7 @@ describe('getLocation', () => {
1265
1046
  api,
1266
1047
  users: api.extend('/users'),
1267
1048
  userDetail: api.extend('/users/:id'),
1268
- userPosts: api.extend('/users/:id/posts&sort&filter'),
1049
+ userPosts: api.extend('/users/:id/posts').search<{ sort: string; filter: string }>(),
1269
1050
  })
1270
1051
 
1271
1052
  const loc = routes._.getLocation('/api/v1/users/42/posts?sort=date&filter=published&extra=value')
@@ -1281,6 +1062,87 @@ describe('getLocation', () => {
1281
1062
  }
1282
1063
  })
1283
1064
 
1065
+ it('resolves overlaps: static > required param > optional > wildcard', () => {
1066
+ const routes = Routes.create({
1067
+ usersStatic: '/users/new',
1068
+ usersRequired: '/users/:id',
1069
+ usersOptional: '/users/:id?',
1070
+ usersWildcard: '/users/*?',
1071
+ })
1072
+
1073
+ const locStatic = routes._.getLocation('/users/new')
1074
+ expect(locStatic.exact).toBe(true)
1075
+ if (locStatic.exact) {
1076
+ expect(locStatic.route).toBe('/users/new')
1077
+ }
1078
+
1079
+ const locRequired = routes._.getLocation('/users/123')
1080
+ expect(locRequired.exact).toBe(true)
1081
+ if (locRequired.exact) {
1082
+ expect(locRequired.route).toBe('/users/:id')
1083
+ expect(locRequired.params).toMatchObject({ id: '123' })
1084
+ }
1085
+
1086
+ const locOptional = routes._.getLocation('/users')
1087
+ expect(locOptional.exact).toBe(true)
1088
+ if (locOptional.exact) {
1089
+ expect(locOptional.route).toBe('/users/:id?')
1090
+ }
1091
+
1092
+ const locWildcard = routes._.getLocation('/users/a/b/c')
1093
+ expect(locWildcard.exact).toBe(true)
1094
+ if (locWildcard.exact) {
1095
+ expect(locWildcard.route).toBe('/users/*?')
1096
+ }
1097
+ })
1098
+
1099
+ it('resolves app wildcard compatibility cases like wouter', () => {
1100
+ const routes = Routes.create({
1101
+ appRoot: '/app',
1102
+ appHome: '/app/home',
1103
+ appId: '/app/:id',
1104
+ appSplat: '/app*',
1105
+ })
1106
+
1107
+ const m1 = routes._.getLocation('/app')
1108
+ expect(m1.exact).toBe(true)
1109
+ if (m1.exact) expect(m1.route).toBe('/app')
1110
+
1111
+ const m2 = routes._.getLocation('/app/home')
1112
+ expect(m2.exact).toBe(true)
1113
+ if (m2.exact) expect(m2.route).toBe('/app/home')
1114
+
1115
+ const m3 = routes._.getLocation('/app/123')
1116
+ expect(m3.exact).toBe(true)
1117
+ if (m3.exact) expect(m3.route).toBe('/app/:id')
1118
+
1119
+ const m4 = routes._.getLocation('/app-1')
1120
+ expect(m4.exact).toBe(true)
1121
+ if (m4.exact) expect(m4.route).toBe('/app*')
1122
+ })
1123
+
1124
+ it('resolves /path/x* and /path/x/* differently in Routes', () => {
1125
+ const routes = Routes.create({
1126
+ inlineWildcard: '/path/x*',
1127
+ segmentWildcard: '/path/x/*',
1128
+ })
1129
+
1130
+ // '/path/x123' only matches inline wildcard.
1131
+ const a = routes._.getLocation('/path/x123')
1132
+ expect(a.exact).toBe(true)
1133
+ if (a.exact) expect(a.route).toBe('/path/x*')
1134
+
1135
+ // '/path/x/123' matches both, but '/path/x/*' should win as more specific.
1136
+ const b = routes._.getLocation('/path/x/123')
1137
+ expect(b.exact).toBe(true)
1138
+ if (b.exact) expect(b.route).toBe('/path/x/*')
1139
+
1140
+ // '/path/x' also matches both; segment wildcard remains preferred.
1141
+ const c = routes._.getLocation('/path/x')
1142
+ expect(c.exact).toBe(true)
1143
+ if (c.exact) expect(c.route).toBe('/path/x/*')
1144
+ })
1145
+
1284
1146
  it('get location for extedned routes', () => {
1285
1147
  const a = Route0.create('/')
1286
1148
  const b = a.extend('/b')
@@ -1311,61 +1173,10 @@ describe('getLocation', () => {
1311
1173
  })
1312
1174
  })
1313
1175
 
1314
- describe('input schemas', () => {
1315
- it('flatInputSchema validate (auto loose route)', () => {
1316
- const route = Route0.create('/:id&a&')
1317
- const result = route.flatInputSchema['~standard'].validate({ id: 1, a: 2, b: 3, c: 4 })
1318
- if (result instanceof Promise) {
1319
- throw new Error('Unexpected async schema result')
1320
- }
1321
- expect(result).toMatchObject({
1322
- value: {
1323
- id: '1',
1324
- a: '2',
1325
- b: '3',
1326
- c: '4',
1327
- },
1328
- })
1329
- })
1330
-
1331
- it('flatInputSchema validate error', () => {
1332
- const route = Route0.create('/:id&a&b')
1333
- const result = route.flatInputSchema['~standard'].validate(undefined)
1334
- if (result instanceof Promise) {
1335
- throw new Error('Unexpected async schema result')
1336
- }
1337
- expect(result).toMatchObject({
1338
- issues: [{ message: 'Missing params: "id"' }],
1339
- })
1340
- })
1341
-
1342
- it('flatInputSchema parse and safeParse', () => {
1343
- const route = Route0.create('/:id&a&')
1344
- expect(route.flatInputSchema.parse({ id: 1, a: 2, c: 3 })).toMatchObject({
1345
- id: '1',
1346
- a: '2',
1347
- c: '3',
1348
- })
1349
- expect(route.flatInputSchema.safeParse({ id: 1, a: 2, c: 3 })).toMatchObject({
1350
- success: true,
1351
- data: {
1352
- id: '1',
1353
- a: '2',
1354
- c: '3',
1355
- },
1356
- error: undefined,
1357
- })
1358
- expect(route.flatInputSchema.safeParse(undefined)).toMatchObject({
1359
- success: false,
1360
- data: undefined,
1361
- error: new Error('Missing params: "id"'),
1362
- })
1363
- expect(() => route.flatInputSchema.parse(undefined)).toThrow('Missing params: "id"')
1364
- })
1365
-
1366
- it('paramsInputSchema validate', () => {
1176
+ describe('params schema', () => {
1177
+ it('paramsSchema validate', () => {
1367
1178
  const route = Route0.create('/:id/:sn')
1368
- const result = route.paramsInputSchema['~standard'].validate({ id: 1, sn: 'x', extra: 'ignored' })
1179
+ const result = route.paramsSchema['~standard'].validate({ id: 1, sn: 'x', extra: 'ignored' })
1369
1180
  if (result instanceof Promise) {
1370
1181
  throw new Error('Unexpected async schema result')
1371
1182
  }
@@ -1374,78 +1185,20 @@ describe('input schemas', () => {
1374
1185
  })
1375
1186
  })
1376
1187
 
1377
- it('strictSearchInputSchema validate', () => {
1378
- const route = Route0.create('/:id&a&b')
1379
- const result = route.strictSearchInputSchema['~standard'].validate({ id: '1', a: 2, b: '3', c: 4 })
1380
- if (result instanceof Promise) {
1381
- throw new Error('Unexpected async schema result')
1382
- }
1383
- expect(result).toMatchObject({
1384
- value: { a: '2', b: '3' },
1385
- })
1386
- })
1387
-
1388
- it('looseSearchInputSchema validate', () => {
1389
- const route = Route0.create('/:id&a&')
1390
- const result = route.looseSearchInputSchema['~standard'].validate({ id: '1', a: 2, b: '3', hash: 'x' })
1391
- if (result instanceof Promise) {
1392
- throw new Error('Unexpected async schema result')
1393
- }
1394
- expect(result).toMatchObject({
1395
- value: { a: '2', b: '3' },
1396
- })
1397
- })
1398
-
1399
- it('params/search schemas parse and safeParse', () => {
1400
- const route = Route0.create('/:id&a&b')
1401
-
1402
- expect(route.paramsInputSchema.parse({ id: 1, x: '2' })).toMatchObject({ id: '1' })
1403
- expect(route.paramsInputSchema.safeParse(undefined)).toMatchObject({
1188
+ it('paramsSchema parse and safeParse', () => {
1189
+ const route = Route0.create('/:id')
1190
+ expect(route.paramsSchema.parse({ id: 1, x: '2' })).toMatchObject({ id: '1' })
1191
+ expect(route.paramsSchema.safeParse(undefined)).toMatchObject({
1404
1192
  success: false,
1405
1193
  data: undefined,
1406
1194
  error: new Error('Missing params: "id"'),
1407
1195
  })
1408
-
1409
- expect(route.strictSearchInputSchema.parse({ id: '1', a: 2, b: 3, c: 4 })).toMatchObject({
1410
- a: '2',
1411
- b: '3',
1412
- })
1413
- expect(route.strictSearchInputSchema.safeParse({ a: () => ({}) })).toMatchObject({
1414
- success: false,
1415
- data: undefined,
1416
- error: new Error('Invalid input: expected string, number, or undefined, got function for "a"'),
1417
- })
1418
-
1419
- expect(route.looseSearchInputSchema.parse({ id: '1', a: 2, c: 3 })).toMatchObject({
1420
- a: '2',
1421
- c: '3',
1422
- })
1423
- expect(route.looseSearchInputSchema.safeParse({ a: () => ({}) })).toMatchObject({
1424
- success: false,
1425
- data: undefined,
1426
- error: new Error('Invalid input: expected string, number, or undefined, got function for "a"'),
1427
- })
1428
1196
  })
1429
1197
 
1430
1198
  it('schema types are assignable to StandardSchemaV1', () => {
1431
- const route = Route0.create('/:id&a&')
1432
- expectTypeOf(route.paramsInputSchema).toMatchTypeOf<StandardSchemaV1>()
1433
- expectTypeOf(route.strictSearchInputSchema).toMatchTypeOf<StandardSchemaV1>()
1434
- expectTypeOf(route.looseSearchInputSchema).toMatchTypeOf<StandardSchemaV1>()
1435
- expectTypeOf(route.flatInputSchema).toMatchTypeOf<StandardSchemaV1>()
1436
-
1437
- expectTypeOf(route.paramsInputSchema).toMatchTypeOf<
1438
- StandardSchemaV1<ParamsInput<'/:id&a&'>, ParamsOutput<'/:id&a&'>>
1439
- >()
1440
- expectTypeOf(route.strictSearchInputSchema).toMatchTypeOf<
1441
- StandardSchemaV1<StrictSearchInput<'/:id&a&'>, StrictSearchOutput<'/:id&a&'>>
1442
- >()
1443
- expectTypeOf(route.looseSearchInputSchema).toMatchTypeOf<
1444
- StandardSchemaV1<LooseSearchInput<'/:id&a&'>, LooseSearchOutput<'/:id&a&'>>
1445
- >()
1446
- expectTypeOf(route.flatInputSchema).toMatchTypeOf<
1447
- StandardSchemaV1<LooseFlatInput<'/:id&a&'>, LooseFlatOutput<'/:id&a&'>>
1448
- >()
1199
+ const route = Route0.create('/:id')
1200
+ expectTypeOf(route.paramsSchema).toExtend<StandardSchemaV1>()
1201
+ expectTypeOf(route.paramsSchema).toExtend<StandardSchemaV1<ParamsInput<'/:id'>, ParamsOutput<'/:id'>>>()
1449
1202
  })
1450
1203
  })
1451
1204
 
@@ -1501,18 +1254,17 @@ describe('Routes', () => {
1501
1254
  it('create with params and search', () => {
1502
1255
  const collection = Routes.create({
1503
1256
  user: '/user/:id',
1504
- search: '/search&q&filter',
1505
- userWithSearch: '/user/:id&tab',
1257
+ search: Route0.create('/search').search<{ q: string; filter: string }>(),
1258
+ userWithSearch: Route0.create('/user/:id').search<{ tab: string }>(),
1506
1259
  })
1507
-
1508
1260
  const user = collection.user
1509
1261
  expect(user.get({ id: '123' })).toBe('/user/123')
1510
-
1511
1262
  const search = collection.search
1512
- expect(search.get({ search: { q: 'test', filter: 'all' } })).toBe('/search?q=test&filter=all')
1513
-
1263
+ expect(search.get({ '?': { q: 'test', filter: 'all' } })).toBe('/search?q=test&filter=all')
1514
1264
  const userWithSearch = collection.userWithSearch
1515
- expect(userWithSearch.get({ id: '456', search: { tab: 'posts' } })).toBe('/user/456?tab=posts')
1265
+ expect(userWithSearch.get({ id: '456', '?': { tab: 'posts' } })).toBe('/user/456?tab=posts')
1266
+ // @ts-expect-error invalid search param key
1267
+ expect(userWithSearch.get({ id: '456', '?': { zxc: 'posts' } })).toBe('/user/456?zxc=posts')
1516
1268
  })
1517
1269
 
1518
1270
  it('get maintains route definitions', () => {
@@ -1527,8 +1279,6 @@ describe('Routes', () => {
1527
1279
  // Verify route definitions are preserved
1528
1280
  expect(home.definition).toBe('/')
1529
1281
  expect(user.definition).toBe('/user/:id')
1530
- expect(home.pathDefinition).toBe('/')
1531
- expect(user.pathDefinition).toBe('/user/:id')
1532
1282
 
1533
1283
  // Verify params work correctly
1534
1284
  expect(user.get({ id: '123' })).toBe('/user/123')
@@ -1545,8 +1295,8 @@ describe('Routes', () => {
1545
1295
  const home = overridden.home
1546
1296
  const about = overridden.about
1547
1297
 
1548
- expect(home.get({ abs: true })).toBe('https://example.com')
1549
- expect(about.get({ abs: true })).toBe('https://example.com/about')
1298
+ expect(home.get(true)).toBe('https://example.com')
1299
+ expect(about.get(true)).toBe('https://example.com/about')
1550
1300
  })
1551
1301
 
1552
1302
  it('clone does not mutate original', () => {
@@ -1558,13 +1308,13 @@ describe('Routes', () => {
1558
1308
  )
1559
1309
 
1560
1310
  const original = collection.home
1561
- expect(original.get({ abs: true })).toBe('https://example.com')
1311
+ expect(original.get(true)).toBe('https://example.com')
1562
1312
 
1563
1313
  const overridden = collection._.clone({ origin: 'https://newdomain.com' })
1564
1314
  const newRoute = overridden.home
1565
1315
 
1566
- expect(original.get({ abs: true })).toBe('https://example.com')
1567
- expect(newRoute.get({ abs: true })).toBe('https://newdomain.com')
1316
+ expect(original.get(true)).toBe('https://example.com')
1317
+ expect(newRoute.get(true)).toBe('https://newdomain.com')
1568
1318
  })
1569
1319
 
1570
1320
  it('clone with extended routes', () => {
@@ -1576,14 +1326,14 @@ describe('Routes', () => {
1576
1326
  users: usersRoute,
1577
1327
  })
1578
1328
 
1579
- expect(collection.api.get({ abs: true })).toBe('https://api.example.com/api')
1580
- expect(collection.api({ abs: true })).toBe('https://api.example.com/api')
1581
- expect(collection.users.get({ abs: true })).toBe('https://api.example.com/api/users')
1329
+ expect(collection.api.get(true)).toBe('https://api.example.com/api')
1330
+ expect(collection.api(true)).toBe('https://api.example.com/api')
1331
+ expect(collection.users.get(true)).toBe('https://api.example.com/api/users')
1582
1332
 
1583
1333
  const overridden = collection._.clone({ origin: 'https://new-api.example.com' })
1584
1334
 
1585
- expect(overridden.api.get({ abs: true })).toBe('https://new-api.example.com/api')
1586
- expect(overridden.users.get({ abs: true })).toBe('https://new-api.example.com/api/users')
1335
+ expect(overridden.api.get(true)).toBe('https://new-api.example.com/api')
1336
+ expect(overridden.users.get(true)).toBe('https://new-api.example.com/api/users')
1587
1337
  })
1588
1338
 
1589
1339
  it('hydrate static method', () => {
@@ -1626,21 +1376,23 @@ describe('Routes', () => {
1626
1376
  api,
1627
1377
  users: api.extend('/users'),
1628
1378
  userDetail: api.extend('/users/:id'),
1629
- userPosts: api.extend('/users/:id/posts&sort&filter'),
1379
+ userPosts: api.extend('/users/:id/posts').search<{ sort: string; filter: string }>(),
1630
1380
  })
1631
1381
 
1632
1382
  expect(collection.root.get()).toBe('/')
1633
- expect(collection.api({ abs: true })).toBe('https://api.example.com/api/v1')
1634
- expect(collection.users.get({ abs: true })).toBe('https://api.example.com/api/v1/users')
1383
+ expect(collection.api(true)).toBe('https://api.example.com/api/v1')
1384
+ expect(collection.users.get(true)).toBe('https://api.example.com/api/v1/users')
1635
1385
 
1636
- const userDetailPath = collection.userDetail.get({ id: '42', abs: true })
1386
+ const userDetailPath = collection.userDetail.get({ id: '42' }, true)
1637
1387
  expect(userDetailPath).toBe('https://api.example.com/api/v1/users/42')
1638
1388
 
1639
- const userPostsPath = collection.userPosts.get({
1640
- id: '42',
1641
- search: { sort: 'date', filter: 'published' },
1642
- abs: true,
1643
- })
1389
+ const userPostsPath = collection.userPosts.get(
1390
+ {
1391
+ id: '42',
1392
+ '?': { sort: 'date', filter: 'published' },
1393
+ },
1394
+ true,
1395
+ )
1644
1396
  expect(userPostsPath).toBe('https://api.example.com/api/v1/users/42/posts?sort=date&filter=published')
1645
1397
  })
1646
1398
  })
@@ -1750,6 +1502,19 @@ describe('specificity', () => {
1750
1502
  // Same depth but different static segments
1751
1503
  expect(route1.isConflict(route2)).toBe(false)
1752
1504
  })
1505
+
1506
+ it('isMayBeSame: optional params can overlap static', () => {
1507
+ const optional = Route0.create('/users/:id?')
1508
+ const staticUsers = Route0.create('/users')
1509
+ expect(optional.isSame(staticUsers)).toBe(false)
1510
+ expect(optional.isMayBeSame(staticUsers)).toBe(true)
1511
+ })
1512
+
1513
+ it('isConflict: wildcard overlaps deeper static routes', () => {
1514
+ const wildcard = Route0.create('/app*')
1515
+ const staticRoute = Route0.create('/app/home')
1516
+ expect(wildcard.isConflict(staticRoute)).toBe(true)
1517
+ })
1753
1518
  })
1754
1519
 
1755
1520
  describe('regex', () => {
@@ -2153,6 +1918,29 @@ describe('ordering', () => {
2153
1918
  expect(ordering).toEqual(['/about', '/contact', '/home'])
2154
1919
  })
2155
1920
 
1921
+ it('_makeOrdering: keeps concrete routes before wildcard overlaps', () => {
1922
+ const routes = {
1923
+ appWildcard: '/app*',
1924
+ appHome: '/app/home',
1925
+ app: '/app',
1926
+ }
1927
+ const { pathsOrdering: ordering } = Routes._.makeOrdering(routes)
1928
+ expect(ordering).toEqual(['/app', '/app/home', '/app*'])
1929
+ })
1930
+
1931
+ it('_makeOrdering: mixed optional and required params are deterministic', () => {
1932
+ const routes = {
1933
+ usersOptional: '/users/:id?',
1934
+ usersRequired: '/users/:id',
1935
+ usersStatic: '/users/new',
1936
+ usersWildcard: '/users/*?',
1937
+ }
1938
+ const { pathsOrdering: ordering } = Routes._.makeOrdering(routes)
1939
+ expect(ordering.indexOf('/users/new')).toBeLessThan(ordering.indexOf('/users/:id'))
1940
+ expect(ordering.indexOf('/users/:id')).toBeLessThan(ordering.indexOf('/users/:id?'))
1941
+ expect(ordering.indexOf('/users/:id?')).toBeLessThan(ordering.indexOf('/users/*?'))
1942
+ })
1943
+
2156
1944
  it('_makeOrdering: complex nested structure', () => {
2157
1945
  const api = Route0.create('/api/v1')
2158
1946
  const routes = {