Skip to content

Instantly share code, notes, and snippets.

@nurmdrafi
Last active November 10, 2025 11:25
Show Gist options
  • Save nurmdrafi/f09de094f6f54ce548c9fa85fda44cb3 to your computer and use it in GitHub Desktop.
Save nurmdrafi/f09de094f6f54ce548c9fa85fda44cb3 to your computer and use it in GitHub Desktop.
Setting Up ESLint v9, Prettier, Git Hooks & Code Quality Tools

Setting Up ESLint v9, Prettier, Git Hooks & Code Quality Tools

This guide outlines how to migrate to ESLint v9, integrate Prettier for code formatting, and leverage lint-staged to automate these checks before each commit.

Table of Contents

Why This Migration?

ESLint v9 Migration: ESLint v9 introduces the new flat configuration format, which is simpler, more performant. The legacy .eslintrc format is deprecated and will be removed in future versions. Previously with ESLint v8, we followed Airbnb rules, but Airbnb rules are now deprecated, so we've removed Airbnb from our ESLint configuration.

Prettier Integration: While ESLint can handle code formatting, Prettier is purpose-built for code formatting with superior algorithms and broader language support. We disable ESLint's stylistic rules and let Prettier handle all formatting concerns.

lint-staged: Running linters on entire codebases during commits can be slow. lint-staged only runs ESLint and Prettier on staged files, making commits faster while maintaining code quality.

Step 1: Update Dependencies

Run the following command to update/install:

npm install --save-dev prettier lint-staged @eslint/eslintrc @eslint/js @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-config-next eslint-config-prettier eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks @commitlint/cli @commitlint/config-conventional

Note: Remove previously installed airbnb related packages.

Step 2: Configure ESLint

Migrate your ESLint configuration to the flat config format (eslint.config.mjs). This is the recommended format for ESLint v9.

Create eslint.config.mjs file in your project root:

import js from '@eslint/js'
import tsParser from '@typescript-eslint/parser'
import globals from 'globals'
import { FlatCompat } from '@eslint/eslintrc'
import prettier from 'eslint-plugin-prettier'

const compat = new FlatCompat({
  baseDirectory: import.meta.dirname,
  recommendedConfig: js.configs.recommended,
})

