- 新增 pyproject.toml 检测,跳过纯 Python 服务(ai/data-ana)和包(shared-py) - 扩展符号提取:新增 TS_CONST_EXPORT_RE 匹配 camelCase/PascalCase 导出常量 - 新增 TS_TYPE_RE 匹配 export type alias - 修复后 arch:scan 输出 22 模块 / 4715 符号 / 475 契约
265 lines
8.3 KiB
TypeScript
265 lines
8.3 KiB
TypeScript
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;
|
||
// 跳过纯 Python 服务(有 pyproject.toml 且无 package.json)
|
||
const hasPyproject = fs.existsSync(
|
||
path.join(servicePath, "pyproject.toml"),
|
||
);
|
||
if (hasPyproject && !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;
|
||
// 跳过纯 Python 包(有 pyproject.toml 且无 package.json)
|
||
const hasPyprojectPkg = fs.existsSync(
|
||
path.join(pkgPath, "pyproject.toml"),
|
||
);
|
||
if (hasPyprojectPkg && !hasPkg) 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;
|
||
// 全大写常量(如 MAX_RETRY_COUNT)
|
||
const TS_CONST_UPPER_RE = /(?:export\s+)?const\s+([A-Z][A-Z0-9_]+)\s*=/g;
|
||
// 导出的 camelCase / PascalCase 常量(如 colors、fontSizes)
|
||
const TS_CONST_EXPORT_RE = /export\s+const\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g;
|
||
// 导出的 type alias(如 ColorScheme)
|
||
const TS_TYPE_RE = /export\s+type\s+(\w+)\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_UPPER_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++;
|
||
}
|
||
|
||
// 导出的 camelCase / PascalCase 常量(去重:已被 UPPER_CASE 匹配的跳过)
|
||
const constExportRe = new RegExp(TS_CONST_EXPORT_RE);
|
||
while ((match = constExportRe.exec(content)) !== null) {
|
||
const name = match[1];
|
||
// 跳过已被 UPPER_CASE 匹配的
|
||
if (/^[A-Z][A-Z0-9_]+$/.test(name)) continue;
|
||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||
insertSymbol.run(
|
||
modRow.id,
|
||
name,
|
||
"const",
|
||
"ts",
|
||
filePath,
|
||
lineNum,
|
||
lineNum,
|
||
1,
|
||
);
|
||
symbols++;
|
||
}
|
||
|
||
// 导出的 type alias
|
||
const typeRe = new RegExp(TS_TYPE_RE);
|
||
while ((match = typeRe.exec(content)) !== null) {
|
||
const name = match[1];
|
||
const lineNum = content.slice(0, match.index).split("\n").length;
|
||
insertSymbol.run(
|
||
modRow.id,
|
||
name,
|
||
"type",
|
||
"ts",
|
||
filePath,
|
||
lineNum,
|
||
lineNum,
|
||
1,
|
||
);
|
||
symbols++;
|
||
}
|
||
}
|
||
}
|
||
|
||
return { modules, symbols };
|
||
}
|