| import fs from "node:fs"; |
| import path from "node:path"; |
| import { spawnSync } from "node:child_process"; |
| import { fileURLToPath } from "node:url"; |
|
|
| const OXFMT_EXTENSIONS = new Set([".cjs", ".js", ".json", ".jsonc", ".jsx", ".mjs", ".ts", ".tsx"]); |
|
|
| 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 filterOxfmtTargets(paths) { |
| return paths |
| .map(normalizeGitPath) |
| .filter( |
| (filePath) => |
| (filePath.startsWith("src/") || filePath.startsWith("test/")) && |
| OXFMT_EXTENSIONS.has(path.posix.extname(filePath)), |
| ); |
| } |
|
|
| function findPartiallyStagedFiles(stagedFiles, unstagedFiles) { |
| const unstaged = new Set(unstagedFiles.map(normalizeGitPath)); |
| return stagedFiles.filter((filePath) => unstaged.has(normalizeGitPath(filePath))); |
| } |
|
|
| function filterOutPartialTargets(targets, partialTargets) { |
| if (partialTargets.length === 0) return targets; |
| const partial = new Set(partialTargets.map(normalizeGitPath)); |
| return targets.filter((filePath) => !partial.has(normalizeGitPath(filePath))); |
| } |
|
|
| function resolveOxfmtCommand(repoRoot) { |
| const binName = process.platform === "win32" ? "oxfmt.cmd" : "oxfmt"; |
| const local = path.join(repoRoot, "node_modules", ".bin", binName); |
| if (fs.existsSync(local)) { |
| return { command: local, args: [] }; |
| } |
|
|
| const result = spawnSync("oxfmt", ["--version"], { stdio: "ignore" }); |
| if (result.status === 0) { |
| return { command: "oxfmt", args: [] }; |
| } |
|
|
| return null; |
| } |
|
|
| function getGitPaths(args, repoRoot) { |
| const result = runGitCommand(args, { cwd: repoRoot }); |
| if (result.status !== 0) return []; |
| return splitNullDelimited(result.stdout ?? ""); |
| } |
|
|
| function formatFiles(repoRoot, oxfmt, files) { |
| const result = spawnSync(oxfmt.command, ["--write", ...oxfmt.args, ...files], { |
| cwd: repoRoot, |
| stdio: "inherit", |
| }); |
| return result.status === 0; |
| } |
|
|
| function stageFiles(repoRoot, files) { |
| if (files.length === 0) return true; |
| const result = runGitCommand(["add", "--", ...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 = filterOxfmtTargets(staged); |
| if (targets.length === 0) return; |
|
|
| const unstaged = getGitPaths(["diff", "--name-only", "-z"], repoRoot); |
| const partial = findPartiallyStagedFiles(targets, unstaged); |
| if (partial.length > 0) { |
| process.stderr.write("[pre-commit] Skipping partially staged files:\n"); |
| for (const filePath of partial) { |
| process.stderr.write(`- ${filePath}\n`); |
| } |
| process.stderr.write("Stage full files to format them automatically.\n"); |
| } |
|
|
| const filteredTargets = filterOutPartialTargets(targets, partial); |
| if (filteredTargets.length === 0) return; |
|
|
| const oxfmt = resolveOxfmtCommand(repoRoot); |
| if (!oxfmt) { |
| process.stderr.write("[pre-commit] oxfmt not found; skipping format.\n"); |
| return; |
| } |
|
|
| if (!formatFiles(repoRoot, oxfmt, filteredTargets)) { |
| process.exitCode = 1; |
| return; |
| } |
|
|
| if (!stageFiles(repoRoot, filteredTargets)) { |
| process.exitCode = 1; |
| } |
| } |
|
|
| export { |
| filterOxfmtTargets, |
| filterOutPartialTargets, |
| findPartiallyStagedFiles, |
| getRepoRoot, |
| normalizeGitPath, |
| resolveOxfmtCommand, |
| splitNullDelimited, |
| }; |
|
|
| if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { |
| main(); |
| } |
|
|