@@ -8,19 +8,34 @@ const collator = new Intl.Collator('en-US')
88
99// --- Types -------------------------------------------------------------------
1010
11- export interface VueRoute {
11+ /**
12+ * Maps an `attrs` record to typed optional properties on the route.
13+ *
14+ * Each key becomes an optional property whose value is a single literal
15+ * from the array. The attr is only set when exactly one mode matches.
16+ *
17+ * @example
18+ * type R = InferAttrs<{ mode: ['client', 'server'] }>
19+ * // { mode?: 'client' | 'server' }
20+ */
21+ export type InferAttrs < T extends Record < string , string [ ] > > = {
22+ [ K in keyof T ] ?: T [ K ] [ number ]
23+ }
24+
25+ // eslint-disable-next-line ts/no-empty-object-type
26+ export type VueRoute < Attrs extends Record < string , string [ ] > = { } > = {
1227 name ?: string
1328 path : string
1429 file ?: string
1530 /** Named view files keyed by view name. Only present when named views exist. */
1631 components ?: Record < string , string >
1732 modes ?: string [ ]
18- children : VueRoute [ ]
33+ children : VueRoute < Attrs > [ ]
1934 meta ?: Record < string , unknown >
20- [ key : string ] : unknown
21- }
35+ } & ( [ keyof Attrs ] extends [ never ] ? { [ key : string ] : unknown } : InferAttrs < Attrs > )
2236
23- export interface VueRouterEmitOptions {
37+ // eslint-disable-next-line ts/no-empty-object-type
38+ export interface VueRouterEmitOptions < Attrs extends Record < string , string [ ] > = { } > {
2439 /**
2540 * Custom route name generator.
2641 * Receives `/`-separated name (e.g. `'users/id'`), returns final name.
@@ -32,13 +47,16 @@ export interface VueRouterEmitOptions {
3247 onDuplicateRouteName ?: ( name : string , file : string , existingFile : string ) => void
3348
3449 /**
35- * Collapse mode arrays into single-value attributes.
50+ * Collapse modes into single-value attributes.
51+ *
52+ * Each key becomes a typed top-level property on the route. When a route has
53+ * exactly one matching mode the attribute is set to that value string; when
54+ * none or multiple modes match, the attribute is omitted and the raw `modes`
55+ * array is emitted instead.
3656 *
37- * Each key becomes a top-level property on the route. Modes that match a
38- * value in the array are collapsed: when a route has exactly one matching
39- * mode, the attribute is set to that value string. When a route has multiple
40- * matching modes, the attribute is set to the array of matching modes.
41- * When none match, the attribute is omitted.
57+ * The return type of `toVueRouter4` infers typed properties from the attrs
58+ * definition so that, e.g., `attrs: { mode: ['client', 'server'] }` produces
59+ * routes with `mode?: 'client' | 'server'`.
4260 *
4361 * @example
4462 * // Input: route has modes: ['server']
@@ -50,7 +68,7 @@ export interface VueRouterEmitOptions {
5068 * toVueRouter4(tree, { attrs: { method: ['get', 'post'] } })
5169 * // For a route with modes: ['get'] → { ..., method: 'get' }
5270 */
53- attrs ?: Record < string , string [ ] >
71+ attrs ?: Attrs
5472}
5573
5674export interface Rou3Route {
@@ -161,7 +179,7 @@ function cloneRoute(route: VueRoute): VueRoute {
161179 return clone
162180}
163181
164- function optionsToKey ( options ?: VueRouterEmitOptions ) : string {
182+ function optionsToKey ( options ?: VueRouterEmitOptions < Record < string , string [ ] > > ) : string {
165183 if ( ! options )
166184 return ''
167185 const parts : string [ ] = [ ]
@@ -184,12 +202,16 @@ function optionsToKey(options?: VueRouterEmitOptions): string {
184202 * to the returned array do not affect the cache. The cache is automatically
185203 * invalidated when `addFile` / `removeFile` mark the tree as dirty.
186204 */
187- export function toVueRouter4 ( tree : RouteTree , options ?: VueRouterEmitOptions ) : VueRoute [ ] {
188- const key = optionsToKey ( options )
205+ export function toVueRouter4 < const Attrs extends Record < string , string [ ] > = never > (
206+ tree : RouteTree ,
207+ // eslint-disable-next-line ts/no-empty-object-type
208+ options ?: VueRouterEmitOptions < [ Attrs ] extends [ never ] ? { } : Attrs > ,
209+ ) : VueRoute < [ Attrs ] extends [ never ] ? { } : Attrs > [ ] { // eslint-disable-line ts/no-empty-object-type
210+ const key = optionsToKey ( options as VueRouterEmitOptions < Record < string , string [ ] > > )
189211 const cached = ( tree as any ) [ '~cachedVueRouter' ] as CachedVueRouterResult | undefined
190212
191213 if ( ! tree [ '~dirty' ] && cached && cached . optionsKey === key ) {
192- return cloneRoutes ( cached . routes )
214+ return cloneRoutes ( cached . routes ) as VueRoute < any > [ ]
193215 }
194216
195217 const fileInfos = flattenTree ( tree )
@@ -250,13 +272,13 @@ export function toVueRouter4(tree: RouteTree, options?: VueRouterEmitOptions): V
250272 parent . push ( route )
251273 }
252274
253- const result = prepareRoutes ( routes , undefined , options )
275+ const result = prepareRoutes ( routes , undefined , options as VueRouterEmitOptions < Record < string , string [ ] > > )
254276
255277 // Cache on the tree
256278 ; ( tree as any ) [ '~cachedVueRouter' ] = { routes : result , optionsKey : key } satisfies CachedVueRouterResult
257279 tree [ '~dirty' ] = false
258280
259- return cloneRoutes ( result )
281+ return cloneRoutes ( result ) as VueRoute < any > [ ]
260282}
261283
262284// --- rou3 --------------------------------------------------------------------
@@ -476,7 +498,7 @@ function defaultGetRouteName(rawName: string): string {
476498function prepareRoutes (
477499 routes : IntermediateRoute [ ] ,
478500 parent ?: IntermediateRoute ,
479- options ?: VueRouterEmitOptions ,
501+ options ?: VueRouterEmitOptions < Record < string , string [ ] > > ,
480502 names = new Map < string , string > ( ) ,
481503) : VueRoute [ ] {
482504 const getRouteName = options ?. getRouteName || defaultGetRouteName
@@ -537,10 +559,6 @@ function prepareRoutes(
537559 out [ attrName ] = matched [ 0 ]
538560 modesConsumed = true
539561 }
540- else if ( matched . length > 1 ) {
541- out [ attrName ] = matched
542- modesConsumed = true
543- }
544562 }
545563 // Only emit `modes` if not fully consumed by attrs
546564 if ( ! modesConsumed )
0 commit comments