Skip to content

Commit 515f484

Browse files
committed
fix: correct route ordering for group nodes with inflated scores
Replace min/avg comparator with element-by-element score comparison ported from pathParserRanker.ts. Add path depth tiebreaker so routes like /:username/settings sort before /:username when group node inflation makes their scores element-by-element equal.
1 parent 797f55d commit 515f484

File tree

3 files changed

+74
-63
lines changed

3 files changed

+74
-63
lines changed

packages/router/src/unplugin/codegen/__snapshots__/generateRouteResolver.spec.ts.snap

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -128,14 +128,29 @@ const __route_2 = normalizeRouteRecord({
128128
parent: __route_1,
129129
})
130130
const __route_3 = normalizeRouteRecord({
131+
name: '/[username]/(user-posts)/posts/(posts-list)',
132+
path: new MatcherPatternPathDynamic(
133+
/^\\/([^/]+?)\\/posts$/i,
134+
{
135+
username: [/* no parser */],
136+
},
137+
[1,"posts"],
138+
/* trailingSlash */
139+
),
140+
components: {
141+
'default': () => import('[username]/(user-posts)/posts/(posts-list).vue')
142+
},
143+
parent: __route_0,
144+
})
145+
const __route_4 = normalizeRouteRecord({
131146
/* (removed) name: false */
132147
path: __route_0.path,
133148
components: {
134149
'default': () => import('[username]/(user-settings)/_parent.vue')
135150
},
136151
parent: __route_0,
137152
})
138-
const __route_4 = normalizeRouteRecord({
153+
const __route_5 = normalizeRouteRecord({
139154
name: '/[username]/(user-settings)/settings',
140155
path: new MatcherPatternPathDynamic(
141156
/^\\/([^/]+?)\\/settings$/i,
@@ -148,28 +163,13 @@ const __route_4 = normalizeRouteRecord({
148163
components: {
149164
'default': () => import('[username]/(user-settings)/settings.vue')
150165
},
151-
parent: __route_3,
152-
})
153-
const __route_5 = normalizeRouteRecord({
154-
name: '/[username]/(user-posts)/posts/(posts-list)',
155-
path: new MatcherPatternPathDynamic(
156-
/^\\/([^/]+?)\\/posts$/i,
157-
{
158-
username: [/* no parser */],
159-
},
160-
[1,"posts"],
161-
/* trailingSlash */
162-
),
163-
components: {
164-
'default': () => import('[username]/(user-posts)/posts/(posts-list).vue')
165-
},
166-
parent: __route_0,
166+
parent: __route_4,
167167
})
168168
169169
export const resolver = createFixedResolver([
170-
__route_5, // /:username/posts
170+
__route_3, // /:username/posts
171+
__route_5, // /:username/settings
171172
__route_2, // /:username
172-
__route_4, // /:username/settings
173173
])
174174
"
175175
`;

packages/router/src/unplugin/codegen/generateRouteResolver.spec.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,10 +420,10 @@ describe('generateRouteResolver', () => {
420420
})
421421
422422
export const resolver = createFixedResolver([
423-
__route_1, // /a
424423
__route_2, // /a/about
425-
__route_4, // /p/:id
424+
__route_1, // /a
426425
__route_5, // /p/:id/details
426+
__route_4, // /p/:id
427427
__route_3, // /p/:id
428428
__route_0, // /a
429429
])
@@ -513,12 +513,12 @@ describe('generateRouteResolver', () => {
513513
})
514514
515515
export const resolver = createFixedResolver([
516-
__route_3, // /a/b
517516
__route_4, // /a/b/c
518517
__route_5, // /a/b/e
519-
__route_1, // /a
518+
__route_3, // /a/b
520519
__route_2, // /a/b
521520
__route_6, // /a/d
521+
__route_1, // /a
522522
__route_0, // /a
523523
])
524524
"
@@ -830,16 +830,15 @@ describe('generateRouteResolver', () => {
830830
'/prefix/static/suffix',
831831
'/prefix/sub-end',
832832
'/prefix/sub-:id-end/suffix',
833-
'/prefix/:id-end/suffix',
834833
'/prefix/sub-:id/suffix',
835834
'/prefix/sub-:id',
835+
'/prefix/sub-:opt?-end/suffix',
836+
'/prefix/sub-:opt?/suffix',
837+
'/prefix/sub-:opt?',
838+
'/prefix/:id-end/suffix',
836839
'/prefix/:id/suffix',
837840
'/prefix/:id',
838-
// this could be before /prefix/:id, but since it has a suffix, it works too
839-
'/prefix/sub-:opt?-end/suffix',
840841
'/prefix/:opt?-end/suffix',
841-
'/prefix/sub-:opt?/suffix',
842-
'/prefix/sub-:opt?', // FIXME: should be before /prefix/:id
843842
'/prefix/:opt?/suffix',
844843
'/prefix/:opt?',
845844
'/prefix/:repeat+/suffix',
@@ -956,8 +955,8 @@ describe('generateRouteResolver', () => {
956955
})
957956
958957
export const resolver = createFixedResolver([
959-
__route_1, // /a
960958
__route_2, // /a/b
959+
__route_1, // /a
961960
])
962961
"
963962
`)

packages/router/src/unplugin/codegen/generateRouteResolver.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -11,47 +11,53 @@ import {
1111
import { generatePageImport, formatMeta } from './generateRouteRecords'
1212

1313
/**
14-
* Compare two score arrays for sorting routes by priority.
15-
* Higher scores should come first (more specific routes).
14+
* Compare two score sub-arrays element by element.
15+
* Ported from pathParserRanker.ts compareScoreArray.
1616
*/
17-
function compareRouteScore(a: number[][], b: number[][]): number {
18-
const maxLength = Math.max(a.length, b.length)
17+
function compareScoreArray(a: number[], b: number[]): number {
18+
let i = 0
19+
while (i < a.length && i < b.length) {
20+
const diff = b[i] - a[i]
21+
if (diff) return diff
22+
i++
23+
}
1924

20-
for (let i = 0; i < maxLength; i++) {
21-
const aSegment = a[i] || []
22-
const bSegment = b[i] || []
25+
// if the shorter array is a pure static segment, it should sort first
26+
// otherwise sort the longer segment first
27+
if (a.length < b.length) {
28+
return a.length === 1 && a[0] === 300 ? -1 : 1
29+
} else if (a.length > b.length) {
30+
return b.length === 1 && b[0] === 300 ? 1 : -1
31+
}
2332

24-
// Compare segment by segment, but consider the "minimum" score of each segment
25-
// since mixed segments with params should rank lower than pure static
26-
const aMinScore = aSegment.length > 0 ? Math.min(...aSegment) : 0
27-
const bMinScore = bSegment.length > 0 ? Math.min(...bSegment) : 0
33+
return 0
34+
}
2835

29-
if (aMinScore !== bMinScore) {
30-
return bMinScore - aMinScore // Higher minimum score wins
31-
}
36+
function isLastScoreNegative(score: number[][]): boolean {
37+
const last = score[score.length - 1]
38+
return score.length > 0 && last[last.length - 1] < 0
39+
}
3240

33-
// If minimum scores are equal, compare average scores
34-
const aAvgScore =
35-
aSegment.length > 0
36-
? aSegment.reduce((sum, s) => sum + s, 0) / aSegment.length
37-
: 0
38-
const bAvgScore =
39-
bSegment.length > 0
40-
? bSegment.reduce((sum, s) => sum + s, 0) / bSegment.length
41-
: 0
42-
43-
if (aAvgScore !== bAvgScore) {
44-
return bAvgScore - aAvgScore // Higher average score wins
45-
}
41+
/**
42+
* Compare two score arrays for sorting routes by priority.
43+
* Ported from pathParserRanker.ts comparePathParserScore.
44+
*/
45+
function compareRouteScore(a: number[][], b: number[][]): number {
46+
let i = 0
47+
while (i < a.length && i < b.length) {
48+
const comp = compareScoreArray(a[i], b[i])
49+
if (comp) return comp
50+
i++
51+
}
4652

47-
// If averages are equal, prefer fewer subsegments (less complexity)
48-
if (aSegment.length !== bSegment.length) {
49-
return aSegment.length - bSegment.length
50-
}
53+
// handle wildcard (splat) routes
54+
if (Math.abs(b.length - a.length) === 1) {
55+
if (isLastScoreNegative(a)) return 1
56+
if (isLastScoreNegative(b)) return -1
5157
}
5258

53-
// If all segments are equal, prefer fewer segments (shorter paths)
54-
return a.length - b.length
59+
// more segments = more specific = sort first
60+
return b.length - a.length
5561
}
5662

5763
interface GenerateRouteResolverState {
@@ -94,7 +100,13 @@ ${records.join('\n\n')}
94100
95101
export const resolver = createFixedResolver([
96102
${state.matchableRecords
97-
.sort((a, b) => compareRouteScore(a.score, b.score))
103+
.sort(
104+
(a, b) =>
105+
compareRouteScore(a.score, b.score) ||
106+
// fallback to sorting by path depth to ensure consistent order between routes with the same score
107+
b.path.split('/').filter(Boolean).length -
108+
a.path.split('/').filter(Boolean).length
109+
)
98110
.map(
99111
({ varName, path }) =>
100112
` ${varName}, ${' '.repeat(String(state.id).length - varName.length + ROUTE_RECORD_VAR_PREFIX.length)}// ${path}`

0 commit comments

Comments
 (0)