Husky + lint-staged
Overviewβ
The application uses Husky for managing Git hooks and lint-staged for running linters on staged files. Together, they enforce code quality by preventing commits that don't meet standards.
What Are Husky and lint-staged?β
Huskyβ
Husky is a Git hooks manager that:
- Manages Git Hooks: Easy setup and management of pre-commit, pre-push, etc.
- Prevents Bad Commits: Run checks before code reaches the repository
- Developer Experience: Clear, actionable error messages
- Cross-Platform: Works on macOS, Linux, and Windows
lint-stagedβ
lint-staged is a tool that:
- Runs Linters on Staged Files: Only checks files you're committing
- Prevents Partial Commits: Ensures entire staged changes are valid
- Performance: Faster than linting entire codebase
- Flexible: Works with any linter or script
App Configurationβ
Installationβ
# Install both tools
pnpm add -D husky lint-staged
# Initialize Husky
npx husky install
# Make Git hook executable
chmod +x .husky/pre-commit
Husky Setupβ
.husky/pre-commitβ
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm lint-staged
.husky/pre-pushβ
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Run tests before push
pnpm test -- --bail
# Validate builds
pnpm build
.husky/commit-msgβ
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Validate commit message format
pnpm exec commitlint --edit $1
lint-staged Configurationβ
.lintstagedrc.js (Alternative to package.json)β
module.exports = {
// TypeScript and JavaScript files
'*.{ts,tsx,js,jsx}': ['eslint --fix', 'prettier --write'],
// React components
'*.tsx': [
'eslint --fix',
'prettier --write',
// Optional: run tests for changed components
// 'jest --bail --findRelatedTests',
],
// JSON files
'*.json': ['prettier --write'],
// Markdown files
'*.md': ['prettier --write'],
// CSS/SCSS
'*.{css,scss}': ['prettier --write'],
// YAML files
'*.{yaml,yml}': ['prettier --write'],
};
Alternative: In package.jsonβ
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": ["eslint --fix", "prettier --write"],
"*.json": ["prettier --write"],
"*.md": ["prettier --write"]
}
}
Commitlint Configurationβ
Enforce conventional commits format:
commitlint.config.tsβ
import { RuleConfigSeverity, type UserConfig } from '@commitlint/types';
const config: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
RuleConfigSeverity.Error,
'always',
[
'feat', // A new feature
'fix', // A bug fix
'docs', // Documentation only changes
'style', // Changes that don't affect code meaning
'refactor', // Code change that neither fixes nor adds feature
'perf', // Code change that improves performance
'test', // Adding or updating tests
'chore', // Changes to build process or dependencies
'ci', // Changes to CI configuration
'revert', // Revert a previous commit
],
],
'type-case': [RuleConfigSeverity.Error, 'always', 'lower-case'],
'type-empty': [RuleConfigSeverity.Error, 'never'],
'scope-empty': [RuleConfigSeverity.Warning, 'never'],
'scope-case': [RuleConfigSeverity.Error, 'always', 'lower-case'],
'subject-empty': [RuleConfigSeverity.Error, 'never'],
'subject-full-stop': [RuleConfigSeverity.Error, 'never', '.'],
'subject-case': [RuleConfigSeverity.Error, 'never', ['upper-case']],
'header-max-length': [RuleConfigSeverity.Error, 'always', 72],
},
};
export default config;
Git Hooks Explainedβ
Pre-commit Hookβ
Runs before each commit:
# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Run lint-staged
pnpm lint-staged
What it does:
- Lints staged files
- Formats code
- Prevents commit if errors found
- Developer must fix and re-stage
Pre-push Hookβ
Runs before pushing to remote:
# .husky/pre-push
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Run tests
pnpm test -- --bail
# Build to ensure no build errors
pnpm build
What it does:
- Runs full test suite
- Builds project
- Prevents push if tests fail
- More comprehensive than pre-commit
Commit-msg Hookβ
Validates commit message format:
# .husky/commit-msg
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# Validate conventional commit format
pnpm exec commitlint --edit $1
What it validates:
- Format:
type(scope): subject - Type must be one of allowed types
- Subject must be lowercase
- No period at end of subject
Workflow Examplesβ
Example 1: Full Workflowβ
# 1. Make changes
echo "new code" > src/feature.ts
# 2. Stage file
git add src/feature.ts
# 3. Attempt commit
git commit -m "feat(feature): add new feature"
# 4. Pre-commit hook runs automatically:
# β eslint --fix (fixes formatting)
# β prettier --write (formats code)
# β If all passes, commit succeeds
# β If errors, commit fails and shows errors
# 5. If commit failed, fix errors:
# - Edit files to fix linting errors
# - Stage changes again
git add .
# 6. Retry commit
git commit -m "feat(feature): add new feature"
# 7. Pre-push hook runs when pushing:
# β pnpm test (runs full test suite)
# β pnpm build (ensures build works)
# β If all passes, push succeeds
Example 2: Automatic Fixesβ
# Some errors are auto-fixable
git add .
git commit -m "fix(auth): improve login validation"
# Pre-commit hook auto-fixes:
# β ESLint fixes auto-fixable issues
# β Prettier formats code
# β Commit proceeds
# Note: You might need to run git add . again
# if changes were made
Example 3: Preventing Bad Commitsβ
# Wrong commit format
git commit -m "Added new feature"
# β commitlint rejects: Type required (feat/fix/docs/etc)
git commit -m "FEAT(AUTH): Add new feature"
# β commitlint rejects: Type must be lowercase
git commit -m "feat(auth): add new feature."
# β commitlint rejects: Subject can't end with period
# Correct format
git commit -m "feat(auth): add new feature"
# β commitlint accepts
# β Pre-commit hook runs
# β Commit succeeds
Commands and Usageβ
Husky Commandsβ
# Install Husky
pnpm add -D husky
pnpm exec husky install
# Create new hook
pnpm exec husky add .husky/pre-commit "pnpm lint"
# Remove hook
rm .husky/pre-commit
# Update hook
echo "pnpm test" > .husky/pre-commit
# Run hook manually (for testing)
sh .husky/pre-commit
# Disable hooks temporarily
HUSKY=0 git commit -m "message"
lint-staged Commandsβ
# Run lint-staged manually
pnpm lint-staged
# Run with debug output
pnpm lint-staged --debug
# Run against specific files
pnpm lint-staged --no-stash src/services/*.ts
# List files that would be processed
pnpm lint-staged --list
commitlint Commandsβ
# Validate specific commit message
pnpm exec commitlint --from HEAD~1 --to HEAD
# Validate entire history
pnpm exec commitlint --from 0
# Generate commit message
pnpm exec commitizen c
Configuration Patternsβ
Monorepo per-Packageβ
// .lintstagedrc.js
module.exports = {
'**/apps/api/**/*.ts': ['eslint --fix --config apps/api/.eslintrc.js'],
'**/apps/web/**/*.tsx': ['eslint --fix --config apps/web/.eslintrc.json'],
'**/*.json': ['prettier --write'],
};
Conditional Rulesβ
// .lintstagedrc.js
const isApiPackage = (filename) => filename.includes('apps/api');
module.exports = {
'*.ts': [
(files) => {
const apiFiles = files.filter(isApiPackage);
if (apiFiles.length) {
return `eslint --fix ${apiFiles.join(' ')}`;
}
},
],
};
Different Stagesβ
// Run different tools for different files
module.exports = {
// Lint and format TypeScript
'*.{ts,tsx}': ['eslint --fix', 'prettier --write'],
// Format JSON
'*.json': ['prettier --write'],
// Run tests for critical files
'src/services/**/*.ts': ['eslint --fix', 'prettier --write', 'jest --bail --findRelatedTests'],
};
Common Issuesβ
Hook Not Runningβ
# Ensure .husky directory exists
ls -la .husky/
# Check if hooks are executable
chmod +x .husky/pre-commit
# Reinstall Husky
pnpm exec husky install
# Verify Git is initialized
git status
lint-staged Not Workingβ
# Check if lint-staged is installed
pnpm list lint-staged
# Verify config file exists
ls -la .lintstagedrc*
# Run manually to test
pnpm lint-staged
# Debug mode
pnpm lint-staged --debug
Committing Without Hooksβ
# Skip hooks if necessary (use sparingly)
HUSKY=0 git commit -m "message"
# Or
git commit --no-verify -m "message"
Best Practicesβ
1. Keep Hooks Fastβ
// β
GOOD: Quick checks
module.exports = {
'*.ts': ['eslint --fix', 'prettier --write'],
};
// β AVOID: Slow operations that block commit
module.exports = {
'*.ts': [
'jest --testPathPattern="service"', // Slow!
'npm run build', // Very slow!
],
};
2. Use Appropriate Hooksβ
# pre-commit: Fast checks (linting, formatting)
# pre-push: Comprehensive tests (unit tests, builds)
# commit-msg: Validate message format
3. Auto-fix Where Possibleβ
// β
GOOD: Auto-fix linting issues
module.exports = {
'*.ts': ['eslint --fix', 'prettier --write'],
};
// Developers see what changed and can review
4. Meaningful Commit Messagesβ
β
GOOD:
feat(auth): implement JWT refresh token rotation
fix(api): handle null values in employee update
docs(readme): add installation instructions
test(employee): add tests for duplicate email validation
β AVOID:
updated files
fixed bugs
changes
updated
5. Document Git Workflowβ
Create CONTRIBUTING.md:
## Commit Message Format
We follow [Conventional Commits](https://www.conventionalcommits.org/).
Format: `type(scope): subject`
### Types
- **feat**: A new feature
- **fix**: A bug fix
- **docs**: Documentation changes
- **style**: Code style changes (formatting)
- **refactor**: Code restructuring
- **test**: Test additions/changes
- **chore**: Build process or dependency changes
### Example
\`\`\`
feat(api): add employee filtering by department
\`\`\`
## Pre-commit Hooks
Husky automatically runs:
1. **Pre-commit**: ESLint and Prettier
2. **Pre-push**: Tests and build
To bypass (use only when necessary):
\`\`\`bash
HUSKY=0 git commit -m "message"
\`\`\`
Setup Checklistβ
- Install Husky:
pnpm add -D husky - Initialize:
pnpm exec husky install - Create
.husky/pre-commithook - Create
.husky/pre-pushhook - Create
.husky/commit-msghook - Configure
.lintstagedrc.js - Configure
commitlint.config.ts - Install commitlint:
pnpm add -D @commitlint/config-conventional @commitlint/cli - Test hooks locally
- Update team documentation
- Commit configuration files