playbook/antigravity-awesome-skills/skills/vercel-optimize/lib/workspace-resolver.mjs

523 lines
17 KiB
JavaScript

// Resolve workspace-package imports to actual source files. Sub-agents need this when the route file is a thin shell that re-exports from a workspace package.
//
// Bounded expansion keeps the brief allowlist small: package export resolution, pure-barrel
// traversal, and suffix fan-out for likely data-loading modules. This stays string-based
// and falls through ("couldn't resolve") on shapes that need a full TS resolver.
import { readFile, readdir, stat } from 'node:fs/promises';
import { dirname, join, resolve as pathResolve } from 'node:path';
const DEFAULT_RESOLVE_OPTIONS = {
pureBarrelDepth: 3,
suffixFanoutDepth: 2,
perSpecifierCap: 3,
};
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
const EXTENSIONS = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
const INDEX_FILES = ['index.ts', 'index.tsx', 'index.js', 'index.jsx', 'index.mjs'];
const SUFFIX_FANOUT_RE = /(^|\/)(content|data|loader|fetch|service|metadata|actions)\.tsx?$/;
const EXPORT_FORWARD_RE = /export\s+(?:type\s+)?(?:\*|\*\s+as\s+[A-Za-z_$][\w$]*|\{[^}]*\})\s+from\s+['"][^'"\n]+['"]\s*;?/gs;
export async function detectMonorepoRoot(startDir) {
let dir = pathResolve(startDir);
for (let depth = 0; depth < 15; depth++) {
if (await fileExists(join(dir, 'pnpm-workspace.yaml'))) return dir;
const pkg = await tryReadJson(join(dir, 'package.json'));
if (pkg && (Array.isArray(pkg.workspaces) || Array.isArray(pkg.workspaces?.packages))) {
return dir;
}
const parent = dirname(dir);
if (parent === dir) return null;
dir = parent;
}
return null;
}
// Zero-dependency — pnpm-workspace.yaml shape is predictable; not pulling in js-yaml.
export async function readWorkspaceGlobs(monorepoRoot) {
const pnpmPath = join(monorepoRoot, 'pnpm-workspace.yaml');
if (await fileExists(pnpmPath)) {
const text = await readFile(pnpmPath, 'utf-8');
return parsePnpmWorkspaceYaml(text);
}
const pkg = await tryReadJson(join(monorepoRoot, 'package.json'));
if (Array.isArray(pkg?.workspaces)) return pkg.workspaces;
if (Array.isArray(pkg?.workspaces?.packages)) return pkg.workspaces.packages;
return [];
}
// Handles `packages:` block with `- glob` entries. Not full YAML grammar.
export function parsePnpmWorkspaceYaml(text) {
const out = [];
let inPackages = false;
for (const rawLine of text.split('\n')) {
const line = rawLine.replace(/#.*$/, '').trimEnd();
if (!line.trim()) continue;
if (/^packages\s*:/.test(line)) { inPackages = true; continue; }
if (!inPackages) continue;
if (!/^\s/.test(line)) { inPackages = false; continue; }
const m = line.match(/^\s*-\s+['"]?([^'"\s]+)['"]?\s*$/);
if (m) out.push(m[1]);
}
return out;
}
export async function listWorkspacePackages(monorepoRoot) {
const globs = await readWorkspaceGlobs(monorepoRoot);
const dirs = new Set();
for (const g of globs) {
const expanded = await expandWorkspaceGlob(monorepoRoot, g);
for (const d of expanded) dirs.add(d);
}
const out = [];
for (const dir of dirs) {
const pkg = await tryReadJson(join(dir, 'package.json'));
if (pkg?.name) out.push({ name: pkg.name, dir, pkg });
}
return out.sort((a, b) => a.name.localeCompare(b.name));
}
// Handles workspace-shape globs only. `**` collapses to one level — npm/pnpm don't document deep `**`.
async function expandWorkspaceGlob(root, glob) {
const parts = glob.replace(/\\/g, '/').split('/');
return await expandParts(root, parts);
}
async function expandParts(currentDir, parts) {
if (parts.length === 0) return [currentDir];
const [head, ...rest] = parts;
if (head === '' || head === '.') return await expandParts(currentDir, rest);
if (head === '*' || head === '**') {
let entries = [];
try {
entries = await readdir(currentDir, { withFileTypes: true });
} catch { return []; }
const childDirs = entries.filter((e) => e.isDirectory()).map((e) => join(currentDir, e.name));
const out = [];
for (const d of childDirs) {
const more = await expandParts(d, rest);
out.push(...more);
}
return out;
}
const next = join(currentDir, head);
try {
const s = await stat(next);
if (!s.isDirectory()) return [];
} catch {
return [];
}
return await expandParts(next, rest);
}
export function buildResolver(packages) {
const byName = new Map();
for (const p of packages) {
byName.set(p.name, buildPackageLookup(p));
}
return function resolveSpecifier(specifier) {
if (typeof specifier !== 'string' || !specifier.length) return null;
// Longest-name match first so `@vercel/foo-bar` wins over `@vercel/foo`.
const candidates = [...byName.keys()]
.filter((name) => specifier === name || specifier.startsWith(name + '/'))
.sort((a, b) => b.length - a.length);
if (candidates.length === 0) return null;
const pkgName = candidates[0];
const subpath = specifier === pkgName ? '.' : './' + specifier.slice(pkgName.length + 1);
const lookup = byName.get(pkgName);
return lookup.resolveSubpath(subpath);
};
}
// Node spec: pattern key has exactly one `*`; target may have one or zero.
function buildPackageLookup(p) {
const exact = new Map();
const wildcards = [];
const exports = p.pkg.exports;
if (exports && typeof exports === 'object' && !Array.isArray(exports)) {
for (const [key, value] of Object.entries(exports)) {
const target = pickConditionalTarget(value);
if (typeof target !== 'string') continue;
if (key.includes('*')) {
const keyStarIdx = key.indexOf('*');
if (keyStarIdx !== key.lastIndexOf('*')) continue;
if ((target.match(/\*/g) ?? []).length > 1) continue;
wildcards.push({
keyPrefix: key.slice(0, keyStarIdx),
keySuffix: key.slice(keyStarIdx + 1),
valueTemplate: target,
});
} else {
exact.set(key, target);
}
}
}
return {
resolveSubpath(subpath) {
const exactHit = exact.get(subpath);
if (exactHit) return joinPackagePath(p.dir, exactHit);
for (const w of wildcards) {
if (subpath.startsWith(w.keyPrefix) && subpath.endsWith(w.keySuffix)) {
const star = subpath.slice(w.keyPrefix.length, subpath.length - w.keySuffix.length);
if (!star) continue;
const target = w.valueTemplate.replaceAll('*', star);
return joinPackagePath(p.dir, target);
}
}
// Unsafe to guess when no exports declared.
if (exact.size === 0 && wildcards.length === 0 && subpath !== '.') {
return null;
}
return null;
},
};
}
// Condition order matches what Next.js / Vite / esbuild would resolve.
function pickConditionalTarget(value) {
if (typeof value === 'string') return value;
if (Array.isArray(value)) {
for (const item of value) {
const target = pickConditionalTarget(item);
if (typeof target === 'string') return target;
}
return null;
}
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
for (const cond of ['default', 'import', 'node', 'browser', 'require', 'types']) {
const v = value[cond];
if (typeof v === 'string') return v;
}
return null;
}
export async function resolveWorkspaceImports(sourceFilePath, resolver, options = {}) {
let text;
try {
text = await readFile(sourceFilePath, 'utf-8');
} catch {
return [];
}
const opts = { ...DEFAULT_RESOLVE_OPTIONS, ...options };
const refs = extractModuleReferences(text);
const out = [];
const seen = new Set();
for (const ref of refs) {
const resolved = await resolveModuleSpecifier(sourceFilePath, ref.specifier, resolver);
if (!resolved) continue;
const expanded = await expandResolvedSpecifier(resolved, ref.importedNames, resolver, opts);
for (const file of expanded) {
if (seen.has(file)) continue;
seen.add(file);
out.push(file);
}
}
return out;
}
// Skips CommonJS `require('foo')` and template-literal dynamic imports (statically unresolvable).
export function extractImportSpecifiers(text) {
return [...new Set(extractModuleReferences(text).map((ref) => ref.specifier))];
}
function joinPackagePath(packageDir, relativeTarget) {
return join(packageDir, relativeTarget.replace(/^\.\//, ''));
}
async function expandResolvedSpecifier(startFile, importedNames, resolver, opts) {
const out = [];
const seen = new Set();
const barrelVisited = new Set();
const fanoutVisited = new Set();
const add = (file) => {
if (seen.has(file)) return false;
if (out.length > 0 && out.length - 1 >= opts.perSpecifierCap) return false;
seen.add(file);
out.push(file);
return true;
};
add(startFile);
await expandPureBarrel(startFile, importedNames, 0);
const fanoutSeeds = out.slice();
for (const file of fanoutSeeds) {
await expandSuffixFanout(file, 0);
}
return out;
async function expandPureBarrel(file, requestedNames, depth) {
if (depth >= opts.pureBarrelDepth) return;
if (barrelVisited.has(file)) return;
barrelVisited.add(file);
const text = await tryReadText(file);
if (text == null || !isPureBarrel(text)) return;
const refs = await selectRelevantForwards(file, extractExportForwardRefs(text), requestedNames, resolver);
for (const { ref, next } of refs) {
if (!add(next)) return;
await expandPureBarrel(next, requestedNamesForForward(ref, requestedNames), depth + 1);
}
}
async function expandSuffixFanout(file, depth) {
if (depth >= opts.suffixFanoutDepth) return;
if (!isSuffixFanoutFile(file)) return;
const visitKey = `${file}:${depth}`;
if (fanoutVisited.has(visitKey)) return;
fanoutVisited.add(visitKey);
const text = await tryReadText(file);
if (text == null) return;
for (const ref of extractModuleReferences(text)) {
const next = await resolveModuleSpecifier(file, ref.specifier, resolver);
if (!next) continue;
if (!add(next)) return;
if (isSuffixFanoutFile(next)) await expandSuffixFanout(next, depth + 1);
}
}
}
async function selectRelevantForwards(fromFile, refs, requestedNames, resolver) {
const resolved = [];
for (const [index, ref] of refs.entries()) {
const next = await resolveModuleSpecifier(fromFile, ref.specifier, resolver);
if (!next) continue;
let score = requestedNames && requestedNames.size > 0
? forwardRelevanceScore(ref, requestedNames, refs.length)
: 1;
if (requestedNames && requestedNames.size > 0 && await fileExportsAnyName(next, requestedNames)) {
score = Math.max(score, 75);
}
resolved.push({ ref, next, index, score });
}
if (!requestedNames || requestedNames.size === 0) return resolved;
const ranked = resolved
.filter((x) => x.score > 0)
.sort((a, b) => b.score - a.score || a.index - b.index);
return ranked.length > 0 ? ranked : resolved;
}
function forwardRelevanceScore(ref, requestedNames, siblingCount) {
if (!requestedNames || requestedNames.size === 0) return 1;
if (ref.exportedNames) {
for (const name of requestedNames) {
if (ref.exportedNames.has(name)) return 100;
}
}
if (specifierMatchesNames(ref.specifier, requestedNames)) return 50;
return siblingCount === 1 ? 1 : 0;
}
function requestedNamesForForward(ref, requestedNames) {
if (!requestedNames || requestedNames.size === 0) return null;
if (ref.star) return requestedNames;
const out = new Set();
for (const name of requestedNames) {
const source = ref.sourceNamesByExported?.get(name);
if (source) out.add(source);
}
return out.size > 0 ? out : requestedNames;
}
async function resolveModuleSpecifier(fromFile, specifier, resolver) {
const raw = specifier.startsWith('.')
? join(dirname(fromFile), specifier)
: resolver(specifier);
if (!raw) return null;
return await resolveExistingPath(raw);
}
async function resolveExistingPath(basePath) {
for (const ext of EXTENSIONS) {
const candidate = ext === '' ? basePath : basePath + ext;
if (!isSourcePath(candidate)) continue;
if (await isFile(candidate)) return candidate;
}
for (const indexFile of INDEX_FILES) {
const candidate = join(basePath, indexFile);
if (await isFile(candidate)) return candidate;
}
return null;
}
function extractModuleReferences(text) {
return [
...extractImportReferences(text),
...extractExportForwardRefs(text).map((ref) => ({
specifier: ref.specifier,
importedNames: ref.star ? null : ref.exportedNames,
})),
...extractDynamicImportReferences(text),
];
}
function extractImportReferences(text) {
const out = [];
const fromRe = /import\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"]([^'"\n]+)['"]/g;
let m;
while ((m = fromRe.exec(text)) !== null) {
out.push({ specifier: m[2], importedNames: parseImportNames(m[1]) });
}
const sideEffectRe = /import\s+['"]([^'"\n]+)['"]/g;
while ((m = sideEffectRe.exec(text)) !== null) {
out.push({ specifier: m[1], importedNames: null });
}
return out;
}
function extractDynamicImportReferences(text) {
const out = [];
const re = /import\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g;
let m;
while ((m = re.exec(text)) !== null) {
out.push({ specifier: m[1], importedNames: null });
}
return out;
}
function extractExportForwardRefs(text) {
const out = [];
const re = /export\s+(?:type\s+)?(\*|\*\s+as\s+[A-Za-z_$][\w$]*|\{[^}]*\})\s+from\s+['"]([^'"\n]+)['"]\s*;?/g;
let m;
while ((m = re.exec(text)) !== null) {
const clause = m[1].trim();
const star = clause.startsWith('*');
const names = star ? null : parseExportNames(clause);
out.push({
specifier: m[2],
star,
exportedNames: names?.exportedNames ?? null,
sourceNamesByExported: names?.sourceNamesByExported ?? null,
});
}
return out;
}
function parseImportNames(clause) {
const names = new Set();
const trimmed = clause.trim();
if (!trimmed) return null;
const named = /\{([^}]+)\}/s.exec(trimmed);
if (named) {
for (const part of splitImportList(named[1])) {
const cleaned = part.replace(/^type\s+/, '').trim();
if (!cleaned) continue;
const [source] = cleaned.split(/\s+as\s+/i);
if (source?.trim()) names.add(source.trim());
}
}
const withoutNamed = trimmed.replace(/\{[^}]*\}/s, '').replace(/,\s*$/, '').trim();
if (withoutNamed && !withoutNamed.startsWith('*')) names.add('default');
return names.size > 0 ? names : null;
}
function parseExportNames(clause) {
const body = clause.replace(/^\{|\}$/g, '');
const exportedNames = new Set();
const sourceNamesByExported = new Map();
for (const part of splitImportList(body)) {
const cleaned = part.replace(/^type\s+/, '').trim();
if (!cleaned) continue;
const [sourceRaw, exportedRaw] = cleaned.split(/\s+as\s+/i);
const source = sourceRaw.trim();
const exported = (exportedRaw ?? sourceRaw).trim();
if (!source || !exported) continue;
exportedNames.add(exported);
sourceNamesByExported.set(exported, source);
}
return { exportedNames, sourceNamesByExported };
}
function splitImportList(value) {
return value.split(',').map((part) => part.trim()).filter(Boolean);
}
function isPureBarrel(text) {
const refs = extractExportForwardRefs(text);
if (refs.length === 0) return false;
const withoutComments = text
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/^\s*\/\/.*$/gm, '');
return withoutComments.replace(EXPORT_FORWARD_RE, '').trim() === '';
}
function specifierMatchesNames(specifier, names) {
const normalizedSpecifier = normalizeName(specifier.split('/').at(-1) ?? specifier);
for (const name of names) {
const normalizedName = normalizeName(name);
if (normalizedSpecifier === normalizedName || normalizedSpecifier.endsWith(normalizedName)) {
return true;
}
}
return false;
}
function normalizeName(value) {
return String(value ?? '').toLowerCase().replace(/[^a-z0-9]/g, '');
}
function isSuffixFanoutFile(file) {
return SUFFIX_FANOUT_RE.test(file.replace(/\\/g, '/'));
}
async function fileExportsAnyName(file, names) {
const text = await tryReadText(file);
if (text == null) return false;
for (const name of names) {
if (textExportsName(text, name)) return true;
}
return false;
}
function textExportsName(text, name) {
const escaped = escapeRegExp(name);
const declaration = new RegExp(`export\\s+(?:async\\s+)?(?:function|const|let|var|class|interface|type)\\s+${escaped}\\b`);
if (declaration.test(text)) return true;
const listRe = /export\s+\{([^}]+)\}(?!\s+from\b)/gs;
let m;
while ((m = listRe.exec(text)) !== null) {
const names = parseExportNames(`{${m[1]}}`).exportedNames;
if (names.has(name)) return true;
}
return false;
}
function isSourcePath(path) {
const match = /\.([A-Za-z0-9]+)$/.exec(path);
if (!match) return true;
return SOURCE_EXTENSIONS.has('.' + match[1]);
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
async function tryReadText(path) {
try {
return await readFile(path, 'utf-8');
} catch {
return null;
}
}
async function fileExists(p) {
try { await stat(p); return true; } catch { return false; }
}
async function isFile(p) {
try {
const s = await stat(p);
return s.isFile();
} catch {
return false;
}
}
async function tryReadJson(path) {
try {
const text = await readFile(path, 'utf-8');
return JSON.parse(text);
} catch {
return null;
}
}