| | |

ESLint 22 🛡️ The Migration Guide — .eslintrc to flat config

⏱️ Estimated read: 13 min | |

The migration from .eslintrc to flat config is not a configuration rename. It is a change in the configuration model. The legacy system is JSON-first, string-based, and cascading: extends names a shareable config, env declares a runtime’s globals, plugins lists package names, and overrides scopes rules to file patterns. The flat config system is JavaScript-first, import-based, and array-based: configs are imported as modules, globals are imported from the globals package, plugins are objects rather than strings, and every config object is scoped by its own files pattern . The migration is the translation of one model into the other, and the translation has specific failure modes that produce a config that loads but does not lint correctly.

The flat config system became the default in ESLint v9, and ESLint v10 no longer reads .eslintrc files at all . A project that upgrades to v10 without migrating its config will fail to lint, and the error will point to the missing config rather than the version change. The migration is mandatory for any project that intends to stay on a supported ESLint version, and the tooling—the @eslint/eslintrc package’s FlatCompat utility and the @eslint/js package’s recommended configs—exists to make the translation mechanical for the common cases and possible for the legacy cases.

Key point: The migration replaces .eslintrc.* with eslint.config.js (or .mjs/.cjs). The extends string becomes an imported config object. The env and globals properties become languageOptions.globals with imported globals. The plugins string array becomes a plugins object with imported plugin modules. The overrides array becomes top-level config objects with files patterns. The .eslintignore file becomes a global ignores entry.


Why the migration is a model change, not a syntax change

The import-based loading problem. The legacy system loads shareable configs and plugins by string name. The string "eslint:recommended" is a built-in config, and "eslint-config-my-config" is an npm package. The flat config system has no string resolution. The config must import the module directly: import js from "@eslint/js" for the recommended config, and import myConfig from "eslint-config-my-config" for the shareable config . The import-based loading is explicit, and it makes the dependency graph visible in the file.

The plugin object problem. The legacy plugins: ["jsdoc"] string is resolved by ESLint to the eslint-plugin-jsdoc package. In flat config, the plugin must be imported and assigned to a key in the plugins object: plugins: { jsdoc: jsdocPlugin }. The key is the namespace that the rules use, and the value is the plugin module itself . The plugin object must also have a meta.name property, because flat config no longer has access to the package name that the plugin was installed under .

The cascading config problem. The legacy system cascades: an .eslintrc file in a parent directory applies to the child directories, and the child .eslintrc files override the parent. The flat config system does not cascade. A single eslint.config.js at the root of the project applies to all files, and the file-pattern scoping is done with files arrays in the config objects . The migration replaces the cascade with explicit file patterns, which is more verbose but also more predictable.

The trade-off. The flat config system is more explicit, which means the config file is longer and the imports are visible. The explicitness is the benefit: a reader can see exactly which plugin provides which rules, and the config does not depend on the file system’s directory structure. The cost is the verbosity and the manual translation of every extends and env property.


a. The translation of the core properties

The migration guide maps each legacy property to its flat config equivalent. The mapping is mechanical for the common cases and requires FlatCompat for the legacy shareable configs that have not been updated.

// .eslintrc.js — the legacy configuration
module.exports = {
  extends: ["eslint:recommended", "eslint-config-my-config"],
  env: { browser: true, node: true },
  globals: { myGlobal: "readonly" },
  parserOptions: { ecmaVersion: 2022, sourceType: "module" },
  plugins: ["jsdoc"],
  rules: { semi: ["warn", "always"] },
  overrides: [
    { files: ["*.test.js"], env: { mocha: true } },
  ],
  ignorePatterns: ["temp.js", "config/*"],
};
// eslint.config.js — the flat config translation
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
import globals from "globals";
import jsdoc from "eslint-plugin-jsdoc";
import myConfig from "eslint-config-my-config";

export default defineConfig([
  // The recommended config is imported, not a string.
  js.configs.recommended,
  // The shareable config is imported, not extended by name.
  myConfig,
  {
    // The language options replace env and parserOptions.
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: "module",
      globals: {
        ...globals.browser,
        ...globals.node,
        myGlobal: "readonly",
      },
    },
    // The plugin is an object, not a string.
    plugins: { jsdoc },
    rules: {
      semi: ["warn", "always"],
    },
  },
  // The override becomes a config object with a files pattern.
  {
    files: ["**/*.test.js"],
    languageOptions: {
      globals: { ...globals.mocha },
    },
  },
  // The ignore patterns become a global ignores entry.
  {
    ignores: ["**/temp.js", "config/*"],
  },
]);

