diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..2ff9795 --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,5 @@ +#!/bin/sh +set -e + +echo "Running pre-commit checks..." +node scripts/run-pre-commit-checks.mjs diff --git a/package.json b/package.json index 7631829..56fac6c 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,16 @@ "homepage": "https://clashparty.org", "scripts": { "format": "prettier --write .", + "format:check": "prettier --list-different .", + "hooks:install": "node scripts/install-git-hooks.mjs", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "lint:check": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts", - "review": "pnpm run lint:check && pnpm run typecheck", + "review": "pnpm run format:check && pnpm run lint:check && pnpm run typecheck", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", "typecheck": "pnpm run typecheck:node && pnpm run typecheck:web", - "prepare": "node scripts/prepare.mjs", - "prepare:dev": "node scripts/update-version.mjs && node scripts/prepare.mjs", + "prepare": "pnpm run hooks:install && node scripts/prepare.mjs", + "prepare:dev": "pnpm run hooks:install && node scripts/update-version.mjs && node scripts/prepare.mjs", "updater": "node scripts/updater.mjs", "checksum": "node scripts/checksum.mjs", "copy-legacy": "node scripts/copy-legacy-artifacts.mjs", diff --git a/scripts/install-git-hooks.mjs b/scripts/install-git-hooks.mjs new file mode 100644 index 0000000..16f8952 --- /dev/null +++ b/scripts/install-git-hooks.mjs @@ -0,0 +1,38 @@ +import fs from 'node:fs' +import path from 'node:path' +import { execFileSync } from 'node:child_process' + +const cwd = process.cwd() +const gitDir = path.join(cwd, '.git') +const hooksDir = path.join(cwd, '.githooks') +const hookFiles = ['pre-commit'] + +if (!fs.existsSync(gitDir)) { + console.log('[git-hooks] .git directory not found, skipping hook installation.') + process.exit(0) +} + +if (!fs.existsSync(hooksDir)) { + console.log('[git-hooks] .githooks directory not found, skipping hook installation.') + process.exit(0) +} + +try { + execFileSync('git', ['config', '--local', 'core.hooksPath', '.githooks'], { + cwd, + stdio: 'inherit' + }) + + for (const hookFile of hookFiles) { + const hookPath = path.join(hooksDir, hookFile) + if (fs.existsSync(hookPath)) { + fs.chmodSync(hookPath, 0o755) + } + } + + console.log('[git-hooks] Installed hooks from .githooks.') +} catch (error) { + console.error('[git-hooks] Failed to install git hooks.') + console.error(error instanceof Error ? error.message : error) + process.exit(1) +} diff --git a/scripts/run-pre-commit-checks.mjs b/scripts/run-pre-commit-checks.mjs new file mode 100644 index 0000000..f30f42c --- /dev/null +++ b/scripts/run-pre-commit-checks.mjs @@ -0,0 +1,94 @@ +import { spawnSync } from 'node:child_process' + +const isWin = process.platform === 'win32' +const windowsShell = process.env.ComSpec || process.env.COMSPEC || 'cmd.exe' + +const runners = [ + { + probe: 'pnpm', + run(command) { + return `pnpm ${command}` + } + }, + { + probe: 'corepack', + run(command) { + return `corepack pnpm ${command}` + } + } +] + +const checks = [ + { + name: 'Format Check', + command: 'run format:check', + help: 'Formatting issues were reported above. Review the listed files and fix them before committing again.' + }, + { + name: 'Lint Check', + command: 'run lint:check', + help: 'Lint errors were reported above. Review them and fix the affected code before committing again.' + }, + { + name: 'Type Check', + command: 'run typecheck', + help: 'Type errors were reported above. Review them and fix the affected code before committing again.' + } +] + +function spawnCommand(command, { stdio = 'inherit' } = {}) { + if (isWin) { + return spawnSync(windowsShell, ['/d', '/s', '/c', command], { + cwd: process.cwd(), + stdio + }) + } + + return spawnSync('sh', ['-lc', command], { + cwd: process.cwd(), + stdio + }) +} + +function commandExists(command) { + const probe = isWin ? `where ${command}` : `command -v ${command}` + const result = spawnCommand(probe, { stdio: 'ignore' }) + return !result.error && result.status === 0 +} + +function printDivider() { + console.log('========================================') +} + +const runner = runners.find(({ probe }) => commandExists(probe)) + +if (!runner) { + console.error('[pre-commit] Unable to find pnpm or corepack in PATH.') + process.exit(1) +} + +printDivider() +console.log('[pre-commit] Running checks before commit') +printDivider() + +for (const check of checks) { + console.log(`\n[pre-commit] ${check.name}`) + const result = spawnCommand(runner.run(check.command)) + + if (result.error) { + console.error(`\n[pre-commit] Failed to run "${check.command}".`) + console.error(result.error.message) + process.exit(1) + } + + if (result.status !== 0) { + console.error(`\n[pre-commit] ${check.name.toUpperCase()} FAILED`) + console.error(`[pre-commit] ${check.help}`) + console.error('[pre-commit] Commit aborted.') + process.exit(result.status ?? 1) + } + + console.log(`[pre-commit] ${check.name} passed.`) +} + +console.log('\n[pre-commit] All checks passed. Proceeding with commit.')