1import * as _ from 'lodash-es' 2import ts from 'typescript' 15export type ReExportBloat = { 18 srcFile: string | null 23const barrelBasenames = new Set(['index', 'common', 'mutationsF', 'queriesF']) 24const codeExtRe = extRe(ptFileExtA) 25const isBarrelByPath = (ptPath: string): boolean => { 26 const base = (ptPath.split('/').pop() || '').replace(codeExtRe, '') 27 return barrelBasenames.has(base) 30const migratedDirPrefixA = ['pt0/', '.opencode/'] 32// Extract `export { X, Y } from './F'` re-exports via TS compiler (handles .mts/.tsx too). 33// Returns [{symbol, srcFile}] where srcFile is resolved to a pt-relative path (or null if bare specifier). 34const extractReExports = (contents: string, ptPath: string): {symbol: string, srcFile: string | null}[] => { 35 const sf = ts.createSourceFile(ptPath, contents, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX) 36 const out: {symbol: string, srcFile: string | null}[] = [] 37 const walk = (node: ts.Node) => { 38 if (ts.isExportDeclaration(node) && node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) { 39 const srcRaw = node.moduleSpecifier.text 40 const dir = ptPath.split('/').slice(0, -1).join('/') 41 let resolved: string | null = null 42 if (srcRaw.startsWith('.')) { 43 const parts = (dir + '/' + srcRaw).split('/') 44 const stack: string[] = [] 45 for (const p of parts) { 46 if (p === '' || p === '.') continue 47 if (p === '..') { stack.pop(); continue } 50 resolved = stack.join('/') 52 const {exportClause} = node 53 if (exportClause && ts.isNamedExports(exportClause)) { 54 for (const el of exportClause.elements) { 55 out.push({symbol: el.name.text, srcFile: resolved}) 59 ts.forEachChild(node, walk) 65export const findReExportBloat = async (): Promise<{reExportBloatA: ReExportBloat[]}> => { 66 // 1. Walk migrated code files 67 const allFiles: string[] = _.flatten(await Promise.all(migratedDirPrefixA.map(async (prefix) => { 73 // 2. Per file: extract re-exports + record imports (path -> names) 74 const reExportByFile = new Map<string, {symbol: string, srcFile: string | null}[]>() 75 const importsByFile = new Map<string, Record<string, (string | null)[]>>() 78 importsByFile.set(ptPath, (importsH || {}) as Record<string, (string | null)[]>) 81 let reExports: {symbol: string, srcFile: string | null}[] = [] 82 try { reExports = extractReExports(contents, ptPath) } catch {// catch:userapproved — best-effort parse, skip on failure 84 if (reExports.length) reExportByFile.set(ptPath, reExports) 87 // 3. For each non-barrel, non-Next-route H with re-exports: count consumers & actual users per symbol 88 const reExportBloatA: ReExportBloat[] = [] 89 for (const [H, reExports] of reExportByFile) { 90 if (isBarrelByPath(H)) continue 92 const consumers: {names: Set<string>}[] = [] 93 for (const [consumer, importsH] of importsByFile) { 94 if (consumer === H) continue 95 if (Object.prototype.hasOwnProperty.call(importsH, H)) { 96 const names = new Set((importsH[H] || []).filter((n): n is string => Boolean(n) && n !== '*')) 97 consumers.push({names}) 100 for (const {symbol, srcFile} of reExports) { 101 const actualUserCnt = consumers.reduce((acc, c) => acc + (c.names.has(symbol) ? 1 : 0), 0) 102 // Threshold A: fully-dead re-exports (no consumer imports the symbol via H). 103 // Locks the invariant at zero ambiguity - ratio-based thresholds invite debate. 104 if (consumers.length > 0 && actualUserCnt === 0) { 105 reExportBloatA.push({reExportFile: H, symbol, srcFile, consumerCnt: consumers.length, actualUserCnt}) 110 return {reExportBloatA}