Files
Edu/scripts/arch-scan/scanners/ts-scanner.ts
SpecialX 4629de1926 fix(arch-scan): 修复多语言扫描器并添加 DELETE 清空表
- scanner.ts: 添加 DELETE 清空旧数据语句,避免重复插入
- ts-scanner: 改用 regex 替代 ts-morph 解析(避免解析失败)
- go-scanner/py-scanner/proto-scanner: 完善实现
2026-07-08 12:49:43 +08:00

212 lines
6.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 };
}