188 lines
5.8 KiB
TypeScript
188 lines
5.8 KiB
TypeScript
import Database from "better-sqlite3";
|
|
import { execSync } from "node:child_process";
|
|
import { initSchema, clearAllData } from "./schema";
|
|
import {
|
|
scanAllModules,
|
|
scanFiles,
|
|
scanSymbols,
|
|
scanCalls,
|
|
scanTechTags,
|
|
scanFileImports,
|
|
scanModuleDeps,
|
|
} from "./scanner";
|
|
import {
|
|
querySymbolRefs,
|
|
querySymbolForwardCalls,
|
|
queryModuleDeps,
|
|
queryModuleReverseDeps,
|
|
queryTechUsage,
|
|
queryViolations,
|
|
} from "./query";
|
|
|
|
const DB_PATH = "scripts/arch-scan/arch.db";
|
|
|
|
function getCommitHash(): string {
|
|
try {
|
|
return execSync("git rev-parse --short HEAD")
|
|
.toString()
|
|
.trim();
|
|
} catch {
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
export function runScan(): void {
|
|
console.log("Starting architecture scan...");
|
|
const db = new Database(DB_PATH);
|
|
db.pragma("journal_mode = WAL");
|
|
initSchema(db);
|
|
clearAllData(db);
|
|
console.log("Schema initialized.");
|
|
scanAllModules(db, process.cwd());
|
|
console.log("Modules scanned.");
|
|
// scanFiles 扫描 DB 中所有模块,只需调用一次
|
|
scanFiles(db, "src/modules");
|
|
console.log("Files scanned.");
|
|
scanSymbols(db);
|
|
console.log("Symbols scanned.");
|
|
scanCalls(db);
|
|
console.log("Calls scanned.");
|
|
scanTechTags(db);
|
|
console.log("Tech tags scanned.");
|
|
scanFileImports(db);
|
|
scanModuleDeps(db);
|
|
console.log("Module dependencies scanned.");
|
|
const now = new Date().toISOString();
|
|
const commit = getCommitHash();
|
|
const meta = db.prepare(
|
|
"INSERT OR REPLACE INTO scan_meta (key, value) VALUES (?, ?)"
|
|
);
|
|
meta.run("scanned_at", now);
|
|
meta.run("commit_hash", commit);
|
|
meta.run("scanner_version", "1.0.0");
|
|
const stats = {
|
|
files: (db.prepare("SELECT count(*) as c FROM files").get() as { c: number }).c,
|
|
symbols: (db.prepare("SELECT count(*) as c FROM symbols").get() as { c: number }).c,
|
|
calls: (db.prepare("SELECT count(*) as c FROM calls").get() as { c: number }).c,
|
|
};
|
|
meta.run("total_files", String(stats.files));
|
|
meta.run("total_symbols", String(stats.symbols));
|
|
meta.run("total_calls", String(stats.calls));
|
|
console.log(
|
|
`Scan complete: ${stats.files} files, ${stats.symbols} symbols, ${stats.calls} calls`
|
|
);
|
|
console.log(`Commit: ${commit}, Time: ${now}`);
|
|
db.close();
|
|
}
|
|
|
|
export function runQuery(args: string[]): void {
|
|
const db = new Database(DB_PATH, { readonly: true });
|
|
const command = args[0];
|
|
switch (command) {
|
|
case "ref": {
|
|
const symbol = args[1];
|
|
if (!symbol) {
|
|
console.error("Usage: arch:query ref <symbolName> [--forward] [--depth=N]");
|
|
process.exit(1);
|
|
}
|
|
const forward = args.includes("--forward");
|
|
const depthArg = args.find((a) => a.startsWith("--depth="));
|
|
const depth = depthArg ? parseInt(depthArg.split("=")[1], 10) : 10;
|
|
if (forward) {
|
|
const calls = querySymbolForwardCalls(db, symbol, depth);
|
|
console.log(`▼ ${symbol}`);
|
|
for (const call of calls) {
|
|
const indent = " ".repeat(call.depth + 1);
|
|
console.log(
|
|
`${indent}└─▶ ${call.callee_name} (${call.callee_path}#L${call.callee_line})`
|
|
);
|
|
}
|
|
} else {
|
|
const refs = querySymbolRefs(db, symbol, depth);
|
|
console.log(`▼ ${symbol}`);
|
|
for (const ref of refs) {
|
|
const indent = " ".repeat(ref.depth + 1);
|
|
console.log(
|
|
`${indent}├─▼ ${ref.caller_name} (${ref.caller_path}#L${ref.caller_line})${ref.is_server_action ? " [Server Action]" : ""}`
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "module": {
|
|
const moduleName = args[1];
|
|
if (!moduleName) {
|
|
console.error("Usage: arch:query module <moduleName> [--reverse] [--depth=N]");
|
|
process.exit(1);
|
|
}
|
|
const reverse = args.includes("--reverse");
|
|
if (reverse) {
|
|
const deps = queryModuleReverseDeps(db, moduleName);
|
|
console.log(`▼ ${moduleName} (reverse deps)`);
|
|
for (const dep of deps) {
|
|
console.log(` └─◀ ${dep.source_module} [${dep.dep_type}]`);
|
|
}
|
|
} else {
|
|
const deps = queryModuleDeps(db, moduleName);
|
|
console.log(`▼ ${moduleName} (forward deps)`);
|
|
for (const dep of deps) {
|
|
console.log(` └─▶ ${dep.target_module} [${dep.dep_type}]`);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "tech": {
|
|
const tag = args[1];
|
|
if (!tag) {
|
|
console.error("Usage: arch:query tech <tagName>");
|
|
process.exit(1);
|
|
}
|
|
const usage = queryTechUsage(db, tag);
|
|
console.log(`▼ tech: ${tag} (${usage.length} usages)`);
|
|
for (const u of usage) {
|
|
console.log(` └─ ${u.symbol_name} (${u.file_path})`);
|
|
}
|
|
break;
|
|
}
|
|
case "violations": {
|
|
const v = queryViolations(db);
|
|
console.log("▼ Architecture Violations");
|
|
console.log(` Long files (>800 lines): ${v.long_files.length}`);
|
|
for (const f of v.long_files) {
|
|
console.log(` └─ ${f.path}: ${f.lines} lines`);
|
|
}
|
|
console.log(
|
|
` Server Actions without requirePermission: ${v.server_actions_without_permission.length}`
|
|
);
|
|
for (const sa of v.server_actions_without_permission) {
|
|
console.log(` └─ ${sa.name} (${sa.path})`);
|
|
}
|
|
break;
|
|
}
|
|
case "sql": {
|
|
const sql = args.slice(1).join(" ");
|
|
if (!sql) {
|
|
console.error('Usage: arch:query sql "<SQL>"');
|
|
process.exit(1);
|
|
}
|
|
const rows = db.prepare(sql).all();
|
|
console.log(JSON.stringify(rows, null, 2));
|
|
break;
|
|
}
|
|
case "repl": {
|
|
console.log("Entering SQLite REPL. Type .exit to quit.");
|
|
console.log(`DB: ${DB_PATH}`);
|
|
console.log(
|
|
'Tip: use "npm run arch:query -- sql \\"<SQL>\\"" for quick queries instead.'
|
|
);
|
|
break;
|
|
}
|
|
default:
|
|
console.error(
|
|
"Unknown command. Available: ref, module, tech, violations, sql, repl"
|
|
);
|
|
process.exit(1);
|
|
}
|
|
db.close();
|
|
}
|