ESLint config
Configs compartilhados de ESLint, em flat config, para Node, React e Next.js.
O que vem junto
- Base
neostandard(sucessor doeslint-config-standard); - TypeScript via
typescript-eslint; - React, React Hooks e JSX a11y;
- Prettier, acoplado — a formatação vem junto, sem
.prettierrcno projeto; - Ordenação de imports (
simple-import-sort) e higiene de import (import-x); - Guarda-corpos de complexidade e clean code — ver a seção abaixo.
Requisitos
- ESLint
^9— este pacote não roda em ESLint 10 ainda, ver ADR-0001; - Node
^18.18 || ^20.9 || >=21.1.
Setup
npm i -D eslint @thasmorato/eslint-config
O ESLint procura por eslint.config.js na raiz. Não use mais .eslintrc.json.
Node.js
// eslint.config.js
import node from '@thasmorato/eslint-config/node'
export default node
React (sem Next.js)
// eslint.config.js
import react from '@thasmorato/eslint-config/react'
export default react
React (com Next.js)
O preset
nexté um fragmento: ele precisa ser composto comnext/core-web-vitals. Sozinho ele falha, e isso é de propósito.
// eslint.config.js
import coreWebVitals from 'eslint-config-next/core-web-vitals'
import next from '@thasmorato/eslint-config/next'
export default [...next, ...coreWebVitals]
O eslint-config-next (v15+) já exporta flat config — não precisa de FlatCompat.
Ele registra os plugins react, react-hooks e jsx-a11y, e o flat config lança
Cannot redefine plugin se dois blocos registram o mesmo plugin. Por isso o preset next não
registra nenhum dos três: ele só ajusta as regras deles e deixa o Next ser o dono.
Ver ADR-0005.
Ajustando no projeto
Flat config é um array; para sobrescrever, acrescente um bloco no fim:
import node from '@thasmorato/eslint-config/node'
export default [
...node,
{
rules: {
'max-lines-per-function': ['warn', { max: 80 }],
},
},
]
Guarda-corpos de clean code
A severidade segue uma linha só: defeito é error, cheiro é warn. Um cheiro aparece no
editor sem quebrar um build que já estava verde.
| Intenção | Regra | Sev | Valor |
|---|---|---|---|
| Funções pequenas | max-lines-per-function |
warn | 50 |
| Arquivo não vira God object | max-lines |
warn | 300 |
| Uma coisa só | max-statements |
warn | 20 |
| Poucos argumentos | max-params |
warn | 4 |
| Complexidade ciclomática | complexity |
warn | 10 |
| Complexidade cognitiva | sonarjs/cognitive-complexity |
warn | 15 |
| Aninhamento | max-depth |
warn | 3 |
| Callback hell | max-nested-callbacks |
warn | 3 |
| Níveis de abstração | sonarjs/no-nested-functions |
warn | 3 |
| I/O serializado em loop | no-await-in-loop |
warn | — |
| Sem efeito colateral escondido | no-param-reassign |
error | props |
| Nunca engolir erro | no-empty, sonarjs/no-ignored-exceptions |
error | — |
| DRY | sonarjs/no-identical-functions |
warn | — |
Em arquivos de teste (*.spec.*, *.test.*, tests/, __tests__/) os limites de tamanho e de
duplicação ficam desligados: um describe é uma função longa por natureza e fixture repete de
propósito.
Sobre "validação de O notation"
Não existe. Complexidade assintótica é estaticamente indecidível — nenhum linter a calcula,
e o eslint-plugin-complexity que aparece nas buscas está sem manutenção desde 2022.
O que este config entrega são os proxies tratáveis: max-depth e max-nested-callbacks
pegam o aninhamento que costuma indicar custo polinomial, no-await-in-loop pega I/O
serializado, e sonarjs/cognitive-complexity pega o que é caro de segurar na cabeça. É honesto
chamar isso de guarda-corpo de complexidade, não de análise de Big-O.
Migrando da v1
A v2 é breaking em todos os eixos:
| v1 | v2 |
|---|---|
.eslintrc.json com extends |
eslint.config.js com import |
eslint@^8 |
eslint@^9 |
eslint-config-standard |
neostandard |
regras import/* |
regras import-x/* |
| CommonJS | ESM ("type": "module") |
Se você tem // eslint-disable-next-line import/no-duplicates espalhado, o prefixo virou
import-x/.
Decisões
Os porquês estão em docs/adr/:
- ADR-0001 — por que ESLint 9 e não 10
- ADR-0002 — por que
neostandard - ADR-0003 — por que
import-xsem as regras de resolução - ADR-0004 — por que o Prettier segue acoplado
- ADR-0005 — por que o preset
nextnão roda sozinho
Desenvolvimento
npm test # node:test — linta fixtures via API do ESLint e afirma os ruleId
npm run lint # o pacote se linta com o preset que publica (dogfood)
npm run test:consumer # empacota, instala num projeto temporário e linta de lá
Release
Publicação automática no push pra main, a partir do tipo do commit
(Conventional Commits):
| Commit | Bump | Use quando |
|---|---|---|
fix: refactor: perf: |
patch | corrige o config sem fazer regra nova disparar |
feat: |
minor | regra nova entrando como warn |
feat!: / BREAKING CHANGE: |
major | regra nova como error, warn→error, preset muda de forma |
chore: docs: ci: test: style: |
— | sem release |
A convenção do meio é o que mantém o ^ seguro: regra nova nasce warn; promover pra error
é major. Num config compartilhado, uma regra nova que dá error quebra o build de quem não
mudou uma linha de código — por isso ela não pode entrar numa minor.
O bump vem de scripts/bump.js, que é função pura e tem teste (tests/bump.spec.js). O workflow
roda testes, lint e o smoke test de consumidor antes de publicar, e só empurra a tag depois
do publish dar certo — assim uma falha nunca deixa tag apontando pra versão que não foi ao ar.
Setup, uma vez
Trusted publishing no npm — em npmjs.com, na página do pacote → Settings → Trusted Publishers, adicione: repositório
ThaSMorato/eslint-config, workflowrelease.yml. Não é preciso criar nem guardarNPM_TOKEN.Tag inicial — o repo não tem tags. Sem uma, o primeiro run olha o histórico inteiro, encontra
feat:antigos e publicaria2.1.0. Marque o ponto de partida:git tag v2.0.0 && git push origin v2.0.0A partir daí a automação conta só o que vier depois.