@furystack/shades 13.2.1 → 14.0.0
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/CHANGELOG.md +150 -0
- package/esm/components/define-nested-routes.d.ts +27 -0
- package/esm/components/define-nested-routes.d.ts.map +1 -0
- package/esm/components/define-nested-routes.js +26 -0
- package/esm/components/define-nested-routes.js.map +1 -0
- package/esm/components/define-nested-routes.spec.d.ts +2 -0
- package/esm/components/define-nested-routes.spec.d.ts.map +1 -0
- package/esm/components/define-nested-routes.spec.js +81 -0
- package/esm/components/define-nested-routes.spec.js.map +1 -0
- package/esm/components/index.d.ts +3 -3
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +3 -3
- package/esm/components/index.js.map +1 -1
- package/esm/components/nested-hooks.d.ts +62 -0
- package/esm/components/nested-hooks.d.ts.map +1 -0
- package/esm/components/nested-hooks.js +73 -0
- package/esm/components/nested-hooks.js.map +1 -0
- package/esm/components/nested-hooks.spec.d.ts +2 -0
- package/esm/components/nested-hooks.spec.d.ts.map +1 -0
- package/esm/components/nested-hooks.spec.js +211 -0
- package/esm/components/nested-hooks.spec.js.map +1 -0
- package/esm/components/nested-navigate.d.ts +52 -21
- package/esm/components/nested-navigate.d.ts.map +1 -1
- package/esm/components/nested-navigate.js +32 -17
- package/esm/components/nested-navigate.js.map +1 -1
- package/esm/components/nested-navigate.spec.js +102 -12
- package/esm/components/nested-navigate.spec.js.map +1 -1
- package/esm/components/nested-replace.d.ts +36 -0
- package/esm/components/nested-replace.d.ts.map +1 -0
- package/esm/components/nested-replace.js +38 -0
- package/esm/components/nested-replace.js.map +1 -0
- package/esm/components/nested-replace.spec.d.ts +2 -0
- package/esm/components/nested-replace.spec.d.ts.map +1 -0
- package/esm/components/nested-replace.spec.js +59 -0
- package/esm/components/nested-replace.spec.js.map +1 -0
- package/esm/components/nested-route-link.d.ts +36 -26
- package/esm/components/nested-route-link.d.ts.map +1 -1
- package/esm/components/nested-route-link.js +17 -19
- package/esm/components/nested-route-link.js.map +1 -1
- package/esm/components/nested-route-link.spec.js +61 -11
- package/esm/components/nested-route-link.spec.js.map +1 -1
- package/esm/components/nested-route-types.d.ts +79 -2
- package/esm/components/nested-route-types.d.ts.map +1 -1
- package/esm/components/nested-router.d.ts +77 -7
- package/esm/components/nested-router.d.ts.map +1 -1
- package/esm/components/nested-router.js +137 -11
- package/esm/components/nested-router.js.map +1 -1
- package/esm/components/nested-router.spec.js +265 -34
- package/esm/components/nested-router.spec.js.map +1 -1
- package/esm/services/location-service.spec.js +33 -0
- package/esm/services/location-service.spec.js.map +1 -1
- package/esm/services/route-match-service.spec.js +4 -0
- package/esm/services/route-match-service.spec.js.map +1 -1
- package/esm/services/route-meta-utils.d.ts +28 -6
- package/esm/services/route-meta-utils.d.ts.map +1 -1
- package/esm/services/route-meta-utils.js +18 -4
- package/esm/services/route-meta-utils.js.map +1 -1
- package/esm/services/route-meta-utils.spec.js +57 -2
- package/esm/services/route-meta-utils.spec.js.map +1 -1
- package/esm/vnode.d.ts +1 -1
- package/esm/vnode.js +1 -1
- package/package.json +9 -9
- package/src/components/define-nested-routes.spec.ts +91 -0
- package/src/components/define-nested-routes.ts +27 -0
- package/src/components/index.ts +3 -3
- package/src/components/nested-hooks.spec.ts +254 -0
- package/src/components/nested-hooks.ts +109 -0
- package/src/components/nested-navigate.spec.ts +128 -25
- package/src/components/nested-navigate.ts +75 -26
- package/src/components/nested-replace.spec.ts +88 -0
- package/src/components/nested-replace.ts +46 -0
- package/src/components/nested-route-link.spec.tsx +84 -16
- package/src/components/nested-route-link.tsx +53 -28
- package/src/components/nested-route-types.ts +82 -2
- package/src/components/nested-router.spec.tsx +316 -33
- package/src/components/nested-router.tsx +182 -22
- package/src/services/location-service.spec.ts +39 -0
- package/src/services/route-match-service.spec.ts +4 -0
- package/src/services/route-meta-utils.spec.ts +75 -7
- package/src/services/route-meta-utils.ts +44 -15
- package/src/vnode.ts +1 -1
- package/esm/components/link-to-route.d.ts +0 -10
- package/esm/components/link-to-route.d.ts.map +0 -1
- package/esm/components/link-to-route.js +0 -16
- package/esm/components/link-to-route.js.map +0 -1
- package/esm/components/link-to-route.spec.d.ts +0 -2
- package/esm/components/link-to-route.spec.d.ts.map +0 -1
- package/esm/components/link-to-route.spec.js +0 -30
- package/esm/components/link-to-route.spec.js.map +0 -1
- package/esm/components/route-link.d.ts +0 -10
- package/esm/components/route-link.d.ts.map +0 -1
- package/esm/components/route-link.js +0 -25
- package/esm/components/route-link.js.map +0 -1
- package/esm/components/route-link.spec.d.ts +0 -2
- package/esm/components/route-link.spec.d.ts.map +0 -1
- package/esm/components/route-link.spec.js +0 -34
- package/esm/components/route-link.spec.js.map +0 -1
- package/esm/components/router.d.ts +0 -44
- package/esm/components/router.d.ts.map +0 -1
- package/esm/components/router.js +0 -66
- package/esm/components/router.js.map +0 -1
- package/esm/components/router.spec.d.ts +0 -2
- package/esm/components/router.spec.d.ts.map +0 -1
- package/esm/components/router.spec.js +0 -161
- package/esm/components/router.spec.js.map +0 -1
- package/src/components/link-to-route.spec.tsx +0 -45
- package/src/components/link-to-route.tsx +0 -26
- package/src/components/route-link.spec.tsx +0 -44
- package/src/components/route-link.tsx +0 -29
- package/src/components/router.spec.tsx +0 -217
- package/src/components/router.tsx +0 -103
|
@@ -11,9 +11,9 @@ import type { ConcatPaths, ExtractRouteParams, ExtractRoutePaths, UrlTree } from
|
|
|
11
11
|
import type { NestedRoute } from './nested-router.js'
|
|
12
12
|
|
|
13
13
|
// Minimal route type for type-level tests. Using Pick avoids the
|
|
14
|
-
// `children?: Record<string, NestedRoute<any>>` from NestedRoute<unknown>
|
|
14
|
+
// `children?: Record<string, NestedRoute<any, any, any>>` from NestedRoute<unknown>
|
|
15
15
|
// which would widen literal keys in intersections.
|
|
16
|
-
type TestRoute = Pick<NestedRoute<unknown>, 'component'>
|
|
16
|
+
type TestRoute = Pick<NestedRoute<unknown, any, any>, 'component'>
|
|
17
17
|
|
|
18
18
|
describe('NestedRouteLink', () => {
|
|
19
19
|
beforeEach(() => {
|
|
@@ -31,7 +31,7 @@ describe('NestedRouteLink', () => {
|
|
|
31
31
|
injector,
|
|
32
32
|
rootElement,
|
|
33
33
|
jsxElement: (
|
|
34
|
-
<NestedRouteLink id="link"
|
|
34
|
+
<NestedRouteLink id="link" path="/buttons">
|
|
35
35
|
Buttons
|
|
36
36
|
</NestedRouteLink>
|
|
37
37
|
),
|
|
@@ -54,7 +54,7 @@ describe('NestedRouteLink', () => {
|
|
|
54
54
|
injector,
|
|
55
55
|
rootElement,
|
|
56
56
|
jsxElement: (
|
|
57
|
-
<NestedRouteLink id="link"
|
|
57
|
+
<NestedRouteLink id="link" path="/buttons">
|
|
58
58
|
Buttons
|
|
59
59
|
</NestedRouteLink>
|
|
60
60
|
),
|
|
@@ -75,7 +75,7 @@ describe('NestedRouteLink', () => {
|
|
|
75
75
|
injector,
|
|
76
76
|
rootElement,
|
|
77
77
|
jsxElement: (
|
|
78
|
-
<NestedRouteLink id="link"
|
|
78
|
+
<NestedRouteLink id="link" path="/users/:id" params={{ id: '42' }}>
|
|
79
79
|
User 42
|
|
80
80
|
</NestedRouteLink>
|
|
81
81
|
),
|
|
@@ -95,7 +95,7 @@ describe('NestedRouteLink', () => {
|
|
|
95
95
|
injector,
|
|
96
96
|
rootElement,
|
|
97
97
|
jsxElement: (
|
|
98
|
-
<NestedRouteLink id="link"
|
|
98
|
+
<NestedRouteLink id="link" path="/users/:userId/posts/:postId" params={{ userId: '1', postId: '99' }}>
|
|
99
99
|
Post
|
|
100
100
|
</NestedRouteLink>
|
|
101
101
|
),
|
|
@@ -106,6 +106,46 @@ describe('NestedRouteLink', () => {
|
|
|
106
106
|
)
|
|
107
107
|
})
|
|
108
108
|
})
|
|
109
|
+
|
|
110
|
+
it('Should append a serialized query string to the rendered href', async () => {
|
|
111
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
112
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
113
|
+
|
|
114
|
+
initializeShadeRoot({
|
|
115
|
+
injector,
|
|
116
|
+
rootElement,
|
|
117
|
+
jsxElement: (
|
|
118
|
+
<NestedRouteLink id="link" path="/buttons" query={{ page: 2 }}>
|
|
119
|
+
Buttons
|
|
120
|
+
</NestedRouteLink>
|
|
121
|
+
),
|
|
122
|
+
})
|
|
123
|
+
await flushUpdates()
|
|
124
|
+
|
|
125
|
+
const link = document.getElementById('link') as HTMLAnchorElement
|
|
126
|
+
expect(link.getAttribute('href')?.startsWith('/buttons?')).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('Should append the hash segment to the rendered href', async () => {
|
|
131
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
132
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
133
|
+
|
|
134
|
+
initializeShadeRoot({
|
|
135
|
+
injector,
|
|
136
|
+
rootElement,
|
|
137
|
+
jsxElement: (
|
|
138
|
+
<NestedRouteLink id="link" path="/buttons" hash="overview">
|
|
139
|
+
Buttons
|
|
140
|
+
</NestedRouteLink>
|
|
141
|
+
),
|
|
142
|
+
})
|
|
143
|
+
await flushUpdates()
|
|
144
|
+
|
|
145
|
+
const link = document.getElementById('link') as HTMLAnchorElement
|
|
146
|
+
expect(link.getAttribute('href')).toBe('/buttons#overview')
|
|
147
|
+
})
|
|
148
|
+
})
|
|
109
149
|
})
|
|
110
150
|
|
|
111
151
|
describe('Type utilities', () => {
|
|
@@ -219,13 +259,13 @@ describe('Type utilities', () => {
|
|
|
219
259
|
describe('TypedNestedRouteLinkProps', () => {
|
|
220
260
|
it('Should make params optional for paths without parameters', () => {
|
|
221
261
|
type Props = TypedNestedRouteLinkProps<'/buttons'>
|
|
222
|
-
expectTypeOf<Props['
|
|
262
|
+
expectTypeOf<Props['path']>().toEqualTypeOf<'/buttons'>()
|
|
223
263
|
expectTypeOf<Props>().toExtend<{ params?: Record<string, string> }>()
|
|
224
264
|
})
|
|
225
265
|
|
|
226
266
|
it('Should require params for parameterized paths', () => {
|
|
227
267
|
type Props = TypedNestedRouteLinkProps<'/users/:id'>
|
|
228
|
-
expectTypeOf<Props['
|
|
268
|
+
expectTypeOf<Props['path']>().toEqualTypeOf<'/users/:id'>()
|
|
229
269
|
expectTypeOf<Props>().toExtend<{ params: { id: string } }>()
|
|
230
270
|
})
|
|
231
271
|
|
|
@@ -236,20 +276,20 @@ describe('Type utilities', () => {
|
|
|
236
276
|
})
|
|
237
277
|
|
|
238
278
|
describe('NestedRouteLink param inference', () => {
|
|
239
|
-
it('Should infer params as optional when
|
|
279
|
+
it('Should infer params as optional when path has no parameters', () => {
|
|
240
280
|
expectTypeOf(NestedRouteLink).parameter(0).toHaveProperty('params')
|
|
241
281
|
expectTypeOf(NestedRouteLink<'/buttons'>)
|
|
242
282
|
.parameter(0)
|
|
243
283
|
.toExtend<{ params?: Record<string, string> }>()
|
|
244
284
|
})
|
|
245
285
|
|
|
246
|
-
it('Should infer params as required when
|
|
286
|
+
it('Should infer params as required when path has a parameter', () => {
|
|
247
287
|
expectTypeOf(NestedRouteLink<'/users/:id'>)
|
|
248
288
|
.parameter(0)
|
|
249
289
|
.toExtend<{ params: { id: string } }>()
|
|
250
290
|
})
|
|
251
291
|
|
|
252
|
-
it('Should infer multiple params from
|
|
292
|
+
it('Should infer multiple params from path', () => {
|
|
253
293
|
expectTypeOf(NestedRouteLink<'/users/:userId/posts/:postId'>)
|
|
254
294
|
.parameter(0)
|
|
255
295
|
.toExtend<{ params: { userId: string; postId: string } }>()
|
|
@@ -257,7 +297,7 @@ describe('Type utilities', () => {
|
|
|
257
297
|
})
|
|
258
298
|
|
|
259
299
|
describe('createNestedRouteLink', () => {
|
|
260
|
-
it('Should constrain
|
|
300
|
+
it('Should constrain path to valid route paths', () => {
|
|
261
301
|
type Routes = {
|
|
262
302
|
'/': TestRoute & {
|
|
263
303
|
children: {
|
|
@@ -267,7 +307,7 @@ describe('Type utilities', () => {
|
|
|
267
307
|
}
|
|
268
308
|
|
|
269
309
|
const AppLink = createNestedRouteLink<Routes>()
|
|
270
|
-
expectTypeOf(AppLink).parameter(0).toHaveProperty('
|
|
310
|
+
expectTypeOf(AppLink).parameter(0).toHaveProperty('path')
|
|
271
311
|
})
|
|
272
312
|
|
|
273
313
|
it('Should reject invalid paths', () => {
|
|
@@ -281,7 +321,7 @@ describe('Type utilities', () => {
|
|
|
281
321
|
|
|
282
322
|
const AppLink = createNestedRouteLink<Routes>()
|
|
283
323
|
// @ts-expect-error -- '/nonexistent' is not a valid route path
|
|
284
|
-
AppLink({
|
|
324
|
+
AppLink({ path: '/nonexistent' })
|
|
285
325
|
})
|
|
286
326
|
|
|
287
327
|
it('Should require params for parameterized routes in the tree', () => {
|
|
@@ -343,13 +383,41 @@ describe('Type utilities', () => {
|
|
|
343
383
|
'/users/:userId': usersRoute,
|
|
344
384
|
},
|
|
345
385
|
},
|
|
346
|
-
} satisfies Record<string, NestedRoute<any>>
|
|
386
|
+
} satisfies Record<string, NestedRoute<any, any, any>>
|
|
347
387
|
|
|
348
388
|
const AppLink = createNestedRouteLink<typeof routes>()
|
|
349
|
-
expectTypeOf(AppLink).parameter(0).toHaveProperty('
|
|
389
|
+
expectTypeOf(AppLink).parameter(0).toHaveProperty('path')
|
|
350
390
|
expectTypeOf(AppLink<'/users/:userId'>)
|
|
351
391
|
.parameter(0)
|
|
352
392
|
.toExtend<{ params: { userId: string } }>()
|
|
353
393
|
})
|
|
394
|
+
|
|
395
|
+
it('Should enforce a declared required query shape on the link', () => {
|
|
396
|
+
const routes = {
|
|
397
|
+
'/list': {
|
|
398
|
+
component: () => null as unknown as JSX.Element,
|
|
399
|
+
query: (raw): { page: number } | null => (typeof raw.page === 'number' ? { page: raw.page } : null),
|
|
400
|
+
},
|
|
401
|
+
} satisfies Record<string, NestedRoute<any, any, any>>
|
|
402
|
+
|
|
403
|
+
const AppLink = createNestedRouteLink<typeof routes>()
|
|
404
|
+
expectTypeOf(AppLink<'/list'>)
|
|
405
|
+
.parameter(0)
|
|
406
|
+
.toExtend<{ query: { page: number } }>()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('Should narrow hash on the link to the declared literal tuple', () => {
|
|
410
|
+
const routes = {
|
|
411
|
+
'/tabs': {
|
|
412
|
+
component: () => null as unknown as JSX.Element,
|
|
413
|
+
hash: ['overview', 'details'] as const,
|
|
414
|
+
},
|
|
415
|
+
} satisfies Record<string, NestedRoute<any, any, any>>
|
|
416
|
+
|
|
417
|
+
const AppLink = createNestedRouteLink<typeof routes>()
|
|
418
|
+
expectTypeOf(AppLink<'/tabs'>)
|
|
419
|
+
.parameter(0)
|
|
420
|
+
.toExtend<{ hash?: 'overview' | 'details' }>()
|
|
421
|
+
})
|
|
354
422
|
})
|
|
355
423
|
})
|
|
@@ -1,31 +1,58 @@
|
|
|
1
|
-
import { compileRoute } from '../compile-route.js'
|
|
2
1
|
import type { ChildrenList } from '../models/children-list.js'
|
|
3
2
|
import type { PartialElement } from '../models/partial-element.js'
|
|
4
3
|
import { LocationService } from '../services/location-service.js'
|
|
5
4
|
import { createComponent } from '../shade-component.js'
|
|
6
5
|
import { Shade } from '../shade.js'
|
|
7
|
-
import
|
|
6
|
+
import { buildNestedNavigateUrl } from './nested-navigate.js'
|
|
7
|
+
import type {
|
|
8
|
+
ExtractRouteHash,
|
|
9
|
+
ExtractRouteQuery,
|
|
10
|
+
ExtractRoutePaths,
|
|
11
|
+
RouteAt,
|
|
12
|
+
TypedHashArg,
|
|
13
|
+
TypedParamsArg,
|
|
14
|
+
TypedQueryArg,
|
|
15
|
+
} from './nested-route-types.js'
|
|
8
16
|
import type { NestedRoute } from './nested-router.js'
|
|
9
17
|
|
|
10
18
|
/**
|
|
11
19
|
* Props for the NestedRouteLink component.
|
|
12
|
-
* Combines SPA navigation
|
|
20
|
+
* Combines SPA navigation with parameter compilation for nested routes,
|
|
21
|
+
* plus optional query string and hash fragment composition.
|
|
13
22
|
*/
|
|
14
23
|
export type NestedRouteLinkProps = {
|
|
15
|
-
|
|
24
|
+
path: string
|
|
16
25
|
params?: Record<string, string>
|
|
26
|
+
query?: Record<string, unknown>
|
|
27
|
+
hash?: string
|
|
17
28
|
} & PartialElement<Omit<HTMLAnchorElement, 'onclick' | 'href'>>
|
|
18
29
|
|
|
19
30
|
/**
|
|
20
31
|
* Props for a type-safe nested route link.
|
|
21
32
|
* When the path contains parameters (e.g. `:id`), the `params` prop becomes required.
|
|
33
|
+
* `query` and `hash` remain loose (untyped) on this variant; use
|
|
34
|
+
* {@link createNestedRouteLink} to narrow them against a specific route tree.
|
|
22
35
|
* @typeParam TPath - A specific route path string
|
|
23
36
|
*/
|
|
24
37
|
export type TypedNestedRouteLinkProps<TPath extends string> = {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
path: TPath
|
|
39
|
+
query?: Record<string, unknown>
|
|
40
|
+
hash?: string
|
|
41
|
+
} & TypedParamsArg<TPath> &
|
|
42
|
+
PartialElement<Omit<HTMLAnchorElement, 'onclick' | 'href'>>
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Props for a route-tree-aware nested route link. Narrows `query` and `hash`
|
|
46
|
+
* against the specific route at `TPath` in `TRoutes`.
|
|
47
|
+
* @typeParam TRoutes - The route tree
|
|
48
|
+
* @typeParam TPath - A composed route path within `TRoutes`
|
|
49
|
+
*/
|
|
50
|
+
export type TreeAwareNestedRouteLinkProps<
|
|
51
|
+
TRoutes extends Record<string, NestedRoute<any, any, any>>,
|
|
52
|
+
TPath extends string,
|
|
53
|
+
> = { path: TPath } & TypedParamsArg<TPath> &
|
|
54
|
+
TypedQueryArg<ExtractRouteQuery<RouteAt<TRoutes, TPath>>> &
|
|
55
|
+
TypedHashArg<ExtractRouteHash<RouteAt<TRoutes, TPath>>> &
|
|
29
56
|
PartialElement<Omit<HTMLAnchorElement, 'onclick' | 'href'>>
|
|
30
57
|
|
|
31
58
|
const _NestedRouteLink = Shade<NestedRouteLinkProps>({
|
|
@@ -37,8 +64,8 @@ const _NestedRouteLink = Shade<NestedRouteLinkProps>({
|
|
|
37
64
|
textDecoration: 'inherit',
|
|
38
65
|
},
|
|
39
66
|
render: ({ children, props, injector, useHostProps }) => {
|
|
40
|
-
const {
|
|
41
|
-
const resolvedUrl =
|
|
67
|
+
const { path, params, query, hash } = props
|
|
68
|
+
const resolvedUrl = buildNestedNavigateUrl({ path, params, query, hash })
|
|
42
69
|
|
|
43
70
|
useHostProps({
|
|
44
71
|
href: resolvedUrl,
|
|
@@ -58,13 +85,15 @@ const _NestedRouteLink = Shade<NestedRouteLinkProps>({
|
|
|
58
85
|
* type-safe route parameter compilation.
|
|
59
86
|
*
|
|
60
87
|
* Intercepts click events to use `history.pushState` for client-side navigation,
|
|
61
|
-
*
|
|
88
|
+
* compiles parameterized routes (e.g. `/users/:id`) when `params` is provided,
|
|
89
|
+
* serializes `query` to the URL search string and appends `hash` when set.
|
|
62
90
|
*
|
|
63
|
-
* Route parameters are automatically inferred from the `
|
|
64
|
-
* - `
|
|
65
|
-
* - `
|
|
91
|
+
* Route parameters are automatically inferred from the `path` pattern:
|
|
92
|
+
* - `path="/buttons"` — `params` is optional
|
|
93
|
+
* - `path="/users/:id"` — `params: { id: string }` is required
|
|
66
94
|
*
|
|
67
|
-
* For additional URL validation against a route tree
|
|
95
|
+
* For additional URL validation against a route tree (including `query` and
|
|
96
|
+
* `hash` narrowing), use {@link createNestedRouteLink}.
|
|
68
97
|
*/
|
|
69
98
|
export const NestedRouteLink = _NestedRouteLink as unknown as <TPath extends string = string>(
|
|
70
99
|
props: TypedNestedRouteLinkProps<TPath>,
|
|
@@ -72,9 +101,10 @@ export const NestedRouteLink = _NestedRouteLink as unknown as <TPath extends str
|
|
|
72
101
|
) => JSX.Element
|
|
73
102
|
|
|
74
103
|
/**
|
|
75
|
-
* Creates a type-safe wrapper around NestedRouteLink constrained to a specific
|
|
76
|
-
* The returned component has the same runtime behavior but narrows
|
|
77
|
-
* valid route paths,
|
|
104
|
+
* Creates a type-safe wrapper around NestedRouteLink constrained to a specific
|
|
105
|
+
* route tree. The returned component has the same runtime behavior but narrows
|
|
106
|
+
* `path` to only accept valid route paths, requires `params` when the route
|
|
107
|
+
* has parameters, and enforces the route's declared `query` and `hash` schemas.
|
|
78
108
|
*
|
|
79
109
|
* @typeParam TRoutes - The route tree type (use `typeof yourRoutes`)
|
|
80
110
|
* @returns A narrowed NestedRouteLink component
|
|
@@ -83,19 +113,14 @@ export const NestedRouteLink = _NestedRouteLink as unknown as <TPath extends str
|
|
|
83
113
|
* ```typescript
|
|
84
114
|
* const AppLink = createNestedRouteLink<typeof appRoutes>()
|
|
85
115
|
*
|
|
86
|
-
*
|
|
87
|
-
* <AppLink
|
|
88
|
-
*
|
|
89
|
-
* // TypeScript error: invalid path
|
|
90
|
-
* <AppLink href="/nonexistent">Error!</AppLink>
|
|
91
|
-
*
|
|
92
|
-
* // Params required for parameterized routes
|
|
93
|
-
* <AppLink href="/users/:id" params={{ id: '123' }}>User</AppLink>
|
|
116
|
+
* <AppLink path="/buttons">Buttons</AppLink>
|
|
117
|
+
* <AppLink path="/users/:id" params={{ id: '123' }}>User</AppLink>
|
|
118
|
+
* <AppLink path="/users/:id" params={{ id: '1' }} query={{ tab: 'profile' }} hash="notes">User</AppLink>
|
|
94
119
|
* ```
|
|
95
120
|
*/
|
|
96
|
-
export const createNestedRouteLink = <TRoutes extends Record<string, NestedRoute<any>>>() => {
|
|
121
|
+
export const createNestedRouteLink = <TRoutes extends Record<string, NestedRoute<any, any, any>>>() => {
|
|
97
122
|
return _NestedRouteLink as unknown as <TPath extends ExtractRoutePaths<TRoutes>>(
|
|
98
|
-
props:
|
|
123
|
+
props: TreeAwareNestedRouteLinkProps<TRoutes, TPath>,
|
|
99
124
|
children?: ChildrenList,
|
|
100
125
|
) => JSX.Element
|
|
101
126
|
}
|
|
@@ -12,10 +12,10 @@ export type ConcatPaths<Parent extends string, Child extends string> = Parent ex
|
|
|
12
12
|
* Recursively extracts all valid full URL paths from a nested route tree.
|
|
13
13
|
* @typeParam T - A record of route patterns to NestedRoute definitions
|
|
14
14
|
*/
|
|
15
|
-
export type ExtractRoutePaths<T extends Record<string, NestedRoute<any>>> = {
|
|
15
|
+
export type ExtractRoutePaths<T extends Record<string, NestedRoute<any, any, any>>> = {
|
|
16
16
|
[K in keyof T & string]:
|
|
17
17
|
| K
|
|
18
|
-
| (T[K] extends { children: infer C extends Record<string, NestedRoute<any>> }
|
|
18
|
+
| (T[K] extends { children: infer C extends Record<string, NestedRoute<any, any, any>> }
|
|
19
19
|
? ConcatPaths<K, ExtractRoutePaths<C> & string>
|
|
20
20
|
: never)
|
|
21
21
|
}[keyof T & string]
|
|
@@ -40,3 +40,83 @@ export type ExtractRouteParams<T extends string> = T extends `${string}:${infer
|
|
|
40
40
|
export type UrlTree<TPaths extends string> = {
|
|
41
41
|
[key: string]: TPaths | UrlTree<TPaths>
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* A query string validator function for a nested route. Receives the deserialized
|
|
46
|
+
* query string object and returns either the typed query value or `null` when
|
|
47
|
+
* the URL's query does not match the route's expected shape.
|
|
48
|
+
* @typeParam TQuery - The typed query shape the route expects
|
|
49
|
+
*/
|
|
50
|
+
export type QueryValidator<TQuery> = (raw: Record<string, unknown>) => TQuery | null
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A readonly tuple of string literals describing the allowed URL hash values
|
|
54
|
+
* for a nested route.
|
|
55
|
+
*/
|
|
56
|
+
export type HashLiterals = readonly string[]
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extracts the typed query shape declared by a {@link NestedRoute}'s `query` validator.
|
|
60
|
+
* Resolves to `never` when the route has no `query` validator.
|
|
61
|
+
* @typeParam R - A NestedRoute value type
|
|
62
|
+
*/
|
|
63
|
+
export type ExtractRouteQuery<R> = R extends { query: QueryValidator<infer Q> } ? Q : never
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extracts the typed hash literal union declared by a {@link NestedRoute}'s `hash` tuple.
|
|
67
|
+
* Resolves to `never` when the route has no `hash` declaration.
|
|
68
|
+
* @typeParam R - A NestedRoute value type
|
|
69
|
+
*/
|
|
70
|
+
export type ExtractRouteHash<R> = R extends { hash: infer H extends HashLiterals } ? H[number] : never
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Walks a nested route tree and resolves the route value at the given composed path.
|
|
74
|
+
* Used to surface per-route `query` / `hash` metadata to call-site helpers.
|
|
75
|
+
* @typeParam TRoutes - The route tree
|
|
76
|
+
* @typeParam TPath - A full composed route path (as produced by {@link ExtractRoutePaths})
|
|
77
|
+
*/
|
|
78
|
+
export type RouteAt<TRoutes extends Record<string, NestedRoute<any, any, any>>, TPath extends string> = {
|
|
79
|
+
[K in keyof TRoutes & string]: TPath extends K
|
|
80
|
+
? TRoutes[K]
|
|
81
|
+
: K extends '/'
|
|
82
|
+
? TRoutes[K] extends { children: infer C extends Record<string, NestedRoute<any, any, any>> }
|
|
83
|
+
? RouteAt<C, TPath>
|
|
84
|
+
: never
|
|
85
|
+
: TPath extends `${K}${infer Rest}`
|
|
86
|
+
? TRoutes[K] extends { children: infer C extends Record<string, NestedRoute<any, any, any>> }
|
|
87
|
+
? RouteAt<C, Rest>
|
|
88
|
+
: never
|
|
89
|
+
: never
|
|
90
|
+
}[keyof TRoutes & string]
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds the `params` portion of a typed navigation / link argument.
|
|
94
|
+
* Resolves to `{ params?: Record<string, string> }` when the path has no `:param`
|
|
95
|
+
* segments and `{ params: {...} }` when at least one is required.
|
|
96
|
+
* @typeParam TPath - A URL pattern string
|
|
97
|
+
*/
|
|
98
|
+
export type TypedParamsArg<TPath extends string> = string extends keyof ExtractRouteParams<TPath>
|
|
99
|
+
? { params?: Record<string, string> }
|
|
100
|
+
: { params: ExtractRouteParams<TPath> }
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds the `query` portion of a typed navigation / link argument.
|
|
104
|
+
* - `never` (route declared no schema) → property is forbidden.
|
|
105
|
+
* - All-optional schema → property itself is optional.
|
|
106
|
+
* - Any required key → property is required.
|
|
107
|
+
* @typeParam TQuery - The route's typed query shape
|
|
108
|
+
*/
|
|
109
|
+
export type TypedQueryArg<TQuery> = [TQuery] extends [never]
|
|
110
|
+
? { query?: never }
|
|
111
|
+
: // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- `{} extends T` is the idiomatic way to detect "all keys optional" on an arbitrary object shape.
|
|
112
|
+
{} extends TQuery
|
|
113
|
+
? { query?: TQuery }
|
|
114
|
+
: { query: TQuery }
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Builds the `hash` portion of a typed navigation / link argument.
|
|
118
|
+
* Hash is always optional at the call site (URL fragments are never required
|
|
119
|
+
* for correctness of navigation).
|
|
120
|
+
* @typeParam THash - The route's hash literal union
|
|
121
|
+
*/
|
|
122
|
+
export type TypedHashArg<THash> = [THash] extends [never] ? { hash?: never } : { hash?: THash }
|