@eslint/v8-to-v9-config
Migrate ESLint v8 to v9 format automatically.
Quick Start
bash
Workflow Params
When running workflow.yaml directly:
Formatting
codeFormattingCommandEnabled(boolean, default:false): Enables/disables the formatting step.codeFormattingCommand(string, default:npx prettier --write "**/eslint.config.mjs" --ignore-path /dev/null --no-config --no-error-on-unmatched-pattern): Command to run when formatting is enabled.
Config discovery
By default the workflow scans the usual ESLint filenames (.eslintrc.{js,mjs,cjs,json,yaml,yml}). Optional params add an extra ast-grep pass for a differently named legacy config whose path ends with your custom fragment:
eslintConfigCustomName(string, default: unset /null): Fragment matched as**/*<value>(for example.eslintrc.local.jsonmatches any file ending in that suffix). Leave unset unless you rely on a non-standard config filename.eslintConfigLanguage(string, default:javascript): ast-grep language for that file — usejavascriptfor.js/.mjs/.cjs,jsonfor.json, oryamlfor.yaml/.yml. Must align with how the fragment is parsed.
Example (formatting + custom config fragment):
bash
Example (custom-named legacy JSON config):
bash
After running, the codemod will display a list of packages that need to be installed. Install them:
bash
Note: If your config uses
extendsor plugins, keep@eslint/eslintrc(FlatCompat) and@eslint/compat(fixupConfigRules/fixupPluginRules).
⚠️ Important: The codemod will display a yellow note reminding you to verify that all packages are not deprecated and still supported for ESLint v9. Please check each package before installing.
Then test your config:
bash
Migration Steps
Step 1: Config File Conversion
Converts .eslintrc.js, .eslintrc.json, .eslintrc.yaml, .eslintrc.yml, optional custom-named configs (via workflow params), and in-repo package.json eslintConfig, to flat config (eslint.config.mjs).
What gets migrated:
envsettings →languageOptions.globals(including per-overrideenv)globals→languageOptions.globalsparserOptions→languageOptions.parserOptionsfiles(root or overrides) →fileson the corresponding flat config objectexcludedFiles(typically on overrides) →ignoreson that same object (patterns that apply alongsidefiles)ignorePatterns(root or per-sector) plus patterns from scanned ignore-list files → merged into a leadingglobalIgnores([...])entry (global ignores shared across configs)overrides→ separate configuration objects in the array (each keeps its ownfiles/ignoreswhere present)linterOptionsfor supported settings:noInlineConfigandreportUnusedDisableDirectivesare collected intolinterOptionson each flat block where they appeared (booleantrue/falseforreportUnusedDisableDirectivesmap to"warn"/"off"; explicit severity strings are preserved)
Step 2: Rule Schema Updates
Updates rules with breaking schema changes in ESLint v9:
| Rule | Migration |
|---|---|
no-unused-vars | Adds caughtErrors: 'none' (v9 changed default to 'all') |
no-useless-computed-key | Adds enforceForClassMembers: false (v9 changed default to true) |
no-sequences | Migrates allowInParentheses to new format |
no-constructor-return | Ensures proper array format |
camelcase | Validates allow option (must be array of strings) |
no-restricted-imports | Restructures paths configuration |
Step 3: JSDoc Rules Migration
The require-jsdoc and valid-jsdoc rules were removed in ESLint v9. This codemod migrates them to eslint-plugin-jsdoc.
After running, install the plugin:
bash
⚠️ Manual step: If you have custom JSDoc settings, look for
// TODO: Migrate settings manuallycomments in your config and update them accordingly.
Step 4: Comment Cleanup
Fixes ESLint comment syntax that became invalid in v9:
- Duplicate
/* eslint */comments: Removes duplicate rule comments for the same rule - Malformed
/* exported */comments: Fixes to proper format
Step 5: Extends & Plugin Migration
All extends and plugins are preserved exactly as they were - no additions or removals.
Extends Migration
The codemod uses FlatCompat from @eslint/eslintrc so legacy extends become fixupConfigRules(<compat>.extends(/* original strings preserved */)) in flat config:
- When
eslint:recommendedappears, aFlatCompatis created withrecommendedConfig: js.configs.recommended - When
eslint:allappears, aFlatCompatis created withallConfig: js.configs.all - When both appear, a combined compat instance wires both presets
- Any other presets reuse a plain
FlatCompat({ baseDirectory: __dirname })or the recommended/all instance above, depending on overlap
All extends string values from the legacy config are passed through unchanged to .extends(...).
Example:
If your original config had:
json
The migrated config will resemble:
javascript
recommendedConfig / allConfig wire the bundled ESLint presets; the same strings usually remain in .extends(...) so shared configs load as before.
Required dependencies:
bash
Plugin Migration
All plugins are preserved exactly as they were - the codemod extracts them from the original config and maintains their exact format, including:
- Plugin names
- Plugin values (imports, require calls, etc.)
- Plugin structure (object or array format)
The codemod automatically adds import statements for plugins when they're detected in the original config.
Plugin Naming Conventions:
The codemod follows ESLint v9 conventions for plugin package names and import identifiers:
- Unscoped packages:
eslint-plugin-foo→ imports asfooPluginfrom"eslint-plugin-foo" - Scoped packages:
@foo/eslint-plugin→ imports asfooPluginfrom"@foo/eslint-plugin" - Scoped packages with suffix:
@foo/eslint-plugin-bar→ imports asfooBarPluginfrom"@foo/eslint-plugin-bar"
The import identifiers are automatically generated to be valid JavaScript identifiers, converting package names to camelCase format.
Step 6: Global ignores (ignorePatterns, ignore-list files)
In flat config, global path ignores are expressed with globalIgnores from @eslint/config-helpers (the codemod emits a leading globalIgnores([...]) object when needed).
Sources merged into globalIgnores:
- Legacy
ignorePatternsfrom the ESLint config (root or applicable blocks), and - Non-comment lines from
.eslintignoreand.gitignorefiles found by the workflow’s ignore scan (same line-based rules; paths are de-duplicated with configignorePatterns).
Per-override excludedFiles are not global: they become ignores on the same flat object as that block’s files (see Step 1).
The workflow renames processed ignore-list files to deleted-eslintignore-backup.txt (relative path) to avoid leaving active ignore files behind; if that fails (permissions, etc.), remove or reconcile them manually.
⚠️ Manual step: If any backup or legacy ignore file remains, delete or merge it after verifying
globalIgnoresineslint.config.mjs. bash
Before (.eslintrc.json):
json
After (eslint.config.mjs):
javascript
Note: Additional
extends(for example"plugin:react/recommended") remain string entries insidefixupConfigRules(compatWithRecommended.extends(/* ... */)).eslint:recommended/eslint:allcontinue to use the dedicatedFlatCompatinstances wired tojs.configs.recommended/js.configs.all.