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`:
- https://typescript-eslint.io/getting-started/
- https://typescript-eslint.io/packages/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
- https://www.npmjs.com/package/@eslint/migrate-config
- https://eslint.org/docs/latest/use/configure/migration-guide
- https://eslint.org/docs/latest/use/configure/configuration-files
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,
}
}