Skip to content

Migrate to ESlint 9 #756

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 0 additions & 58 deletions .eslintrc.json

This file was deleted.

3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ node_modules/
# Vite
/vite.config.ts-timestamp-*

# ESLint
/.eslintcache

# misc
.env.local
.env.development.local
Expand Down
6 changes: 2 additions & 4 deletions demo/src/app.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/* eslint-disable func-names, no-nested-ternary, no-return-assign, @typescript-eslint/no-unused-vars, no-promise-executor-return, @typescript-eslint/no-unused-expressions, no-alert, no-undef, @typescript-eslint/no-shadow, react/jsx-no-bind, react/prop-types, import/no-extraneous-dependencies */
/* eslint-disable func-names, no-nested-ternary, no-return-assign, no-unused-vars, no-promise-executor-return, no-unused-expressions, no-alert, no-undef, no-shadow, react/jsx-no-bind, react/prop-types */

import {
Box,
Expand Down Expand Up @@ -32,7 +32,7 @@ import { BrowserRouter, useLocation, useMatch, useNavigate } from 'react-router'
import { IntlProvider, useIntl } from 'react-intl';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import translations from './demo_intl';
import PowsyblLogo from '../images/powsybl_logo.svg?react'; // eslint-disable-line import/no-unresolved
import PowsyblLogo from '../images/powsybl_logo.svg?react';
import AppPackage from '../../package.json';
import TreeViewFinderConfig from './TreeViewFinderConfig';
import {
Expand Down Expand Up @@ -197,7 +197,6 @@ const CustomTreeViewFinder = styled(TreeViewFinder)(TreeViewFinderCustomStylesEm
function Crasher() {
const [crash, setCrash] = useState(false);
if (crash) {
// eslint-disable-next-line no-undef
window.foonotexists.bar();
}
return <Button onClick={() => setCrash(true)}>CRASH ME</Button>;
Expand Down Expand Up @@ -298,7 +297,6 @@ function PermanentSnackButton() {
const validateUser = () => {
// change to false to simulate user unauthorized access
return new Promise((resolve) => {
// eslint-disable-next-line no-undef
window.setTimeout(() => resolve(true), 500);
});
};
Expand Down
1 change: 0 additions & 1 deletion demo/src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { createRoot } from 'react-dom/client';

import App from './app';

// eslint-disable-next-line no-undef
const container = document.querySelector('#demo');
const root = createRoot(container);
root.render(<App />);
2 changes: 1 addition & 1 deletion demo/src/right-resizable-box.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ function RightResizableBox(props) {
setResizedTreePercentage(newPercentage);
}
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
// eslint-disable-next-line no-unused-vars
const onResize = (event, { element, size }) => {
updateResizedTreePercentage(size.width, windowWidth);
};
Expand Down
239 changes: 239 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
/*
* Copyright © 2025, RTE (http://www.rte-france.com)
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { braceExpand } from 'minimatch';
import { defineConfig, globalIgnores } from 'eslint/config';
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import globals from 'globals';
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
import tsEslint from 'typescript-eslint';
import pluginReact from 'eslint-plugin-react';
import pluginReactHooks from 'eslint-plugin-react-hooks';
import pluginImport from 'eslint-plugin-import';
import pluginJest from 'eslint-plugin-jest';
import pluginTestingLibrary from 'eslint-plugin-testing-library';
import { getSupportInfo, resolveConfig, resolveConfigFile } from 'prettier';

/**
* @typedef {import('eslint').Linter.ParserOptions} EsParserOptions
* @typedef {import('@typescript-eslint/parser').ParserOptions} TsParserOptions
* @typedef {import('type-fest').MergeDeep<EsParserOptions, TsParserOptions>} AllParserOptions
*/

// Helper to translate old ESLintRC-style to new flat-style config
const compat = new FlatCompat({
baseDirectory: import.meta.dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});

const JsFiles = [`**/*.{${braceExpand('{,c,m}js{,x}').join(',')}}`];
const TsFiles = [`**/*.{${braceExpand('{,m}ts{,x}').join(',')}}`];
// const JsTsFiles = [`**/*.{${['cjs', 'cjsx', ...braceExpand('{,m}{j,t}s{,x}')].join(',')}}`];
const JsTsFiles = [...JsFiles, ...TsFiles];
const TestFiles = ['**/__tests__/**/*.{js,cjs,mjs,jsx,ts,mts,tsx}', '**/*.{spec,test}.{js,cjs,mjs,jsx,ts,mts,tsx}'];

function setRuleLevel(rules, rule, level) {
if (Array.isArray(rules[rule])) {
// eslint-disable-next-line no-param-reassign
rules[rule][0] = level;
} else {
// eslint-disable-next-line no-param-reassign
rules[rule] = level;
}
return rules; // just a helper for functional chaining
}

/**
* Files checked by Prettier
*/
async function getPrettierCheckedExt() {
const configPath = await resolveConfigFile(import.meta.filename);
const config = await resolveConfig(configPath, { useCache: false });
const supportInfo = await getSupportInfo({ plugins: config.plugins });
return supportInfo.languages
.flatMap((lng) => lng.extensions ?? []) // extract extensions checked by plugins
.concat('.env') // also checked by prettier in override section
.map((dotExt) => dotExt.substring(1)); // remove dot from ext string
}

const airbnbTs = compat.extends('@kesills/airbnb-typescript');
const importNoExtraneousDependencies = airbnbTs[0].rules['import/no-extraneous-dependencies'];
export default defineConfig([
globalIgnores([
// .git & node_modules is implicitly always ignored
'dist/**',
'coverage/**',
]),
{
// We set "default files" checked when another config object don't define "files" field
name: 'ProjectCheckedFiles',
files: [
`**/*.{${[getPrettierCheckedExt(), JsTsFiles]
.flat()
.filter((ext, index, self) => self.indexOf(ext) === index) // dedupe
.join(',')}}`,
],
},
{ name: 'eslint base declare', files: JsTsFiles, plugins: { js } },
{ files: TsFiles, ...tsEslint.configs.base },
{ name: 'eslint-plugin-react declare', files: JsTsFiles, plugins: { react: pluginReact } },
{ name: 'eslint-plugin-react-hooks declare', files: JsTsFiles, plugins: { 'react-hooks': pluginReactHooks } },
{
name: 'eslint-plugin-import/typescript rules',
files: TsFiles,
rules: pluginImport.flatConfigs.typescript.rules,
},
{
name: 'eslint-plugin-testing-library/react',
files: TestFiles,
...pluginTestingLibrary.configs['flat/react'],
},
{
name: 'eslint-plugin-jest/recommended',
files: TestFiles,
...pluginJest.configs['flat/recommended'],
},
// eslint-plugin-jsx-a11y is re-declared in airbnb config and eslint don't let redeclaration of plugins
// requires eslint, eslint-plugin-import, eslint-plugin-react, eslint-plugin-react-hooks, and eslint-plugin-jsx-a11y
// TODO migrate when eslint v9 & flat-config supported: https://github.com/airbnb/javascript/issues/2961
{
files: JsTsFiles,
name: 'AirBnB',
extends: compat.extends('airbnb'),
rules: { 'import/extensions': airbnbTs[0].rules['import/extensions'] }, // set "import/extensions" rule in js files importing ts file
},
{ files: JsTsFiles, name: 'AirBnB hooks', extends: compat.extends('airbnb/hooks') },
{
files: TsFiles, // there a strange error when applied on js files
name: 'AirBnB Typescript',
extends: airbnbTs,
languageOptions: {
parserOptions: /** @type AllParserOptions */ ({
// https://typescript-eslint.io/packages/parser
projectService: true,
tsconfigRootDir: import.meta.dirname,
}),
},
},
{
// merge react & ts config, configure eslint-import-resolver-typescript, and finally keep airbnb-typescript override
name: 'eslint-plugin-import config with TS support compliant with AirBnB configs',
files: JsTsFiles,
...pluginImport.flatConfigs.react, // do languageOptions jsx
settings: {
...pluginImport.flatConfigs.typescript.settings,
'import/parsers': {
'@typescript-eslint/parser': [
...pluginImport.flatConfigs.typescript.settings['import/parsers']['@typescript-eslint/parser'],
'.d.ts',
],
},
'import/resolver': {
node: {
extensions: [
...pluginImport.flatConfigs.typescript.settings['import/resolver'].node.extensions,
'.json',
'.d.ts',
],
},
// See also https://github.com/import-js/eslint-import-resolver-typescript#configuration
typescript:
/** @type {import('eslint-import-resolver-typescript').TypeScriptResolverOptions} */
({
alwaysTryTypes: true, // always try to resolve types under `<root>@types` directory even it doesn't contain any source code, like `@types/unist`
project: `${import.meta.dirname}/tsconfig.json`,
}),
},
'import/extensions': [...pluginImport.flatConfigs.typescript.settings['import/extensions'], '.d.ts'],
},
// need all these for parsing dependencies (even if _your_ code doesn't need all of them)
languageOptions: { parser: tsEslint.parser, ecmaVersion: 'latest', sourceType: 'module' },
rules: { 'import/no-unresolved': 'error' }, // not all files are in typescript yet
},

{
name: 'General configuration',
files: JsTsFiles,
settings: {
babel: true,
// compat: true,
},
languageOptions: {
globals: {
...globals.es2020, // https://vite.dev/guide/build.html#browser-compatibility & https://vite.dev/config/build-options.html#build-target
...globals.browser,
},
ecmaVersion: 2020,
sourceType: 'script',
},
rules: {
curly: 'error',
'no-console': 'off',
'react/jsx-props-no-spreading': 'off',
'react/require-default-props': 'off',
'no-plusplus': ['error', { allowForLoopAfterthoughts: true }],
'import/prefer-default-export': 'off',

// some libraries in ESM don't play nicely with ESBuild and need to only import from root
'no-restricted-imports': [
'warn',
{
patterns: [
{
group: ['@mui/*/*', '!@mui/material/colors', '!@mui/material/locale'],
message:
'Deep imports from MUI libraries are forbidden. Import only from the library root.',
},
],
},
],

// extend https://github.com/airbnb/javascript/blob/master/packages/eslint-config-airbnb-base/rules/imports.js
'import/no-extraneous-dependencies': [
importNoExtraneousDependencies[0],
{
...importNoExtraneousDependencies[1],
devDependencies: [
...importNoExtraneousDependencies[1].devDependencies,
'**/eslint.config.js',
'**/prettier.config.js',
'**/vite.config.ts',
],
},
],
},
},
{
name: 'General configuration for tests',
files: ['**/jest.setup.ts', '**/*.{test,spec}.{js,jsx,ts,tsx}'],
languageOptions: {
globals: {
...globals.browser,
...globals.jest, // globals.vitest
},
},
},
{
name: 'React: jsx-runtime',
files: JsTsFiles,
// concretely disable react/react-in-jsx-scope & react/jsx-uses-react
...pluginReact.configs.flat['jsx-runtime'], // using React 17+
},
{
name: 'ProjectToolsConfigs',
files: ['**/*.config.{js,ts}'],
languageOptions: { globals: globals.node, ecmaVersion: 'latest', sourceType: 'module' },
},
// keep last in case we have reactivated a rule that conflict with Prettier (turn off the rules of some core & eslint plugins rules)
{
...eslintPluginPrettierRecommended, // include eslint-config-prettier
// format isn't mandatory during dev session, so we pass it to warn level instead of error
rules: setRuleLevel({ ...eslintPluginPrettierRecommended.rules }, 'prettier/prettier', 'warn'),
},
]);
Loading
Loading