How I Collected Bumps During the Migration to ESLint 9
As a software engineer, you must maintain and keep your infrastructure up-to-date.
I don’t consider myself a programmer but a QA engineer, so I use some part of the JavaScript tooling as a helpful feature for my primary job: autotests. Tools like ESLint and Prettier help write clean code — they are good and easy to set up with most default settings. Maximum profit without having to dive deep into the details. However, when the time came to migrate from ESLint v8 to ESLint v9, it forced me to figure out how everything works there.
First of all, ESLint is a tool used to analyze and ensure code quality in JavaScript (and TypeScript). The ESLint v9.0.0 was released in April 2024, and support for the v8.x version ends on the 5th of October 2024. Half a year for migration sounds like enough time, but the ninth version completely breaks backward compatibility with a new «flat config» configuration system. The need to completely rewrite the config file became the reason for postponing migration at the very last moment. It is so painful for developers that ESLint even introduced special tools to make the process more accessible: Configuration Migrator and Config Inspector.
Before even touching configs on any production repository at work, I decided to experiment with my own boilerplate testing repository.
ESLint v8’s .eslintrc
config file was quite simple:
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["tsconfig.json"]
},
"plugins": ["@typescript-eslint", "simple-import-sort"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"rules": {
"@typescript-eslint/no-floating-promises": ["error"],
"comma-dangle": ["error", "always-multiline"],
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error"
}
}
And the whole «ESLint infrastructure» required only four packages:
eslint
eslint-plugin-simple-import-sort
@typescript-eslint/eslint-plugin
@typescript-eslint/parser
My initial idea was just to take the documentation and rewrite a new configuration file from scratch. It looked pretty straightforward: create eslint.config.mjs
instead of .eslintrc
and copy-paste the existing rules, but it turned out to be not so simple.
Because for each config’s key, you have to check pages of documentation to match the new format (new way of extending by «recommended» configs, new way of plugins connection, and so on), I immediately decided to try Configuration Migrator.
After executing the migration script:
npx @eslint/migrate-config .eslintrc
I got a new config, twice as long as the previous one:
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import tsParser from '@typescript-eslint/parser';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import js from '@eslint/js';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
...compat.extends(
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
),
{
plugins: {
'@typescript-eslint': typescriptEslint,
'simple-import-sort': simpleImportSort,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 5,
sourceType: 'script',
parserOptions: {
project: ['tsconfig.json'],
},
},
rules: {
'@typescript-eslint/no-floating-promises': ['error'],
'comma-dangle': ['error', 'always-multiline'],
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
},
},
];
To make it work, I also had to install two more packages: @eslint/js
and @eslint/eslintrc
, and add eslint.config.mjs
to tsconfig.json
.
The overhead with export default […compat.extends
and a bunch of constants with file paths did not look trustworthy, especially since anything like this is not presented in the documentation.
The first launch attempt immediately returned an error:
Parsing error: "parserOptions.project" has been provided for @typescript-eslint/parser.
The file was not found in any of the provided project(s): eslint.config.mjs
OK, the migration did not go smoothly. In order to fix it, I decided to stay in the config only lines that look comprehensible and fill ignores
and files
keys to get rid of the .eslintignore
file.
Then, I decided to manually set up typescript-eslint
plugin by their documentation, which also presents a different way of configuration:
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
);
For a new ESLint, I also changed @typescript-eslint/eslint-plugin
package to typescript-eslint
as a new proper way to use this tooling.
Somehow, I combined the config, but it did not work until I put the main config’s object above any «recommended» ones:
export default [
{
ignores: [
'**/node_modules/*',
…
],
files: ['**/*.js', '**/*.ts'],
…
},
eslint.configs.recommended,
...tseslint.configs.recommended,
];
This lore, I dug up the issues.
Then, with Config Inspector, I found that my only rule comma-dangle
is deprecated, and I need to use a corresponding rule from the additional package: @stylistic/eslint-plugin
(thank goodness that ESLint Stylistic’s documentation is excellent).
The only thing I did not get was why the migrator chose the language option as ecmaVersion: 5
, while in tsconfig.json
I had es2020
. To make everything smooth, I switched ecmaVersion
to the default value = latest
and updated tsconfig.json
— rechecked that ain’t broken, and pleased that my small repository allowed me to make such a fundamental change.
At least ESLint was working, but when I tried to test how it would catch code errors, I noticed that Prettier started to remove TypeScript’s generic annotations on file save.
Function like this:
async getLang(): Promise<Locator> {
return await this.lang;
}
Turned to this:
async getLang(): Promise {
return await this.lang;
}
That was a complete disaster, and StackOverflow did no help. I assumed that Prettier started to conflict with new ESLint rules and decided to fix it with eslint-plugin-prettier
package, but it was no use.
Reboot VS Code editor fixed the last problem.
Finally, I got the working configuration, working linters, and working project.
ESLint v9’s eslint.config.mjs
config started to look like this:
import eslint from '@eslint/js';
import stylisticTs from '@stylistic/eslint-plugin-ts';
import tsParser from '@typescript-eslint/parser';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import simpleImportSort from 'eslint-plugin-simple-import-sort';
import tseslint from 'typescript-eslint';
export default [
{
ignores: [
'**/node_modules/*',
'**/test-results/*',
'**/playwright-report/*',
],
files: ['**/*.js', '**/*.ts'],
plugins: {
'@stylistic/ts': stylisticTs,
'simple-import-sort': simpleImportSort,
},
languageOptions: {
parser: tsParser,
ecmaVersion: 'latest',
sourceType: 'script',
parserOptions: {
project: './tsconfig.json',
},
},
rules: {
'@stylistic/ts/comma-dangle': ['error', 'always-multiline'],
'@typescript-eslint/no-floating-promises': ['error'],
'simple-import-sort/imports': 'error',
'simple-import-sort/exports': 'error',
},
},
eslint.configs.recommended,
...tseslint.configs.recommended,
eslintPluginPrettierRecommended,
];
Unfortunately, the number of required packages for «ESLint infrastructure» has doubled:
- @eslint/js
- @stylistic/eslint-plugin-ts
- @types/eslint__js
- @typescript-eslint/parser
- eslint
- eslint-config-prettier
- eslint-plugin-prettier
- eslint-plugin-simple-import-sort
- typescript-eslint
But I simplified npm scripts from this:
"lint": "eslint '**/*.{js,ts}'",
"lint:fix": "eslint --fix '**/*.{js,ts}'",
To this:
"lint": "eslint",
"lint:fix": "eslint --fix",
Despite the small initial config, I ran into too many bumps. I have no idea how people with multiple plugins and dozens of rules will do the migration without problems (some of them have already given up), but I hope the benefits of future versions of ESLint will exceed the cost of migration.
Related articles:
- Eslint v9: Migrate from Older Versions;
- Embrace the Future: Navigating the New Flat Configuration of ESLint;
- How does eslint-config-prettier works?
P.S. When trying to update ESLint in production projects, it turned out that some plugins were simply not ready to work with ESLint 9, and it was impossible to switch to a new version without changing the habitual dev environment. That means we will use the eighth version (v8.57) for a long time.