How to migrate Eslint configuration to flat config format

In the last iterations, ESLint has changed the default format for their configuration file. They have also deprecated the stylistic rules, and moved to a new, independent, project.
Hence, to get more value from the latest improvements, a upgrade is needed. But, as today, typescript-eslint doesn’t support the newest ESLint 9.
This means we can upgrade the file format to “flat”, but we need to pick the latest supported version, 8.57.

To migrate a “legacy” ESLint config file, you can use a dedicated tool: @eslint/migrate-config

Here some notes about the process and my findings. I hope it can help someone else to save some time. Let me a comment if you found this useful 👍

How to enable Eslint in the config file “eslint.config.mjs” to get the @stylistic/migrate rules working

To enable Eslint in the file “eslint.config.mjs” we have different options. ESLint is needed to help us to get the @stylistic/migrate rules working and help us to detect which rules could be migrated to @stylistic.
I chose `lint with type-aware linting` from this guide:
https://typescript-eslint.io/troubleshooting/typed-linting/#i-get-errors-telling-me-eslint-was-configured-to-run–however-that-tsconfig-does-not–none-of-those-tsconfigs-include-this-file

ESLint Stylistic migration guide

typescript-eslint and ESLint 9

When `typescript-eslint` will support ESLint 9, we can migrate from “legacy” (@typescript-eslint/parser and @typescript-eslint/eslint-plugin)
https://typescript-eslint.io/getting-started/legacy-eslint-setup/

to “flat” config format, that requires `typescript-eslint`:

typescript-eslint` doesn’t support ESLint 9 yet

`typescript-eslint` doesn’t support ESLint 9 yet (only alpha via `typescript-eslint@8.0.0-alpha.10`)

and we need to stick to ESLint versions >= 8.57 < 9.0, and `typescript-eslint` 7.17.0 or newer.

Migrate a “legacy” ESLint config file with @eslint/migrate-config

This `flat config file` (new format) has been generated from `.eslintrc.json` (deprecated format) using the ESLint Configuration Migrator tool
> npx @eslint/migrate-config .eslintrc.json

A complete example before the migration, using .eslintrc.json in a Svelte + Typescript project, with custom rules configured.


// .eslintrc.json
{
    "ignorePatterns": [
        "**/*.js"
    ],
    "parser": "@typescript-eslint/parser",
    "parserOptions": {
        "project": [
            "**/tsconfig.json"
        ],
        "extraFileExtensions": [
            ".svelte"
        ],
        "sourceType": "module"
    },
    "extends": [
        "eslint:recommended",
        "plugin:@eslint-community/eslint-comments/recommended",
        "plugin:@typescript-eslint/recommended",
        "plugin:@typescript-eslint/recommended-requiring-type-checking",
        "plugin:svelte/recommended"
    ],
    "rules": {
        /* eslint */
        "eol-last": [
            "warn",
            "always"
        ],
        "eqeqeq": [
            "warn",
            "always"
        ],
        "no-multiple-empty-lines": [
            "warn",
            {
                "max": 1,
                "maxBOF": 0,
                "maxEOF": 0
            }
        ],
        "quotes": [
            "warn",
            "single",
            {
                "avoidEscape": true
            }
        ],
        "no-self-compare": "warn",
        "no-template-curly-in-string": "warn",
        "no-unmodified-loop-condition": "warn",
        "no-unused-private-class-members": "warn",
        "default-case": "warn",
        "curly": "warn",
        "default-case-last": "warn",
        "default-param-last": [
            "error"
        ],
        "guard-for-in": "warn",
        "no-console": "warn",
        "no-else-return": "warn",
        "no-eval": "error",
        "no-implied-eval": "error",
        "no-lonely-if": "warn",
        "no-multi-assign": "warn",
        "no-nested-ternary": "warn",
        "no-return-assign": "warn",
        "no-return-await": "warn",
        "no-script-url": "warn",
        "no-unneeded-ternary": "warn",
        "no-useless-return": "warn",
        "no-var": "warn",
        "brace-style": [
            "warn",
            "1tbs"
        ],
        "max-len": [
            "warn",
            {
                "code": 200,
                "ignoreStrings": true,
                "ignorePattern": "^import .*"
            }
        ],
        /* @eslint-community/eslint-comments */
        "@eslint-community/eslint-comments/disable-enable-pair": [
            "error",
            {
                "allowWholeFile": true
            }
        ],
        "@eslint-community/eslint-comments/no-unused-disable": "warn",
        "@eslint-community/eslint-comments/require-description": "error",
        /* @typescript-eslint */
        "@typescript-eslint/no-triple-slash-reference": "off",
        "@typescript-eslint/no-namespace": "off",
        "@typescript-eslint/consistent-type-imports": "error",
        //
        // svelte rules
        // svelte has some limitation in type-aware mode
        //
        "@typescript-eslint/ban-ts-comment": [
            "error",
            {
                "ts-expect-error": true,
                "ts-ignore": "allow-with-description",
                "ts-nocheck": true,
                "ts-check": false
            }
        ],
        "@typescript-eslint/no-unsafe-assignment": "off",
        "@typescript-eslint/no-unsafe-argument": "off",
        "@typescript-eslint/no-unsafe-member-access": "off",
        // Note: you must disable the base rule as it can report incorrect errors
        "no-use-before-define": "off",
        "@typescript-eslint/no-use-before-define": "error",
        "@typescript-eslint/restrict-template-expressions": [
            "error",
            {
                "allowNumber": true,
                "allowBoolean": false,
                "allowAny": false,
                "allowNullish": true,
                "allowRegExp": false
            }
        ],
        "no-unused-vars": "off",
        "@typescript-eslint/no-unused-vars": [
            "warn", // or "error"
            {
                "argsIgnorePattern": "^_",
                "varsIgnorePattern": "^_",
                "caughtErrorsIgnorePattern": "^_"
            }
        ],
        "svelte/no-spaces-around-equal-signs-in-attribute": "warn",
        "svelte/no-unused-svelte-ignore": "off",
        "svelte/require-event-dispatcher-types": "warn",
        "svelte/require-optimized-style-attribute": "warn",
        "svelte/require-stores-init": "warn",
        "svelte/spaced-html-comment": [
            "warn",
            "always" // or "never"
        ],
        "no-trailing-spaces": "off", // Don't need ESLint's no-trailing-spaces rule, so turn it off.
        "semi": "warn",
        "svelte/no-trailing-spaces": [
            "warn",
            {
                "skipBlankLines": false,
                "ignoreComments": false
            }
        ],
        "svelte/valid-compile": "off"
    },
    "plugins": [
        "@typescript-eslint"
    ],
    "overrides": [
        {
            "files": [
                "*.svelte"
            ],
            "parser": "svelte-eslint-parser",
            "parserOptions": {
                "parser": "@typescript-eslint/parser"
            }
        }
    ],
    "globals": {
        // svelte reactive block $: does not recognize global variables, as in $: { console.log('...') }
        "console": "readonly",
        //
        "window": "readonly",
        "setTimeout": "readonly",
        "clearTimeout": "readonly",
        "document": "readonly",
        "location": "readonly"
    }
    /* prefer "globals" over "env" to keep the global variables at minimum
    "env": {
        "browser": true,
        "node": true
    }
    */
}

and the new config file in “flat” format “eslint.config.mjs”


// eslint.config.mjs
/* eslint-disable @typescript-eslint/no-unsafe-call -- not required */
/* eslint-disable @stylistic/quotes -- keep standard config quotes */

// Enable the @stylistic/migrate plugin to hint to migrate built-in rules to @stylistic/js namespace
/* eslint @stylistic/migrate/migrate-js: "error" -- migration hint */

// Enable the @stylistic/migrate plugin to hint to migrate `@typescript-eslint` rules to @stylistic/ts namespace
/* eslint @stylistic/migrate/migrate-ts: "error" -- migration hint */

import path from "node:path";
import { fileURLToPath } from "node:url";
import js from "@eslint/js";
import { FlatCompat } from "@eslint/eslintrc";
import stylistic from "@stylistic/eslint-plugin";
import stylisticMigrate from "@stylistic/eslint-plugin-migrate";
import typescriptEslint from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import svelteParser from "svelte-eslint-parser";

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 [{
    ignores: [
        "node_modules/**",
        "**/*.js"
    ],
}, ...compat.extends(
    "eslint:recommended",
    "plugin:@eslint-community/eslint-comments/recommended",
    "plugin:@typescript-eslint/recommended",
    "plugin:@typescript-eslint/recommended-type-checked",
    "plugin:svelte/recommended",
), {
    plugins: {
        "@stylistic": stylistic,
        "@stylistic/migrate": stylisticMigrate,
        "@typescript-eslint": typescriptEslint,
    },

    languageOptions: {
        globals: {
            console: "readonly",
            window: "readonly",
            setTimeout: "readonly",
            clearTimeout: "readonly",
            document: "readonly",
            location: "readonly",
        },

        parser: tsParser,
        ecmaVersion: 5,
        sourceType: "module",

        parserOptions: {
            project: ["**/tsconfig.json"],
            extraFileExtensions: [".svelte"],
        },
    },

    rules: {
        /* rules: eslint */

        "eqeqeq": ["warn", "always"],
        "no-self-compare": "warn",
        "no-template-curly-in-string": "warn",
        "no-unmodified-loop-condition": "warn",
        "no-unused-private-class-members": "warn",
        "default-case": "warn",
        "curly": "warn",
        "default-case-last": "warn",
        "default-param-last": ["error"],
        "guard-for-in": "warn",
        "no-console": "warn",
        "no-else-return": "warn",
        "no-eval": "error",
        "no-implied-eval": "error",
        "no-lonely-if": "warn",
        "no-multi-assign": "warn",
        "no-nested-ternary": "warn",
        "no-return-assign": "warn",
        "no-return-await": "warn",
        "no-script-url": "warn",
        "no-unneeded-ternary": "warn",
        "no-useless-return": "warn",
        "no-var": "warn",

        /* rules: eslint@stylistic */

        "@stylistic/eol-last": [
            "warn",
            "always"
        ],

        "@stylistic/no-multiple-empty-lines": ["warn", {
            max: 1,
            maxBOF: 0,
            maxEOF: 0,
        }],

        '@stylistic/quotes': ["warn", "single", {
            avoidEscape: true,
        }],

        "@stylistic/brace-style": ["warn", "1tbs"],

        "@stylistic/max-len": ["warn", {
            code: 200,
            ignoreStrings: true,
            ignorePattern: "^import .*",
        }],

        '@stylistic/semi': "warn",

        /* rules: @typescript-eslint */

        "@typescript-eslint/no-triple-slash-reference": "off",
        "@typescript-eslint/no-namespace": "off",
        "@typescript-eslint/consistent-type-imports": "error",

        /* rules: @typescript-eslint | svelte has some limitation in type-aware mode */

        "@typescript-eslint/ban-ts-comment": ["error", {
            "ts-expect-error": true,
            "ts-ignore": "allow-with-description",
            "ts-nocheck": true,
            "ts-check": false,
        }],

        "@typescript-eslint/no-non-null-assertion": "error",
        "@typescript-eslint/no-unsafe-assignment": "off",
        "@typescript-eslint/no-unsafe-argument": "off",
        "@typescript-eslint/no-unsafe-member-access": "off",

        "no-use-before-define": "off",
        "@typescript-eslint/no-use-before-define": "error",

        "@typescript-eslint/restrict-template-expressions": ["error", {
            allowNumber: true,
            allowBoolean: false,
            allowAny: false,
            allowNullish: true,
            allowRegExp: false,
        }],

        "no-unused-vars": "off",
        "@typescript-eslint/no-unused-vars": ["warn", {
            argsIgnorePattern: "^_",
            varsIgnorePattern: "^_",
            caughtErrorsIgnorePattern: "^_",
        }],

        /* rules: @eslint-community/eslint-comments */

        "@eslint-community/eslint-comments/disable-enable-pair": ["error", {
            allowWholeFile: true,
        }],

        "@eslint-community/eslint-comments/no-unused-disable": "warn",
        "@eslint-community/eslint-comments/require-description": "error",

        /* rules: svelte */

        "svelte/no-spaces-around-equal-signs-in-attribute": "warn",
        "svelte/no-unused-svelte-ignore": "off",
        "svelte/require-event-dispatcher-types": "warn",
        "svelte/require-optimized-style-attribute": "warn",
        "svelte/require-stores-init": "warn",
        "svelte/spaced-html-comment": ["warn", "always"],

        "@stylistic/no-trailing-spaces": "off",
        "svelte/no-trailing-spaces": ["warn", {
            skipBlankLines: false,
            ignoreComments: false,
        }],
        "svelte/valid-compile": "off",
    },
}, {
    files: ["**/*.svelte"],

    languageOptions: {
        parser: svelteParser,
        ecmaVersion: 5,
        sourceType: "script",

        parserOptions: {
            parser: "@typescript-eslint/parser",
        },
    },
}];


The final package.json, I removed the unrelated parts.


{
    "name": "myapp",
    "description": "MyApp",
    "version": "1.0.0",
    "private": true,
    "scripts": {
        "eslint": "eslint --report-unused-disable-directives App/**",
    },
    "devDependencies": {
        "@eslint-community/eslint-plugin-eslint-comments": "^4.3.0",
        "@eslint/js": "^8.57.0",
        "@eslint/eslintrc": "^3.1.0",
        "@stylistic/eslint-plugin": "^2.3.0",
        "@stylistic/eslint-plugin-migrate": "^2.3.0",
        "@tsconfig/svelte": "^5.0.4",
        "@types/lodash-es": "^4.17.12",
        "@typescript-eslint/eslint-plugin": "^7.14.1",
        "@typescript-eslint/parser": "^7.17.0",
        "eslint": "^8.57.0",
        "eslint-plugin-svelte": "^2.43.0",
        "svelte": "^4.2.18",
        "svelte-check": "^3.8.4",
        "svelte-jester": "^5.0.0",
        "svelte-preprocess": "^6.0.2",
        "typescript": "^5.5.4",
        "tslib": "^2.6.3"
    }
}

and the “tsconfig.json”


{
    "extends": "@tsconfig/svelte/tsconfig.json",
    "include": [
        "App/*",
        "App/**/*",
        "eslint.config.mjs"
    ],
    "exclude": [
        "node_modules/*",
        ".vscode/*"
    ],
    "compilerOptions": {
        "target": "ES2020",
        "strict": true,
        "forceConsistentCasingInFileNames": true,
    }
}

Add a Comment

Your email address will not be published. Required fields are marked *