const eslintconfig = [
  // Global ignores
  {
    ignores: [
      '**/.eslintrc.json',
      '**/tsconfig.json',
      '**/index.d.ts',
      '**/*.config.js',
      '.next/',
      'out/',
      'node_modules/',
      'public/',
      'scripts/update-version.js',
      './next-env.d.ts',
      'coverage/',
      '__tests__/',
      'redux/',
    ],
  },

  // Base JS configuration
  js.configs.recommended,

  // Prettier integration
  {
    plugins: {
      prettier, // Enable Prettier as an ESLint plugin
    },
    rules: {
      'prettier/prettier': 'error', // Run Prettier as an ESLint rule
    },
  },

  // Next.js and other plugins using compat
  ...compat.extends(
    'next/core-web-vitals',
    'next/typescript',
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'plugin:jsx-a11y/recommended',
    'plugin:import/recommended',
    'plugin:prettier/recommended'
  ),

  // Main configuration for all files
  {
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.node,
      },
      ecmaVersion: 2020,
      sourceType: 'module',
    },
    settings: {
      react: {
        version: 'detect',
      },
    },
    rules: {
      // Your existing rules (keep non-stylistic ones)
      'react/prop-types': 'off',
      'react/forbid-prop-types': 'error',
      'react/default-props-match-prop-types': 'error',
      'react/self-closing-comp': 'error',
      'react/no-unused-prop-types': 'error',
      'react/jsx-key': 'error',
      'react/no-unused-state': 'error',
      'react/state-in-constructor': 'error',
      'react/function-component-definition': 'off',
      'react/require-default-props': 'off',
      'react/no-array-index-key': 'off',
      'react/no-unescaped-entities': 'off',
      'react/no-unstable-nested-components': 'off',
      'react/no-danger': 'off',
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'error',
      'jsx-a11y/img-redundant-alt': 'error',
      'jsx-a11y/anchor-is-valid': 'error',
      'jsx-a11y/alt-text': 'error',
      'jsx-a11y/mouse-events-have-key-events': 'error',
      'jsx-a11y/no-static-element-interactions': 'off',
      'jsx-a11y/no-noninteractive-element-interactions': 'off',
      'jsx-a11y/click-events-have-key-events': 'off',
      'jsx-a11y/interactive-supports-focus': 'off',
      'import/first': 'error',
      'import/no-mutable-exports': 'error',
      'import/no-useless-path-segments': 'error',
      'import/no-named-as-default': 'error',
      'import/no-duplicates': 'error',
      'import/newline-after-import': 'error',
      'import/no-extraneous-dependencies': 'off',
      'import/order': 'off',
      'import/named': 'off',
      'import/no-cycle': 'off',
      'import/extensions': 'off',
      'no-console': ['error', { allow: ['warn', 'error'] }],
      'no-bitwise': 'off',
      'no-underscore-dangle': 'off',
      'no-nested-ternary': 'off',
      'no-restricted-syntax': 'off',
      'no-unused-vars': 'off',
      'no-return-await': 'off',

      // Remove stylistic rules (handled by Prettier)
      'react/jsx-indent-props': 'off',
      'react/jsx-curly-newline': 'off',
      'react/jsx-equals-spacing': 'off',
      'react/jsx-indent': 'off',
      'react/jsx-props-no-multi-spaces': 'off',
      'react/jsx-curly-brace-presence': 'off',
      'react/jsx-closing-bracket-location': 'off',
      'react/jsx-tag-spacing': 'off',
      'react/jsx-props-no-spreading': 'off',
      'react/react-in-jsx-scope': 'off',
      'react/jsx-closing-tag-location': 'off',
      'react/jsx-boolean-value': 'off',
      'react/jsx-wrap-multilines': 'off',
      'react/jsx-no-useless-fragment': 'off',
      'react/jsx-one-expression-per-line': 'off',
      'react/jsx-max-props-per-line': 'off',
      'react/jsx-curly-spacing': 'off',
      'object-curly-spacing': 'off',
      'space-before-blocks': 'off',
      'keyword-spacing': 'off',
      'comma-spacing': 'off',
      'space-infix-ops': 'off',
      'quote-props': 'off',
      'function-paren-newline': 'off',
      'function-call-argument-newline': 'off',
      'spaced-comment': 'off',
      'operator-linebreak': 'off',
      'computed-property-spacing': 'off',
      'array-callback-return': 'off',
      'space-unary-ops': 'off',
      'object-shorthand': 'off',
      'key-spacing': 'off',
      'prefer-const': 'off',
      'prefer-destructuring': 'off',
      'prefer-template': 'off',
      'prefer-regex-literals': 'off',
      'prefer-promise-reject-errors': 'off',
      'guard-for-in': 'off',
      'no-cond-assign': 'off',
      'no-sequences': 'off',
      'no-unneeded-ternary': 'off',
      'no-extra-boolean-cast': 'off',
      'no-lonely-if': 'off',
      'no-unsafe-optional-chaining': 'off',
      'no-mixed-operators': 'off',
      'no-confusing-arrow': 'off',
      'no-plusplus': 'off',
      'no-constant-condition': 'off',
      'no-floating-decimal': 'off',
      'eol-last': 'off',
      'array-bracket-spacing': 'off',
      'space-in-parens': 'off',
      'template-curly-spacing': 'off',
      'no-tabs': 'off',
      'dot-notation': 'off',
      'implicit-arrow-linebreak': 'off',
      'padded-blocks': 'off',
      'no-multiple-empty-lines': 'off',
      'no-multi-spaces': 'off',
      'no-else-return': 'off',
      indent: 'off',
      'linebreak-style': 'off',
      semi: 'off',
      'no-param-reassign': 'off',
      'jsx-quotes': 'off',
      'max-len': 'off',
      'object-curly-newline': 'off',
      'arrow-body-style': 'off',
      'arrow-parens': 'off',
      'consistent-return': 'off',
      eqeqeq: 'off',
      'no-trailing-spaces': 'off',
      'no-useless-return': 'off',
    },
  },

  // TypeScript-specific configuration
  {
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      parser: tsParser,
      parserOptions: {
        project: './tsconfig.json',
        ecmaFeatures: {
          jsx: true,
        },
      },
    },
    rules: {
      '@typescript-eslint/return-await': 'error',
      '@typescript-eslint/restrict-plus-operands': 'error',
      '@typescript-eslint/no-shadow': 'error',
      '@typescript-eslint/no-misused-promises': [
        'error',
        {
          checksVoidReturn: false,
        },
      ],
      '@typescript-eslint/ban-ts-comment': 'off',
      '@typescript-eslint/no-unused-expressions': 'warn',
      '@typescript-eslint/no-unused-vars': 'off',
      '@typescript-eslint/prefer-as-const': 'warn',
      '@typescript-eslint/no-explicit-any': 'off',
      '@typescript-eslint/no-unsafe-member-access': 'off',
      '@typescript-eslint/no-unsafe-argument': 'off',
      '@typescript-eslint/no-unsafe-call': 'off',
      '@typescript-eslint/no-unsafe-assignment': 'off',
      '@typescript-eslint/no-unsafe-return': 'off',
      '@typescript-eslint/restrict-template-expressions': 'off',
      '@typescript-eslint/no-floating-promises': 'off',
      '@typescript-eslint/ban-types': 'off',
      '@typescript-eslint/no-use-before-define': 'off',
      '@typescript-eslint/semi': 'off',
      '@typescript-eslint/comma-dangle': 'off',
      '@typescript-eslint/quotes': 'off',
      '@typescript-eslint/naming-convention': 'off',
      '@typescript-eslint/no-var-requires': 'off',
    },
  },
]

export default eslintconfig

Step 3: Configure Prettier

