UNPKG

@bomb.sh/tools

Version:

The internal dev, build, and lint CLI for Bombshell projects

191 lines (189 loc) 5.53 kB
import { local } from "../utils.mjs"; import { parse } from "@bomb.sh/args"; import { x } from "tinyexec"; import { fileURLToPath } from "node:url"; import { publint } from "publint"; //#region src/commands/lint.ts const oxlintConfig = fileURLToPath(new URL("../../oxlintrc.json", import.meta.url)); async function runOxlint(targets, fix) { const args = [ "-c", oxlintConfig, "--format=json", ...targets ]; if (fix) args.push("--fix"); const result = await x(local("oxlint"), args, { throwOnError: false }); return (JSON.parse(result.stdout).diagnostics ?? []).map((d) => ({ tool: "oxlint", level: d.severity === "error" ? "error" : "warning", code: d.code ?? "unknown", message: d.message, file: d.filename, line: d.labels?.[0]?.span?.line, column: d.labels?.[0]?.span?.column })); } async function runPublint() { return (await publint({ strict: true })).messages.map((m) => ({ tool: "publint", level: m.type === "error" ? "error" : m.type === "warning" ? "warning" : "suggestion", code: m.code, message: m.code, file: "package.json", line: void 0, column: void 0 })); } async function runKnip() { const result = await x(local("knip"), [ "--no-progress", "--reporter", "json" ], { throwOnError: false }); if (!result.stdout.trim()) return []; const json = JSON.parse(result.stdout); const violations = []; for (const issue of json.issues) { for (const dep of issue.dependencies) violations.push({ tool: "knip", level: "warning", code: "unused-dependency", message: `Unused dependency '${dep.name}'`, file: issue.file, line: dep.line, column: dep.col }); for (const dep of issue.devDependencies) violations.push({ tool: "knip", level: "warning", code: "unused-devDependency", message: `Unused devDependency '${dep.name}'`, file: issue.file, line: dep.line, column: dep.col }); for (const exp of issue.exports) violations.push({ tool: "knip", level: "warning", code: "unused-export", message: `Unused export '${exp.name}'`, file: issue.file, line: exp.line, column: exp.col }); for (const t of issue.types) violations.push({ tool: "knip", level: "warning", code: "unused-type", message: `Unused type '${t.name}'`, file: issue.file, line: t.line, column: t.col }); } for (const file of json.files) violations.push({ tool: "knip", level: "warning", code: "unused-file", message: `Unused file`, file }); return violations; } async function runTypeScript(targets) { const args = targets.length > 0 ? [ "--noEmit", "--pretty", "false", ...targets ] : [ "--noEmit", "--pretty", "false" ]; const result = await x(local("tsgo"), args, { throwOnError: false }); const output = result.stdout + result.stderr; if (!output.trim()) return []; const violations = []; const re = /^(.+)\((\d+),(\d+)\): (error|warning) (TS\d+): (.+)$/gm; let match; while ((match = re.exec(output)) !== null) violations.push({ tool: "tsc", level: match[4] === "error" ? "error" : "warning", code: match[5], message: match[6], file: match[1], line: Number(match[2]), column: Number(match[3]) }); return violations; } function printViolations(violations) { const grouped = /* @__PURE__ */ new Map(); for (const v of violations) { const key = v.file ?? "(project)"; if (!grouped.has(key)) grouped.set(key, []); grouped.get(key).push(v); } const colors = { error: "\x1B[31m", warning: "\x1B[33m", suggestion: "\x1B[34m", dim: "\x1B[2m", reset: "\x1B[0m" }; for (const [file, items] of grouped) { console.log(`\n${file}`); for (const v of items) { const loc = v.line != null ? ` ${v.line}:${v.column ?? 0}` : " -"; const color = colors[v.level]; const tag = `${v.tool}/${v.code}`; console.log(`${colors.dim}${loc.padEnd(10)}${colors.reset}${color}${v.level.padEnd(12)}${colors.reset}${v.message} ${colors.dim}${tag}${colors.reset}`); } } const counts = { error: 0, warning: 0, suggestion: 0 }; for (const v of violations) counts[v.level]++; const parts = []; if (counts.error) parts.push(`${colors.error}${counts.error} error${counts.error > 1 ? "s" : ""}${colors.reset}`); if (counts.warning) parts.push(`${colors.warning}${counts.warning} warning${counts.warning > 1 ? "s" : ""}${colors.reset}`); if (counts.suggestion) parts.push(`${colors.suggestion}${counts.suggestion} suggestion${counts.suggestion > 1 ? "s" : ""}${colors.reset}`); if (parts.length > 0) console.log(`\n${parts.join(", ")}`); else console.log("\nNo issues found."); } async function collectViolations(targets) { const results = await Promise.allSettled([ runOxlint(targets), runPublint(), runKnip(), runTypeScript(targets) ]); const violations = []; for (const result of results) if (result.status === "fulfilled") violations.push(...result.value); else console.error(result.reason); return violations; } async function lint(ctx) { const args = parse(ctx.args, { boolean: ["fix"] }); const targets = args._.length > 0 ? args._.map(String) : ["./src"]; if (args.fix) { await runOxlint(targets, true); const remaining = await collectViolations(targets); if (remaining.length > 0) { printViolations(remaining); process.exit(1); } console.log("No issues found."); return; } const violations = await collectViolations(targets); printViolations(violations); if (violations.some((v) => v.level === "error")) process.exit(1); } //#endregion export { lint }; //# sourceMappingURL=lint.mjs.map