@@ -125,47 +125,49 @@ function splitPath(path: string): string[] {
125125 return path . split ( '/' ) . filter ( Boolean ) ;
126126}
127127
128+ /**
129+ * React Router scoring weights.
130+ * https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts
131+ */
132+ const STATIC_SEGMENT_SCORE = 10 ;
133+ const DYNAMIC_SEGMENT_SCORE = 3 ;
134+ const SPLAT_PENALTY = - 2 ;
135+
136+ /** Computes a specificity score for a route pattern. */
137+ function computeScore ( pattern : string ) : number {
138+ const segments = splitPath ( pattern ) ;
139+ let score = 0 ;
140+
141+ for ( const segment of segments ) {
142+ if ( segment === '*' ) {
143+ score += SPLAT_PENALTY ;
144+ } else if ( segment . startsWith ( ':' ) ) {
145+ score += DYNAMIC_SEGMENT_SCORE ;
146+ } else {
147+ score += STATIC_SEGMENT_SCORE ;
148+ }
149+ }
150+
151+ return score ;
152+ }
153+
128154/**
129155 * Sorts route patterns by specificity (most specific first).
130- * Mimics React Router's ranking algorithm from computeScore():
156+ * Implements React Router's ranking algorithm from computeScore():
131157 * https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/router/utils.ts
132158 *
133- * React Router scoring: static=10, dynamic=3, splat=-2 penalty, index=+2 bonus
134- * Our simplified approach produces equivalent ordering:
135- * - Non-wildcard patterns are more specific than wildcard patterns
136- * - More segments = more specific
137- * - Among same-length patterns, more literal segments = more specific
138- * - Equal specificity: preserves manifest order (same as React Router)
159+ * React Router scoring: static=10, dynamic=3, splat=-2 penalty
160+ * Higher score = more specific pattern.
161+ * Equal scores preserve manifest order (same as React Router).
139162 *
140163 * Note: Users should order their manifest from most specific to least specific
141164 * when patterns have equal specificity (e.g., `/users/:id/settings` and `/:type/123/settings`).
142165 */
143166function sortBySpecificity ( manifest : string [ ] ) : string [ ] {
144167 return [ ...manifest ] . sort ( ( a , b ) => {
145- const aSegments = splitPath ( a ) ;
146- const bSegments = splitPath ( b ) ;
147- const aHasWildcard = aSegments . length > 0 && aSegments [ aSegments . length - 1 ] === '*' ;
148- const bHasWildcard = bSegments . length > 0 && bSegments [ bSegments . length - 1 ] === '*' ;
149-
150- // Non-wildcard patterns are more specific than wildcard patterns
151- if ( aHasWildcard !== bHasWildcard ) {
152- return aHasWildcard ? 1 : - 1 ;
153- }
154-
155- // For comparison, exclude wildcard from segment count
156- const aLen = aHasWildcard ? aSegments . length - 1 : aSegments . length ;
157- const bLen = bHasWildcard ? bSegments . length - 1 : bSegments . length ;
158-
159- // More segments = more specific
160- if ( aLen !== bLen ) {
161- return bLen - aLen ;
162- }
163-
164- // Same length: count literal segments (non-params, non-wildcards)
165- const aLiterals = aSegments . filter ( s => ! s . startsWith ( ':' ) && s !== '*' ) . length ;
166- const bLiterals = bSegments . filter ( s => ! s . startsWith ( ':' ) && s !== '*' ) . length ;
168+ const aScore = computeScore ( a ) ;
169+ const bScore = computeScore ( b ) ;
167170
168- // More literals = more specific (equal specificity preserves original order)
169- return bLiterals - aLiterals ;
171+ return bScore - aScore ;
170172 } ) ;
171173}
0 commit comments