chore: add lint check to pre-commit hook

- Add scripts/lint-staged.js to run oxlint on staged files
- Extend format-staged.js to include ui/src/ directory
- Update pre-commit hook to run both format and lint checks

The pre-commit hook now blocks commits if there are linting
errors, catching issues like unused imports before they land.
This commit is contained in:
Cyrus Goh 2026-01-25 13:36:00 -08:00
parent 2d451b30f9
commit 13661abbd8
No known key found for this signature in database
GPG Key ID: D547D2AF3FD0FD5A
4 changed files with 112 additions and 2 deletions

View File

@ -1,4 +1,9 @@
#!/bin/sh
ROOT=$(git rev-parse --show-toplevel 2>/dev/null)
[ -z "$ROOT" ] && exit 0
node "$ROOT/scripts/format-staged.js"
# Format staged files
node "$ROOT/scripts/format-staged.js" || exit 1
# Lint staged files
node "$ROOT/scripts/lint-staged.js" || exit 1

View File

@ -61,6 +61,7 @@
"LICENSE",
"scripts/postinstall.js",
"scripts/format-staged.js",
"scripts/lint-staged.js",
"scripts/setup-git-hooks.js",
"git-hooks/**",
"dist/terminal/**",

View File

@ -41,7 +41,7 @@ function filterOxfmtTargets(paths) {
return paths
.map(normalizeGitPath)
.filter((filePath) =>
(filePath.startsWith("src/") || filePath.startsWith("test/")) &&
(filePath.startsWith("src/") || filePath.startsWith("test/") || filePath.startsWith("ui/src/")) &&
OXFMT_EXTENSIONS.has(path.posix.extname(filePath)),
);
}

104
scripts/lint-staged.js Normal file
View File

@ -0,0 +1,104 @@
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const LINT_EXTENSIONS = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"]);
function getRepoRoot() {
const here = path.dirname(fileURLToPath(import.meta.url));
return path.resolve(here, "..");
}
function runGitCommand(args, options = {}) {
return spawnSync("git", args, {
cwd: options.cwd,
encoding: "utf-8",
stdio: options.stdio ?? "pipe",
});
}
function splitNullDelimited(value) {
if (!value) return [];
const text = String(value);
return text.split("\0").filter(Boolean);
}
function normalizeGitPath(filePath) {
return filePath.replace(/\\/g, "/");
}
function filterLintTargets(paths) {
return paths
.map(normalizeGitPath)
.filter((filePath) =>
(filePath.startsWith("src/") ||
filePath.startsWith("test/") ||
filePath.startsWith("ui/src/")) &&
LINT_EXTENSIONS.has(path.posix.extname(filePath)),
);
}
function resolveOxlintCommand(repoRoot) {
const binName = process.platform === "win32" ? "oxlint.cmd" : "oxlint";
const local = path.join(repoRoot, "node_modules", ".bin", binName);
if (fs.existsSync(local)) {
return { command: local, args: [] };
}
const result = spawnSync("oxlint", ["--version"], { stdio: "ignore" });
if (result.status === 0) {
return { command: "oxlint", args: [] };
}
return null;
}
function getGitPaths(args, repoRoot) {
const result = runGitCommand(args, { cwd: repoRoot });
if (result.status !== 0) return [];
return splitNullDelimited(result.stdout ?? "");
}
function lintFiles(repoRoot, oxlint, files) {
const result = spawnSync(oxlint.command, [...oxlint.args, ...files], {
cwd: repoRoot,
stdio: "inherit",
});
return result.status === 0;
}
function main() {
const repoRoot = getRepoRoot();
const staged = getGitPaths(
["diff", "--cached", "--name-only", "-z", "--diff-filter=ACMR"],
repoRoot,
);
const targets = filterLintTargets(staged);
if (targets.length === 0) return;
const oxlint = resolveOxlintCommand(repoRoot);
if (!oxlint) {
process.stderr.write("[pre-commit] oxlint not found; skipping lint.\n");
return;
}
if (!lintFiles(repoRoot, oxlint, targets)) {
process.exitCode = 1;
}
}
export {
filterLintTargets,
getRepoRoot,
normalizeGitPath,
resolveOxlintCommand,
splitNullDelimited,
};
if (
process.argv[1] &&
path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
main();
}