Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
What you'll learn
- Understand lint-staged fundamentals 🎯
- Apply lint-staged in real projects 🏗️
- Debug common issues 🐛
- Write type-safe code ✨
🎯 Introduction
Welcome to this exciting tutorial on lint-staged! 🎉 In this guide, we’ll explore how to set up automated pre-commit checks that keep your TypeScript code clean and consistent.
You’ll discover how lint-staged can transform your development workflow by catching issues before they reach your repository. Whether you’re building web applications 🌐, server-side code 🖥️, or libraries 📚, understanding lint-staged is essential for maintaining code quality and team productivity.
By the end of this tutorial, you’ll feel confident setting up automated pre-commit checks in your own projects! Let’s dive in! 🏊♂️
📚 Understanding Lint-Staged
🤔 What is Lint-Staged?
Lint-staged is like having a careful editor 📝 who reviews only the pages you’ve changed before you submit your story. Think of it as a quality gate 🚪 that ensures only the files you’re committing meet your project’s standards.
In TypeScript terms, lint-staged runs linting, formatting, and testing scripts only on staged files (the ones you’re about to commit) ⚡. This means you can:
- ✨ Catch issues before they reach the repository
- 🚀 Speed up your checks by only running on changed files
- 🛡️ Maintain consistent code quality across your team
- 🎯 Prevent broken code from entering your main branch
💡 Why Use Lint-Staged?
Here’s why developers love lint-staged:
- Speed ⚡: Only checks files you’re committing, not the entire codebase
- Team Consistency 👥: Everyone follows the same standards automatically
- Error Prevention 🛡️: Catches issues before they become problems
- Automation 🤖: No more forgetting to run linters manually
- Flexible Configuration 🔧: Run different tools on different file types
Real-world example: Imagine working on a shopping cart feature 🛒. With lint-staged, when you commit your changes, it automatically runs TypeScript checks, formats your code, and runs tests only on the files you modified!
🔧 Basic Syntax and Usage
📝 Installation
Let’s start by setting up lint-staged in your project:
# 📦 Install lint-staged and husky for git hooks
npm install --save-dev lint-staged husky
# 🎨 Or if you're using pnpm
pnpm add -D lint-staged husky
🎯 Basic Configuration
Here’s how to configure lint-staged in your package.json
:
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
},
"scripts": {
"prepare": "husky install"
}
}
💡 Explanation: This configuration runs ESLint and Prettier on TypeScript files, and just Prettier on JSON and Markdown files!
🚀 Setting Up Git Hooks
Create a pre-commit hook to run lint-staged:
# 🎯 Initialize husky
npx husky install
# 🔗 Create pre-commit hook
npx husky add .husky/pre-commit "npx lint-staged"
Now your .husky/pre-commit
file should contain:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
# 🎨 Run lint-staged on commit
npx lint-staged
💡 Practical Examples
🛒 Example 1: E-commerce Project Setup
Let’s configure lint-staged for a TypeScript e-commerce project:
{
"lint-staged": {
"src/**/*.{ts,tsx}": [
"eslint --fix --max-warnings 0",
"prettier --write",
"git add"
],
"src/**/*.test.{ts,tsx}": [
"jest --findRelatedTests --passWithNoTests"
],
"*.{json,md,yml,yaml}": [
"prettier --write"
],
"package.json": [
"sort-package-json"
]
}
}
🎯 What this does:
- ✨ Runs ESLint with zero warnings allowed on TypeScript files
- 🎨 Formats code with Prettier
- 🧪 Runs related tests for changed files
- 📄 Formats configuration files
- 📦 Sorts package.json automatically
🎮 Example 2: Game Development Project
For a TypeScript game project with specific needs:
// 📁 lint-staged.config.js
const path = require('path');
module.exports = {
// 🎮 Game source files
'src/game/**/*.{ts,tsx}': [
'eslint --fix',
'prettier --write',
// 🎯 Custom type checking for game files
() => 'tsc --noEmit --project tsconfig.game.json'
],
// 🧪 Test files
'**/*.{test,spec}.{ts,tsx}': [
'jest --findRelatedTests --passWithNoTests',
'eslint --fix',
'prettier --write'
],
// 🎨 Asset configuration
'src/assets/**/*.json': [
// 🔍 Validate game asset JSON files
(filenames) => filenames.map(filename =>
`node scripts/validate-asset.js ${filename}`
)
],
// 📝 Documentation
'docs/**/*.md': [
'markdownlint --fix',
'prettier --write'
]
};
🏦 Example 3: Enterprise Application
For a large TypeScript application with multiple teams:
// 📁 lint-staged.config.js
module.exports = {
// 🏢 Core business logic
'src/core/**/*.{ts,tsx}': [
'eslint --fix --config .eslintrc.core.json',
'prettier --write',
// 🛡️ Extra strict type checking for core
() => 'tsc --noEmit --strict --project tsconfig.core.json'
],
// 🌐 Frontend components
'src/components/**/*.{ts,tsx}': [
'eslint --fix',
'prettier --write',
// 🎨 Run component tests
'jest --findRelatedTests --passWithNoTests',
// 📸 Update snapshots if needed
(filenames) => {
const testFiles = filenames
.filter(f => f.includes('.test.') || f.includes('.spec.'))
.join(' ');
return testFiles ? `jest --updateSnapshot ${testFiles}` : '';
}
],
// 🔧 Configuration files
'*.{json,yml,yaml}': [
'prettier --write',
// 🔍 Validate configuration syntax
(filenames) => filenames
.filter(f => f.includes('config'))
.map(f => `node scripts/validate-config.js ${f}`)
]
};
🚀 Advanced Concepts
🧙♂️ Advanced Configuration: Conditional Checks
When you’re ready to level up, try conditional configurations:
// 📁 lint-staged.config.js
const { CLIEngine } = require('eslint');
const cli = new CLIEngine({});
module.exports = {
'*.{ts,tsx}': (filenames) => {
// 🎯 Only run on files that aren't ignored by ESLint
const filteredFiles = filenames.filter(file => !cli.isPathIgnored(file));
const commands = [];
if (filteredFiles.length > 0) {
// 🔍 Standard checks
commands.push(`eslint --fix ${filteredFiles.join(' ')}`);
commands.push(`prettier --write ${filteredFiles.join(' ')}`);
// 🧪 Run tests only if more than 5 files changed
if (filteredFiles.length > 5) {
commands.push('npm run test:unit');
} else {
commands.push(`jest --findRelatedTests ${filteredFiles.join(' ')}`);
}
// 🎯 Type check all files if core files changed
const hasCoreChanges = filteredFiles.some(f => f.includes('src/core/'));
if (hasCoreChanges) {
commands.push('tsc --noEmit');
}
}
return commands;
}
};
🏗️ Advanced Workflow: Multiple Environments
For complex projects with different environments:
// 📁 lint-staged.config.js
const isProduction = process.env.NODE_ENV === 'production';
const config = {
// 🎯 Base configuration for all environments
'*.{ts,tsx}': [
'eslint --fix',
'prettier --write'
]
};
if (isProduction) {
// 🏭 Production-specific checks
config['*.{ts,tsx}'].push(
// 🛡️ Strict type checking
() => 'tsc --noEmit --strict',
// 📊 Bundle size check
() => 'npm run build:analyze',
// 🔒 Security audit
() => 'npm audit --audit-level moderate'
);
} else {
// 🧪 Development-specific checks
config['*.{ts,tsx}'].push(
// ⚡ Quick tests only
'jest --findRelatedTests --passWithNoTests'
);
}
module.exports = config;
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Slow Pre-commit Hooks
# ❌ Wrong - runs on entire codebase!
"pre-commit": "eslint src/ && prettier --write src/ && jest"
# ✅ Correct - only runs on staged files!
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.test.{ts,tsx}": ["jest --findRelatedTests"]
}
🤯 Pitfall 2: Forgetting to Stage Fixed Files
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
// ❌ Missing: files won't be staged after fixes!
]
}
}
{
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write",
"git add"
// ✅ Now fixed files are automatically staged!
]
}
}
🚫 Pitfall 3: Ignoring Exit Codes
// ❌ Dangerous - ignores failures!
module.exports = {
'*.{ts,tsx}': [
'eslint --fix || true', // Don't do this!
'prettier --write'
]
};
// ✅ Safe - proper error handling
module.exports = {
'*.{ts,tsx}': [
'eslint --fix', // Will fail commit if errors found
'prettier --write'
]
};
🛠️ Best Practices
- ⚡ Keep It Fast: Only run necessary checks on staged files
- 🎯 Be Specific: Use file patterns to target the right tools
- 🔄 Auto-fix When Possible: Let tools fix issues automatically
- 🧪 Test Smartly: Run related tests, not the entire suite
- 📝 Document Your Setup: Help teammates understand the workflow
- 🚀 Fail Fast: Stop on first error to save time
- 🔧 Use Configs: Keep configuration in dedicated files for complex setups
💡 Pro Tips:
// 🎯 Pro tip: Use functions for dynamic commands
module.exports = {
'*.{ts,tsx}': (filenames) => [
`eslint --fix ${filenames.join(' ')}`,
`prettier --write ${filenames.join(' ')}`,
// 🧪 Only run tests if test files changed
...(filenames.some(f => f.includes('.test.'))
? [`jest --findRelatedTests ${filenames.join(' ')}`]
: [])
]
};
🧪 Hands-On Exercise
🎯 Challenge: Set Up Complete Pre-commit Workflow
Create a comprehensive lint-staged setup for a TypeScript project:
📋 Requirements:
- ✅ Format TypeScript files with Prettier
- 🔍 Lint TypeScript files with ESLint (zero warnings)
- 🧪 Run tests for changed files
- 📦 Sort package.json automatically
- 🔒 Type check the entire project if core files change
- 📝 Format markdown files
- 🎨 Validate JSON configuration files
🚀 Bonus Points:
- Add custom validation for specific file types
- Implement conditional checks based on file count
- Add performance monitoring for the pre-commit process
💡 Solution
🔍 Click to see solution
// 📁 lint-staged.config.js
const fs = require('fs');
const path = require('path');
// 🎯 Helper function to validate JSON files
const validateJson = (filenames) => {
return filenames.map(filename => {
try {
const content = fs.readFileSync(filename, 'utf8');
JSON.parse(content);
return `echo "✅ ${filename} is valid JSON"`;
} catch (error) {
return `echo "❌ ${filename} has invalid JSON: ${error.message}" && exit 1`;
}
});
};
// 🚀 Performance monitoring
const timeCommand = (command) => `time ${command}`;
module.exports = {
// 🎨 TypeScript source files
'src/**/*.{ts,tsx}': (filenames) => {
const commands = [
// 🔧 Fix linting issues
`eslint --fix --max-warnings 0 ${filenames.join(' ')}`,
// 🎨 Format code
`prettier --write ${filenames.join(' ')}`
];
// 🧪 Run tests for changed files
commands.push(`jest --findRelatedTests ${filenames.join(' ')} --passWithNoTests`);
// 🔒 Type check if core files changed
const hasCoreChanges = filenames.some(f => f.includes('src/core/') || f.includes('src/types/'));
if (hasCoreChanges) {
commands.push('echo "🔒 Core files changed, running full type check..."');
commands.push(timeCommand('tsc --noEmit'));
}
return commands;
},
// 🧪 Test files
'**/*.{test,spec}.{ts,tsx}': [
'eslint --fix',
'prettier --write',
// 🚀 Run the specific test files
(filenames) => `jest ${filenames.join(' ')} --passWithNoTests`
],
// 📦 Package configuration
'package.json': [
'sort-package-json',
'prettier --write',
// 🔍 Validate package.json structure
'node -e "console.log(\'✅ package.json is valid\')"'
],
// 🔧 Configuration files
'*.{json,jsonc}': [
...validateJson,
'prettier --write'
],
// 📝 Documentation
'**/*.md': [
'markdownlint --fix',
'prettier --write'
],
// 🎨 Style files
'**/*.{css,scss}': [
'stylelint --fix',
'prettier --write'
],
// 🌐 Configuration validation
'config/**/*': [
// 🔍 Custom validation for config files
(filenames) => filenames.map(f => `node scripts/validate-config.js ${f}`)
]
};
// 📁 package.json scripts section
{
"scripts": {
"prepare": "husky install",
"lint": "eslint src --ext .ts,.tsx",
"lint:fix": "eslint src --ext .ts,.tsx --fix",
"format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
"type-check": "tsc --noEmit",
"test:unit": "jest",
"test:related": "jest --findRelatedTests",
"pre-commit": "lint-staged"
},
"lint-staged": {
// 🔗 Reference to config file
"*": "lint-staged --config lint-staged.config.js"
}
}
#!/usr/bin/env sh
# 📁 .husky/pre-commit
. "$(dirname -- "$0")/_/husky.sh"
echo "🚀 Running pre-commit checks..."
# ⏰ Track execution time
START_TIME=$(date +%s)
# 🎯 Run lint-staged
npx lint-staged
# ⏱️ Show execution time
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
echo "✅ Pre-commit checks completed in ${DURATION}s"
🎓 Key Takeaways
You’ve learned so much! Here’s what you can now do:
- ✅ Set up lint-staged with confidence 💪
- ✅ Configure pre-commit hooks that keep your code clean 🛡️
- ✅ Optimize performance by running checks only on staged files ⚡
- ✅ Debug hook issues like a pro 🐛
- ✅ Maintain code quality automatically across your team! 🚀
Remember: Lint-staged is your quality guardian, not a roadblock! It’s here to help you ship better code faster. 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered lint-staged pre-commit checks!
Here’s what to do next:
- 💻 Set up lint-staged in your current project
- 🏗️ Experiment with different configurations for your team’s needs
- 📚 Learn about GitHub Actions for additional CI/CD checks
- 🌟 Share your setup with your team and help them level up!
Remember: Every clean codebase started with good habits. Keep committing quality code, and most importantly, have fun building amazing things! 🚀
Happy coding! 🎉🚀✨