+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 256 of 355

๐Ÿ“˜ Lint-Staged: Pre-Commit Checks

Master lint-staged: pre-commit checks in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

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:

  1. Speed โšก: Only checks files youโ€™re committing, not the entire codebase
  2. Team Consistency ๐Ÿ‘ฅ: Everyone follows the same standards automatically
  3. Error Prevention ๐Ÿ›ก๏ธ: Catches issues before they become problems
  4. Automation ๐Ÿค–: No more forgetting to run linters manually
  5. 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

  1. โšก Keep It Fast: Only run necessary checks on staged files
  2. ๐ŸŽฏ Be Specific: Use file patterns to target the right tools
  3. ๐Ÿ”„ Auto-fix When Possible: Let tools fix issues automatically
  4. ๐Ÿงช Test Smartly: Run related tests, not the entire suite
  5. ๐Ÿ“ Document Your Setup: Help teammates understand the workflow
  6. ๐Ÿš€ Fail Fast: Stop on first error to save time
  7. ๐Ÿ”ง 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:

  1. ๐Ÿ’ป Set up lint-staged in your current project
  2. ๐Ÿ—๏ธ Experiment with different configurations for your teamโ€™s needs
  3. ๐Ÿ“š Learn about GitHub Actions for additional CI/CD checks
  4. ๐ŸŒŸ 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! ๐ŸŽ‰๐Ÿš€โœจ