Skip to content

Commit f2fb721

Browse files
Anoesjposva
andauthored
feat(types): add children route names to generated RouteNamedMap (#602)
Co-authored-by: Eduardo San Martin Morote <[email protected]>
1 parent 871b304 commit f2fb721

File tree

11 files changed

+208
-56
lines changed

11 files changed

+208
-56
lines changed

docs/.vitepress/twoslash-files.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,37 @@ declare module 'vue-router/auto-routes' {
88
ParamValueZeroOrOne,
99
} from 'vue-router'
1010
11+
/**
12+
* Route name map generated by unplugin-vue-router
13+
*/
1114
export interface RouteNamedMap {
12-
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>
15+
'/': RouteRecordInfo<
16+
'/',
17+
'/',
18+
Record<never, never>,
19+
Record<never, never>,
20+
never
21+
>
1322
'/users': RouteRecordInfo<
1423
'/users',
1524
'/users',
1625
Record<never, never>,
17-
Record<never, never>
26+
Record<never, never>,
27+
never
1828
>
1929
'/users/[id]': RouteRecordInfo<
2030
'/users/[id]',
2131
'/users/:id',
2232
{ id: ParamValue<true> },
23-
{ id: ParamValue<false> }
33+
{ id: ParamValue<false> },
34+
'/users/[id]/edit'
2435
>
2536
'/users/[id]/edit': RouteRecordInfo<
2637
'/users/[id]/edit',
2738
'/users/:id/edit',
2839
{ id: ParamValue<true> },
29-
{ id: ParamValue<false> }
40+
{ id: ParamValue<false> },
41+
never
3042
>
3143
}
3244
}

docs/.vitepress/twoslash/code/typed-router.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,33 @@ declare module 'vue-router/auto-routes' {
88
} from 'vue-router'
99

1010
export interface RouteNamedMap {
11-
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>
11+
'/': RouteRecordInfo<
12+
'/',
13+
'/',
14+
Record<never, never>,
15+
Record<never, never>,
16+
never
17+
>
1218
'/users': RouteRecordInfo<
1319
'/users',
1420
'/users',
1521
Record<never, never>,
16-
Record<never, never>
22+
Record<never, never>,
23+
never
1724
>
1825
'/users/[id]': RouteRecordInfo<
1926
'/users/[id]',
2027
'/users/:id',
2128
{ id: ParamValue<true> },
22-
{ id: ParamValue<false> }
29+
{ id: ParamValue<false> },
30+
'/users/[id]/edit'
2331
>
2432
'/users/[id]/edit': RouteRecordInfo<
2533
'/users/[id]/edit',
2634
'/users/:id/edit',
2735
{ id: ParamValue<true> },
28-
{ id: ParamValue<false> }
36+
{ id: ParamValue<false> },
37+
never
2938
>
3039
}
3140
}

