🌳
pt0/deployF/acornF/acornSingleFileAI.mts
6import path from 'path'
7import * as _ from 'lodash-es'
9import { acornParse } from './acornParseF.mts'
10import ts from 'typescript'
12import { getTsExtContents, tsSrcFile, isShellScript, type AcornNode } from './libAcornF.mts'
17import fs from 'fs'
18import { tsAbsPath, type absFileDirPath } from '../../ptDirF.mts'
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}) ?? {}
25 const absPath = pathDownJoin(ptDir, ptPath)
26 const mtime = fs.statSync(absPath, {throwIfNoEntry: false})?.mtimeMs || 0
27 const cacheKeyA = ['acornSingleFile-v6', ptPath, String(mtime)]
28 const cached = await readMemoTempfileIfExist({cacheKeyA})
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 {}
35 await runMemoTempfile({cacheKeyA}, async () => result)
36 return result
39const acornSingleFileUncached = async (ptPath: absFileDirPath, {onBrokenImport, contents: providedContents}: SingleFileOpts = {}) => {
40 if (isPtExtNotCode(ptPath)) return {}
42 let tsExtContents
43 if (providedContents !== undefined) {
44 const ext = path.extname(ptPath).slice(1)
45 tsExtContents = { contents: providedContents, tsExt: ptTsFileExtA.includes(ext), isMaybeBorked: false }
46 } else {
47 try {
48 tsExtContents = await getTsExtContents({ptPath})
49 } catch (err: any) {
50 mcpDebugLog(`warn skipping unreadable file: ${ptPath} - ${err.message}`)
51 return null
52 }
53 }
54 const {contents, isMaybeBorked, tsExt} = tsExtContents
56 if (isShellScript(contents)) return {}
57 const importsObj: Record<string, string[]> = {}
58 const parentDir = path.dirname(pathDownJoin(ptDir, ptPath))
60 const warnBrokenImport = (importPath: string, resolvedPtPath: string) => {
61 mcpDebugLog(`broken import: ${ptPath} -> ${importPath} (${resolvedPtPath} not exist)`)
62 onBrokenImport?.(importPath, resolvedPtPath)
63 }
65 const addImport = (resolvedPath: string, importName: string | null) => {
66 importsObj[resolvedPath] ||= []
67 if (importName && !importsObj[resolvedPath].includes(importName)) {
68 importsObj[resolvedPath].push(importName)
69 }
70 }
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)')
77 return
78 }
79 const importPtPath = absPathToPtPath(importAbsPath) as string
81 const tgtFileExt = ptFileExtA.find(ext => importPtPath.endsWith('.' + ext))
82 if (tgtFileExt || isPtExtNotCode(importPtPath)) {
83 if (!await fileExists(importAbsPath)) {
84 if (isMaybeBorked) return
85 warnBrokenImport(importPath, importPtPath)
86 return
87 }
88 addImport(importPtPath, importName)
89 return
90 }
92 let possiblePathsA = ptFileExtA.map(ext => tsAbsPath(importAbsPath + '.' + ext))
94 if (await getIsDir(importAbsPath)) {
95 possiblePathsA.push(...ptFileExtA.map(ext => tsAbsPath(pathDownJoin(importAbsPath, 'index.' + ext))))
96 }
98 const correctedPath = _.compact(await allPromCalls(possiblePathsA, async (pathWithExt) => {
99 if (!await fileExists(pathWithExt)) return
100 return absPathToPtPath(pathWithExt)
101 }))[0]
102 if (!correctedPath) {
103 warnBrokenImport(importPath, importPtPath)
104 return
105 }
106 addImport(correctedPath, importName)
107 }
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)
114 }
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))
120 let targetPtPath = absPathToPtPath(importAbsPath) as string
122 if (!await fileExists(importAbsPath)) {
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))
128 }
130 if (await getIsDir(importAbsPath)) {
131 possiblePathsA.push(...ptFileExtA.map(ext => tsAbsPath(pathDownJoin(importAbsPath, 'index.' + ext))))
132 }
133 const correctedPath = _.compact(await allPromCalls(possiblePathsA, async (pathWithExt) => {
134 if (!await fileExists(pathWithExt)) return
135 return absPathToPtPath(pathWithExt) as string
136 }))[0]
137 if (!correctedPath) {
138 warnBrokenImport(importPath, targetPtPath)
139 return { resolvedPath: null, exportNames: [] }
140 }
141 targetPtPath = correctedPath
142 }
144 if (!targetPtPath) return { resolvedPath: null, exportNames: [] }
146 const targetDir = path.dirname(tsAbsPath(targetPtPath))
148 const {contents: targetContents, tsExt: targetTsExt} = await getTsExtContents({ptPath: tsAbsPath(targetPtPath)})
149 const exportNames: string[] = []
151 if (targetTsExt) {
152 const targetSourceFile = tsSrcFile({ptPath: tsAbsPath(targetPtPath), contents: targetContents})
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)
162 }
163 } else if (node.moduleSpecifier?.kind === ts.SyntaxKind.StringLiteral) {
164 reExportStarSources.push(node.moduleSpecifier.text)
165 }
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)
170 if (hasExport) {
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)
177 }
178 }
179 }
180 }
181 ts.forEachChild(node, visitExports)
182 }
183 visitExports(targetSourceFile)
184 for (const src of reExportStarSources) {
185 const inner = await expandNamespaceImport(src, targetDir)
186 exportNames.push(...inner.exportNames)
187 }
188 } else {
189 const targetAst = acornParse({contents: targetContents, ptPath: tsAbsPath(targetPtPath)})
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)
197 }
198 } else if (node.declaration.id) {
199 exportNames.push(node.declaration.id.name)
200 }
201 } else if (node.specifiers) {
202 for (const spec of node.specifiers as AcornNode[]) exportNames.push(spec.exported.name ?? spec.exported.value)
203 }
204 } else if (node.type === 'ExportDefaultDeclaration') {
205 exportNames.push('default')
206 } else if (node.type === 'ExportAllDeclaration') {
207 if (node.exported) {
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)
212 }
213 }
214 }
215 }
216 return { resolvedPath: targetPtPath, exportNames: _.uniq(exportNames) }
217 }
219 if (tsExt) {
220 const sourceFile = tsSrcFile({ptPath, contents})
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))
232 }
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))
240 }
241 }
242 }
243 } else {
244 importPromises.push(pushImportPath(importPath, null))
245 }
246 }
247 }
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))
255 }
256 } else {
257 importPromises.push(pushImportPath(importPath, '*'))
258 }
259 }
260 }
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))
266 }
267 }
269 ts.forEachChild(node, visit)
270 }
272 visit(sourceFile)
273 await Promise.all(importPromises)
274 return importsObj
275 }
277 const ast = acornParse({contents, ptPath})
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, '*')
283 }
285 // Handle import() expressions (two AST representations across acorn versions)
286 const dynImportSource = (node.type === 'ImportExpression' && node.source?.type === 'Literal' && node.source.value)
287 ? node.source.value
288 : (node.type === 'CallExpression' && node.callee.type === 'Import' && node.arguments[0]?.type === 'Literal')
289 ? node.arguments[0].value
290 : null
291 if (dynImportSource) {
292 const extractedNames = extractDynamicImportNames(node, ancestors)
293 if (extractedNames) {
294 for (const name of extractedNames) await pushImportPath(dynImportSource, name)
295 } else {
296 await addNamespaceImport(dynImportSource)
297 }
298 }
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)
308 }
309 }
310 }
312 const promises: Promise<void>[] = []
313 for (const node of ast.body as AcornNode[]) {
314 if (node.type === 'ExportNamedDeclaration') {
315 if (node.source) {
316 for (const spec of node.specifiers) {
317 promises.push(pushImportPath(node.source.value as string, spec.local.name))
318 }
319 }
320 }
321 if (node.type === 'ImportDeclaration') {
322 if (node.specifiers.length === 0) {
323 promises.push(pushImportPath(node.source.value as string, null))
324 }
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))
332 }
333 }
334 }
335 }
336 await Promise.all(promises)
337 await findDynamicImports(ast)
338 return importsObj