The translation has three details that are easy to miss. The globals package must be installed (npm install -D globals), and the spread operator combines multiple runtime global sets. The files pattern in the override must be a full glob (**/*.test.js), not a bare filename, because the flat config matches against the full path . The ignores entry must be in an object with no other properties, or the patterns are scoped rather than global .


b. The FlatCompat bridge for legacy shareable configs

A shareable config that has not been updated to flat config format can be used through the FlatCompat utility from @eslint/eslintrc. The utility translates the legacy format into flat config format at runtime.

// eslint.config.js — using FlatCompat
import { FlatCompat } from "@eslint/eslintrc";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const compat = new FlatCompat({
  baseDirectory: __dirname,
});

export default [
  // The legacy config is translated by FlatCompat.
  ...compat.extends("eslint-config-my-config"),
  // The flat config objects follow.
  {
    rules: { semi: ["warn", "always"] },
  },
];

The FlatCompat instance is created with a baseDirectory, which is the directory that the legacy config’s relative paths are resolved against. The compat.extends() method returns an array of flat config objects, and the spread operator inserts them into the exported array . The utility is the bridge for the migration period, not a permanent solution. The recommended path is to replace the FlatCompat entry with the flat config export from the package once the package provides one.

The FlatCompat utility is also the mechanism for a legacy config that the project owns. A local .eslintrc.js file can be translated with compat.config() rather than compat.extends(). The config() method reads the legacy config object and returns the flat config array.


Complete Example Session

// ============================================
// PART 1: THE LEGACY .eslintrc
// ============================================

// .eslintrc.js
module.exports = {
  root: true,
  extends: ["eslint:recommended", "prettier"],
  env: { browser: true, node: true },
  parserOptions: { ecmaVersion: 2022, sourceType: "module" },
  rules: {
    "no-unused-vars": "error",
    eqeqeq: "error",
  },
  overrides: [
    { files: ["*.test.js"], env: { mocha: true } },
  ],
  ignorePatterns: ["dist/", "node_modules/"],
};

// ============================================
// PART 2: THE FLAT CONFIG
// ============================================

// eslint.config.mjs
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
import globals from "globals";
import eslintConfigPrettier from "eslint-config-prettier/flat";

export default defineConfig([
  // The recommended config is imported.
  js.configs.recommended,
  // The language options replace env and parserOptions.
  {
    languageOptions: {
      ecmaVersion: 2022,
      sourceType: "module",
      globals: {
        ...globals.browser,
        ...globals.node,
      },
    },
    rules: {
      "no-unused-vars": "error",
      eqeqeq: "error",
    },
  },
  // The override becomes a config object with a files pattern.
  {
    files: ["**/*.test.js"],
    languageOptions: {
      globals: { ...globals.mocha },
    },
  },
  // The ignore patterns become a global ignores entry.
  {
    ignores: ["dist/", "node_modules/"],
  },
  // eslint-config-prettier is last.
  eslintConfigPrettier,
]);

// ============================================
// PART 3: THE FLAT COMPAT BRIDGE
// ============================================

// If a shareable config is not yet in flat format:
import { FlatCompat } from "@eslint/eslintrc";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({ baseDirectory: __dirname });

export default defineConfig([
  ...compat.extends("eslint-config-legacy-config"),
  // ... the rest of the flat config
]);

// ============================================
// PART 4: THE ESLINT-ENV REMOVAL
// ============================================

// ❌ The legacy eslint-env comment is not recognized.
// /* eslint-env mocha */

// ✅ Replace it with a global comment.
// /* global describe, it -- Globals defined by Mocha */

// ✅ Or define the globals in the config.
{
  files: ["tests/**"],
  languageOptions: { globals: { ...globals.mocha } },
}

// ============================================
// PART 5: THE ESLINTIGNORE REMOVAL
// ============================================

// .eslintignore
// temp.js
// config/*

// The flat config equivalent:
{
  ignores: ["**/temp.js", "config/*"],
}

// The pattern must be a full glob.
// "temp.js" in flat config only ignores the file
// in the same directory as the config.

// ============================================
// PART 6: THE PROCESSOR MIGRATION
// ============================================

// The legacy processor is referenced by string:
// processor: "someplugin/someProcessor"

// The flat config processor is referenced from the plugin object:
import somePlugin from "eslint-plugin-someplugin";
{
  processor: somePlugin.processors.someProcessor,
}

// The file-extension-named processors are not automatic.
// A ".md" processor must be renamed and explicitly configured.

