@esrf/eslint-config
v2.0.1
Published
ESLint config
Downloads
1,800
Readme
Shared ESLint config @ ESRF
Dynamic linting configuration for JavaScript, TypeScript and Node projects, with support for React/JSX, Vitest, Storybook, Testing Library, and Cypress.
In addition to core ESLint rules,
@esrf/eslint-config includes rules from the following ESLint plugins:
- typescript-eslint
- eslint-plugin-unicorn
- eslint-plugin-simple-import-sort
- eslint-plugin-import
- eslint-plugin-promise
- eslint-plugin-regexp
- eslint-plugin-react
- eslint-plugin-react-hooks
- eslint-plugin-jsx-a11y
- @vitest/eslint-plugin
- eslint-plugin-testing-library
- eslint-plugin-storybook
- @stylistic/eslint-plugin
Written in ESLint's flat config format, available since ESLint v9, this shared configuration supports monorepos and can be easily loosened, tweaked or extended to adapt to the needs of any front-end project.
Prerequisites
The configuration makes the following assumptions:
- All JS files are ES modules (i.e.
package.jsonhas"type": "module"). - All JS/JSX/TS/TSX files inside the
srcfolder target a browser environment, except for test files. - All JS/TS files outside the
srcfolder, as well as test files inside thesrcfolder, target a Node environment.
By "test files inside the
srcfolder", we mean any file matching eithersrc/**/__tests__/**/*.{js,jsx,ts,tsx}orsrc/**/*.test.{js,jsx,ts,tsx}.
CommonJS projects
If, for whatever reason you cannot set "type": "module" in package.json, you
can still benefit from this linting configuration by switching to TypeScript,
and/or by using the explicit .mjs or .cjs extensions for files outside the
src folder.
We do not support MJS/CJS files inside the
srcfolder at this time.
Getting started
Install the config, together with the required version of ESLint (cf.
peerDependencies in package.json):
pnpm add --save-dev @esrf/eslint-config eslint@<x.y>Create a file called eslint.config.js in the root of your project, with the
following content:
import { createConfig, detectOpts } from '@esrf/eslint-config';
import { defineConfig, globalIgnores } from 'eslint/config';
const opts = detectOpts(import.meta.dirname);
const config = defineConfig([
globalIgnores(['dist/', 'folder/to/ignore/']),
...createConfig(opts),
]);
export default config;If your project's
package.jsondoesn't have"type": "module"(or has"type": "commonjs"), use theMJSextension to enable ESM:eslint.config.mjs.
Adjust the list of ignored folders as needed. Don't forget folders that might be
generated by your CI or your toolchain (e.g. pnpm store, test coverage report,
etc.) Don't include node_modules or .git, which are already ignored by
ESLint out of the box.
Add a linting script to your package.json. In a typical project with
TypeScript, ESLint and Prettier, we recommend having four scripts as shown below
to ensure linting, formatting and type-checking are performed in parallel:
"scripts": {
"lint": "pnpm \"/^lint:/\"",
"lint:eslint": "eslint --max-warnings=0",
"lint:tsc": "tsc",
"lint:prettier": "prettier . --cache --check"
},Make sure to run the
lintscript above as part of your CI workflow.
The
--max-warnings=0option ensures that ESLint exits with a non-zero code if it finds warning-level violations. Thewarninglevel helps to make cosmetic, low-impact violations less intrusive during development, but those must still be fixed before committing.
Finally, in a TypeScript project, make sure tsconfig.json includes all TS and
JS files in the codebase (including eslint.config.js itself).
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "react-jsx", // if React
"allowJs": true, // include JS files when linting
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"resolveJsonModule": true,
"noEmit": true, // allow running `tsc` for type-checking
"incremental": true,
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
},
"include": ["*", "src"], // include root files like `eslint.config.js`
}{
"compilerOptions": {
"module": "nodenext",
"moduleResolution": "nodenext",
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, // include JS files when linting
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"resolveJsonModule": true,
"noEmit": true, // allow running `tsc` for type-checking
"incremental": true,
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
},
"include": ["*", "src"], // include root files like `eslint.config.js`
}Monorepos
In monorepos, install and configure ESLint at the root, as well as in every project:
.
├── cypress/
│ └── tsconfig.json
├── node_modules/
├── packages/
│ ├── foo/
│ │ ├── node_modules/
│ | ├── src/
│ │ ├── eslint.config.js
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── bar/
│ ├── node_modules/
│ ├── src/
│ ├── eslint.config.js
│ ├── package.json
│ └── tsconfig.json
├── eslint.config.js
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
└── tsconfig.jsonThis allows each project to have a linting config tailored to its own needs (with or without React, with or without Vitest, etc.)
In the root eslint.config.js, ignore the packages folder to avoid linting
the same files twice:
const config = defineConfig([
globalIgnores(['packages/']),
...createConfig(opts),
]);Here is one way of defining the linting scripts in the root package.json to
ensure that the root folder is linted as well:
"scripts": {
"lint": "pnpm \"/^lint:/\"",
"lint:prettier": "prettier . --cache --check",
"lint:eslint": "pnpm -r --parallel lint:eslint",
"lint:tsc": "pnpm -r --parallel lint:tsc",
"lint:root:eslint": "eslint --max-warnings=0",
"lint:root:tsc": "tsc"
}{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"target": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "react-jsx",
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"isolatedModules": true,
"resolveJsonModule": true,
"noEmit": true,
"incremental": true,
"strict": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
},
"include": ["*"], // only root files to avoid linting/type-checking project files twice
}{
"extends": "../../tsconfig.json", // extend root `tsconfig.json`
"include": ["*", "src"], // project root files + source files
}Migrating an existing codebase
If your project was already set up for linting with ESLint<=8, start with these steps:
- Upgrade ESLint to the version required by this config (cf.
peerDependenciesinpackage.json). - Remove the legacy
.eslintignorefile (replaced withglobalIgnores()ineslint.config.js). - Remove any
legacy configuration
(
eslintConfigproperty inpackage.json,.eslintrc.jsfile, etc.)
You're now ready to apply the new configuration to your codebase.
Run the lint script, pnpm lint:eslint, and check the output. Depending on the
size of your codebase, you may see thousands of warnings and errors. Most will
be rule violations, but some might also be "Unused eslint-disable directive"
warnings, meaning that the new configuration no longer reports violations
everywhere your previous config used to.
The most sane way to proceed from here is to start by turning off every single
rule that reports a violation in eslint.config.js:
const config = defineConfig([
globalIgnores(['dist/', 'folder/to/ignore/']),
...createConfig(opts),
{
rules: {
'simple-import-sort/imports': 'off',
'import/consistent-type-specifier-style': 'off',
// ...
},
},
]);Keep turning off rules until ESLint no longer reports any violations — only
"Unused eslint-disable directive" warnings. You can now automatically remove all
unused directives with pnpm lint:eslint --fix. Make sure to reformat all files
afterwards with pnpm lint:prettier --write.
At this point, pnpm lint:eslint should pass, so it's a good time to commit the
new linting set up and open a PR. Once merged, you can start to actually fix the
new violations.
Proceed one rule at a time, ideally starting with the most impactful,
auto-fixable rules, like import/consistent-type-specifier-style,
import/no-duplicates, simple-import-sort/imports, etc.:
- Turn the rule back on by removing it from
eslint.config.js. - Run
pnpm lint:eslint --fix. - If the rule is not auto-fixable, fix the violations manually until
pnpm lint:eslintpasses. - Commit, open a PR, request a review, and merge into the main branch once approved.
Rules with few violations can of course be fixed together in the same PR as long as the diff remains reviewable. When a fix is non-trivial, or when the rationale behind a rule might not be clear to the reviewer, make sure to comment and link to the documentation of the rule in question.
Usage guidelines
If you strongly disagree with a rule, or if it goes against agreed-upon practices in your project, or if it's really not worth fixing, either disable it entirely or configure it as you see fit, making sure to explain why in a comment:
const config = defineConfig([
// ...
{
rules: {
'react/prop-types': 'off', // legacy code, not worth fixing
/* Default is "avoid", but there are lots of complicated `switch` statements,
* notably in Redux reducers, which benefit from clear case blocks. */
'unicorn/switch-case-braces': ['warn', 'always'],
},
},
{
/* Some rules apply only to specific files.
* Make sure to use the same `files` array as in `src/index.js`. */
files: ['**/*.{jsx,tsx}'],
rules: {
'jsx-a11y/control-has-associated-label': 'off',
},
},
]);If you turn off a rule completely, beware that it will not be applied to new
code. To disable a rule on existing code only, use
eslint-disable directives
instead (assuming the number of violations is within reason):
/* eslint-disable react/prop-types --
* Long explanation why the rule is disabled. */
// eslint-disable-next-line react/no-multi-comp -- short explanation
export function MyComponent(props) {
let foo; // eslint-disable-line no-unused-vars -- same-line syntax
return (
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid -- JSX syntax */}
<a onClick={(e) => e.stopPropagation()}>{props.value}</a>
);
}Of course, don't hesitate to open an issue in this repo if you think the config should be changed, if you'd like to better understand the rationale behind some of the rules and options, or if you're unsure how to fix a specific violation.
Troubleshooting
ESLint provides a brilliant
config inspector to debug
configuration issues, notably when ignoring/filtering files. You can run it
with:
pnpm lint:eslint --inspect-config
pnpm --filter <project-in-monorepo> lint:eslint --inspect-configAPI reference
detectOpts(projectDir)
This function looks at the dependencies installed in <projectDir>/package.json
and whether a <projectDir>/tsconfig.json file exists. It returns an object
that can then be passed to createConfig() in order generate an ESLint
configuration tailored to your project.
Calling this function is entirely optional; the options object can be declared manually. It's also possible to override specific options as needed:
const opts = { react: true };
createConfig(opts);
const opts = detectOpts(import.meta.dirname);
createConfig({ ...opts, typescript: false });All options accept boolean values, except for the typescript option, which
accepts either false or an object with the a tsconfigRootDir property. By
default, the project dir is used: { tsconfigRootDir: <projectDir> } but you
can override it as follows:
import path from 'node:path';
createConfig({
...detectOpts(import.meta.dirname),
typescript: {
tsconfigRootDir: path.join(import.meta.dirname, '..'), // `tsconfig.json` in parent directory
},
});For the full list of options, please refer to the
DEFAULT_OPTSobject insrc/index.js.
createConfig(opts)
This function generates a flat config array with the given options.
The returned array contains ESLint config objects — you can spread it, filter it, extend it, etc. before exporting it:
const config = createConfig(opts);
const unicornConfig = config.find(c => c.name.endsWith('unicorn'));
export default {
...config.filter(c => c !== unicornConfig),
{
...unicornConfig,
files: ['**/*.{ts,tsx}'] // apply unicorn rules to TS/TSX files only
}
};