docs/guide/typescript.md

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,17 @@ declare module 'vue-router/auto-routes' {
6060
// these are the raw param types (accept numbers, strings, booleans, etc)
6161
{ path: ParamValue<true> },
6262
// these are the normalized params as found in useRoute().params
63-
{ path: ParamValue<false> }
63+
{ path: ParamValue<false> },
64+
// this is a union of all children route names
65+
// if the route does not have nested routes, pass `never` or omit this generic entirely
66+
'custom-dynamic-child-name'
67+
>
68+
'custom-dynamic-child-name': RouteRecordInfo<
69+
'custom-dynamic-child-name',
70+
'/added-during-runtime/[...path]/child',
71+
{ path: ParamValue<true> },
72+
{ path: ParamValue<false> },
73+
never
6474
>
6575
}
6676
}
@@ -76,13 +86,20 @@ import { useRoute, type RouteLocationNormalizedLoaded } from 'vue-router'
7686
// ---cut-end---
7787
// @errors: 2322 2339
7888
// @moduleResolution: bundler
79-
// these are all valid
80-
const userWithIdCasted = useRoute() as RouteLocationNormalizedLoaded<'/users/[id]'>
81-
userWithIdCasted.params.id
82-
const userWithIdTypeParam = useRoute<'/users/[id]'>()
83-
userWithIdTypeParam.params.id
84-
// 👇 this one is the easiest to write because it autocompletes
85-
const userWithIdParam = useRoute('/users/[id]')
86-
userWithIdParam.params
87-
// ^?
89+
// These are all valid ways to get a typed route and return the
90+
// provided route's and any of its child routes' typings.
91+
// Note that `/users/[id]/edit` is a child route
92+
// of `/users/[id]` in this example.
93+
94+
// Not recommended, since this leaves out any child routes' typings.
95+
const userRouteWithIdCasted =
96+
useRoute() as RouteLocationNormalizedLoaded<'/users/[id]'>
97+
userRouteWithIdCasted.params.id
98+
// Better way, but no autocompletion.
99+
const userRouteWithIdTypeParam = useRoute<'/users/[id]'>()
100+
userRouteWithIdTypeParam.params.id
101+
// 👇 This one is the easiest to write because it autocompletes.
102+
const userRouteWithIdParam = useRoute('/users/[id]')
103+
userRouteWithIdParam.name
104+
// ^?
88105
```

docs/introduction.md

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -102,31 +102,39 @@ After adding this plugin, **start the dev server** (usually `npm run dev`) **to
102102
// It's recommended to commit this file.
103103
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
104104

105-
import type {
106-
RouteRecordInfo,
107-
ParamValue,
108-
ParamValueOneOrMore,
109-
ParamValueZeroOrMore,
110-
ParamValueZeroOrOne,
111-
} from 'vue-router'
112-
113105
declare module 'vue-router/auto-routes' {
106+
import type {
107+
RouteRecordInfo,
108+
ParamValue,
109+
ParamValueOneOrMore,
110+
ParamValueZeroOrMore,
111+
ParamValueZeroOrOne,
112+
} from 'vue-router'
113+
114114
/**
115115
* Route name map generated by unplugin-vue-router
116116
*/
117117
export interface RouteNamedMap {
118-
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>
118+
'/': RouteRecordInfo<
119+
'/',
120+
'/',
121+
Record<never, never>,
122+
Record<never, never>,
123+
never
124+
>
119125
'/about': RouteRecordInfo<
120126
'/about',
121127
'/about',
122128
Record<never, never>,
123-
Record<never, never>
129+
Record<never, never>,
130+
never
124131
>
125132
'/users/[id]': RouteRecordInfo<
126-
'/[id]',
127-
'/:id',
133+
'/users/[id]',
134+
'/users/:id',
128135
{ id: ParamValue<true> },
129-
{ id: ParamValue<false> }
136+
{ id: ParamValue<false> },
137+
never
130138
>
131139
}
132140
}

package.json

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
"vitest": "vitest --typecheck",
112112
"docs": "vitepress dev docs",
113113
"docs:build": "vitepress build docs",
114+
"docs:preview": "vitepress preview docs",
114115
"lint": "prettier -c '{src,test,e2e,examples,playground}/**/*.{ts,vue}'",
115116
"play": "npm -C playground run dev",
116117
"play:build": "npm -C playground run build",
@@ -148,7 +149,7 @@
148149
"yaml": "^2.8.0"
149150
},
150151
"peerDependencies": {
151-
"vue-router": "^4.4.0"
152+
"vue-router": "^4.5.1"
152153
},
153154
"peerDependenciesMeta": {
154155
"vue-router": {
@@ -199,5 +200,14 @@
199200
"vuefire": "^3.2.1",
200201
"webpack": "^5.99.9",
201202
"yorkie": "^2.0.0"
203+
},
204+
"pnpm": {
205+
"onlyBuiltDependencies": [
206+
"@parcel/watcher",
207+
"esbuild",
208+
"protobufjs",
209+
"vue-demi",
210+
"yorkie"
211+
]
202212
}
203213
}

playground/src/router.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,15 @@ declare module 'vue-router/auto-routes' {
3131
'custom-dynamic-name',
3232
'/added-during-runtime/[...path]',
3333
{ path: ParamValue<true> },
34-
{ path: ParamValue<false> }
34+
{ path: ParamValue<false> },
35+
'custom-dynamic-child-name'
36+
>
37+
'custom-dynamic-child-name': RouteRecordInfo<
38+
'custom-dynamic-child-name',
39+
'/added-during-runtime/[...path]/child',
40+
{ path: ParamValue<true> },
41+
{ path: ParamValue<false> },
42+
never
3543
>
3644
}
3745
}

playground/typed-router.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ declare module 'vue-router/auto-routes' {
1818
* Route name map generated by unplugin-vue-router
1919
*/
2020
export interface RouteNamedMap {
21-
'/(test-group)': RouteRecordInfo<'/(test-group)', '/', Record<never, never>, Record<never, never>>,
21+
'/(test-group)': RouteRecordInfo<'/(test-group)', '/', Record<never, never>, Record<never, never>, '/(test-group)/test-group-child'>,
2222
'/(test-group)/test-group-child': RouteRecordInfo<'/(test-group)/test-group-child', '/test-group-child', Record<never, never>, Record<never, never>>,
2323
'home': RouteRecordInfo<'home', '/', Record<never, never>, Record<never, never>>,
2424
'/[name]': RouteRecordInfo<'/[name]', '/:name', { name: ParamValue<true> }, { name: ParamValue<false> }>,
@@ -27,7 +27,7 @@ declare module 'vue-router/auto-routes' {
2727
'/@[profileId]': RouteRecordInfo<'/@[profileId]', '/@:profileId', { profileId: ParamValue<true> }, { profileId: ParamValue<false> }>,
2828
'/about': RouteRecordInfo<'/about', '/about', Record<never, never>, Record<never, never>>,
2929
'/about.extra.nested': RouteRecordInfo<'/about.extra.nested', '/about/extra/nested', Record<never, never>, Record<never, never>>,
30-
'/articles': RouteRecordInfo<'/articles', '/articles', Record<never, never>, Record<never, never>>,
30+
'/articles': RouteRecordInfo<'/articles', '/articles', Record<never, never>, Record<never, never>, '/articles/' | '/articles/[id]' | '/articles/[id]+'>,
3131
'/articles/': RouteRecordInfo<'/articles/', '/articles', Record<never, never>, Record<never, never>>,
3232
'/articles/[id]': RouteRecordInfo<'/articles/[id]', '/articles/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
3333
'/articles/[id]+': RouteRecordInfo<'/articles/[id]+', '/articles/:id+', { id: ParamValueOneOrMore<true> }, { id: ParamValueOneOrMore<false> }>,

src/codegen/generateRouteMap.spec.ts

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -156,9 +156,9 @@ describe('generateRouteNamedMap', () => {
156156
tree.insert('a/[id]/index', 'a/[id]/index.vue')
157157
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
158158
"export interface RouteNamedMap {
159-
'/a': RouteRecordInfo<'/a', '/a', Record<never, never>, Record<never, never>>,
159+
'/a': RouteRecordInfo<'/a', '/a', Record<never, never>, Record<never, never>, '/a/' | '/a/[id]/' | '/a/[id]'>,
160160
'/a/': RouteRecordInfo<'/a/', '/a', Record<never, never>, Record<never, never>>,
161-
'/a/[id]': RouteRecordInfo<'/a/[id]', '/a/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
161+
'/a/[id]': RouteRecordInfo<'/a/[id]', '/a/:id', { id: ParamValue<true> }, { id: ParamValue<false> }, '/a/[id]/'>,
162162
'/a/[id]/': RouteRecordInfo<'/a/[id]/', '/a/:id', { id: ParamValue<true> }, { id: ParamValue<false> }>,
163163
}"
164164
`)
@@ -172,12 +172,75 @@ describe('generateRouteNamedMap', () => {
172172
expect(child.fullPath).toBe('/child')
173173
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
174174
"export interface RouteNamedMap {
175-
'/parent': RouteRecordInfo<'/parent', '/', Record<never, never>, Record<never, never>>,
175+
'/parent': RouteRecordInfo<'/parent', '/', Record<never, never>, Record<never, never>, '/parent/child'>,
176176
'/parent/child': RouteRecordInfo<'/parent/child', '/child', Record<never, never>, Record<never, never>>,
177177
}"
178178
`)
179179
})
180180

181+
it('adds children route names', () => {
182+
const tree = new PrefixTree(DEFAULT_OPTIONS)
183+
tree.insert('parent', 'parent.vue')
184+
tree.insert('parent/child', 'parent/child.vue')
185+
tree.insert('parent/child/subchild', 'parent/child/subchild.vue')
186+
tree.insert(
187+
'parent/child/subchild/grandchild',
188+
'parent/child/subchild/grandchild.vue'
189+
)
190+
tree.insert('parent/other-child', 'parent/other-child.vue')
191+
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
192+
"export interface RouteNamedMap {
193+
'/parent': RouteRecordInfo<'/parent', '/parent', Record<never, never>, Record<never, never>, '/parent/child' | '/parent/child/subchild/grandchild' | '/parent/other-child' | '/parent/child/subchild'>,
194+
'/parent/child': RouteRecordInfo<'/parent/child', '/parent/child', Record<never, never>, Record<never, never>, '/parent/child/subchild/grandchild' | '/parent/child/subchild'>,
195+
'/parent/child/subchild': RouteRecordInfo<'/parent/child/subchild', '/parent/child/subchild', Record<never, never>, Record<never, never>, '/parent/child/subchild/grandchild'>,
196+
'/parent/child/subchild/grandchild': RouteRecordInfo<'/parent/child/subchild/grandchild', '/parent/child/subchild/grandchild', Record<never, never>, Record<never, never>>,
197+
'/parent/other-child': RouteRecordInfo<'/parent/other-child', '/parent/other-child', Record<never, never>, Record<never, never>>,
198+
}"
199+
`)
200+
})
201+
202+
it('skips children without components', () => {
203+
const tree = new PrefixTree(DEFAULT_OPTIONS)
204+
tree.insert('parent', 'parent.vue')
205+
tree.insert('parent/child/a/b/c', 'parent/child/a/b/c.vue')
206+
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
207+
"export interface RouteNamedMap {
208+
'/parent': RouteRecordInfo<'/parent', '/parent', Record<never, never>, Record<never, never>, '/parent/child/a/b/c'>,
209+
'/parent/child/a/b/c': RouteRecordInfo<'/parent/child/a/b/c', '/parent/child/a/b/c', Record<never, never>, Record<never, never>>,
210+
}"
211+
`)
212+
})
213+
214+
it('skips the children in the index route', () => {
215+
const tree = new PrefixTree(DEFAULT_OPTIONS)
216+
tree.insert('parent/index', 'parent/index.vue')
217+
tree.insert('parent/child', 'parent/child.vue')
218+
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
219+
"export interface RouteNamedMap {
220+
'/parent/': RouteRecordInfo<'/parent/', '/parent', Record<never, never>, Record<never, never>>,
221+
'/parent/child': RouteRecordInfo<'/parent/child', '/parent/child', Record<never, never>, Record<never, never>>,
222+
}"
223+
`)
224+
})
225+
226+
it('does not mix children of an adjacent route', () => {
227+
const tree = new PrefixTree(DEFAULT_OPTIONS)
228+
tree.insert('parent/index', 'parent/index.vue')
229+
tree.insert('parent/a/index', 'parent/a/index.vue')
230+
tree.insert('parent/a/b', 'parent/a/b.vue')
231+
tree.insert('parent/a/b/index', 'parent/a/b/index.vue')
232+
tree.insert('parent/a/b/c', 'parent/a/b/c.vue')
233+
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
234+
"export interface RouteNamedMap {
235+
'/parent/': RouteRecordInfo<'/parent/', '/parent', Record<never, never>, Record<never, never>>,
236+
'/parent/a/': RouteRecordInfo<'/parent/a/', '/parent/a', Record<never, never>, Record<never, never>>,
237+
'/parent/a/b': RouteRecordInfo<'/parent/a/b', '/parent/a/b', Record<never, never>, Record<never, never>, '/parent/a/b/' | '/parent/a/b/c'>,
238+
'/parent/a/b/': RouteRecordInfo<'/parent/a/b/', '/parent/a/b', Record<never, never>, Record<never, never>>,
239+
'/parent/a/b/c': RouteRecordInfo<'/parent/a/b/c', '/parent/a/b/c', Record<never, never>, Record<never, never>>,
240+
}"
241+
`)
242+
})
243+
181244
it('adds params from the path option', () => {
182245
const tree = new PrefixTree(
183246
resolveOptions({
@@ -204,10 +267,10 @@ describe('generateRouteNamedMap', () => {
204267
tree.insert('(group)/a', 'a.vue')
205268

206269
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
207-
"export interface RouteNamedMap {
208-
'/(group)/a': RouteRecordInfo<'/(group)/a', '/a', Record<never, never>, Record<never, never>>,
209-
}"
210-
`)
270+
"export interface RouteNamedMap {
271+
'/(group)/a': RouteRecordInfo<'/(group)/a', '/a', Record<never, never>, Record<never, never>>,
272+
}"
273+
`)
211274
})
212275

213276
it('ignores nested folder names in parentheses', () => {
@@ -216,10 +279,10 @@ describe('generateRouteNamedMap', () => {
216279
tree.insert('(group)/(subgroup)/c', 'c.vue')
217280

218281
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
219-
"export interface RouteNamedMap {
220-
'/(group)/(subgroup)/c': RouteRecordInfo<'/(group)/(subgroup)/c', '/c', Record<never, never>, Record<never, never>>,
221-
}"
222-
`)
282+
"export interface RouteNamedMap {
283+
'/(group)/(subgroup)/c': RouteRecordInfo<'/(group)/(subgroup)/c', '/c', Record<never, never>, Record<never, never>>,
284+
}"
285+
`)
223286
})
224287

225288
it('treats files named with parentheses as index inside static folder', () => {
@@ -228,10 +291,10 @@ describe('generateRouteNamedMap', () => {
228291
tree.insert('folder/(group)', 'folder/(group).vue')
229292

230293
expect(formatExports(generateRouteNamedMap(tree))).toMatchInlineSnapshot(`
231-
"export interface RouteNamedMap {
232-
'/folder/(group)': RouteRecordInfo<'/folder/(group)', '/folder', Record<never, never>, Record<never, never>>,
233-
}"
234-
`)
294+
"export interface RouteNamedMap {
295+
'/folder/(group)': RouteRecordInfo<'/folder/(group)', '/folder', Record<never, never>, Record<never, never>>,
296+
}"
297+
`)
235298
})
236299
})
237300

0 commit comments

Comments
 (0)