// ============================================
// PART 7: THE PARSER MIGRATION
// ============================================

// The legacy parser is referenced by string:
// parser: "@babel/eslint-parser"

// The flat config parser is imported and assigned:
import babelParser from "@babel/eslint-parser";
{
  languageOptions: { parser: babelParser },
}

// ============================================
// PART 8: THE TYPESCRIPT CONFIG
// ============================================

// The typescript-eslint package provides a flat config.
import tseslint from "typescript-eslint";
export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
  // ...
);

// The tseslint.config() helper is the equivalent of defineConfig()
// for the typescript-eslint configs.

// ============================================
// PART 9: THE VERIFICATION
// ============================================

// npx eslint . --print-config src/index.js
//
// The command prints the resolved config for the file.
// It is the diagnostic for a config that loads but does not lint.
//
// The --print-config output shows the rules, the globals,
// and the plugins that apply to the file.

// ============================================
// PART 10: THE CHECKLIST
// ============================================

// [ ] The .eslintrc file is replaced with eslint.config.js/.mjs.
// [ ] The extends strings are imported as modules.
// [ ] The env properties are replaced with languageOptions.globals.
// [ ] The parserOptions are moved to languageOptions.
// [ ] The plugins strings are replaced with plugin objects.
// [ ] The overrides are replaced with files-scoped config objects.
// [ ] The .eslintignore patterns are moved to ignores.
// [ ] The eslint-env comments are removed or replaced.
// [ ] The FlatCompat is used for the legacy shareable configs.
// [ ] The config is verified with --print-config.

The ten parts covered the legacy config, the flat config, the FlatCompat bridge, the eslint-env removal, the .eslintignore removal, the processor migration, the parser migration, the TypeScript config, the verification, and the checklist. The through-line is that the migration is a translation of a string-based cascading model into an import-based array model.


Quick Reference

The property mapping

LegacyFlat config
extends: ["eslint:recommended"]import js from "@eslint/js"; js.configs.recommended
extends: ["my-config"]import myConfig from "my-config"; myConfig
env: { browser: true }languageOptions.globals: { ...globals.browser }
globals: { myGlobal: "readonly" }languageOptions.globals: { myGlobal: "readonly" }
parserOptions: { ecmaVersion: 2022 }languageOptions: { ecmaVersion: 2022 }
parser: "@babel/eslint-parser"import parser from "@babel/eslint-parser"; languageOptions.parser
plugins: ["jsdoc"]import jsdoc from "eslint-plugin-jsdoc"; plugins: { jsdoc }
overrides: [{ files: ["*.test.js"] }]{ files: ["**/*.test.js"] }
ignorePatterns: ["temp.js"]{ ignores: ["**/temp.js"] }
processor: "plugin/processor"processor: plugin.processors.processor

The FlatCompat usage

StepCode
Installnpm install -D @eslint/eslintrc
Importimport { FlatCompat } from "@eslint/eslintrc"
Createconst compat = new FlatCompat({ baseDirectory })
Extend...compat.extends("legacy-config")

The ignore file migration

LegacyFlat config
.eslintignore file{ ignores: [...] } object
temp.js**/temp.js (full glob)
config/*config/*

Best Practices

✅ Do This:

// Import the recommended config
import js from "@eslint/js"; js.configs.recommended                  // ✅

// Import the globals
import globals from "globals"; globals: { ...globals.browser }       // ✅

// Use a full glob for the ignores
{ ignores: ["**/temp.js"] }                                          // ✅

// Use FlatCompat for the legacy shareable configs
...compat.extends("legacy-config")                                   // ✅

❌ Don’t Do This:

// Use the string name for the recommended config
extends: ["eslint:recommended"] // not valid in flat config          // ❌

// Use the env property
env: { browser: true } // not valid in flat config                   // ❌

// Use the string plugin name
plugins: ["jsdoc"] // not valid in flat config                       // ❌

// Use a bare filename in the ignores
{ ignores: ["temp.js"] } // only ignores the file in the config dir  // ❌

Common Pitfalls

PitfallWhy It HappensFix
The config loads but rules do not applyThe plugin is imported but not in the plugins objectAssign the plugin to a key
The parser fails on TypeScriptThe parser is not importedImport and assign languageOptions.parser
The ignore pattern does not matchA bare filename was usedUse a full glob (**/temp.js)
The shareable config does not workIt is a legacy configUse FlatCompat
The eslint-env comment is ignoredFlat config does not recognize itReplace with a global comment or config globals

