🌳
pt0/deployF/codeStatsF/statsAI.mts
1import fs from 'fs'
2import { builtinModules } from 'module'
16const nodeBuiltins = new Set([...builtinModules, ...builtinModules.map(m => `node:${m}`)])
17const isRealNpmPkg = (name: string) => !nodeBuiltins.has(name) && !/[\$\{\}<>]/.test(name)
18const isTestPath = (f: string) => f.includes('testsF/') || f.includes('testF/')
20type MadgeH = Record<string, string[]>
22export const getImportTreeFromMadgeH = (seedPath: string, madgeH: MadgeH): Set<string> => {
23 const visited = new Set<string>()
24 const queue = [seedPath]
25 while (queue.length) {
26 const current = queue.shift()!
27 if (visited.has(current)) continue
28 visited.add(current)
29 for (const dep of (madgeH[current] || [])) queue.push(dep)
30 }
31 return visited
34const readPtFile = (ptPath: string) => {
35 try { return fs.readFileSync(`${ptDir}/${ptPath}`, 'utf8') } catch { return '' }
38const fileStatsCache = new Map()
40const getFileStats = (ptPath: string, countTokensFn?: (s: string) => number) => {
41 const cacheKey = ptPath + (countTokensFn ? ':tok' : '')
42 if (fileStatsCache.has(cacheKey)) return fileStatsCache.get(cacheKey)
43 const rawContent = readPtFile(ptPath)
44 const transformFn = contentTransformCtx.getStore() as ((s: string) => string) | undefined
45 const content = transformFn ? transformFn(rawContent) : rawContent
46 const locCnt = content.split('\n').length
47 const npmImportsA = [...extractNpmImports(content)].filter(isRealNpmPkg)
48 const tokenCnt = countTokensFn ? countTokensFn(content) : null
49 const result = {locCnt, npmImportsA, tokenCnt}
50 fileStatsCache.set(cacheKey, result)
51 return result
54const sumStatsForSet = (fileSet: Set<string>, countTokensFn?: (s: string) => number) => {
55 let locCnt = 0, tokenCnt = countTokensFn ? 0 : null
56 const npmPkgsSet = new Set()
57 for (const f of fileSet) {
58 const stats = getFileStats(f, countTokensFn)
59 locCnt += stats.locCnt
60 if (countTokensFn) tokenCnt += stats.tokenCnt
61 for (const pkg of stats.npmImportsA) npmPkgsSet.add(pkg)
62 }
63 return {locCnt, tokenCnt, npmPkgNamesA: [...npmPkgsSet].sort()}
66const getTestOnlyFiles = (epTree: Set<string>, madgeH: MadgeH, runtimeFiles: Set<string>) => {
67 const testOnly = new Set()
68 const importersH = new Map()
69 for (const f of epTree) {
70 for (const dep of (madgeH[f] || [])) {
71 if (!importersH.has(dep)) importersH.set(dep, [])
72 importersH.get(dep).push(f)
73 }
74 }
75 let changed = true
76 const isEffectivelyTest = (f: string) => isTestPath(f) || testOnly.has(f)
77 while (changed) {
78 changed = false
79 for (const f of epTree) {
80 if (testOnly.has(f) || isTestPath(f) || runtimeFiles.has(f)) continue
81 const importers = importersH.get(f)
82 if (importers?.length && importers.every(isEffectivelyTest)) {
83 testOnly.add(f)
84 changed = true
85 }
86 }
87 }
88 return testOnly
91const collectTestOnlyPkgs = (epTree: Set<string>, madgeH: MadgeH, runtimeFiles: Set<string>) => {
92 const testOnlyFiles = getTestOnlyFiles(epTree, madgeH, runtimeFiles)
93 const pkgImporters = new Map()
94 for (const f of epTree) {
95 for (const pkg of getFileStats(f).npmImportsA) {
96 if (!pkgImporters.has(pkg)) pkgImporters.set(pkg, [])
97 pkgImporters.get(pkg).push(f)
98 }
99 }
100 const testOnlyPkgs = new Set()
101 for (const [pkg, files] of pkgImporters) {
102 if (files.every((f: string) => isTestPath(f) || testOnlyFiles.has(f))) testOnlyPkgs.add(pkg)
103 }
104 return testOnlyPkgs
107export const getEpStatsForAll = async (epPathsA: string[], epTypesA: (string|null)[], {countTokensFn}: {countTokensFn?: (s: string) => number} = {}) => {
108 fileStatsCache.clear()
109 const epTypeDefPaths = epTypesA.map(t => getEpTypeDefPath(t)).filter(Boolean) as string[]
110 const allSeeds = [...epPathsA, ...epTypeDefPaths].filter(isPtCodeFileExt)
112 madgeDepFilterCtx.enterWith({dependencyFilter: () => true})
113 const madgeH = await ptMadge(allSeeds)
115 // Merge ptAnchorInclude/ptAnchorPath/epFromPath refs into madgeH (these aren't ES imports so acorn misses them)
116 const anchorH = await getAnchoredPathsAstH()
117 for (const [anchorer, anchoredA] of Object.entries(anchorH)) {
118 if (!madgeH[anchorer]) continue
119 for (const anchored of anchoredA) madgeH[anchorer].push(anchored)
120 }
122 const allInfraFiles = new Set<string>()
123 for (const fp of [...new Set(epTypeDefPaths)]) {
124 for (const f of getImportTreeFromMadgeH(fp, madgeH)) allInfraFiles.add(f)
125 }
126 const infraPkgSet = new Set(sumStatsForSet(allInfraFiles).npmPkgNamesA)
128 const virtualDataPerEp = await Promise.all(epPathsA.map(async (epPath: string) => {
129 const virtualPaths = await getVirtualImportsForDeployScript(epPath)
130 if (!virtualPaths.length) return {runtimeFiles: new Set<string>(), virtualMadgeH: {} as MadgeH, virtualPaths: [] as string[]}
131 const codePaths = virtualPaths.filter(isPtCodeFileExt) as string[]
132 const virtualMadgeH: MadgeH = await ptMadge(codePaths)
133 const runtimeFiles = new Set<string>()
134 for (const vp of codePaths) {
135 for (const f of getImportTreeFromMadgeH(vp, virtualMadgeH)) runtimeFiles.add(f)
136 runtimeFiles.add(vp)
137 }
138 return {runtimeFiles, virtualMadgeH, virtualPaths: codePaths}
139 }))
140 const runtimeFilesPerEp: Set<string>[] = virtualDataPerEp.map(d => d.runtimeFiles)
142 // Phase 1: compute full import trees per ep
143 const epTreesA = epPathsA.map((epPath: string) => getImportTreeFromMadgeH(epPath, madgeH))
145 // Phase 2: build global file→epIndices map from full trees + virtual imports
146 const fileToFullEpIndices = new Map()
147 for (let i = 0; i < epPathsA.length; i++) {
148 for (const f of epTreesA[i]) {
149 if (!fileToFullEpIndices.has(f)) fileToFullEpIndices.set(f, [])
150 fileToFullEpIndices.get(f).push(i)
151 }
152 for (const f of runtimeFilesPerEp[i]) {
153 if (!isPtCodeFileExt(f)) continue
154 const arr = fileToFullEpIndices.get(f)
155 if (arr && !arr.includes(i)) arr.push(i)
156 else if (!arr) fileToFullEpIndices.set(f, [i])
157 }
158 }
159 const totalEps = epPathsA.length
160 const epAppPathsA = await Promise.all(epPathsA.map(getMonorSubdirForRegEp))
161 const uniqueAppGroups = [...new Set(epAppPathsA.filter(Boolean))]
162 const epAppGroupIdxA = epAppPathsA.map(ap => ap ? uniqueAppGroups.indexOf(ap) : -1)
163 const totalAppGroups = uniqueAppGroups.length
164 const getFileScope = (path: string) => {
165 const epIndices = fileToFullEpIndices.get(path) || []
166 const appGroupIndices = [...new Set(epIndices.map((i: number) => epAppGroupIdxA[i]).filter((g: number) => g >= 0))]
167 if (appGroupIndices.length <= 1) return {scope: 'this'}
168 if (appGroupIndices.length === totalAppGroups) return {scope: 'all'}
169 return {scope: 'some', epIndices}
170 }
171 const globalDeployFiles = new Set()
172 for (const epTree of epTreesA) for (const f of epTree) globalDeployFiles.add(f)
173 const globalDockerFiles = new Set()
174 for (const runtimeSet of runtimeFilesPerEp) for (const f of runtimeSet) globalDockerFiles.add(f)
176 const globalFeFiles = new Set()
177 const isPagePath = (f: string) => isPtCodeFileExt(f) && !f.includes('/api/') && !f.endsWith('/route.js') && !f.endsWith('/route.ts') && isNextJsRoutePath(f)
178 for (const {virtualMadgeH, virtualPaths} of virtualDataPerEp) {
179 const pagePaths = virtualPaths.filter(isPagePath)
180 for (const pagePath of pagePaths) {
181 const {clientSources} = await getPageExportSources(pagePath)
182 for (const src of clientSources) {
183 for (const f of getImportTreeFromMadgeH(src, virtualMadgeH)) globalFeFiles.add(f)
184 globalFeFiles.add(src)
185 }
186 globalFeFiles.add(pagePath)
187 }
188 for (const f of virtualPaths) if (f.includes('/public/')) globalFeFiles.add(f)
189 }
191 const toScopedFile = (f: string) => {
192 const inDeploy = globalDeployFiles.has(f), inDocker = globalDockerFiles.has(f), inFe = globalFeFiles.has(f)
193 const clr = fileClrFromEpTypes(inDeploy, inDocker, inFe)
194 return {path: f, loc: getFileStats(f).locCnt, clr, inDeploy, inDocker, inFe, ...getFileScope(f)}
195 }
197 // Phase 3: compute epType trees and build file→epTypeIndices map
198 const uniqueEpTypes = [...new Set(epTypesA.filter(Boolean))]
199 const epTypeToIdx = new Map(uniqueEpTypes.map((t, i) => [t, i]))
200 const epTypeTreesH: Record<string, Set<string>> = Object.fromEntries(uniqueEpTypes.map(t => {
201 const factoryPath = getEpTypeDefPath(t)
202 return [t, factoryPath ? getImportTreeFromMadgeH(factoryPath, madgeH) : new Set()]
203 }))
204 const fileToEpTypeIndices = new Map()
205 for (const [epType, tree] of Object.entries(epTypeTreesH)) {
206 const idx = epTypeToIdx.get(epType)
207 for (const f of tree) {
208 if (!fileToEpTypeIndices.has(f)) fileToEpTypeIndices.set(f, [])
209 fileToEpTypeIndices.get(f).push(idx)
210 }
211 }
212 const totalEpTypes = uniqueEpTypes.length
213 const getEpTypeFileScope = (path: string) => {
214 const indices = fileToEpTypeIndices.get(path) || []
215 if (indices.length <= 1) return {epTypeScope: 'this'}
216 if (indices.length === totalEpTypes) return {epTypeScope: 'all'}
217 return {epTypeScope: 'some', epTypeIndices: indices}
218 }
220 // Phase 4: per-ep stats
221 const resultsA = epPathsA.map((epPath: string, i: number) => {
222 const epType = epTypesA[i], epTree = epTreesA[i]
223 const epTypeTree = epType ? epTypeTreesH[epType] : new Set()
225 const appFiles = new Set([
226 ...[...epTree].filter((f: string) => !epTypeTree.has(f)),
227 ...[...runtimeFilesPerEp[i]].filter((f: string) => !epTypeTree.has(f) && isPtCodeFileExt(f)),
228 ])
229 const toAppScopedFile = (f: string) => {
230 const scoped = toScopedFile(f)
231 if (f === epPath) scoped.scope = 'this' // ep's own file always counts as 'this' for itself
232 return scoped
233 }
234 const appFilesA = [...appFiles].sort().map(toAppScopedFile)
235 const appStats = sumStatsForSet(appFiles, countTokensFn)
236 const appThisOnlyLocCnt = appFilesA.filter(f => f.scope === 'this').reduce((s, f) => s + f.loc, 0)
238 const toEpTypeScopedFile = (f: string) => ({...toScopedFile(f), ...getEpTypeFileScope(f)})
239 const epTypeFilesA = epType ? ([...epTypeTree] as string[]).sort().map(toEpTypeScopedFile) : []
240 const epTypeStats = sumStatsForSet(epTypeTree as Set<string>, countTokensFn)
241 const epTypeThisOnlyLocCnt = epTypeFilesA.filter(f => f.epTypeScope === 'this').reduce((s, f) => s + f.loc, 0)
243 const runtimePkgs = new Set(sumStatsForSet(runtimeFilesPerEp[i]).npmPkgNamesA)
244 const allEpPkgs = [...new Set([...sumStatsForSet(epTree).npmPkgNamesA, ...runtimePkgs] as string[])].sort()
245 const testOnlyPkgs = collectTestOnlyPkgs(epTree, madgeH, runtimeFilesPerEp[i] as Set<string>)
246 const isDevDep = (p: string) => !runtimePkgs.has(p) && (infraPkgSet.has(p) || testOnlyPkgs.has(p))
248 return {
249 epPath, epType,
250 filesCnt: epTree.size,
251 appLocCnt: appStats.locCnt, appThisOnlyLocCnt, appTokenCnt: appStats.tokenCnt,
252 epTypeLocCnt: epType ? epTypeStats.locCnt : null, epTypeThisOnlyLocCnt: epType ? epTypeThisOnlyLocCnt : null, epTypeTokenCnt: epType ? epTypeStats.tokenCnt : null,
253 appPkgNamesA: epType ? allEpPkgs.filter((p: string) => !isDevDep(p)) : [],
254 devPkgNamesA: epType ? allEpPkgs.filter(isDevDep) : allEpPkgs,
255 appFilesA,
256 epTypeFilesA,
257 allFilesA: ([...new Set<string>([...appFiles, ...(epTypeTree as Set<string>)])]).sort().map(toScopedFile),
258 }
259 })
261 return resultsA