diff --git a/TODO.md b/TODO.md index c78697f..c8d8cb8 100644 --- a/TODO.md +++ b/TODO.md @@ -20,6 +20,6 @@ - [x] 代码格式化与校验 - [x] 冗余代码检查 - [x] git hook 的配置 -- [ ] 代码健康度检查 +- [x] 代码健康度检查 - [x] 单元测试基础设施搭建 (test runner 配置) - [x] CI/CD 流水线 (GitHub Actions) diff --git a/package.json b/package.json index 7e6789f..e299d37 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "format": "biome format --write src/", "prepare": "git config core.hooksPath .githooks", "test": "bun test", - "check:unused": "knip-bun" + "check:unused": "knip-bun", + "health": "bun run scripts/health-check.ts" }, "dependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", diff --git a/scripts/health-check.ts b/scripts/health-check.ts new file mode 100644 index 0000000..4584209 --- /dev/null +++ b/scripts/health-check.ts @@ -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);