Real-World Examples

1. Recommended config

import js from "@eslint/js"; js.configs.recommended

2. Shareable config

import myConfig from "eslint-config-my-config"; myConfig

3. Globals

import globals from "globals"; languageOptions: { globals: { ...globals.browser } }

4. Parser

import babelParser from "@babel/eslint-parser"; languageOptions: { parser: babelParser }

5. Plugin

import jsdoc from "eslint-plugin-jsdoc"; plugins: { jsdoc }

6. Override

{ files: ["**/*.test.js"], languageOptions: { globals: { ...globals.mocha } } }

7. Ignore

{ ignores: ["**/temp.js", "config/*"] }

8. FlatCompat

...compat.extends("legacy-config")

9. Parser migration

import babelParser from "@babel/eslint-parser";

10. Verification

npx eslint . --print-config src/index.js

Visual

The Legacy vs Flat Config Model

┌──────────────────────────────────────────────────────────────┐
│  LEGACY (.eslintrc)          FLAT (eslint.config.js)          │
│                                                              │
│  String-based                Import-based                    │
│  ┌─────────────────────┐    ┌─────────────────────┐         │
│  │ extends: ["..."]    │    │ import js from      │         │
│  │ env: { browser }    │    │ "@eslint/js"        │         │
│  │ plugins: ["jsdoc"]  │    │                     │         │
│  └─────────────────────┘    │ globals: { ... }    │         │
│                              │ plugins: { jsdoc }  │         │
│  Cascading                   └─────────────────────┘         │
│  (parent directory)                                          │
│                              Array-based                     │
│                              (files patterns)                │
└──────────────────────────────────────────────────────────────┘

The Translation of extends

┌──────────────────────────────────────────────────────────────┐
│  extends: ["eslint:recommended", "my-config"]                 │
│         │                                                    │
│         ▼                                                    │
│  import js from "@eslint/js";                                │
│  import myConfig from "my-config";                           │
│                                                              │
│  export default [                                            │
│    js.configs.recommended,                                   │
│    myConfig,                                                 │
│  ];                                                          │
└──────────────────────────────────────────────────────────────┘

The Translation of env

┌──────────────────────────────────────────────────────────────┐
│  env: { browser: true, node: true }                           │
│         │                                                    │
│         ▼                                                    │
│  import globals from "globals";                              │
│                                                              │
│  languageOptions: {                                          │
│    globals: {                                                │
│      ...globals.browser,                                     │
│      ...globals.node,                                        │
│    },                                                        │
│  }                                                           │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
Flat config fileeslint.config.js / .mjs / .cjs
Recommended configimport js from "@eslint/js"; js.configs.recommended
Globalsimport globals from "globals"
Pluginsimport plugin from "eslint-plugin-*"; plugins: { key: plugin }
Parserimport parser from "parser-package"; languageOptions.parser
Overrides{ files: ["**/*.test.js"] }
Ignores{ ignores: ["**/temp.js"] }
Legacy bridgeFlatCompat from @eslint/eslintrc
Verificationeslint --print-config

Key takeaways:

  • The migration is a model change, not a syntax change. The legacy system is string-based and cascading. The flat config system is import-based and array-based. The extends string becomes an imported config object, and the env property becomes languageOptions.globals with imported globals .
  • The plugins array becomes a plugins object. The string "jsdoc" becomes import jsdoc from "eslint-plugin-jsdoc" and plugins: { jsdoc }. The plugin must also have a meta.name property, because flat config no longer has access to the package name .
  • The overrides array becomes file-scoped config objects. Each override becomes a top-level config object with a files pattern. The pattern must be a full glob (**/*.test.js), not a bare filename, because flat config matches against the full path .
  • The .eslintignore file is removed. The ignore patterns move into a { ignores: [...] } object. The patterns must be full globs (**/temp.js), and the object must have no other properties for the patterns to be global .
  • The FlatCompat utility is the bridge for the legacy shareable configs. It translates the legacy format into flat config format at runtime. It is the migration-period tool, not a permanent solution .

Remember: The migration from .eslintrc to flat config is a translation of a string-based cascading model into an import-based array model. The extends string becomes an import, the env property becomes languageOptions.globals, the plugins array becomes a plugin object, and the overrides array becomes file-scoped config objects. The .eslintignore file becomes an ignores object, and the eslint-env comments must be removed. The FlatCompat utility is the bridge for the legacy shareable configs. The migration is mandatory for ESLint v10, and the --print-config flag is the diagnostic for a config that loads but does not lint correctly.

Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!