Setting the file. One moment.
Route Normalize · Vercel Optimize · vercel-labs/agent-skills · Skills Docs
ContentsBack to the top of the page This file
Number 7.118
Position 118 of 155
Type JavaScript
Size 8 KB
Lines 220 lib/ route-normalize.mjs
JavaScript · 220 lines · 8 KB
|
5C
|
7B
|
7D
|
20
|
3C
|
3E
|
26)
|
localhost:
|
https
?
:
\/
|
\/\/
(?!
$
)
|
[:,
\$
\s]
$|
\.
segments
?
\/
|
__PAGE__
|
@
[a-z]
/
i
;
8
9 export function isSegmentTreePath ( route ) {
10 if ( typeof route !== 'string' ) return false ;
11 return / \. segments( \/ |$ )/ . test (route);
12 }
13
14 export function canonicalizeRoute ( route ) {
15 if ( typeof route !== 'string' || route. length === 0 ) return route;
16 if ( ! isSegmentTreePath (route)) return stripRouteGroups ( replaceBase64WithDynamic (route));
17
18 // Discard prefix (flag-state + dynamic value, both noise). Tail is the segment-tree node.
19 const idx = route. indexOf ( '.segments' );
20 if (idx < 0 ) return route;
21 const segmentTail = route. slice (idx + '.segments' . length ). replace ( / ^ \/ / , '' );
22
23 // _tree.segment / _index.segment have no per-segment tail — fall back to static head of prefix.
24 if (segmentTail === '_tree.segment' || segmentTail === '_index.segment' ) {
25 return canonicalizeBranchPrefix (route, idx);
26 }
27
28 const parts = segmentTail. split ( '/' ). filter (( p ) => p && ! isMetricLeaf (p));
29 if (parts. length === 0 ) return canonicalizeBranchPrefix (route, idx);
30 const decoded = parts. map (decodeSegmentToken). filter (Boolean);
31 if (decoded. length === 0 ) return canonicalizeBranchPrefix (route, idx);
32 // scan-codebase's routePath enumeration drops route groups — match it or the route→file lookup breaks.
33 return stripRouteGroups ( '/' + decoded. join ( '/' ));
34 }
35
36 function canonicalizeBranchPrefix ( route , segmentsIdx ) {
37 const prefix = route. slice ( 0 , segmentsIdx);
38 const parts = prefix. split ( '/' ). filter (Boolean);
39 // Drop trailing dynamic value (e.g. "london") and base64 flag-state — neither is a route segment.
40 const cleaned = parts
41 . filter (( p ) => ! isBase64FlagState (p))
42 . slice ( 0 , - 1 );
43 if (cleaned. length === 0 ) return prefix || '/' ;
44 return '/' + cleaned. join ( '/' );
45 }
46
47 function isMetricLeaf ( token ) {
48 return (
49 token === '__PAGE__.segment' ||
50 token === '_tree.segment' ||
51 token === '_index.segment' ||
52 token === '__page__.segment' ||
53 token. endsWith ( '.segment' ) && token. startsWith ( '_' )
54 );
55 }
56
57 // Conservative heuristic for `eyJoYXNTZXNzaW9uIjpmYWxzZX0`-shape tokens: URL-safe base64 alphabet, length ≥16, mixed case.
58 function isBase64FlagState ( token ) {
59 if ( typeof token !== 'string' ) return false ;
60 if (token. length < 16 ) return false ;
61 return / ^ [A-Za-z0-9_-] +$ / . test (token) && / [A-Z] / . test (token) && / [a-z] / . test (token);
62 }
63
64 // `/event/<base64>/teaser` → `/event/[*]/teaser`. Stripping entirely (old behavior) corrupted segment count and broke route→file lookup.
65 function replaceBase64WithDynamic ( route ) {
66 if ( typeof route !== 'string' || ! route. startsWith ( '/' )) return route;
67 const parts = route. split ( '/' );
68 let mutated = false ;
69 const replaced = parts. map (( p , i ) => {
70 if (i === 0 ) return p;
71 if ( isBase64FlagState (p)) { mutated = true ; return '[*]' ; }
72 return p;
73 });
74 if ( ! mutated) return route;
75 return replaced. join ( '/' ) || '/' ;
76 }
77
78 // Route groups `(default)` never appear in rendered URLs — scan-codebase drops them, so canonical form must match.
79 function stripRouteGroups ( route ) {
80 if ( typeof route !== 'string' || ! route. includes ( '(' )) return route;
81 const parts = route. split ( '/' );
82 const kept = parts. filter (( p ) => ! / ^ \( [ ^ )] + \) $ / . test (p));
83 const joined = kept. join ( '/' );
84 return joined. startsWith ( '/' ) ? (joined || '/' ) : '/' + joined;
85 }
86
87 // $d$X → [X] · $oc$X → [[...X]] · $c$X → [...X] · !K…p → (group) · metric-leaves → dropped.
88 function decodeSegmentToken ( token ) {
89 if ( isMetricLeaf (token)) return '' ;
90
91 let t = token. endsWith ( '.segment' ) ? token. slice ( 0 , - '.segment' . length ) : token;
92
93 if ( / ^ \$ d \$ / . test (t)) return `[${ t . slice ( 3 ) }]` ;
94 if ( / ^ \$ oc \$ / . test (t)) return `[[...${ t . slice ( 4 ) }]]` ;
95 if ( / ^ \$ c \$ / . test (t)) return `[...${ t . slice ( 3 ) }]` ;
96
97 // `!` is segment-tree marker; body is base64 of `(default)` etc. Accept only when decoded looks like `(name)`.
98 if (t. startsWith ( '!' ) && t. length > 1 ) {
99 const body = t. slice ( 1 );
100 try {
101 const decoded = Buffer. from (body, 'base64' ). toString ( 'utf-8' );
102 if ( / ^ \( . * \) $ / . test (decoded)) return decoded;
103 } catch {
104 /* fall through on decode failure */
105 }
106 }
107
108 return t;
109 }
110
111 export function candidateKey ( candidate ) {
112 const route = candidate?.route ?? candidate?.hostname ?? null ;
113 const kind = candidate?.kind ?? '?' ;
114 const canonical = route ? canonicalizeRoute (route) : '<account>' ;
115 return `${ kind }::${ canonical }` ;
116 }
117
118 // Records alternates so the brief shows "all 4 cities collapse here" rather than a single per-city dupe.
119 export function mergeCandidates ( a , b ) {
120 if ( ! a) return b;
121 if ( ! b) return a;
122 const winner = (b.priority ?? 0 ) > (a.priority ?? 0 ) ? b : a;
123 const loser = winner === a ? b : a;
124 const altRoutes = new Set ([
125 ... (Array. isArray (winner.aliasRoutes) ? winner.aliasRoutes : []),
126 ... (Array. isArray (loser.aliasRoutes) ? loser.aliasRoutes : []),
127 ]);
128 if (loser.route && loser.route !== winner.route) altRoutes. add (loser.route);
129 return {
130 ... winner,
131 // Canonicalize so briefs/reports/deep-dives see the clean path.
132 route: canonicalizeRoute (winner.route),
133 aliasRoutes: [ ... altRoutes]. sort (),
134 mergedCount: (winner.mergedCount ?? 1 ) + (loser.mergedCount ?? 1 ),
135 };
136 }
137
138 // Input assumed sorted priority desc; output preserves that order. Account-scope pass through unchanged.
139 export function dedupeCandidates ( candidates ) {
140 const byKey = new Map ();
141 const order = [];
142 const dropped = [];
143 for ( const c of candidates) {
144 if ( ! c || c.scope === 'account' || ( ! c.route && ! c.hostname)) {
145 order. push (c);
146 continue ;
147 }
148 const key = candidateKey (c);
149 if (byKey. has (key)) {
150 const merged = mergeCandidates (byKey. get (key), c);
151 byKey. set (key, merged);
152 dropped. push ({
153 candidate: c,
154 mergedInto: key,
155 reason: 'duplicate of higher-priority sibling (same source route after canonicalization)' ,
156 });
157 } else {
158 byKey. set (key, { ... c, route: c.route ? canonicalizeRoute (c.route) : c.route });
159 order. push ({ __key: key });
160 }
161 }
162 const deduped = order. map (( c ) => (c && c.__key ? byKey. get (c.__key) : c));
163 return { deduped, dropped };
164 }
165
166 export function isLikelyNextRouteShape ( route ) {
167 return typeof route === 'string' && route. length > 0 && ! ROUTE_SHAPE_RE . test (route);
168 }
169
170 export function routeShapeWarning ( route , signals = {}) {
171 return routeShapeWarnings (route, signals)[ 0 ] ?? null ;
172 }
173
174 export function routeShapeWarnings ( route , signals = {}) {
175 if ( typeof route !== 'string' || route. length === 0 ) return [];
176 const warnings = [];
177 if ( ROUTE_SHAPE_RE . test (route)) warnings. push ( 'route-shape:suspicious-metric-label' );
178 const first = firstRouteSegment ( canonicalizeRoute (route));
179 if (first && shouldWarnUnknownFirstSegment (first, signals)) {
180 warnings. push ( `route-shape:unknown-first-segment:${ first }` );
181 }
182 return warnings;
183 }
184
185 export function withRouteShapeWarnings ( candidate , signals = {}) {
186 const warnings = routeShapeWarnings (candidate?.route, signals);
187 if (warnings. length === 0 ) return candidate;
188 return {
189 ... candidate,
190 warnings: [ ...new Set ([ ... (Array. isArray (candidate.warnings) ? candidate.warnings : []), ... warnings])],
191 };
192 }
193
194 function firstRouteSegment ( route ) {
195 if ( typeof route !== 'string' ) return null ;
196 return route. split ( '/' ). filter (Boolean)[ 0 ] ?? null ;
197 }
198
199 function shouldWarnUnknownFirstSegment ( first , signals ) {
200 const exempt = new Set ([ '_next' , '_vercel' , 'api' , '.well-known' ]);
201 if (exempt. has (first)) return false ;
202 const known = knownFirstSegments (signals);
203 if (known.size === 0 ) return false ;
204 if ([ ... known]. some (isDynamicPlaceholder)) return false ;
205 return ! known. has (first);
206 }
207
208 function knownFirstSegments ( signals ) {
209 const out = new Set ();
210 const routes = signals.codebase?.routes ?? [];
211 for ( const route of routes) {
212 const first = firstRouteSegment (route?.routePath);
213 if (first) out. add (first);
214 }
215 return out;
216 }
217
218 function isDynamicPlaceholder ( segment ) {
219 return / ^ \[ . * \] $ / . test (segment);
220 }