@@ -17,6 +17,7 @@ export interface VueRoute {
1717 modes ?: string [ ]
1818 children : VueRoute [ ]
1919 meta ?: Record < string , unknown >
20+ [ key : string ] : unknown
2021}
2122
2223export interface VueRouterEmitOptions {
@@ -29,6 +30,27 @@ export interface VueRouterEmitOptions {
2930
3031 /** Called when two routes resolve to the same generated name. */
3132 onDuplicateRouteName ?: ( name : string , file : string , existingFile : string ) => void
33+
34+ /**
35+ * Collapse mode arrays into single-value attributes.
36+ *
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.
42+ *
43+ * @example
44+ * // Input: route has modes: ['server']
45+ * toVueRouter4(tree, { attrs: { mode: ['client', 'server'] } })
46+ * // Output: { ..., mode: 'server' } (no `modes` property)
47+ *
48+ * @example
49+ * // Custom method-based routing
50+ * toVueRouter4(tree, { attrs: { method: ['get', 'post'] } })
51+ * // For a route with modes: ['get'] → { ..., method: 'get' }
52+ */
53+ attrs ?: Record < string , string [ ] >
3254}
3355
3456export interface Rou3Route {
@@ -95,10 +117,81 @@ function flattenTree(tree: RouteTree): FlatFileInfo[] {
95117
96118// --- Vue Router 4 ------------------------------------------------------------
97119
120+ /**
121+ * Cached intermediate result stored on the tree.
122+ * @internal
123+ */
124+ interface CachedVueRouterResult {
125+ /** The computed routes (used as template for cloning). */
126+ routes : VueRoute [ ]
127+ /** The options fingerprint — if options change, cache is invalid. */
128+ optionsKey : string
129+ }
130+
131+ /** Deep-clone a VueRoute array. */
132+ function cloneRoutes ( routes : VueRoute [ ] ) : VueRoute [ ] {
133+ return routes . map ( r => cloneRoute ( r ) )
134+ }
135+
136+ function cloneRoute ( route : VueRoute ) : VueRoute {
137+ const clone : VueRoute = {
138+ path : route . path ,
139+ file : route . file ,
140+ children : route . children . length ? cloneRoutes ( route . children ) : [ ] ,
141+ }
142+ if ( route . name !== undefined )
143+ clone . name = route . name
144+ if ( route . modes )
145+ clone . modes = [ ...route . modes ]
146+ if ( route . meta ) {
147+ clone . meta = { ...route . meta }
148+ if ( route . meta . groups )
149+ clone . meta . groups = [ ...( route . meta . groups as string [ ] ) ]
150+ }
151+ if ( route . components )
152+ clone . components = { ...route . components }
153+
154+ // Clone any extra attrs (e.g. mode, method)
155+ for ( const key of Object . keys ( route ) ) {
156+ if ( ! ( key in clone ) ) {
157+ clone [ key ] = route [ key ]
158+ }
159+ }
160+
161+ return clone
162+ }
163+
164+ function optionsToKey ( options ?: VueRouterEmitOptions ) : string {
165+ if ( ! options )
166+ return ''
167+ const parts : string [ ] = [ ]
168+ if ( options . getRouteName )
169+ parts . push ( 'n' )
170+ if ( options . onDuplicateRouteName )
171+ parts . push ( 'd' )
172+ if ( options . attrs ) {
173+ for ( const [ k , v ] of Object . entries ( options . attrs ) ) {
174+ parts . push ( `a:${ k } =${ v . join ( ',' ) } ` )
175+ }
176+ }
177+ return parts . join ( '|' )
178+ }
179+
98180/**
99181 * Convert a route tree to Vue Router 4 route definitions.
182+ *
183+ * Results are cached on the tree and deep-cloned on return, so mutations
184+ * to the returned array do not affect the cache. The cache is automatically
185+ * invalidated when `addFile` / `removeFile` mark the tree as dirty.
100186 */
101187export function toVueRouter4 ( tree : RouteTree , options ?: VueRouterEmitOptions ) : VueRoute [ ] {
188+ const key = optionsToKey ( options )
189+ const cached = ( tree as any ) [ '~cachedVueRouter' ] as CachedVueRouterResult | undefined
190+
191+ if ( ! tree [ '~dirty' ] && cached && cached . optionsKey === key ) {
192+ return cloneRoutes ( cached . routes )
193+ }
194+
102195 const fileInfos = flattenTree ( tree )
103196
104197 fileInfos . sort ( ( a , b ) =>
@@ -157,7 +250,13 @@ export function toVueRouter4(tree: RouteTree, options?: VueRouterEmitOptions): V
157250 parent . push ( route )
158251 }
159252
160- return prepareRoutes ( routes , undefined , options )
253+ const result = prepareRoutes ( routes , undefined , options )
254+
255+ // Cache on the tree
256+ ; ( tree as any ) [ '~cachedVueRouter' ] = { routes : result , optionsKey : key } satisfies CachedVueRouterResult
257+ tree [ '~dirty' ] = false
258+
259+ return cloneRoutes ( result )
161260}
162261
163262// --- rou3 --------------------------------------------------------------------
@@ -381,6 +480,7 @@ function prepareRoutes(
381480 names = new Map < string , string > ( ) ,
382481) : VueRoute [ ] {
383482 const getRouteName = options ?. getRouteName || defaultGetRouteName
483+ const attrs = options ?. attrs
384484
385485 for ( const route of routes ) {
386486 route . scoreSegments = computeScoreSegments ( route )
@@ -420,15 +520,35 @@ function prepareRoutes(
420520 out . components [ v . viewName ] = v . path
421521 }
422522
423- // Modes
523+ // Collect modes from all sibling files
424524 const allModes = new Set < string > ( )
425525 for ( const f of route . siblingFiles ) {
426526 if ( f . modes ) {
427527 for ( const m of f . modes ) allModes . add ( m )
428528 }
429529 }
430- if ( allModes . size > 0 )
530+
531+ // Apply attrs: collapse modes into named attributes
532+ if ( attrs && allModes . size > 0 ) {
533+ let modesConsumed = false
534+ for ( const [ attrName , attrValues ] of Object . entries ( attrs ) ) {
535+ const matched = attrValues . filter ( v => allModes . has ( v ) )
536+ if ( matched . length === 1 ) {
537+ out [ attrName ] = matched [ 0 ]
538+ modesConsumed = true
539+ }
540+ else if ( matched . length > 1 ) {
541+ out [ attrName ] = matched
542+ modesConsumed = true
543+ }
544+ }
545+ // Only emit `modes` if not fully consumed by attrs
546+ if ( ! modesConsumed )
547+ out . modes = [ ...allModes ]
548+ }
549+ else if ( allModes . size > 0 ) {
431550 out . modes = [ ...allModes ]
551+ }
432552
433553 return out
434554 } )
0 commit comments