Skip to content

Instantly share code, notes, and snippets.

@nurmdrafi
Created September 10, 2025 08:45
Show Gist options
  • Save nurmdrafi/dcb97e048d586bf9230d4465d6111063 to your computer and use it in GitHub Desktop.
Save nurmdrafi/dcb97e048d586bf9230d4465d6111063 to your computer and use it in GitHub Desktop.
EsLint migrate to v9

Modernizing Your JavaScript/TypeScript Workflow: ESLint v9, Prettier, and lint-staged

In modern web development, maintaining code quality and consistency is paramount. This article outlines how to migrate to the latest ESLint v9, integrate Prettier for code formatting, and leverage lint-staged to automate these checks before each commit. We'll assume you already have Husky set up for Git hooks.

Prerequisites

  • Node.js (>=22.4.1)
  • npm or yarn
  • Git
  • Husky (already installed)

Step 1: Update ESLint and Related Dependencies

First, update your ESLint packages and install the necessary plugins. Here's the relevant section from package.json:

{
  "devDependencies": {
    "@eslint/cli": "^9.0.0",
    "@eslint/eslintrc": "^3.3.1",
    "@eslint/js": "^9.35.0",
    "@typescript-eslint/eslint-plugin": "^8.43.0",
    "@typescript-eslint/parser": "^8.43.0",
    "eslint": "^9.35.0",
    "eslint-config-next": "^15.5.2",
    "eslint-config-prettier": "^10.1.8",
    "eslint-plugin-import": "^2.32.0",
    "eslint-plugin-jsx-a11y": "^6.10.2",
    "eslint-plugin-prettier": "^5.5.4",
    "eslint-plugin-react": "^7.37.5",
    "eslint-plugin-react-hooks": "^5.2.0",
    "prettier": "^3.6.2",
    "lint-staged": "^16.1.6"
  },
  "overrides": {
    "eslint": "$eslint"
  }
}

Run the following command to update/install:

npm install --save-dev @eslint/cli @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 prettier lint-staged

Step 2: Configure ESLint

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

Here's an example eslint.config.mjs:

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' // Add Prettier plugin

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' // Adds Prettier config to disable conflicting rules
  ),

  // 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/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

Create a .prettierrc.js or .prettierrc.json file to configure Prettier's formatting rules. A basic example:

// .prettierrc.js
module.exports = {
  semi: false,
  singleQuote: true,
  trailingComma: 'all',
  printWidth: 120,
  tabWidth: 2,
};

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

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

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: Integrate with Husky

Since you already have Husky set up, modify the pre-commit hook to use lint-staged. If you don't have it, install husky using npm install husky --save-dev and enable git hooks husky install.

Your .husky/pre-commit file should contain:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx lint-staged

Make sure the pre-commit file is executable:

chmod +x .husky/pre-commit

Step 6: Commitlint (Optional)

If you're using Commitlint, ensure it's configured correctly. The commitlint.config.js file might look like this:

module.exports = {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'subject-case': [0] // Disable case checking entirely
  }
};

And the commit-msg hook in .husky should point to commitlint:

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npx --no-install commitlint --edit "$1"

Step 7: Test Your Configuration

Now, try making a change to a JavaScript or TypeScript file, stage it, and commit. lint-staged will automatically run ESLint and Prettier on the staged files.

git add .
git commit -m "Test:  Test ESLint, Prettier, and lint-staged integration"

Troubleshooting

  • ESLint errors not being fixed: Double-check your ESLint configuration and ensure that the rules are correctly set up.
  • Prettier not formatting: Verify your Prettier configuration and ensure that it's not conflicting with ESLint rules.
  • lint-staged not running: Make sure Husky is properly installed and the pre-commit hook is correctly configured. Also, verify that the file extensions in lint-staged match your project's file types.
  • Conflicting rules: ESLint and Prettier can sometimes have conflicting rules. Use eslint-config-prettier to disable ESLint rules that conflict with Prettier.

Conclusion

By integrating ESLint v9, Prettier, and lint-staged with Husky, you can create a robust and automated workflow that ensures code quality and consistency across your project. This setup helps catch errors early, enforce coding standards, and improve collaboration among

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