Install the Prettier extension in your editor (for VS Code: esbenp.prettier-vscode) and create .vscode/settings.json in your project root. These settings enable format on save which will automatically format the current file when you save it:

{
  "editor.formatOnSave": true,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit"
  },
  "eslint.useFlatConfig": true,
  "prettier.requireConfig": true,
  "[javascript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescript]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[json]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[css]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[scss]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  },
  "[html]": {
    "editor.defaultFormatter": "esbenp.prettier-vscode"
  }
}

Create a .prettierrc file in your project root to configure Prettier's formatting rules:

{
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "useTabs": false,
  "trailingComma": "es5",
  "bracketSpacing": true,
  "bracketSameLine": false,
  "arrowParens": "avoid",
  "endOfLine": "lf",
  "printWidth": 100,
  "quoteProps": "as-needed",
  "jsxSingleQuote": true
}

Also, create a .prettierignore file in your project root to specify files and directories that Prettier should ignore:

node_modules/
nginx/
.next/
out/
public/
scripts/update-version.js
next-env.d.ts
coverage/
__tests__/
redux/
*.config.js
tsconfig.json
.eslintrc.json
index.d.ts
CHANGELOG.md

Step 4: Configure lint-staged

Configure lint-staged in your package.json to run ESLint and Prettier on staged files before committing:

{
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"]
  }
}

Step 5: Update Scripts

Add these scripts to your package.json:

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint .",
    "lint:fix": "eslint . --fix",
    "lint:cache": "eslint . --cache",
    "lint:fix:cache": "eslint . --fix --cache",
    "check-types": "tsc --pretty --noEmit",
    "test": "vitest",
    "test:coverage": "vitest run --coverage",
    "test-all": "npm run lint && npm run check-types && npm run build",
    "postinstall": "npx next telemetry disable"
  }
}

Step 6: Set Up Git Hooks

Install Husky and initialize git hooks:

npm install --save-dev husky
npx husky init

Create the .husky/pre-commit file with the following content:

# Get the branch name
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)

# This script runs linting, type checking, and builds before allowing a commit.
echo 'πŸ› οΈ  Preparing to commit: Running linting, type checking, test coverage, and build'


# Run lint-staged for ESLint/Prettier fixes on staged files
echo "πŸ” Running lint-staged (ESLint/Prettier)..."
npx lint-staged ||
(
  echo '❌ Lint-staged failed. Please fix ESLint/Prettier issues and try committing again.'
  exit 1
)

# Run TypeScript type checking
echo "πŸ” Running TypeScript type checking..."
npm run check-types ||
(
  echo '❌ TypeScript type checking failed. Please fix the type errors before committing.'
  exit 1
)

# Run test coverage
# echo "πŸ§ͺ Running test coverage..."
# npm run test:coverage ||
# (
#   echo '❌ Test coverage failed. Please ensure all tests pass and coverage thresholds are met before committing.'
#   exit 1
# )

# Run build only for main branch
if [ "$BRANCH_NAME" = "main" ]; then
  echo "πŸ—οΈ  Running build for main branch..."
  npm run build ||
  (
    echo '❌ Build failed. Please ensure the build process completes successfully before committing.'
    exit 1
  )
fi

# All checks passed, allow the commit
echo 'βœ… All pre-commit checks passed. You may now commit your changes.'

Create the .husky/commit-msg file with the following content:

echo "Received commit message: $1"

NAME=$(git config user.name)
EMAIL=$(git config user.email)

if [ -z "$NAME" ]; then
  echo "empty git config user.name"
  exit 1
fi

if [ -z "$EMAIL" ]; then
  echo "empty git config user.email"
  exit 1
fi

git interpret-trailers --if-exists doNothing --trailer \
  "Signed-off-by: $NAME <$EMAIL>" \
  --in-place "$1"

npm exec --no -- commitlint --edit $1

Make both hook executable:

chmod +x .husky/pre-commit
chmod +x .husky/commit-msg

Step 7: Set Up Commitlint

Create the configuration in your project root:

Create commitlint.config.js:

module.exports = {
  extends: ['@commitlint/config-conventional'],
}

Step 8: Test Configuration

Test the setup following the package.json scripts pattern:

Test with Wrong Commit Pattern

First, let's test that commitlint is working by trying to commit with an invalid commit message. This should fail and show you that commitlint is properly configured.

  1. Stage your change: Make a small change to any file then stage your change

    git add .
  2. Try to commit with an invalid message (this should fail):

    git commit -m "invalid commit message"

    You should see an error message from commitlint indicating that the commit message doesn't follow the conventional commit format.

Test Linting

Run the linting script:

npm run lint

This should run ESLint and show any linting issues.

Test Type Checking

Run TypeScript type checking:

npm run check-types

This should run tsc --pretty --noEmit and show any type errors.

Test with Valid Commit

Now try with a valid conventional commit message:

git commit -m "feat: add test comment for validation"

Run Comprehensive Test

Finally, run the comprehensive test script:

npm run test-all

This runs npm run lint && npm run check-types && npm run build to ensure everything works together.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment