@bomb.sh/tools
Version:
The internal dev, build, and lint CLI for Bombshell projects
191 lines (189 loc) • 5.53 kB
JavaScript
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