DEV Community

HK Lee
HK Lee

Posted on • Originally published at pockit.tools

ESLint 10 Migration Guide: Everything You Need to Know About the Biggest Update Yet

ESLint 10 just dropped, and it's not a minor update. The .eslintrc configuration system you've been clinging to? Completely removed—not deprecated, removed. The --env flag? Gone. Node.js 18? Not supported anymore.

This is the ESLint team's statement: flat config is the only way forward.

If you've been putting off the migration, January 2026 is your deadline. ESLint 9 is now in maintenance mode. ESLint 10 is the present. And this guide will get you there without breaking your CI pipeline.

What's New in ESLint 10

Let's start with what changed before we dive into migration.

Node.js 20.19.0+ Required

ESLint 10 drops support for Node.js 18, 19, 21, and 23. You need:

  • Node.js ^20.19.0 (20.19.0 or higher in 20.x)
  • Node.js ^22.13.0 (22.13.0 or higher in 22.x LTS)
  • Node.js >=24.0.0 (24.x Current)

Check your CI/CD pipelines. This will break builds on older Node versions immediately.

node --version  # Must be v20.19.0+
Enter fullscreen mode Exit fullscreen mode

Complete Removal of .eslintrc

This is the big one. ESLint 10 removes everything related to the legacy configuration:

Removed Replacement
.eslintrc.* files eslint.config.js
.eslintignore ignores array in config
--env flag globals in languageOptions
--rulesdir flag Direct plugin imports
--resolve-plugins-relative-to ES module imports
/* eslint-env */ comments Now reported as errors
ESLINT_USE_FLAT_CONFIG env var Always flat config
LegacyESLint API Removed entirely

If your project still uses .eslintrc.json, ESLint 10 will simply ignore it.

New Configuration File Lookup

ESLint 10 changes how it finds config files:

ESLint 9 (old behavior):

Search from CWD upward for eslint.config.js
Enter fullscreen mode Exit fullscreen mode

ESLint 10 (new behavior):

Search from each linted file's directory upward for eslint.config.js
Enter fullscreen mode Exit fullscreen mode

This is huge for monorepos. Each package can now have its own eslint.config.js, and ESLint will find it automatically.

my-monorepo/
├── eslint.config.js          # Root config (fallback)
├── packages/
│   ├── web/
│   │   ├── eslint.config.js  # Web-specific config
│   │   └── src/
│   └── api/
│       ├── eslint.config.js  # API-specific config
│       └── src/
Enter fullscreen mode Exit fullscreen mode

JSX Reference Tracking

ESLint 10 now tracks JSX element references for accurate scope analysis. This means:

  • Custom components are properly tracked as references
  • no-unused-vars works correctly with JSX
  • Some existing code might trigger new warnings (false positives in v9)
// ESLint 10 now correctly sees Button as a reference
import { Button } from './components';

function App() {
  return <Button>Click me</Button>;  // Button is tracked
}
Enter fullscreen mode Exit fullscreen mode

Updated eslint:recommended

The recommended ruleset has been updated. Run ESLint after migration to catch any new violations.

Step-by-Step Migration from ESLint 9

Step 1: Check Node.js Version

node --version
# If below v20.19.0, upgrade first
Enter fullscreen mode Exit fullscreen mode

Update your CI/CD to use Node.js 20.x LTS or 22.x.

Step 2: Update ESLint and Dependencies

npm install eslint@10 --save-dev

# Update TypeScript ESLint if using TypeScript
npm install @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest --save-dev

# Update other plugins
npm install eslint-plugin-react@latest eslint-plugin-react-hooks@latest --save-dev
Enter fullscreen mode Exit fullscreen mode

