Skip to content

Dev Tools

ESLint 10 Removed Legacy .eslintrc Config Support: The 10-Minute Migration to Flat Config

Flat config only — faster lints, saner extends, no more eslintrc archaeology

🧹📏🪦

Spoiler: .eslintrc.json is dead. ESLint 10 dropped this week and the legacy config system finally went into the dirt where it belongs. Do not @ me — you've been getting deprecation warnings for two years.

What ESLint 10 Actually Removed

Legacy config support is gone. Not deprecated, not hidden behind a warning — removed. ESLint 10 no longer reads .eslintrc.js, .eslintrc.cjs, .eslintrc.json, .eslintrc.yml, or the eslintConfig key in package.json. The .eslintignore file is out too. If your repo has no eslint.config.js, ESLint has nothing to run, and it will tell you so the moment your CI runner picks up the new major.

Flat config is now the only config. There is no legacy fallback mode to switch back on, so "we'll migrate later" quietly stopped being an option this week.

Why Flat Config Is Better (Not Just Different)

This isn't churn for churn's sake. A flat config is a real JS module — you import your plugins, compose an array, and export it. What you see in the file is exactly what lints.

The benefits in practice: ordered rule overrides (later entries in the array win, top to bottom), no more mystical extends cascade to reverse-engineer, and roughly 30% faster lints on a Tailwind 4 codebase because the resolver isn't crawling node_modules for shareable configs. The eslintrc archaeology sessions — five extends deep, wondering which config turned a rule off — are over.

How to Migrate to Flat Config, Step by Step

The whole migration, compressed:

{`bun add -d eslint@10 @eslint/js typescript-eslint
# delete .eslintrc.* and create eslint.config.js
bun x eslint .`}

1. Upgrade and install the pieces

Bump ESLint to 10 and pull in @eslint/js for the core recommended rules plus typescript-eslint if you're on TS. Framework plugins (Astro, React, whatever you run) need their flat-config-ready versions — check for a configs.recommended export.

2. Delete every legacy config file

Remove .eslintrc.* in every package, delete .eslintignore, and strip the eslintConfig key from package.json if you have one. ESLint 10 ignores all of them anyway — leaving them around just confuses the next person who greps the repo.

3. Create eslint.config.js

Behold a flat config that covers a TypeScript + Astro project — this is the whole file:

{`// eslint.config.js
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import astro from 'eslint-plugin-astro';

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  ...astro.configs.recommended,
  {
    rules: {
      '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
      'no-console': ['warn', { allow: ['warn', 'error'] }],
    },
  },
  { ignores: ['dist/**', '.astro/**'] },
];`}

4. Port your ignores

Each line of your old .eslintignore becomes a glob in an ignores entry. In the config above that's the final array item — dist/** and .astro/**. A config object that contains only ignores applies globally, which is the flat-config replacement for the entire .eslintignore file.

5. Run it and fix what screams

Run the lint across the repo. Expect a handful of rules to fire differently once the extends cascade stops silently reordering them — that's the ordered array doing its job. Fix the code, or move your override lower in the array so it wins.

Translating Old .eslintrc Patterns

extends → import and spread

The extends string array was the worst part of eslintrc — a resolver crawling node_modules for shareable configs and merging them in an order you could only guess at. In flat config you import the config object and spread it into your array. js.configs.recommended, tseslint.configs.recommended, astro.configs.recommended — the file above uses all three, and you can see exactly where each sits because the cascade is just array order.

overrides → config objects with files globs

The overrides block becomes a plain config object with a files glob. Scoped rules for tests, for generated code, for scripts — each one is just another array entry:

{`import globals from 'globals';

export default [
  // ...base configs from above
  {
    files: ['**/*.test.ts'],
    languageOptions: { globals: globals.node },
    rules: { 'no-console': 'off' },
  },
];`}

That one shape — files glob plus scoped rules — replaces the entire overrides system. And because it's ordinary JavaScript, you can generate entries, share them from a workspace package, or compute them per environment.

env → languageOptions.globals

The env: { node: true, browser: true } shorthand is gone. Install the globals package and assign the environments you need under languageOptions.globals, as in the snippet above. One line more typing, infinitely less magic — you can finally see exactly which globals your linter believes in.

parser and parserOptions → languageOptions

Custom parsers move to languageOptions.parser, parser options to languageOptions.parserOptions. Plugins are imported and referenced directly instead of resolved by string name — which means a typo now fails at import time instead of halfway through a lint run.

Common Migration Errors (and What They Mean)

Config not found: ESLint can't see an eslint.config.js at the project root. Usually the file landed in a subpackage while the lint runs from the repo root — move it, or run ESLint per package.

Plugin resolution failures: a leftover string plugin reference from eslintrc land that never became an import. Find it, import the plugin module, reference it directly.

Rules suddenly firing that never fired before: not a bug. The old cascade was masking them; the ordered array stopped covering for you. Decide whether the rule is right, then fix the code or the config — deliberately this time.

The Catch: Monorepos, PnP, and Patched Configs

The official migration script is still buggy on weird monorepo setups with shared extends. Anything with a Yarn PnP loader or a vendored @rushstack/eslint-patch will scream. Expect to hand-port one or two configs.

For everyone else it's genuinely painless — most Next.js, Astro, and Vite repos take ten minutes, and the config file above is most of the work.

The Verdict

Bite the bullet and migrate. Cursor and Claude Code both handle flat config noticeably better — they can actually read the rules array instead of guessing at a JSON tree. Faster CI, simpler config, less mental overhead.

ESLint gave everyone years of warning shots before pulling the trigger. Ten minutes of migration buys you faster lints and a config you can read top to bottom. About time.

Let us make some quick suggestions?

Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.