7import * as _ from 'lodash-es' 10import ts from 'typescript' 20type TsNode = ts.Node & {[k: string]: any} 21type SingleFileOpts = {onBrokenImport?: (importPath: string, resolvedPtPath: string) => void, contents?: string} 23export const acornSingleFile = async (ptPath: absFileDirPath, {onBrokenImport, contents: providedContents}: SingleFileOpts = {}) => { 24 if (onBrokenImport || providedContents !== undefined) return await acornSingleFileUncached(ptPath, {onBrokenImport, contents: providedContents}) ?? {} 26 const mtime = fs.statSync(absPath, {throwIfNoEntry: false})?.mtimeMs || 0 27 const cacheKeyA = ['acornSingleFile-v6', ptPath, String(mtime)] 29 if (cached) return cached as Record<string, string[]> 30 const result = await acornSingleFileUncached(ptPath) 31 // null = error-induced empty (acorn budget exhausted, transient read failure) — never cache; caching poisoned 32 // the import graph (famreproc deploy omitted simplify1HF.mjs despite do1SetTagF.mts importing it). 33 // Legit {} (leaf files with no relative imports) caches fine. 34 if (result === null) return {} 39const acornSingleFileUncached = async (ptPath: absFileDirPath, {onBrokenImport, contents: providedContents}: SingleFileOpts = {}) => { 43 if (providedContents !== undefined) { 44 const ext = path.extname(ptPath).slice(1) 45 tsExtContents = { contents: providedContents, tsExt: ptTsFileExtA.includes(ext), isMaybeBorked: false } 50 mcpDebugLog(`warn skipping unreadable file: ${ptPath} - ${err.message}`) 54 const {contents, isMaybeBorked, tsExt} = tsExtContents 57 const importsObj: Record<string, string[]> = {} 60 const warnBrokenImport = (importPath: string, resolvedPtPath: string) => { 61 mcpDebugLog(`broken import: ${ptPath} -> ${importPath} (${resolvedPtPath} not exist)`) 62 onBrokenImport?.(importPath, resolvedPtPath) 65 const addImport = (resolvedPath: string, importName: string | null) => { 66 importsObj[resolvedPath] ||= [] 67 if (importName && !importsObj[resolvedPath].includes(importName)) { 68 importsObj[resolvedPath].push(importName) 72 const pushImportPath = async (importPath: string, importName: string | null) => { 73 if (!importPath.startsWith('.')) return 74 const importAbsPath = tsAbsPath(path.resolve(parentDir, importPath)) 75 if (!importAbsPath.startsWith(ptDir)) { 76 warnBrokenImport(importPath, importAbsPath + ' (outside monorepo)') 81 const tgtFileExt = ptFileExtA.find(ext => importPtPath.endsWith('.' + ext)) 84 if (isMaybeBorked) return 85 warnBrokenImport(importPath, importPtPath) 88 addImport(importPtPath, importName) 92 let possiblePathsA = ptFileExtA.map(ext => tsAbsPath(importAbsPath + '.' + ext)) 98 const correctedPath = _.compact(await allPromCalls(possiblePathsA, async (pathWithExt) => { 102 if (!correctedPath) { 103 warnBrokenImport(importPath, importPtPath) 106 addImport(correctedPath, importName) 109 const addNamespaceImport = async (importPath: string) => { 110 const { resolvedPath, exportNames } = await expandNamespaceImport(importPath) 111 if (!resolvedPath) return 112 addImport(resolvedPath, null) 113 for (const name of exportNames) addImport(resolvedPath, name) 116 const expandNamespaceImport = async (importPath: string, fromDir: string = parentDir) => { 117 if (!importPath.startsWith('.')) return { resolvedPath: null, exportNames: [] } 119 const importAbsPath = tsAbsPath(path.resolve(fromDir, importPath)) 123 let possiblePathsA = ptFileExtA.map(ext => tsAbsPath(importAbsPath + '.' + ext)) 125 const importerExt = path.extname(ptPath).slice(1) 126 if (importerExt && !ptFileExtA.includes(importerExt)) { 127 possiblePathsA.unshift(tsAbsPath(importAbsPath + '.' + importerExt)) 133 const correctedPath = _.compact(await allPromCalls(possiblePathsA, async (pathWithExt) => { 137 if (!correctedPath) { 138 warnBrokenImport(importPath, targetPtPath) 139 return { resolvedPath: null, exportNames: [] } 141 targetPtPath = correctedPath 144 if (!targetPtPath) return { resolvedPath: null, exportNames: [] } 149 const exportNames: string[] = [] 153 const reExportStarSources: string[] = [] 155 function visitExports(node: TsNode) { 156 if (node.kind === ts.SyntaxKind.ExportDeclaration) { 157 if (node.exportClause) { 158 if (node.exportClause.kind === ts.SyntaxKind.NamedExports) { 159 for (const element of node.exportClause.elements) exportNames.push(element.name.text) 160 } else if (node.exportClause.kind === ts.SyntaxKind.NamespaceExport) { 161 exportNames.push(node.exportClause.name.text) 163 } else if (node.moduleSpecifier?.kind === ts.SyntaxKind.StringLiteral) { 164 reExportStarSources.push(node.moduleSpecifier.text) 166 } else if (node.kind === ts.SyntaxKind.ExportAssignment) { 167 if (!node.isExportEquals) exportNames.push('default') 168 } else if (node.modifiers) { 169 const hasExport = node.modifiers.some((m: TsNode) => m.kind === ts.SyntaxKind.ExportKeyword) 171 const hasDefault = node.modifiers.some((m: TsNode) => m.kind === ts.SyntaxKind.DefaultKeyword) 172 if (node.kind === ts.SyntaxKind.FunctionDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration) { 173 exportNames.push(hasDefault ? 'default' : node.name?.text) 174 } else if (node.kind === ts.SyntaxKind.VariableStatement) { 175 for (const decl of node.declarationList.declarations) { 176 if (decl.name?.kind === ts.SyntaxKind.Identifier) exportNames.push(decl.name.text) 181 ts.forEachChild(node, visitExports) 183 visitExports(targetSourceFile) 184 for (const src of reExportStarSources) { 185 const inner = await expandNamespaceImport(src, targetDir) 186 exportNames.push(...inner.exportNames) 191 for (const node of targetAst.body) { 192 if (node.type === 'ExportNamedDeclaration') { 193 if (node.declaration) { 194 if (node.declaration.type === 'VariableDeclaration') { 195 for (const decl of node.declaration.declarations) { 196 if (decl.id.type === 'Identifier') exportNames.push(decl.id.name) 198 } else if (node.declaration.id) { 199 exportNames.push(node.declaration.id.name) 201 } else if (node.specifiers) { 202 for (const spec of node.specifiers as AcornNode[]) exportNames.push(spec.exported.name ?? spec.exported.value) 204 } else if (node.type === 'ExportDefaultDeclaration') { 205 exportNames.push('default') 206 } else if (node.type === 'ExportAllDeclaration') { 208 exportNames.push((node.exported as AcornNode).name) 209 } else if (node.source?.value) { 210 const inner = await expandNamespaceImport(node.source.value as string, targetDir) 211 exportNames.push(...inner.exportNames) 216 return { resolvedPath: targetPtPath, exportNames: _.uniq(exportNames) } 221 const importPromises: Promise<void>[] = [] 223 function visit(node: TsNode) { 224 if (node.kind === ts.SyntaxKind.ImportDeclaration) { 225 const moduleSpecifier = node.moduleSpecifier 226 if (moduleSpecifier?.kind === ts.SyntaxKind.StringLiteral) { 227 const importPath = moduleSpecifier.text 229 if (node.importClause) { 230 if (node.importClause.name) { 231 importPromises.push(pushImportPath(importPath, node.importClause.name.text)) 233 if (node.importClause.namedBindings) { 234 if (node.importClause.namedBindings.kind === ts.SyntaxKind.NamespaceImport) { 235 importPromises.push(addNamespaceImport(importPath)) 236 } else if (node.importClause.namedBindings.kind === ts.SyntaxKind.NamedImports) { 237 for (const element of node.importClause.namedBindings.elements) { 238 const origName = element.propertyName?.text || element.name.text 239 importPromises.push(pushImportPath(importPath, origName)) 244 importPromises.push(pushImportPath(importPath, null)) 249 if (node.kind === ts.SyntaxKind.ExportDeclaration && node.moduleSpecifier) { 250 if (node.moduleSpecifier.kind === ts.SyntaxKind.StringLiteral) { 251 const importPath = node.moduleSpecifier.text 252 if (node.exportClause?.kind === ts.SyntaxKind.NamedExports) { 253 for (const element of node.exportClause.elements) { 254 importPromises.push(pushImportPath(importPath, element.propertyName?.text || element.name.text)) 257 importPromises.push(pushImportPath(importPath, '*')) 262 if (node.kind === ts.SyntaxKind.CallExpression && node.expression?.kind === ts.SyntaxKind.ImportKeyword) { 263 const arg = node.arguments?.[0] 264 if (arg?.kind === ts.SyntaxKind.StringLiteral) { 265 importPromises.push(addNamespaceImport(arg.text)) 269 ts.forEachChild(node, visit) 273 await Promise.all(importPromises) 278 const findDynamicImports = async (node: AcornNode, ancestors: AcornNode[] = []) => { 279 if (!node || typeof node !== 'object') return 281 if (node.type === 'ExportAllDeclaration') { 282 await pushImportPath(node.source.value, '*') 285 // Handle import() expressions (two AST representations across acorn versions) 286 const dynImportSource = (node.type === 'ImportExpression' && node.source?.type === 'Literal' && node.source.value) 288 : (node.type === 'CallExpression' && node.callee.type === 'Import' && node.arguments[0]?.type === 'Literal') 289 ? node.arguments[0].value 291 if (dynImportSource) { 293 if (extractedNames) { 294 for (const name of extractedNames) await pushImportPath(dynImportSource, name) 296 await addNamespaceImport(dynImportSource) 300 const newAncestors = [node, ...ancestors] 301 for (const key in node) { 302 if (key === 'parent') continue 303 const value = node[key] 304 if (Array.isArray(value)) { 305 await Promise.all(value.map(child => findDynamicImports(child, newAncestors))) 306 } else if (value && typeof value === 'object') { 307 await findDynamicImports(value, newAncestors) 312 const promises: Promise<void>[] = [] 313 for (const node of ast.body as AcornNode[]) { 314 if (node.type === 'ExportNamedDeclaration') { 316 for (const spec of node.specifiers) { 317 promises.push(pushImportPath(node.source.value as string, spec.local.name)) 321 if (node.type === 'ImportDeclaration') { 322 if (node.specifiers.length === 0) { 323 promises.push(pushImportPath(node.source.value as string, null)) 325 for (const spec of node.specifiers) { 326 if (spec.type === 'ImportDefaultSpecifier') { 327 promises.push(pushImportPath(node.source.value as string, spec.local.name)) 328 } else if (spec.type === 'ImportSpecifier') { 329 promises.push(pushImportPath(node.source.value as string, (spec.imported?.name ?? spec.local.name) as string)) 330 } else if (spec.type === 'ImportNamespaceSpecifier') { 331 promises.push(addNamespaceImport(node.source.value as string)) 336 await Promise.all(promises) 337 await findDynamicImports(ast)