trace-imports.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. #!/usr/bin/env bun
  2. import * as path from "path"
  3. import * as ts from "typescript"
  4. const BASE_DIR = "/home/thdxr/dev/projects/anomalyco/kirincode/packages/opencode"
  5. // Get entry file from command line arg or use default
  6. const ENTRY_FILE = process.argv[2] || "src/plugin/tui/runtime.ts"
  7. const visited = new Set<string>()
  8. function resolveImport(importPath: string, fromFile: string): string | null {
  9. if (importPath.startsWith("@/")) {
  10. return path.join(BASE_DIR, "src", importPath.slice(2))
  11. }
  12. if (importPath.startsWith("./") || importPath.startsWith("../")) {
  13. const dir = path.dirname(fromFile)
  14. return path.resolve(dir, importPath)
  15. }
  16. return null
  17. }
  18. function isInternalImport(importPath: string): boolean {
  19. return importPath.startsWith("@/") || importPath.startsWith("./") || importPath.startsWith("../")
  20. }
  21. async function tryExtensions(filePath: string): Promise<string | null> {
  22. const extensions = [".ts", ".tsx", ".js", ".jsx"]
  23. try {
  24. const file = Bun.file(filePath)
  25. const stat = await file.stat()
  26. if (stat?.isDirectory()) {
  27. for (const ext of extensions) {
  28. const indexPath = path.join(filePath, "index" + ext)
  29. const indexFile = Bun.file(indexPath)
  30. if (await indexFile.exists()) return indexPath
  31. }
  32. return null
  33. }
  34. // It's a file
  35. return filePath
  36. } catch {
  37. // Path doesn't exist, try adding extensions
  38. for (const ext of extensions) {
  39. const withExt = filePath + ext
  40. const extFile = Bun.file(withExt)
  41. if (await extFile.exists()) return withExt
  42. }
  43. return null
  44. }
  45. }
  46. function extractImports(sourceFile: ts.SourceFile): string[] {
  47. const imports: string[] = []
  48. function visit(node: ts.Node) {
  49. // import x from "path" or import { x } from "path"
  50. if (ts.isImportDeclaration(node)) {
  51. // Skip type-only imports
  52. if (node.importClause?.isTypeOnly) return
  53. const moduleSpec = node.moduleSpecifier
  54. if (ts.isStringLiteral(moduleSpec)) {
  55. imports.push(moduleSpec.text)
  56. }
  57. }
  58. // export { x } from "path"
  59. if (ts.isExportDeclaration(node) && node.moduleSpecifier) {
  60. if (ts.isStringLiteral(node.moduleSpecifier)) {
  61. imports.push(node.moduleSpecifier.text)
  62. }
  63. }
  64. // Dynamic import: import("path")
  65. if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
  66. const arg = node.arguments[0]
  67. if (arg && ts.isStringLiteral(arg)) {
  68. imports.push(arg.text)
  69. }
  70. }
  71. ts.forEachChild(node, visit)
  72. }
  73. visit(sourceFile)
  74. return imports
  75. }
  76. async function traceFile(filePath: string, depth = 0): Promise<void> {
  77. const normalizedPath = path.relative(BASE_DIR, filePath)
  78. if (visited.has(filePath)) {
  79. return
  80. }
  81. // Only trace TypeScript/JavaScript files
  82. if (!filePath.match(/\.(ts|tsx|js|jsx)$/)) {
  83. return
  84. }
  85. visited.add(filePath)
  86. console.log("\t".repeat(depth) + normalizedPath)
  87. let content: string
  88. try {
  89. content = await Bun.file(filePath).text()
  90. } catch {
  91. return
  92. }
  93. const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true)
  94. const imports = extractImports(sourceFile)
  95. const internalImports = imports.filter(isInternalImport)
  96. const externalImports = imports.filter((imp) => !isInternalImport(imp))
  97. // Print external imports
  98. for (const imp of externalImports) {
  99. console.log("\t".repeat(depth + 1) + `[ext] ${imp}`)
  100. }
  101. for (const imp of internalImports) {
  102. const resolved = resolveImport(imp, filePath)
  103. if (!resolved) continue
  104. const actualPath = await tryExtensions(resolved)
  105. if (!actualPath) continue
  106. await traceFile(actualPath, depth + 1)
  107. }
  108. }
  109. async function main() {
  110. const entryPath = path.join(BASE_DIR, ENTRY_FILE)
  111. // Check if file exists
  112. const file = Bun.file(entryPath)
  113. if (!(await file.exists())) {
  114. console.error(`File not found: ${ENTRY_FILE}`)
  115. console.error(`Resolved to: ${entryPath}`)
  116. process.exit(1)
  117. }
  118. await traceFile(entryPath)
  119. }
  120. main().catch(console.error)