💻 Coding

ESLint v9 Flat Config Migration: .eslintrc and .eslintignore to eslint.config.js with typescript-eslint, React Plugins, and FlatCompat Fallbacks

Move a real project from legacy .eslintrc and .eslintignore files to an ESLint v9 eslint.config.js: overrides become file scoped objects, env becomes languageOptions.globals, plugins move to flat configs, typescript-eslint uses the project service, legacy shareable configs go through FlatCompat, and you get a package.json diff, CI command changes, and a parity check so no rule silently disappears.

0.0
0Reviews
P
October 5, 2026

Prompt

Act as a JavaScript tooling engineer who has migrated monorepos and single apps from ESLint 8 eslintrc files to ESLint 9 flat config and knows which plugins ship flat configs and which still need the compatibility layer.

Inputs:
- Current .eslintrc file contents (json, js, or yaml) including overrides: [LegacyConfig]
- Current .eslintignore contents and any ignorePatterns: [IgnoreFile]
- package.json devDependencies with versions for eslint and every plugin or shared config: [LintDeps]
- Module type (package.json type field) and TypeScript tsconfig layout: [ModuleSetup]
- Lint scripts and CI commands that call eslint today: [LintScripts]
- Inline eslint-env or eslint comments used in source files, if any: [InlineDirectives]
- Output format: [Format]

Generate:
1. Translation table: every key in LegacyConfig (root, env, parser, parserOptions, plugins, extends, settings, rules, overrides, ignorePatterns) with its flat config equivalent, for example env to languageOptions.globals using the globals package, overrides to separate objects with files and ignores, parser to languageOptions.parser.
2. Plugin readiness list from LintDeps: which packages export flat configs at the listed versions, which need a version bump, and which must be wrapped with FlatCompat from @eslint/eslintrc. Mark anything you are not sure of as VERIFY with the command to check.
3. A complete eslint.config.js (or eslint.config.mjs if ModuleSetup is CommonJS) in the right order: global ignores object first, js.configs.recommended, typescript-eslint configs, React and hooks, FlatCompat entries, file scoped overrides, then eslint-config-prettier last if Prettier is used. Use projectService for type aware rules when typescript-eslint is v8.
4. IgnoreFile converted into the ignores array with directory patterns ending in a slash, plus a note that ESLint 9 no longer reads .eslintignore.
5. package.json diff: packages to add, bump, and remove, and LintScripts rewritten without the removed --ext flag.
6. InlineDirectives fixes: replace eslint-env comments with file scoped globals and list the files to edit.
7. Parity check: run npx eslint --print-config on two representative files before and after, diff the rules, and list rules that changed severity or vanished, with the reason.

Constraints:
- Do not drop a rule silently. Every rule in LegacyConfig appears in the new file or in a REMOVED list with a reason.
- Do not invent plugin export names. Unknown exports are VERIFY items.
- No em dashes.

Instructions

Replace every [bracket] with your details before running. Works on ChatGPT, Claude, and Gemini.

Generated Output

This image was generated using the prompt above.

ESLint v9 Flat Config Migration: .eslintrc and .eslintignore to eslint.config.js with typescript-eslint, React Plugins, and FlatCompat Fallbacks - Result

Examples

Example Input

LegacyConfig: .eslintrc.json with root true, env browser and es2022, parser @typescript-eslint/parser, parserOptions.project ./tsconfig.json, extends eslint:recommended, plugin:@typescript-eslint/recommended, plugin:react/recommended, plugin:react-hooks/recommended, @acme/eslint-config-legacy, prettier; rules: no-console warn, @typescript-eslint/no-floating-promises error, react/prop-types off; overrides for scripts/**/*.js with env node
IgnoreFile: dist, coverage, src/generated
LintDeps: eslint 8.57.0, @typescript-eslint/* 6.21.0, eslint-plugin-react 7.33.2, eslint-plugin-react-hooks 4.6.0, eslint-config-prettier 9.1.0, @acme/eslint-config-legacy 2.4.0 (internal, eslintrc only)
ModuleSetup: type module; one tsconfig.json at the root
LintScripts: eslint . --ext .ts,.tsx,.js
InlineDirectives: /* eslint-env jest */ in 14 test files

Example Output

1. Translation table
- root: true -> delete (flat config is always rooted at eslint.config.js)
- env browser, es2022 -> languageOptions.globals: globals.browser (ES2022 syntax is the default ecmaVersion latest)
- parser + parserOptions.project -> typescript-eslint configs + parserOptions.projectService
- extends eslint:recommended -> js.configs.recommended
- extends prettier -> eslint-config-prettier object placed last
- overrides scripts/**/*.js env node -> object with files and globals.node

2. Plugin readiness
- eslint 8.57.0: bump to ^9
- @typescript-eslint/* 6.21.0: replace with typescript-eslint ^8 (one package, flat configs, projectService)
- eslint-plugin-react 7.33.2: bump to ^7.37; exposes configs.flat.recommended and configs.flat['jsx-runtime']
- eslint-plugin-react-hooks 4.6.0: bump to ^5; register plugin and spread its recommended rules (VERIFY flat preset name with npm view eslint-plugin-react-hooks)
- @acme/eslint-config-legacy: wrap with FlatCompat until the platform team ships a flat export

3. eslint.config.js
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import prettier from "eslint-config-prettier";
import globals from "globals";
import { FlatCompat } from "@eslint/eslintrc";
import path from "node:path";
import { fileURLToPath } from "node:url";

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

export default tseslint.config(
  { ignores: ["dist/", "coverage/", "src/generated/"] },
  js.configs.recommended,
  ...tseslint.configs.recommendedTypeChecked,
  react.configs.flat.recommended,
  react.configs.flat["jsx-runtime"],
  { plugins: { "react-hooks": reactHooks }, rules: reactHooks.configs.recommended.rules },
  ...compat.extends("@acme/eslint-config-legacy"),
  {
    languageOptions: {
      globals: { ...globals.browser },
      parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
    },
    settings: { react: { version: "detect" } },
    rules: {
      "no-console": "warn",
      "@typescript-eslint/no-floating-promises": "error",
      "react/prop-types": "off",
    },
  },
  { files: ["**/*.js"], ...tseslint.configs.disableTypeChecked },
  { files: ["scripts/**/*.js"], languageOptions: { globals: { ...globals.node } } },
  { files: ["**/*.test.{ts,tsx}"], languageOptions: { globals: { ...globals.jest } } },
  prettier,
);

4. Ignores: .eslintignore is not read by ESLint 9. Delete it after the ignores object above is merged.

5. package.json diff
+ "eslint": "^9", "@eslint/js": "^9", "typescript-eslint": "^8", "globals": "^15", "@eslint/eslintrc": "^3"
~ eslint-plugin-react ^7.37, eslint-plugin-react-hooks ^5
- @typescript-eslint/parser, @typescript-eslint/eslint-plugin
Script: "lint": "eslint ." (the --ext flag is gone; files patterns decide what is linted)

6. Inline directives: remove /* eslint-env jest */ from the 14 test files; the *.test globals object replaces it.

7. Parity check
npx eslint --print-config src/App.tsx > after.json on the new branch, same on main for before.json, then diff the rules keys.
Expected change: recommendedTypeChecked adds rules such as no-unsafe-assignment. REMOVED: none. If the extra type checked rules are too noisy, swap to recommended and keep no-floating-promises explicitly.

Reviews (0)

Please login to leave a review.
Loading reviews...