Step 3: Migrate Config (If You Haven't Already)

If you're still on .eslintrc, use the migration tool:

npx @eslint/migrate-config .eslintrc.json
Enter fullscreen mode Exit fullscreen mode

This generates an eslint.config.mjs file. Review and adjust it.

Step 4: Update Your Flat Config for v10 Compatibility

If you already have eslint.config.js, check for these v10-specific updates:

Remove eslint-env Comments

// ❌ ESLint 10 reports this as an error
/* eslint-env browser, node */

// ✅ Use globals in config instead
import globals from "globals";

export default [
  {
    languageOptions: {
      globals: {
        ...globals.browser,
        ...globals.node,
      }
    }
  }
];
Enter fullscreen mode Exit fullscreen mode

Update SourceCode Method Usage (For Plugin Authors)

// ❌ Removed in ESLint 10
sourceCode.getTokenOrCommentBefore(node);

// ✅ Use replacement
sourceCode.getTokenBefore(node, { includeComments: true });
Enter fullscreen mode Exit fullscreen mode

Update Context Methods (For Rule Authors)

// ❌ Removed in ESLint 10
context.getFilename();
context.getCwd();
context.getSourceCode();

// ✅ Use replacements
context.filename;
context.cwd;
context.sourceCode;
Enter fullscreen mode Exit fullscreen mode

Step 5: Delete Legacy Files

rm .eslintrc.* .eslintignore 2>/dev/null || true
Enter fullscreen mode Exit fullscreen mode

Step 6: Update VS Code Settings

{
  "eslint.useFlatConfig": true
}
Enter fullscreen mode Exit fullscreen mode

Note: The eslint.useFlatConfig setting is now the default. You can remove it.

Step 7: Test

npx eslint .
Enter fullscreen mode Exit fullscreen mode

Fix any new violations from the updated eslint:recommended or JSX tracking changes.

Complete ESLint 10 Config Examples

TypeScript Project

// eslint.config.js
import js from "@eslint/js";
import typescript from "typescript-eslint";
import globals from "globals";

export default typescript.config(
  js.configs.recommended,
  ...typescript.configs.recommendedTypeChecked,
  {
    languageOptions: {
      globals: globals.node,
      parserOptions: {
        projectService: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
  },
  {
    files: ["**/*.js", "**/*.mjs"],
    ...typescript.configs.disableTypeChecked,
  },
  {
    ignores: ["dist/**", "node_modules/**"],
  }
);
Enter fullscreen mode Exit fullscreen mode

React + TypeScript (Next.js)

// eslint.config.mjs
import js from "@eslint/js";
import typescript from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
import next from "@next/eslint-plugin-next";
import globals from "globals";

export default typescript.config(
  js.configs.recommended,
  ...typescript.configs.recommended,

  // React + JSX
  {
    files: ["**/*.{jsx,tsx}"],
    plugins: {
      react,
      "react-hooks": reactHooks,
      "jsx-a11y": jsxA11y,
    },
    languageOptions: {
      globals: globals.browser,
      parserOptions: {
        ecmaFeatures: { jsx: true },
      },
    },
    settings: {
      react: { version: "detect" },
    },
    rules: {
      ...react.configs.recommended.rules,
      ...react.configs["jsx-runtime"].rules,
      ...reactHooks.configs.recommended.rules,
      ...jsxA11y.configs.recommended.rules,
    },
  },

  // Next.js
  {
    plugins: {
      "@next/next": next,
    },
    rules: {
      ...next.configs.recommended.rules,
      ...next.configs["core-web-vitals"].rules,
    },
  },

  {
    ignores: [".next/**", "node_modules/**", "out/**"],
  }
);
Enter fullscreen mode Exit fullscreen mode

Monorepo with Package-Specific Configs

// Root: eslint.config.js
import js from "@eslint/js";
import typescript from "typescript-eslint";

// Base config that all packages inherit
export default typescript.config(
  js.configs.recommended,
  ...typescript.configs.recommended,
  {
    ignores: ["**/dist/**", "**/node_modules/**"],
  }
);
Enter fullscreen mode Exit fullscreen mode
// packages/web/eslint.config.js
import baseConfig from "../../eslint.config.js";
import react from "eslint-plugin-react";
import globals from "globals";

export default [
  ...baseConfig,
  {
    files: ["**/*.{jsx,tsx}"],
    plugins: { react },
    languageOptions: {
      globals: globals.browser,
    },
    rules: {
      ...react.configs.recommended.rules,
    },
  },
];
Enter fullscreen mode Exit fullscreen mode

ESLint 10's new lookup algorithm will automatically use the closest eslint.config.js to each file.

Common Migration Errors and Fixes

Error: "ESLint requires Node.js version ^20.19.0 || ^22.0.0 || >=24.0.0"

Fix: Upgrade Node.js to 20.19.0 or later.

# Using nvm
nvm install 22
nvm use 22
Enter fullscreen mode Exit fullscreen mode

Error: "eslint-env is no longer supported"

Fix: Remove /* eslint-env */ comments and use globals:

import globals from "globals";

{
  languageOptions: {
    globals: globals.browser
  }
}
Enter fullscreen mode Exit fullscreen mode

Error: "ConfigArray.normalize is not a function"

Fix: Update @eslint/eslintrc to the latest version:

npm install @eslint/eslintrc@latest
Enter fullscreen mode Exit fullscreen mode

Error: "Cannot find config" in monorepo

Fix: ESLint 10 looks from the file's directory, not CWD. Either:

  1. Add eslint.config.js to each package, or
  2. Run ESLint from the root with explicit file patterns

Warning: New violations from JSX tracking

Fix: These are likely real issues that v9 missed. Review each one. If it's a false positive, disable the rule for that line.

The Migration Checklist

Use this checklist for your migration:

  • [ ] Node.js is v20.19.0+ or v22.x
  • [ ] ESLint updated to v10
  • [ ] All plugins updated to v10-compatible versions
  • [ ] Config migrated to eslint.config.js
  • [ ] All /* eslint-env */ comments removed
  • [ ] .eslintrc.* files deleted
  • [ ] .eslintignore deleted (use ignores in config)
  • [ ] CI/CD updated to use Node.js 20+
  • [ ] VS Code settings updated
  • [ ] Full lint run passes

Should You Migrate Now?

Yes. Here's why:

  1. ESLint 9 is in maintenance mode—no new features, only critical fixes
  2. Plugin ecosystem is moving—popular plugins are dropping v9 support
  3. The new lookup algorithm is powerful—especially for monorepos
  4. JSX tracking fixes real bugs—your existing code might have issues you didn't know about

The migration is mostly mechanical. If you already moved to flat config in v9, you're 90% done. If you're still on .eslintrc, now is the time—there's no more runway.

Conclusion

ESLint 10 is a clean break from the past. The legacy configuration system is gone for good. But what you get in return is:

  • Simpler mental model: One file, one format, no magic
  • Better monorepo support: Per-package configs that just work
  • More accurate linting: JSX tracking catches real issues
  • Future-proof: You're on the path the ecosystem is taking

The migration might take an hour or two, but you'll be set for the next several years of ESLint releases.

One less piece of tech debt. Now go update those configs! 🚀


🛠️ Developer Toolkit: This post first appeared on the Pockit Blog.

Need a Regex Tester, JWT Decoder, or Image Converter? Use them on Pockit.tools or install the Extension to avoid switching tabs. No signup required.

Top comments (0)