1import * as _ from 'lodash-es' 2import { execSync } from 'child_process' 3import { existsSync } from 'fs' 28const epsToAllPathsTagged = (tag: string, epPtPathsA: string[]) => 31const migratedDirPrefixA = ['pt0/', '.opencode/'] 33const getSubmodulePrefixA = (): string[] => { 34 const gitmodulesPath = `${ptDir}/.gitmodules` 35 if (!existsSync(gitmodulesPath)) return [] 36 const out = execSync(`git config --file .gitmodules --get-regexp '^submodule\\..*\\.path$'`, {cwd: ptDir, encoding: 'utf8'}) 37 return out.trim().split('\n').filter(Boolean).map(line => line.split(' ')[1] + '/') 40const submodulePrefixA = getSubmodulePrefixA() 41const excludedDirPrefixA = [`${tmpDirName}/`, ...submodulePrefixA] 42const buildArtifactDirInfixA = ['/.next/', '/.next-test/'] 43const isExcludedPath = (p: string) => excludedDirPrefixA.some(prefix => p.startsWith(prefix)) || buildArtifactDirInfixA.some(infix => p.includes(infix)) 44const isSubmodulePath = (p: string) => submodulePrefixA.some(prefix => p.startsWith(prefix)) 46const deadExportExcludePrefixes = ['.opencode/'] 47const isDeadExportExcluded = (p: string) => deadExportExcludePrefixes.some(prefix => p.startsWith(prefix)) || p.includes('/public/') 48const nextRouteExports = new Set(['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS', 'HEAD', 'runtime', 'dynamic', 'revalidate']) 49const knownDynamicExports = new Set<string>() 51export const getPtSubdirFromEp = async (epPath: string) => { 53 if (!appPath) return null 54 const nextConfigPath = pathDownJoin(ptDir, appPath, 'next.config.mjs') 59export const getPageEpsFromNextEps = async (epA: string[]) => { 60 const ptSubdirA = _.compact(await Promise.all(epA.map(getPtSubdirFromEp))) 61 const uniquePtSubdirs = _.uniq(ptSubdirA) 62 const pageEpA: string[] = [] 63 for (const appPath of uniquePtSubdirs) { 64 appCfgCtx.enterWith({ appPath }) 68 return _.uniq(pageEpA) 71const getVirtualImportsForEps = async (eps: string[]) => _.flatten(await Promise.all(eps.map(getVirtualImportsForDeployScript))) 73const getAnchorIncludedImportPaths = async () => { 75 const existingA = _.compact(await Promise.all(anchorFilesA.map(async (p: string) => await fileExists(pathDownJoin(ptDir, p)) ? p : null))) 76 return existingA.length > 0 ? await epsToAllPathsTagged('anchor', existingA) : [] 79const getPathBinInfo = async () => { 82 const entries = (await lsFilePathsRec(binDir)).filter((p: string) => !path.extname(p)) 83 const jsBinScripts: string[] = [], importTargets: string[] = [] 84 for (const absP of entries) { 86 if (!/^#!.*\b(node|ptnode)\b/.test(contents.split('\n')[0])) continue 88 jsBinScripts.push(ptPath) 90 importTargets.push(...Object.keys(importsH)) 92 const transitiveImportPaths = importTargets.length > 0 ? await epsToAllPathsTagged('pathBin', importTargets) : [] 93 return { jsBinScripts, transitiveImportPaths } 94 } catch { return { jsBinScripts: [] as string[], transitiveImportPaths: [] as string[] } } 97export const getDynImportPaths = async (epPathsA: string[]) => { 98 const ptDirJoinRe = /path\.join\(ptDir,\s*['"]([^'"]+)['"]\)/g 99 const dynPaths: string[] = [] 100 for (const p of epPathsA) { 104 for (const m of contents.matchAll(ptDirJoinRe)) { 109 return dynPaths.length > 0 ? await epsToAllPathsTagged('dynImports', dynPaths) : [] 112const deployDeclaredPropA = [{name: 'inclPtPathA', isArray: true}, {name: 'bundleEntryPtPath', isArray: false}] as const 114const getDeployDeclaredPaths = async (epPathsA: string[]) => { 115 const paths: string[] = [] 116 for (const p of epPathsA) { 120 if (!deployDeclaredPropA.some(prop => contents.includes(prop.name))) continue 121 const ast = acornParse({contents, ptPath: p as unknown as absFileDirPath}) 123 if (node.type !== 'Property') return 124 const prop = deployDeclaredPropA.find(prop => node.key?.type === 'Identifier' && node.key.name === prop.name) 127 if (node.value?.type !== 'ArrayExpression') return 128 for (const elem of node.value.elements) { 129 if (elem?.type === 'Literal' && typeof elem.value === 'string') paths.push(elem.value) 131 } else if (node.value?.type === 'Literal' && typeof node.value.value === 'string') { 132 paths.push(node.value.value) 137 return paths.length > 0 ? await epsToAllPathsTagged('deployDeclared', paths) : [] 140export const buildEpPathSets = async ({onlyEpA}: {onlyEpA: string[]}) => { 141 return await betDurMs('buildEpPathSets:total', async () => { 142 // Virtual imports folded into the ep seeds so their transitive closure is computed by the single 143 // pt0Eps/allEps pass (closure(A∪B)=closure(A)∪closure(B)), avoiding two dedicated madge passes. 144 const pt0VirtualPaths = await getVirtualImportsForEps(onlyEpA) 145 const pageEpA = await getPageEpsFromNextEps(onlyEpA) 146 const combinedEpA = _.uniq([...onlyEpA, ...pageEpA, ...pt0VirtualPaths]) 147 let epPaths = await epsToAllPathsTagged('pt0Eps', combinedEpA) 149 const appPathA = _.compact(await Promise.all(onlyEpA.map(getPtSubdirFromEp))) 150 const pkgJsonPathsA = (await Promise.all(_.uniq(appPathA).map(appPath => getPkgJsonPtPathsA({appPath})))).flat() as unknown as string[] 151 epPaths = _.uniq([...epPaths, ...pkgJsonPathsA]).filter(p => !isExcludedPath(p)) 153 const anchorImportPaths = await getAnchorIncludedImportPaths() 154 const { jsBinScripts: pathBinScriptsA, transitiveImportPaths: pathBinPaths } = await getPathBinInfo() 155 const pt0DynImportPaths = await getDynImportPaths(epPaths) 157 const pt0DeployDeclaredPaths = await getDeployDeclaredPaths(onlyEpA) 158 const pt0EpPathsSet = new Set([...onlyEpA, ...epPaths, ...pt0VirtualPaths, ...anchorImportPaths, ...pt0DynImportPaths, ...pt0DeployDeclaredPaths, ...pathBinPaths, ...ossReplaceTargets]) 161 const allVirtualPaths = await getVirtualImportsForEps(allRegisteredEpPaths) 162 const allEpPagePaths = await getPageEpsFromNextEps(allRegisteredEpPaths) 163 const allCombinedEpA = _.uniq([...allRegisteredEpPaths, ...allEpPagePaths, ...allVirtualPaths]) 164 const allEpPaths = await epsToAllPathsTagged('allEps', allCombinedEpA) 165 const dynImportPaths = await getDynImportPaths(allEpPaths) 166 const deployDeclaredPaths = await getDeployDeclaredPaths(allRegisteredEpPaths) 167 const allEpPathsSet = new Set([...allCombinedEpA, ...allEpPaths, ...allVirtualPaths, ...anchorImportPaths, ...dynImportPaths, ...deployDeclaredPaths, ...pathBinPaths, ...ossReplaceTargets]) 169 return { epPaths, pt0EpPathsSet, allEpPathsSet, pathBinScriptsA } 173export const findDeadCode = async ({showAll}: {showAll?: boolean} = {}) => { 174 const pt0EpA = getAllRegisteredEps().filter((p: string) => migratedDirPrefixA.some(prefix => p.startsWith(prefix))) 175 const { pt0EpPathsSet, allEpPathsSet, pathBinScriptsA } = await buildEpPathSets({onlyEpA: pt0EpA}) 177 // Dead files: migrated code files not in any EP import tree 178 const allMigratedDirFilesA = _.flatten(await Promise.all( 179 migratedDirPrefixA.map(async prefix => { 182 return absFiles.map(absPathToPtPath) 185 const migratedCodeFilesA = allMigratedDirFilesA 186 .filter(isPtCodeFileExt) 187 .filter((p: string) => !p.includes('/migrationsF/') && !p.endsWith('.d.ts') && !isExcludedPath(p) && !p.includes('/public/')) 189 const deadFilesA = migratedCodeFilesA.filter((p: string) => !allEpPathsSet.has(p)) 190 const nonPt0OnlyFilesA = migratedCodeFilesA.filter((p: string) => !pt0EpPathsSet.has(p) && allEpPathsSet.has(p)) 192 // Dead exports: unused exports across all EP code paths 193 const allEpCodePaths = [...allEpPathsSet].filter(isPtCodeFileExt).filter((p: string) => !isSubmodulePath(p)) 194 const ptmvpinnedPaths = allMigratedDirFilesA.filter((p: string) => p.includes(ptmvpinnedInfix)) 195 const reportPaths = allEpCodePaths.filter((p: string) => !isDeadExportExcluded(p)) 196 const extraImportScanPaths = [...allEpCodePaths.filter(isDeadExportExcluded), ...pathBinScriptsA, ...ptmvpinnedPaths] 198 pathsA: reportPaths.map(p => pathDownJoin(ptDir, p) as unknown as absFileDirPath), 199 extraImportScanPathsA: extraImportScanPaths.map(p => pathDownJoin(ptDir, p) as unknown as absFileDirPath), 202 const deadExportsByFile = Object.fromEntries( 203 Object.entries(allDeadExportsByFile) 204 .filter(([p]) => showAll || p.startsWith('pt0/')) 205 .map(([p, exports]) => { 206 let filtered = exports.filter(name => !knownDynamicExports.has(name) && !name.endsWith('K8sRes')) 210 .filter(([, exports]) => (exports as string[]).length > 0) 215 return { deadFilesA, nonPt0OnlyFilesA, deadExportsByFile, reExportBloatA }