Pre-commit Formatting with Husky & lint-staged | Web Formatter Blog

Pre-commit Formatting with Husky & lint-staged
Automate code formatting in your Git workflow to maintain consistent code style across your team.
Introduction to Pre-commit Formatting
Maintaining consistent code style across a project is essential for readability, maintainability, and collaboration. However, manually formatting code before each commit is tedious and error-prone. This is where pre-commit formatting tools come into play.
Pre-commit hooks are scripts that run automatically before a commit is completed. By leveraging these hooks to run code formatters, you can ensure that all code committed to your repository follows your project's style guidelines—without requiring manual intervention.
In this guide, we'll explore how to implement automated code formatting using two powerful tools:
- Husky: A tool that makes working with Git hooks easy
- lint-staged: A tool that runs commands on files staged for commit
Together, these tools create a seamless workflow that formats your code automatically right before it's committed to your repository.
Why Use Pre-commit Hooks for Formatting?
Implementing pre-commit formatting offers several significant benefits:
- Consistency: Ensures all code in your repository follows the same style guidelines
- Automation: Eliminates the need to remember to format code manually
- Efficiency: Only formats files that have been changed, saving time
- Collaboration: Reduces style-related merge conflicts and code review comments
- Quality: Catches formatting issues before they enter your codebase
By catching and fixing formatting issues at the commit stage, you prevent style inconsistencies from ever reaching your repository, leading to cleaner code and more productive collaboration.
Tools Overview
Before diving into implementation, let's understand the key tools we'll be using.
Husky: Git Hooks Made Easy
Husky is a popular npm package that simplifies working with Git hooks. Git hooks are scripts that run at specific points in the Git workflow, such as before a commit or push.
Key features of Husky include:
- Easy installation and configuration
- Support for all Git hooks
- Automatic hook installation when teammates install dependencies
- Modern, lightweight architecture
Husky v6+ uses a more Git-native approach compared to earlier versions, making it more reliable and efficient.
lint-staged: Run Commands on Staged Files
lint-staged is a tool that runs commands on files that are staged in Git. It's particularly useful for running linters and formatters only on the files that have been changed, rather than processing your entire codebase.
Key features of lint-staged include:
- Only processes files that are staged for commit
- Supports multiple commands for different file types
- Automatically adds formatted files back to the staging area
- Prevents commits if formatting or linting fails
- Highly configurable for different project needs
When combined with Husky, lint-staged creates a powerful workflow for ensuring code quality at the commit stage.
Setting Up Pre-commit Formatting
Now, let's walk through the process of setting up pre-commit formatting with Husky and lint-staged.
Installation
First, install the required packages:
# Using npm
npm install --save-dev husky lint-staged
# Using yarn
yarn add --dev husky lint-staged
# Using pnpm
pnpm add --save-dev husky lint-staged
After installation, set up Husky:
# For Husky v7+
npx husky install
# Add this script to package.json to ensure Husky is installed on npm install
npm set-script prepare "husky install"
Create a pre-commit hook:
npx husky add .husky/pre-commit "npx lint-staged"
Basic Configuration
Next, configure lint-staged by adding a configuration to your package.json or creating a dedicated .lintstagedrc file:
// In package.json
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["prettier --write"],
"*.{css,scss}": ["prettier --write"],
"*.{json,md}": ["prettier --write"]
}
}
// Or in .lintstagedrc.json
{
"*.{js,jsx,ts,tsx}": ["prettier --write"],
"*.{css,scss}": ["prettier --write"],
"*.{json,md}": ["prettier --write"]
}
This basic configuration runs Prettier on different file types when they're staged for commit.
Integrating Popular Formatters
Let's explore how to integrate some popular code formatters with our pre-commit setup.
Prettier Integration
Prettier is a widely-used opinionated code formatter that supports many languages and file types.
Install Prettier:
npm install --save-dev prettier
Configure lint-staged to use Prettier:
{
"lint-staged": {
"*.{js,jsx,ts,tsx,css,scss,json,md,html,yaml,yml}": ["prettier --write"]
}
}
You can also create a .prettierrc file to customize Prettier's formatting rules:
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100
}
ESLint Integration
ESLint is a popular JavaScript linter that can also fix many code style issues.
Install ESLint:
npm install --save-dev eslint
Configure lint-staged to use ESLint:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"]
}
}
Note that we're running ESLint before Prettier to ensure that any automatic fixes from ESLint don't conflict with Prettier's formatting.
Stylelint Integration
Stylelint is a powerful linter for CSS and CSS-like syntaxes.
Install Stylelint:
npm install --save-dev stylelint stylelint-config-standard
Configure lint-staged to use Stylelint:
{
"lint-staged": {
"*.{css,scss}": ["stylelint --fix", "prettier --write"],
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"]
}
}
Create a .stylelintrc file to configure Stylelint:
{
"extends": "stylelint-config-standard",
"rules": {
"indentation": 2,
"string-quotes": "single"
}
}
Advanced Configurations
Let's explore some advanced configurations to enhance your pre-commit workflow.
Custom Scripts
You can run custom scripts as part of your pre-commit process:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write",
"node scripts/custom-validation.js"
]
}
}
This allows you to add project-specific validations or transformations.
Combining Multiple Formatters
For a comprehensive setup that handles various file types:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{css,scss}": ["stylelint --fix", "prettier --write"],
"*.html": ["htmlhint", "prettier --write"],
"*.json": ["prettier --write"],
"*.md": ["markdownlint --fix", "prettier --write"]
}
}
This configuration applies appropriate linters and formatters based on file type.
Performance Optimization
For large projects, you might want to optimize performance:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": ["eslint --fix --cache", "prettier --write"],
"*.{css,scss}": ["stylelint --fix --cache", "prettier --write"]
}
}
Using the --cache flag with linters can significantly improve performance by only checking files that have changed since the last run.
You can also use the --concurrent flag with lint-staged to run tasks in parallel:
npx husky add .husky/pre-commit "npx lint-staged --concurrent false"
Setting concurrent to false can be helpful when you have tasks that depend on each other.
CI/CD Integration
While pre-commit hooks are great for local development, you should also enforce formatting in your CI/CD pipeline to catch any issues that might slip through.
GitHub Actions
Here's an example GitHub Actions workflow that checks code formatting:
# .github/workflows/code-quality.yml
name: Code Quality
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check formatting
run: npx prettier --check .
- name: Lint JavaScript
run: npx eslint .
- name: Lint CSS
run: npx stylelint "**/*.{css,scss}"
GitLab CI
For GitLab CI, you can use a similar configuration:
# .gitlab-ci.yml
code_quality:
image: node:16
stage: test
script:
- npm ci
- npx prettier --check .
- npx eslint .
- npx stylelint "**/*.{css,scss}"
only:
- merge_requests
- main
Troubleshooting Common Issues
Let's address some common issues you might encounter when setting up pre-commit formatting.
Hooks Not Running
If your hooks aren't running, check the following:
- Ensure Husky is properly installed and initialized
- Check that the .husky directory exists and contains the pre-commit file
- Make sure the pre-commit file has executable permissions
- Verify that Git is using the hooks from the .husky directory
You can run this command to check if Husky is properly set up:
git config core.hooksPath
It should output .husky.
Merge Conflicts
If you encounter merge conflicts related to formatting:
- Ensure all team members are using the same formatter versions
- Use consistent configuration files across branches
- Consider running formatters on the entire codebase after major merges
You can also add a post-merge hook to automatically format code after merging:
npx husky add .husky/post-merge "npx prettier --write ."
Best Practices
Follow these best practices to get the most out of your pre-commit formatting setup:
- Commit configuration files: Ensure .prettierrc, .eslintrc, etc. are committed to your repository
- Document your setup: Include setup instructions in your README
- Pin dependency versions: Use exact versions for formatters to ensure consistency
- Start simple: Begin with basic formatting and add more complex rules gradually
- Combine with editor integration: Use editor plugins for real-time feedback
- Enforce in CI: Double-check formatting in your CI pipeline
- Provide an escape hatch: Allow bypassing hooks in emergency situations with git commit --no-verify
Conclusion
Implementing pre-commit formatting with Husky and lint-staged is a powerful way to maintain code quality and consistency in your projects. By automating the formatting process, you eliminate style debates, reduce merge conflicts, and ensure that your codebase always adheres to your team's standards.
The initial setup might require some effort, but the long-term benefits in terms of code quality, team productivity, and reduced friction are well worth it. As your project grows, you can expand your pre-commit workflow to include additional checks and validations, creating a robust quality gate for your codebase.
Remember that the goal of automated formatting is not just to make your code look pretty, but to eliminate unnecessary discussions about style, allowing your team to focus on what truly matters: building great software.