2import * as _ from 'lodash-es' 19type MadgeH = Record<string, string[]> 21// BFS path finder in madge graph 22const findPathInMadge = (madgeH: MadgeH, source: string, target: string): string[] | null => { 23 if (source === target) return [source] 24 const visited = new Set([source]) 25 const queue: string[][] = [[source]] 26 while (queue.length > 0) { 27 const path = queue.shift()! 28 const current = path[path.length - 1] 29 const imports = madgeH[current] || [] 30 for (const imp of imports) { 31 if (imp === target) return [...path, imp] 32 if (!visited.has(imp)) { 34 queue.push([...path, imp]) 41// Unified howimports with deploy-include awareness (used by action and MCP tool) 42export const getHowImportsWithDeployIncludes = async ({epA, tgtPath, useReverseFormat = false}: {epA: string[], tgtPath: string, useReverseFormat?: boolean}) => { 43 // 1. Check direct deploy-includes (file is directly in pages/app/public) 46 // 2. Get ES import paths from entrypoints 47 const {retH, transitiveImporters, madgeH} = await get1HowImportsFromEpA({epA, tgtPath}) 50 const shownEpSet = new Set() 52 // 3. Format ES import trees 53 if (retH && (retH instanceof Set ? retH.size > 0 : Object.keys(retH).length > 0)) { 54 const filteredEpA = _.filter(epA, p => isPtCodeFileExt(p) && transitiveImporters.has(p)) 56 if (epPathsA.length > 0) { 57 const formatter = useReverseFormat ? formatImportTreeReverse : formatImportTree 58 outputA.push(formatter({epPathsA, tgtPath})) 59 filteredEpA.forEach(ep => shownEpSet.add(ep)) 63 // 4. Add direct deploy-include entries 64 for (const regEpPath of directDeployIncluders) { 65 if (shownEpSet.has(regEpPath)) continue 66 shownEpSet.add(regEpPath) 68 outputA.push(`\n${regEpPath} (deploy-include)`) 69 outputA.push(` └─[deploys ${appPath}/]─> ${tgtPath}`) 72 // 5. Trace through virtual imports (transitive deploy-includes) 73 const epVirtualImportsH: Record<string, string[]> = {} 74 const allVirtualImportsSet = new Set<string>() 75 for (const ep of epA) { 76 if (shownEpSet.has(ep)) continue 78 if (!appPath) continue 80 if (!virtualImportsA.length) continue 81 const codeVirtualImportsA = _.filter(virtualImportsA, isPtCodeFileExt) 82 if (!codeVirtualImportsA.length) continue 83 epVirtualImportsH[ep] = codeVirtualImportsA 84 codeVirtualImportsA.forEach(vi => allVirtualImportsSet.add(vi)) 87 // Build madge once for all virtual imports (batch optimization) 88 const allVirtualImportsA = [...allVirtualImportsSet] as string[] 89 let combinedMadgeH: MadgeH = {} 90 if (allVirtualImportsA.length) { 92 combinedMadgeH = await ptMadge(allVirtualImportsA) 93 } catch { /* skip if madge fails */ } 96 // Find paths through virtual imports 97 for (const ep of _.keys(epVirtualImportsH)) { 98 const codeVirtualImportsA = epVirtualImportsH[ep]! 100 for (const pageFile of codeVirtualImportsA) { 101 const pathToTarget = findPathInMadge(combinedMadgeH, pageFile, tgtPath) 104 outputA.push(`\n${ep} (deploy-include)`) 105 outputA.push(` └─[deploys ${appPath}/]─> ${pathToTarget.join('\n └─> ')}`) 111 return outputA.join('\n') || 'No entrypoints import this file' 114export const getHowImports = async () => { 118 const lastNonFlagArg = _.findLast(argv, arg => !arg.startsWith('-')) 119 const tgtPath = lastNonFlagArg != action && lastNonFlagArg 120 if (!tgtPath || tgtPath.includes('..')) return 122 const epScope = cliArg(epScopeKey) || epScopes.deployjs 124 if (epScope === epScopes.deployjs) { 126 if (!deployEp && !importMetaUrl) { 128 const output = await getHowImportsWithDeployIncludes({epA, tgtPath, useReverseFormat: true}) 132 await getHowImportsFromEpA({epA, tgtPath}) 134 } else if (epScope === epScopes.dockerjs || epScope === epScopes.pubfejs) { 135 // Trace from current app's docker entrypoints (pages/app/public) with deploy-include awareness 137 const output = await getHowImportsWithDeployIncludes({epA, tgtPath, useReverseFormat: true}) 140 // alljs: use all registered entrypoints 142 const output = await getHowImportsWithDeployIncludes({epA, tgtPath, useReverseFormat: true}) 147export const getHowImportsFromEpA = async ({epA, tgtPath}: {epA: string[], tgtPath: string}) => { 148 const {retH, transitiveImporters, madgeH} = await get1HowImportsFromEpA({epA, tgtPath}) 149 if (retH && (retH instanceof Set ? retH.size > 0 : Object.keys(retH).length > 0)) { 150 const filteredEpA = _.filter(epA, ptPath => 154 if (epPathsA.length > 0) { 156 console.log(treeView) 158 console.log('No entrypoints import this file') 161 console.log('No entrypoints import this file') 165type HowImportsResult = {retH: Set<string> | MadgeH, transitiveImporters: Set<string>, madgeH: MadgeH} 167export const get1HowImportsFromEpA = async ({epA, tgtPath}: {epA: string[], tgtPath?: string}): Promise<HowImportsResult> => { 169 const doesInclTgtPath = _.includes(calcDockerPaths, tgtPath) 173 let retH: Set<string> | MadgeH = madgeH 174 let transitiveImporters = new Set<string>() 177 // Get direct importers for display 180 // Get all transitive importers using existing function 182 transitiveImporters = new Set(_.keys(relevantMadgeH)) 185 if (doesInclTgtPath || !tgtPath) { 186 return {retH, transitiveImporters, madgeH} 188 return {retH: new Set<string>(), transitiveImporters: new Set<string>(), madgeH}