Setting the file. One moment.
Workspace Resolver · Vercel Optimize · vercel-labs/agent-skills · Skills Docs
ContentsBack to the top of the page — line 218
This file
Number 7.153
Position 153 of 155
Type JavaScript
Size 17 KB
Lines 521 lib/ workspace-resolver.mjs
JavaScript · 521 lines · 17 KB
pureBarrelDepth:
3
,
12 suffixFanoutDepth: 2 ,
13 perSpecifierCap: 3 ,
14 };
15 const SOURCE_EXTENSIONS = new Set ([ '.ts' , '.tsx' , '.js' , '.jsx' , '.mjs' , '.cjs' ]);
16 const EXTENSIONS = [ '' , '.ts' , '.tsx' , '.js' , '.jsx' , '.mjs' , '.cjs' ];
17 const INDEX_FILES = [ 'index.ts' , 'index.tsx' , 'index.js' , 'index.jsx' , 'index.mjs' ];
18 const SUFFIX_FANOUT_RE = /( ^| \/ )(content | data | loader | fetch | service | metadata | actions) \. tsx ?$ / ;
19 const EXPORT_FORWARD_RE = /export \s + (?:type \s + ) ? (?: \* | \* \s + as \s + [A-Za-z_$][\w$] *| \{ [ ^ }] * \} ) \s + from \s + ['"][ ^ '"\n] + ['"]\s * ; ? / gs ;
20
21 export async function detectMonorepoRoot ( startDir ) {
22 let dir = pathResolve (startDir);
23 for ( let depth = 0 ; depth < 15 ; depth ++ ) {
24 if ( await fileExists ( join (dir, 'pnpm-workspace.yaml' ))) return dir;
25 const pkg = await tryReadJson ( join (dir, 'package.json' ));
26 if (pkg && (Array. isArray (pkg.workspaces) || Array. isArray (pkg.workspaces?.packages))) {
27 return dir;
28 }
29 const parent = dirname (dir);
30 if (parent === dir) return null ;
31 dir = parent;
32 }
33 return null ;
34 }
35
36 // Zero-dependency — pnpm-workspace.yaml shape is predictable; not pulling in js-yaml.
37 export async function readWorkspaceGlobs ( monorepoRoot ) {
38 const pnpmPath = join (monorepoRoot, 'pnpm-workspace.yaml' );
39 if ( await fileExists (pnpmPath)) {
40 const text = await readFile (pnpmPath, 'utf-8' );
41 return parsePnpmWorkspaceYaml (text);
42 }
43 const pkg = await tryReadJson ( join (monorepoRoot, 'package.json' ));
44 if (Array. isArray (pkg?.workspaces)) return pkg.workspaces;
45 if (Array. isArray (pkg?.workspaces?.packages)) return pkg.workspaces.packages;
46 return [];
47 }
48
49 // Handles `packages:` block with `- glob` entries. Not full YAML grammar.
50 export function parsePnpmWorkspaceYaml ( text ) {
51 const out = [];
52 let inPackages = false ;
53 for ( const rawLine of text. split ( ' \n ' )) {
54 const line = rawLine. replace ( /# . *$ / , '' ). trimEnd ();
55 if ( ! line. trim ()) continue ;
56 if ( / ^ packages \s * :/ . test (line)) { inPackages = true ; continue ; }
57 if ( ! inPackages) continue ;
58 if ( ! / ^ \s / . test (line)) { inPackages = false ; continue ; }
59 const m = line. match ( / ^ \s * - \s + ['"] ? ( [ ^ '"\s] + ) ['"] ? \s *$ / );
60 if (m) out. push (m[ 1 ]);
61 }
62 return out;
63 }
64
65 export async function listWorkspacePackages ( monorepoRoot ) {
66 const globs = await readWorkspaceGlobs (monorepoRoot);
67 const dirs = new Set ();
68 for ( const g of globs) {
69 const expanded = await expandWorkspaceGlob (monorepoRoot, g);
70 for ( const d of expanded) dirs. add (d);
71 }
72 const out = [];
73 for ( const dir of dirs) {
74 const pkg = await tryReadJson ( join (dir, 'package.json' ));
75 if (pkg?.name) out. push ({ name: pkg.name, dir, pkg });
76 }
77 return out. sort (( a , b ) => a.name. localeCompare (b.name));
78 }
79
80 // Handles workspace-shape globs only. `**` collapses to one level — npm/pnpm don't document deep `**`.
81 async function expandWorkspaceGlob ( root , glob ) {
82 const parts = glob. replace ( / \\ / g , '/' ). split ( '/' );
83 return await expandParts (root, parts);
84 }
85
86 async function expandParts ( currentDir , parts ) {
87 if (parts. length === 0 ) return [currentDir];
88 const [ head , ... rest ] = parts;
89 if (head === '' || head === '.' ) return await expandParts (currentDir, rest);
90 if (head === '*' || head === '**' ) {
91 let entries = [];
92 try {
93 entries = await readdir (currentDir, { withFileTypes: true });
94 } catch { return []; }
95 const childDirs = entries. filter (( e ) => e. isDirectory ()). map (( e ) => join (currentDir, e.name));
96 const out = [];
97 for ( const d of childDirs) {
98 const more = await expandParts (d, rest);
99 out. push ( ... more);
100 }
101 return out;
102 }
103 const next = join (currentDir, head);
104 try {
105 const s = await stat (next);
106 if ( ! s. isDirectory ()) return [];
107 } catch {
108 return [];
109 }
110 return await expandParts (next, rest);
111 }
112
113 export function buildResolver ( packages ) {
114 const byName = new Map ();
115 for ( const p of packages) {
116 byName. set (p.name, buildPackageLookup (p));
117 }
118 return function resolveSpecifier ( specifier ) {
119 if ( typeof specifier !== 'string' || ! specifier. length ) return null ;
120 // Longest-name match first so `@vercel/foo-bar` wins over `@vercel/foo`.
121 const candidates = [ ... byName. keys ()]
122 . filter (( name ) => specifier === name || specifier. startsWith (name + '/' ))
123 . sort (( a , b ) => b. length - a. length );
124 if (candidates. length === 0 ) return null ;
125 const pkgName = candidates[ 0 ];
126 const subpath = specifier === pkgName ? '.' : './' + specifier. slice (pkgName. length + 1 );
127 const lookup = byName. get (pkgName);
128 return lookup. resolveSubpath (subpath);
129 };
130 }
131
132 // Node spec: pattern key has exactly one `*`; target may have one or zero.
133 function buildPackageLookup ( p ) {
134 const exact = new Map ();
135 const wildcards = [];
136 const exports = p.pkg.exports;
137 if ( exports && typeof exports === 'object' && ! Array. isArray ( exports )) {
138 for ( const [ key , value ] of Object. entries ( exports )) {
139 const target = pickConditionalTarget (value);
140 if ( typeof target !== 'string' ) continue ;
141 if (key. includes ( '*' )) {
142 const keyStarIdx = key. indexOf ( '*' );
143 if (keyStarIdx !== key. lastIndexOf ( '*' )) continue ;
144 wildcards. push ({
145 keyPrefix: key. slice ( 0 , keyStarIdx),
146 keySuffix: key. slice (keyStarIdx + 1 ),
147 valueTemplate: target,
148 });
149 } else {
150 exact. set (key, target);
151 }
152 }
153 }
154 return {
155 resolveSubpath ( subpath ) {
156 const exactHit = exact. get (subpath);
157 if (exactHit) return joinPackagePath (p.dir, exactHit);
158 for ( const w of wildcards) {
159 if (subpath. startsWith (w.keyPrefix) && subpath. endsWith (w.keySuffix)) {
160 const star = subpath. slice (w.keyPrefix. length , subpath. length - w.keySuffix. length );
161 if ( ! star) continue ;
162 const target = w.valueTemplate. replace ( '*' , star);
163 return joinPackagePath (p.dir, target);
164 }
165 }
166 // Unsafe to guess when no exports declared.
167 if (exact.size === 0 && wildcards. length === 0 && subpath !== '.' ) {
168 return null ;
169 }
170 return null ;
171 },
172 };
173 }
174
175 // Condition order matches what Next.js / Vite / esbuild would resolve.
176 function pickConditionalTarget ( value ) {
177 if ( typeof value === 'string' ) return value;
178 if (Array. isArray (value)) {
179 for ( const item of value) {
180 const target = pickConditionalTarget (item);
181 if ( typeof target === 'string' ) return target;
182 }
183 return null ;
184 }
185 if ( ! value || typeof value !== 'object' || Array. isArray (value)) return null ;
186 for ( const cond of [ 'default' , 'import' , 'node' , 'browser' , 'require' , 'types' ]) {
187 const v = value[cond];
188 if ( typeof v === 'string' ) return v;
189 }
190 return null ;
191 }
192
193 export async function resolveWorkspaceImports ( sourceFilePath , resolver , options = {}) {
194 let text;
195 try {
196 text = await readFile (sourceFilePath, 'utf-8' );
197 } catch {
198 return [];
199 }
200 const opts = { ... DEFAULT_RESOLVE_OPTIONS , ... options };
201 const refs = extractModuleReferences (text);
202 const out = [];
203 const seen = new Set ();
204 for ( const ref of refs) {
205 const resolved = await resolveModuleSpecifier (sourceFilePath, ref.specifier, resolver);
206 if ( ! resolved) continue ;
207 const expanded = await expandResolvedSpecifier (resolved, ref.importedNames, resolver, opts);
208 for ( const file of expanded) {
209 if (seen. has (file)) continue ;
210 seen. add (file);
211 out. push (file);
212 }
213 }
214 return out;
215 }
216
217 // Skips CommonJS `require('foo')` and template-literal dynamic imports (statically unresolvable).
218 export function extractImportSpecifiers ( text ) {
219 return [ ...new Set ( extractModuleReferences (text). map (( ref ) => ref.specifier))];
220 }
221
222 function joinPackagePath ( packageDir , relativeTarget ) {
223 return join (packageDir, relativeTarget. replace ( / ^ \.\/ / , '' ));
224 }
225
226 async function expandResolvedSpecifier ( startFile , importedNames , resolver , opts ) {
227 const out = [];
228 const seen = new Set ();
229 const barrelVisited = new Set ();
230 const fanoutVisited = new Set ();
231
232 const add = ( file ) => {
233 if (seen. has (file)) return false ;
234 if (out. length > 0 && out. length - 1 >= opts.perSpecifierCap) return false ;
235 seen. add (file);
236 out. push (file);
237 return true ;
238 };
239
240 add (startFile);
241 await expandPureBarrel (startFile, importedNames, 0 );
242 const fanoutSeeds = out. slice ();
243 for ( const file of fanoutSeeds) {
244 await expandSuffixFanout (file, 0 );
245 }
246 return out;
247
248 async function expandPureBarrel ( file , requestedNames , depth ) {
249 if (depth >= opts.pureBarrelDepth) return ;
250 if (barrelVisited. has (file)) return ;
251 barrelVisited. add (file);
252 const text = await tryReadText (file);
253 if (text == null || ! isPureBarrel (text)) return ;
254 const refs = await selectRelevantForwards (file, extractExportForwardRefs (text), requestedNames, resolver);
255 for ( const { ref , next } of refs) {
256 if ( ! add (next)) return ;
257 await expandPureBarrel (next, requestedNamesForForward (ref, requestedNames), depth + 1 );
258 }
259 }
260
261 async function expandSuffixFanout ( file , depth ) {
262 if (depth >= opts.suffixFanoutDepth) return ;
263 if ( ! isSuffixFanoutFile (file)) return ;
264 const visitKey = `${ file }:${ depth }` ;
265 if (fanoutVisited. has (visitKey)) return ;
266 fanoutVisited. add (visitKey);
267 const text = await tryReadText (file);
268 if (text == null ) return ;
269 for ( const ref of extractModuleReferences (text)) {
270 const next = await resolveModuleSpecifier (file, ref.specifier, resolver);
271 if ( ! next) continue ;
272 if ( ! add (next)) return ;
273 if ( isSuffixFanoutFile (next)) await expandSuffixFanout (next, depth + 1 );
274 }
275 }
276 }
277
278 async function selectRelevantForwards ( fromFile , refs , requestedNames , resolver ) {
279 const resolved = [];
280 for ( const [ index , ref ] of refs. entries ()) {
281 const next = await resolveModuleSpecifier (fromFile, ref.specifier, resolver);
282 if ( ! next) continue ;
283 let score = requestedNames && requestedNames.size > 0
284 ? forwardRelevanceScore (ref, requestedNames, refs. length )
285 : 1 ;
286 if (requestedNames && requestedNames.size > 0 && await fileExportsAnyName (next, requestedNames)) {
287 score = Math. max (score, 75 );
288 }
289 resolved. push ({ ref, next, index, score });
290 }
291 if ( ! requestedNames || requestedNames.size === 0 ) return resolved;
292 const ranked = resolved
293 . filter (( x ) => x.score > 0 )
294 . sort (( a , b ) => b.score - a.score || a.index - b.index);
295 return ranked. length > 0 ? ranked : resolved;
296 }
297
298 function forwardRelevanceScore ( ref , requestedNames , siblingCount ) {
299 if ( ! requestedNames || requestedNames.size === 0 ) return 1 ;
300 if (ref.exportedNames) {
301 for ( const name of requestedNames) {
302 if (ref.exportedNames. has (name)) return 100 ;
303 }
304 }
305 if ( specifierMatchesNames (ref.specifier, requestedNames)) return 50 ;
306 return siblingCount === 1 ? 1 : 0 ;
307 }
308
309 function requestedNamesForForward ( ref , requestedNames ) {
310 if ( ! requestedNames || requestedNames.size === 0 ) return null ;
311 if (ref.star) return requestedNames;
312 const out = new Set ();
313 for ( const name of requestedNames) {
314 const source = ref.sourceNamesByExported?. get (name);
315 if (source) out. add (source);
316 }
317 return out.size > 0 ? out : requestedNames;
318 }
319
320 async function resolveModuleSpecifier ( fromFile , specifier , resolver ) {
321 const raw = specifier. startsWith ( '.' )
322 ? join ( dirname (fromFile), specifier)
323 : resolver (specifier);
324 if ( ! raw) return null ;
325 return await resolveExistingPath (raw);
326 }
327
328 async function resolveExistingPath ( basePath ) {
329 for ( const ext of EXTENSIONS ) {
330 const candidate = ext === '' ? basePath : basePath + ext;
331 if ( ! isSourcePath (candidate)) continue ;
332 if ( await isFile (candidate)) return candidate;
333 }
334 for ( const indexFile of INDEX_FILES ) {
335 const candidate = join (basePath, indexFile);
336 if ( await isFile (candidate)) return candidate;
337 }
338 return null ;
339 }
340
341 function extractModuleReferences ( text ) {
342 return [
343 ... extractImportReferences (text),
344 ... extractExportForwardRefs (text). map (( ref ) => ({
345 specifier: ref.specifier,
346 importedNames: ref.star ? null : ref.exportedNames,
347 })),
348 ... extractDynamicImportReferences (text),
349 ];
350 }
351
352 function extractImportReferences ( text ) {
353 const out = [];
354 const fromRe = /import \s + (?:type \s + ) ? ( [\s\S] *? ) \s + from \s + ['"] ( [ ^ '"\n] + ) ['"] / g ;
355 let m;
356 while ((m = fromRe. exec (text)) !== null ) {
357 out. push ({ specifier: m[ 2 ], importedNames: parseImportNames (m[ 1 ]) });
358 }
359 const sideEffectRe = /import \s + ['"] ( [ ^ '"\n] + ) ['"] / g ;
360 while ((m = sideEffectRe. exec (text)) !== null ) {
361 out. push ({ specifier: m[ 1 ], importedNames: null });
362 }
363 return out;
364 }
365
366 function extractDynamicImportReferences ( text ) {
367 const out = [];
368 const re = /import \s * \( \s * ['"] ( [ ^ '"\n] + ) ['"]\s * \) / g ;
369 let m;
370 while ((m = re. exec (text)) !== null ) {
371 out. push ({ specifier: m[ 1 ], importedNames: null });
372 }
373 return out;
374 }
375
376 function extractExportForwardRefs ( text ) {
377 const out = [];
378 const re = /export \s + (?:type \s + ) ? ( \* | \* \s + as \s + [A-Za-z_$][\w$] *| \{ [ ^ }] * \} ) \s + from \s + ['"] ( [ ^ '"\n] + ) ['"]\s * ; ? / g ;
379 let m;
380 while ((m = re. exec (text)) !== null ) {
381 const clause = m[ 1 ]. trim ();
382 const star = clause. startsWith ( '*' );
383 const names = star ? null : parseExportNames (clause);
384 out. push ({
385 specifier: m[ 2 ],
386 star,
387 exportedNames: names?.exportedNames ?? null ,
388 sourceNamesByExported: names?.sourceNamesByExported ?? null ,
389 });
390 }
391 return out;
392 }
393
394 function parseImportNames ( clause ) {
395 const names = new Set ();
396 const trimmed = clause. trim ();
397 if ( ! trimmed) return null ;
398 const named = / \{ ( [ ^ }] + ) \} / s . exec (trimmed);
399 if (named) {
400 for ( const part of splitImportList (named[ 1 ])) {
401 const cleaned = part. replace ( / ^ type \s + / , '' ). trim ();
402 if ( ! cleaned) continue ;
403 const [ source ] = cleaned. split ( / \s + as \s + / i );
404 if (source?. trim ()) names. add (source. trim ());
405 }
406 }
407 const withoutNamed = trimmed. replace ( / \{ [ ^ }] * \} / s , '' ). replace ( /, \s *$ / , '' ). trim ();
408 if (withoutNamed && ! withoutNamed. startsWith ( '*' )) names. add ( 'default' );
409 return names.size > 0 ? names : null ;
410 }
411
412 function parseExportNames ( clause ) {
413 const body = clause. replace ( / ^ \{ | \} $ / g , '' );
414 const exportedNames = new Set ();
415 const sourceNamesByExported = new Map ();
416 for ( const part of splitImportList (body)) {
417 const cleaned = part. replace ( / ^ type \s + / , '' ). trim ();
418 if ( ! cleaned) continue ;
419 const [ sourceRaw , exportedRaw ] = cleaned. split ( / \s + as \s + / i );
420 const source = sourceRaw. trim ();
421 const exported = (exportedRaw ?? sourceRaw). trim ();
422 if ( ! source || ! exported) continue ;
423 exportedNames. add (exported);
424 sourceNamesByExported. set (exported, source);
425 }
426 return { exportedNames, sourceNamesByExported };
427 }
428
429 function splitImportList ( value ) {
430 return value. split ( ',' ). map (( part ) => part. trim ()). filter (Boolean);
431 }
432
433 function isPureBarrel ( text ) {
434 const refs = extractExportForwardRefs (text);
435 if (refs. length === 0 ) return false ;
436 const withoutComments = text
437 . replace ( / \/\* [\s\S] *? \*\/ / g , '' )
438 . replace ( / ^ \s * \/\/ . *$ / gm , '' );
439 return withoutComments. replace ( EXPORT_FORWARD_RE , '' ). trim () === '' ;
440 }
441
442 function specifierMatchesNames ( specifier , names ) {
443 const normalizedSpecifier = normalizeName (specifier. split ( '/' ). at ( - 1 ) ?? specifier);
444 for ( const name of names) {
445 const normalizedName = normalizeName (name);
446 if (normalizedSpecifier === normalizedName || normalizedSpecifier. endsWith (normalizedName)) {
447 return true ;
448 }
449 }
450 return false ;
451 }
452
453 function normalizeName ( value ) {
454 return String (value ?? '' ). toLowerCase (). replace ( / [ ^ a-z0-9] / g , '' );
455 }
456
457 function isSuffixFanoutFile ( file ) {
458 return SUFFIX_FANOUT_RE . test (file. replace ( / \\ / g , '/' ));
459 }
460
461 async function fileExportsAnyName ( file , names ) {
462 const text = await tryReadText (file);
463 if (text == null ) return false ;
464 for ( const name of names) {
465 if ( textExportsName (text, name)) return true ;
466 }
467 return false ;
468 }
469
470 function textExportsName ( text , name ) {
471 const escaped = escapeRegExp (name);
472 const declaration = new RegExp ( `export \\ s+(?:async \\ s+)?(?:function|const|let|var|class|interface|type) \\ s+${ escaped } \\ b` );
473 if (declaration. test (text)) return true ;
474 const listRe = /export \s + \{ ( [ ^ }] + ) \} (?! \s + from \b )/ gs ;
475 let m;
476 while ((m = listRe. exec (text)) !== null ) {
477 const names = parseExportNames ( `{${ m [ 1 ] }}` ).exportedNames;
478 if (names. has (name)) return true ;
479 }
480 return false ;
481 }
482
483 function isSourcePath ( path ) {
484 const match = / \. ( [A-Za-z0-9] + ) $ / . exec (path);
485 if ( ! match) return true ;
486 return SOURCE_EXTENSIONS . has ( '.' + match[ 1 ]);
487 }
488
489 function escapeRegExp ( value ) {
490 return String (value). replace ( / [.*+?^${}()|[ \]\\ ] / g , ' \\ $&' );
491 }
492
493 async function tryReadText ( path ) {
494 try {
495 return await readFile (path, 'utf-8' );
496 } catch {
497 return null ;
498 }
499 }
500
501 async function fileExists ( p ) {
502 try { await stat (p); return true ; } catch { return false ; }
503 }
504
505 async function isFile ( p ) {
506 try {
507 const s = await stat (p);
508 return s. isFile ();
509 } catch {
510 return false ;
511 }
512 }
513
514 async function tryReadJson ( path ) {
515 try {
516 const text = await readFile (path, 'utf-8' );
517 return JSON . parse (text);
518 } catch {
519 return null ;
520 }
521 }