From 13661abbd81468bfd1633164042523b5aee758e8 Mon Sep 17 00:00:00 2001 From: Cyrus Goh Date: Sun, 25 Jan 2026 13:36:00 -0800 Subject: [PATCH] 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. --- git-hooks/pre-commit | 7 ++- package.json | 1 + scripts/format-staged.js | 2 +- scripts/lint-staged.js | 104 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 scripts/lint-staged.js diff --git a/git-hooks/pre-commit b/git-hooks/pre-commit index c2fe5149b..9f8f2a5bb 100755 --- a/git-hooks/pre-commit +++ b/git-hooks/pre-commit @@ -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 diff --git a/package.json b/package.json index 9be21860a..47472e584 100644 --- a/package.json +++ b/package.json @@ -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/**", diff --git a/scripts/format-staged.js b/scripts/format-staged.js index 0ad2d7dd7..66d66f81a 100644 --- a/scripts/format-staged.js +++ b/scripts/format-staged.js @@ -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)), ); } diff --git a/scripts/lint-staged.js b/scripts/lint-staged.js new file mode 100644 index 000000000..542ccf6d2 --- /dev/null +++ b/scripts/lint-staged.js @@ -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(); +}