health-check.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. #!/usr/bin/env bun
  2. /**
  3. * 代码健康度检查脚本
  4. *
  5. * 汇总项目各维度指标,输出健康度报告:
  6. * - 代码规模(文件数、代码行数)
  7. * - Lint 问题数(Biome)
  8. * - 测试结果(Bun test)
  9. * - 冗余代码(Knip)
  10. * - 构建状态
  11. */
  12. import { $ } from "bun";
  13. const DIVIDER = "─".repeat(60);
  14. interface Metric {
  15. label: string;
  16. value: string | number;
  17. status: "ok" | "warn" | "error" | "info";
  18. }
  19. const metrics: Metric[] = [];
  20. function add(label: string, value: string | number, status: Metric["status"] = "info") {
  21. metrics.push({ label, value, status });
  22. }
  23. function icon(status: Metric["status"]): string {
  24. switch (status) {
  25. case "ok":
  26. return "[OK]";
  27. case "warn":
  28. return "[!!]";
  29. case "error":
  30. return "[XX]";
  31. case "info":
  32. return "[--]";
  33. }
  34. }
  35. // ---------------------------------------------------------------------------
  36. // 1. 代码规模
  37. // ---------------------------------------------------------------------------
  38. async function checkCodeSize() {
  39. const tsFiles = await $`find src -name '*.ts' -o -name '*.tsx' | grep -v node_modules`.text();
  40. const fileCount = tsFiles.trim().split("\n").filter(Boolean).length;
  41. add("TypeScript 文件数", fileCount, "info");
  42. const loc = await $`find src -name '*.ts' -o -name '*.tsx' | grep -v node_modules | xargs wc -l | tail -1`.text();
  43. const totalLines = loc.trim().split(/\s+/)[0] ?? "?";
  44. add("总代码行数 (src/)", totalLines, "info");
  45. }
  46. // ---------------------------------------------------------------------------
  47. // 2. Lint 检查
  48. // ---------------------------------------------------------------------------
  49. async function checkLint() {
  50. try {
  51. const result = await $`bunx biome check src/ 2>&1`.quiet().nothrow().text();
  52. const errorMatch = result.match(/Found (\d+) errors?/);
  53. const warnMatch = result.match(/Found (\d+) warnings?/);
  54. const errors = errorMatch ? Number.parseInt(errorMatch[1]) : 0;
  55. const warnings = warnMatch ? Number.parseInt(warnMatch[1]) : 0;
  56. add("Lint 错误", errors, errors === 0 ? "ok" : errors < 100 ? "warn" : "info");
  57. add("Lint 警告", warnings, warnings === 0 ? "ok" : "info");
  58. } catch {
  59. add("Lint 检查", "执行失败", "error");
  60. }
  61. }
  62. // ---------------------------------------------------------------------------
  63. // 3. 测试
  64. // ---------------------------------------------------------------------------
  65. async function checkTests() {
  66. try {
  67. const result = await $`bun test 2>&1`.quiet().nothrow().text();
  68. const passMatch = result.match(/(\d+) pass/);
  69. const failMatch = result.match(/(\d+) fail/);
  70. const pass = passMatch ? Number.parseInt(passMatch[1]) : 0;
  71. const fail = failMatch ? Number.parseInt(failMatch[1]) : 0;
  72. add("测试通过", pass, pass > 0 ? "ok" : "warn");
  73. add("测试失败", fail, fail === 0 ? "ok" : "error");
  74. } catch {
  75. add("测试", "执行失败", "error");
  76. }
  77. }
  78. // ---------------------------------------------------------------------------
  79. // 4. 冗余代码
  80. // ---------------------------------------------------------------------------
  81. async function checkUnused() {
  82. try {
  83. const result = await $`bunx knip-bun 2>&1`.quiet().nothrow().text();
  84. const unusedFiles = result.match(/Unused files \((\d+)\)/);
  85. const unusedExports = result.match(/Unused exports \((\d+)\)/);
  86. const unusedDeps = result.match(/Unused dependencies \((\d+)\)/);
  87. add("未使用文件", unusedFiles?.[1] ?? "0", "info");
  88. add("未使用导出", unusedExports?.[1] ?? "0", "info");
  89. add("未使用依赖", unusedDeps?.[1] ?? "0", unusedDeps && Number(unusedDeps[1]) > 0 ? "warn" : "ok");
  90. } catch {
  91. add("冗余代码检查", "执行失败", "error");
  92. }
  93. }
  94. // ---------------------------------------------------------------------------
  95. // 5. 构建
  96. // ---------------------------------------------------------------------------
  97. async function checkBuild() {
  98. try {
  99. const result = await $`bun run build 2>&1`.quiet().nothrow();
  100. if (result.exitCode === 0) {
  101. // 获取产物大小
  102. const stat = Bun.file("dist/cli.js");
  103. const mb = (stat.size / 1024 / 1024).toFixed(1);
  104. const size = `${mb} MB`;
  105. add("构建状态", "成功", "ok");
  106. add("产物大小 (dist/cli.js)", size, "info");
  107. } else {
  108. add("构建状态", "失败", "error");
  109. }
  110. } catch {
  111. add("构建", "执行失败", "error");
  112. }
  113. }
  114. // ---------------------------------------------------------------------------
  115. // Run
  116. // ---------------------------------------------------------------------------
  117. console.log("");
  118. console.log(DIVIDER);
  119. console.log(" 代码健康度检查报告");
  120. console.log(` ${new Date().toLocaleString("zh-CN")}`);
  121. console.log(DIVIDER);
  122. await checkCodeSize();
  123. await checkLint();
  124. await checkTests();
  125. await checkUnused();
  126. await checkBuild();
  127. console.log("");
  128. for (const m of metrics) {
  129. const tag = icon(m.status);
  130. console.log(` ${tag} ${m.label.padEnd(20)} ${m.value}`);
  131. }
  132. const errorCount = metrics.filter((m) => m.status === "error").length;
  133. const warnCount = metrics.filter((m) => m.status === "warn").length;
  134. console.log("");
  135. console.log(DIVIDER);
  136. if (errorCount > 0) {
  137. console.log(` 结果: ${errorCount} 个错误, ${warnCount} 个警告`);
  138. } else if (warnCount > 0) {
  139. console.log(` 结果: 无错误, ${warnCount} 个警告`);
  140. } else {
  141. console.log(" 结果: 全部通过");
  142. }
  143. console.log(DIVIDER);
  144. console.log("");
  145. process.exit(errorCount > 0 ? 1 : 0);