feat: 添加代码健康度检查脚本
scripts/health-check.ts 汇总项目各维度指标: 代码规模、lint 问题、测试结果、冗余代码、构建状态和产物大小。 新增 health script 一键运行。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c587a64320
commit
173d18bea8
2
TODO.md
2
TODO.md
@ -20,6 +20,6 @@
|
|||||||
- [x] 代码格式化与校验
|
- [x] 代码格式化与校验
|
||||||
- [x] 冗余代码检查
|
- [x] 冗余代码检查
|
||||||
- [x] git hook 的配置
|
- [x] git hook 的配置
|
||||||
- [ ] 代码健康度检查
|
- [x] 代码健康度检查
|
||||||
- [x] 单元测试基础设施搭建 (test runner 配置)
|
- [x] 单元测试基础设施搭建 (test runner 配置)
|
||||||
- [x] CI/CD 流水线 (GitHub Actions)
|
- [x] CI/CD 流水线 (GitHub Actions)
|
||||||
|
|||||||
@ -18,7 +18,8 @@
|
|||||||
"format": "biome format --write src/",
|
"format": "biome format --write src/",
|
||||||
"prepare": "git config core.hooksPath .githooks",
|
"prepare": "git config core.hooksPath .githooks",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"check:unused": "knip-bun"
|
"check:unused": "knip-bun",
|
||||||
|
"health": "bun run scripts/health-check.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@alcalzone/ansi-tokenize": "^0.3.0",
|
"@alcalzone/ansi-tokenize": "^0.3.0",
|
||||||
|
|||||||
163
scripts/health-check.ts
Normal file
163
scripts/health-check.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* 代码健康度检查脚本
|
||||||
|
*
|
||||||
|
* 汇总项目各维度指标,输出健康度报告:
|
||||||
|
* - 代码规模(文件数、代码行数)
|
||||||
|
* - Lint 问题数(Biome)
|
||||||
|
* - 测试结果(Bun test)
|
||||||
|
* - 冗余代码(Knip)
|
||||||
|
* - 构建状态
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { $ } from "bun";
|
||||||
|
|
||||||
|
const DIVIDER = "─".repeat(60);
|
||||||
|
|
||||||
|
interface Metric {
|
||||||
|
label: string;
|
||||||
|
value: string | number;
|
||||||
|
status: "ok" | "warn" | "error" | "info";
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics: Metric[] = [];
|
||||||
|
|
||||||
|
function add(label: string, value: string | number, status: Metric["status"] = "info") {
|
||||||
|
metrics.push({ label, value, status });
|
||||||
|
}
|
||||||
|
|
||||||
|
function icon(status: Metric["status"]): string {
|
||||||
|
switch (status) {
|
||||||
|
case "ok":
|
||||||
|
return "[OK]";
|
||||||
|
case "warn":
|
||||||
|
return "[!!]";
|
||||||
|
case "error":
|
||||||
|
return "[XX]";
|
||||||
|
case "info":
|
||||||
|
return "[--]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 1. 代码规模
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function checkCodeSize() {
|
||||||
|
const tsFiles = await $`find src -name '*.ts' -o -name '*.tsx' | grep -v node_modules`.text();
|
||||||
|
const fileCount = tsFiles.trim().split("\n").filter(Boolean).length;
|
||||||
|
add("TypeScript 文件数", fileCount, "info");
|
||||||
|
|
||||||
|
const loc = await $`find src -name '*.ts' -o -name '*.tsx' | grep -v node_modules | xargs wc -l | tail -1`.text();
|
||||||
|
const totalLines = loc.trim().split(/\s+/)[0] ?? "?";
|
||||||
|
add("总代码行数 (src/)", totalLines, "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 2. Lint 检查
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function checkLint() {
|
||||||
|
try {
|
||||||
|
const result = await $`bunx biome check src/ 2>&1`.quiet().nothrow().text();
|
||||||
|
const errorMatch = result.match(/Found (\d+) errors?/);
|
||||||
|
const warnMatch = result.match(/Found (\d+) warnings?/);
|
||||||
|
const errors = errorMatch ? Number.parseInt(errorMatch[1]) : 0;
|
||||||
|
const warnings = warnMatch ? Number.parseInt(warnMatch[1]) : 0;
|
||||||
|
add("Lint 错误", errors, errors === 0 ? "ok" : errors < 100 ? "warn" : "info");
|
||||||
|
add("Lint 警告", warnings, warnings === 0 ? "ok" : "info");
|
||||||
|
} catch {
|
||||||
|
add("Lint 检查", "执行失败", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 3. 测试
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function checkTests() {
|
||||||
|
try {
|
||||||
|
const result = await $`bun test 2>&1`.quiet().nothrow().text();
|
||||||
|
const passMatch = result.match(/(\d+) pass/);
|
||||||
|
const failMatch = result.match(/(\d+) fail/);
|
||||||
|
const pass = passMatch ? Number.parseInt(passMatch[1]) : 0;
|
||||||
|
const fail = failMatch ? Number.parseInt(failMatch[1]) : 0;
|
||||||
|
add("测试通过", pass, pass > 0 ? "ok" : "warn");
|
||||||
|
add("测试失败", fail, fail === 0 ? "ok" : "error");
|
||||||
|
} catch {
|
||||||
|
add("测试", "执行失败", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 4. 冗余代码
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function checkUnused() {
|
||||||
|
try {
|
||||||
|
const result = await $`bunx knip-bun 2>&1`.quiet().nothrow().text();
|
||||||
|
const unusedFiles = result.match(/Unused files \((\d+)\)/);
|
||||||
|
const unusedExports = result.match(/Unused exports \((\d+)\)/);
|
||||||
|
const unusedDeps = result.match(/Unused dependencies \((\d+)\)/);
|
||||||
|
add("未使用文件", unusedFiles?.[1] ?? "0", "info");
|
||||||
|
add("未使用导出", unusedExports?.[1] ?? "0", "info");
|
||||||
|
add("未使用依赖", unusedDeps?.[1] ?? "0", unusedDeps && Number(unusedDeps[1]) > 0 ? "warn" : "ok");
|
||||||
|
} catch {
|
||||||
|
add("冗余代码检查", "执行失败", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 5. 构建
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function checkBuild() {
|
||||||
|
try {
|
||||||
|
const result = await $`bun run build 2>&1`.quiet().nothrow();
|
||||||
|
if (result.exitCode === 0) {
|
||||||
|
// 获取产物大小
|
||||||
|
const stat = Bun.file("dist/cli.js");
|
||||||
|
const mb = (stat.size / 1024 / 1024).toFixed(1);
|
||||||
|
const size = `${mb} MB`;
|
||||||
|
add("构建状态", "成功", "ok");
|
||||||
|
add("产物大小 (dist/cli.js)", size, "info");
|
||||||
|
} else {
|
||||||
|
add("构建状态", "失败", "error");
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
add("构建", "执行失败", "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Run
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
console.log("");
|
||||||
|
console.log(DIVIDER);
|
||||||
|
console.log(" 代码健康度检查报告");
|
||||||
|
console.log(` ${new Date().toLocaleString("zh-CN")}`);
|
||||||
|
console.log(DIVIDER);
|
||||||
|
|
||||||
|
await checkCodeSize();
|
||||||
|
await checkLint();
|
||||||
|
await checkTests();
|
||||||
|
await checkUnused();
|
||||||
|
await checkBuild();
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
for (const m of metrics) {
|
||||||
|
const tag = icon(m.status);
|
||||||
|
console.log(` ${tag} ${m.label.padEnd(20)} ${m.value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorCount = metrics.filter((m) => m.status === "error").length;
|
||||||
|
const warnCount = metrics.filter((m) => m.status === "warn").length;
|
||||||
|
|
||||||
|
console.log("");
|
||||||
|
console.log(DIVIDER);
|
||||||
|
if (errorCount > 0) {
|
||||||
|
console.log(` 结果: ${errorCount} 个错误, ${warnCount} 个警告`);
|
||||||
|
} else if (warnCount > 0) {
|
||||||
|
console.log(` 结果: 无错误, ${warnCount} 个警告`);
|
||||||
|
} else {
|
||||||
|
console.log(" 结果: 全部通过");
|
||||||
|
}
|
||||||
|
console.log(DIVIDER);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
|
process.exit(errorCount > 0 ? 1 : 0);
|
||||||
Loading…
Reference in New Issue
Block a user