import path from "node:path"; import fs from "node:fs"; import type { Database as DBType } from "better-sqlite3"; interface ScanStats { modules: number; symbols: number; } /** 递归收集目录下匹配扩展名的所有文件 */ function walkDir(dir: string, exts: string[]): string[] { if (!fs.existsSync(dir)) return []; const results: string[] = []; const stack: string[] = [dir]; while (stack.length > 0) { const current = stack.pop()!; let entries: fs.Dirent[]; try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; } for (const entry of entries) { const fullPath = path.join(current, entry.name); if (entry.isDirectory()) { // 跳过 node_modules / dist / .git if ( ["node_modules", "dist", ".git", ".next", "build"].includes( entry.name, ) ) continue; stack.push(fullPath); } else if (entry.isFile()) { const ext = path.extname(entry.name); if (exts.includes(ext)) results.push(fullPath); } } } return results; } export function scanTypeScript(db: DBType, root: string): ScanStats { const insertModule = db.prepare( "INSERT OR IGNORE INTO modules (name, path, language, service, type) VALUES (?, ?, ?, ?, ?)", ); const insertSymbol = db.prepare( "INSERT INTO symbols (module_id, name, kind, language, file_path, line_start, line_end, is_exported) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ); const getModuleId = db.prepare("SELECT id FROM modules WHERE name = ?") as { get: (name: string) => { id: number } | undefined; }; let modules = 0; let symbols = 0; // 扫描 services/* 和 apps/* 和 packages/* 下的 TS 服务 const serviceDirs = ["services", "apps"]; const moduleList: { name: string; path: string; service: string }[] = []; for (const dir of serviceDirs) { const baseDir = path.join(root, dir); if (!fs.existsSync(baseDir)) continue; const entries = fs.readdirSync(baseDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const serviceName = entry.name; const servicePath = path.join(baseDir, serviceName); // 确认是 TS 服务(有 src 目录或 package.json) const hasSrc = fs.existsSync(path.join(servicePath, "src")); const hasPkg = fs.existsSync(path.join(servicePath, "package.json")); if (!hasSrc && !hasPkg) continue; const moduleName = serviceName; insertModule.run(moduleName, servicePath, "ts", serviceName, "service"); const row = getModuleId.get(moduleName); if (row) { modules++; moduleList.push({ name: moduleName, path: servicePath, service: serviceName, }); } } } // packages/* 下的 TS 包 const packagesDir = path.join(root, "packages"); if (fs.existsSync(packagesDir)) { const entries = fs.readdirSync(packagesDir, { withFileTypes: true }); for (const entry of entries) { if (!entry.isDirectory()) continue; const pkgName = entry.name; const pkgPath = path.join(packagesDir, pkgName); const hasSrc = fs.existsSync(path.join(pkgPath, "src")); const hasPkg = fs.existsSync(path.join(pkgPath, "package.json")); const hasProto = fs.existsSync(path.join(pkgPath, "proto")); if (!hasSrc && !hasPkg && !hasProto) continue; const moduleName = pkgName; insertModule.run(moduleName, pkgPath, "ts", pkgName, "package"); const row = getModuleId.get(moduleName); if (row) { modules++; moduleList.push({ name: moduleName, path: pkgPath, service: pkgName }); } } } // 收集所有 .ts 文件并用 regex 提取符号(ts-morph 对未安装依赖的文件可能解析失败) const TS_FUNC_RE = /(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(/g; const TS_CLASS_RE = /(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/g; const TS_INTERFACE_RE = /(?:export\s+)?interface\s+(\w+)/g; const TS_CONST_RE = /(?:export\s+)?const\s+([A-Z][A-Z0-9_]+)\s*=/g; for (const mod of moduleList) { const srcDir = path.join(mod.path, "src"); if (!fs.existsSync(srcDir)) continue; const tsFiles = walkDir(srcDir, [".ts", ".tsx"]); const modRow = getModuleId.get(mod.name); if (!modRow) continue; for (const filePath of tsFiles) { let content: string; try { content = fs.readFileSync(filePath, "utf-8"); } catch { continue; } // 函数 let match: RegExpExecArray | null; const funcRe = new RegExp(TS_FUNC_RE); while ((match = funcRe.exec(content)) !== null) { const name = match[1]; const lineNum = content.slice(0, match.index).split("\n").length; const isExported = match[0].includes("export") ? 1 : 0; insertSymbol.run( modRow.id, name, "function", "ts", filePath, lineNum, lineNum, isExported, ); symbols++; } // 类 const classRe = new RegExp(TS_CLASS_RE); while ((match = classRe.exec(content)) !== null) { const name = match[1]; const lineNum = content.slice(0, match.index).split("\n").length; const isExported = match[0].includes("export") ? 1 : 0; insertSymbol.run( modRow.id, name, "class", "ts", filePath, lineNum, lineNum, isExported, ); symbols++; } // 接口 const ifaceRe = new RegExp(TS_INTERFACE_RE); while ((match = ifaceRe.exec(content)) !== null) { const name = match[1]; const lineNum = content.slice(0, match.index).split("\n").length; const isExported = match[0].includes("export") ? 1 : 0; insertSymbol.run( modRow.id, name, "interface", "ts", filePath, lineNum, lineNum, isExported, ); symbols++; } // 常量(UPPER_CASE) const constRe = new RegExp(TS_CONST_RE); while ((match = constRe.exec(content)) !== null) { const name = match[1]; const lineNum = content.slice(0, match.index).split("\n").length; const isExported = match[0].includes("export") ? 1 : 0; insertSymbol.run( modRow.id, name, "const", "ts", filePath, lineNum, lineNum, isExported, ); symbols++; } } } return { modules, symbols }; }