🌳
pt0/deployF/testsF/findReExportBloatAI.mts
1import * as _ from 'lodash-es'
2import ts from 'typescript'
13import type { absFileDirPath } from '../../ptDirF.mts'
15export type ReExportBloat = {
16 reExportFile: string
17 symbol: string
18 srcFile: string | null
19 consumerCnt: number
20 actualUserCnt: number
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 }
48 stack.push(p)
49 }
50 resolved = stack.join('/')
51 }
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})
56 }
57 }
58 }
59 ts.forEachChild(node, walk)
60 }
61 walk(sf)
62 return out
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) => {
68 const dirPath = pathDownJoin(ptDir, prefix.replace(/\/$/, ''))
69 const entries = await lsFilePathsRec(dirPath)
70 return entries.map(absPathToPtPath).filter(p => isPtCodeFileExt(p) && !p.startsWith(`${tmpDirName}/`))
71 })))
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)[]>>()
76 await allPromCalls(allFiles, async (ptPath: string) => {
77 const importsH = await acornSingleFile(ptPath as unknown as absFileDirPath)
78 importsByFile.set(ptPath, (importsH || {}) as Record<string, (string | null)[]>)
79 let contents: string
80 try { contents = await read1File(pathDownJoin(ptDir, ptPath)) } catch { return }
81 let reExports: {symbol: string, srcFile: string | null}[] = []
82 try { reExports = extractReExports(contents, ptPath) } catch {// catch:userapproved — best-effort parse, skip on failure
83 }
84 if (reExports.length) reExportByFile.set(ptPath, reExports)
85 })
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
91 if (isNextJsRoutePath(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})
98 }
99 }
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})
106 }
107 }
108 }
110 return {reExportBloatA}