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 29 for (const dep of (madgeH[current] || [])) queue.push(dep) 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 48 const tokenCnt = countTokensFn ? countTokensFn(content) : null 49 const result = {locCnt, npmImportsA, tokenCnt} 50 fileStatsCache.set(cacheKey, 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) 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) 76 const isEffectivelyTest = (f: string) => isTestPath(f) || testOnly.has(f) 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)) { 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) 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) 107export const getEpStatsForAll = async (epPathsA: string[], epTypesA: (string|null)[], {countTokensFn}: {countTokensFn?: (s: string) => number} = {}) => { 108 fileStatsCache.clear() 110 const allSeeds = [...epPathsA, ...epTypeDefPaths].filter(isPtCodeFileExt) 112 madgeDepFilterCtx.enterWith({dependencyFilter: () => true}) 115 // Merge ptAnchorInclude/ptAnchorPath/epFromPath refs into madgeH (these aren't ES imports so acorn misses them) 117 for (const [anchorer, anchoredA] of Object.entries(anchorH)) { 118 if (!madgeH[anchorer]) continue 119 for (const anchored of anchoredA) madgeH[anchorer].push(anchored) 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) 126 const infraPkgSet = new Set(sumStatsForSet(allInfraFiles).npmPkgNamesA) 128 const virtualDataPerEp = await Promise.all(epPathsA.map(async (epPath: string) => { 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) 138 return {runtimeFiles, virtualMadgeH, virtualPaths: codePaths} 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) 152 for (const f of runtimeFilesPerEp[i]) { 154 const arr = fileToFullEpIndices.get(f) 155 if (arr && !arr.includes(i)) arr.push(i) 156 else if (!arr) fileToFullEpIndices.set(f, [i]) 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} 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() 178 for (const {virtualMadgeH, virtualPaths} of virtualDataPerEp) { 179 const pagePaths = virtualPaths.filter(isPagePath) 180 for (const pagePath of pagePaths) { 182 for (const src of clientSources) { 183 for (const f of getImportTreeFromMadgeH(src, virtualMadgeH)) globalFeFiles.add(f) 184 globalFeFiles.add(src) 186 globalFeFiles.add(pagePath) 188 for (const f of virtualPaths) if (f.includes('/public/')) globalFeFiles.add(f) 191 const toScopedFile = (f: string) => { 192 const inDeploy = globalDeployFiles.has(f), inDocker = globalDockerFiles.has(f), inFe = globalFeFiles.has(f) 194 return {path: f, loc: getFileStats(f).locCnt, clr, inDeploy, inDocker, inFe, ...getFileScope(f)} 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 => { 202 return [t, factoryPath ? getImportTreeFromMadgeH(factoryPath, madgeH) : new Set()] 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) 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} 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)), 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 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)) 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, 257 allFilesA: ([...new Set<string>([...appFiles, ...(epTypeTree as Set<string>)])]).sort().map(toScopedFile),