14 — Qualidade e pré-commit
14 — Qualidade e pré-commit
Três gates:
vite-plugin-checkerdurante o dev,pre-commitno commit,npm run buildno build. Não há suíte de testes — assumido e declarado, não esquecido. Regra: o gate corrige o que dá para corrigir e barra o que não dá.
Os gates
| Quando | O que roda | Barra? |
|---|---|---|
npm run dev |
vite-plugin-checker (type-check contínuo) |
Não — mostra overlay |
git commit |
Node version, flags de debug, lint-staged
|
Sim |
git pull |
post-merge → npm install se preciso |
Não |
npm run build |
lint → tsc → vite build
|
Sim |
Ausência de testes — declarado
Este padrão não tem suíte de testes automatizados. Não há Vitest, Jest, Testing Library nem Playwright. Isso não é omissão da documentação: é o estado da referência.
O que substitui:
-
strict: true+noUnusedLocals+noUnusedParametersno TypeScript - ESLint com
--max-warnings 0 -
npm run buildcomo verificação de integração (compila tudo) - Verificação manual da tela contra o backend real
Se o projeto novo quiser testes, decida no início. Adicionar depois exige refatorar componentes que
nasceram acoplados a Context e ao UseCaseManager. Se adotar, atualize 19 para
incluir o comando na Definition of Done.
ESLint
Flat config (eslint.config.js) — ESLint 9+ abandonou o formato .eslintrc.json. Não é uma
escolha do guia, é obrigatório a partir da v9; --ext ts,tsx também deixou de ser aceito no CLI (o
escopo de arquivos agora vem do files de cada bloco de config).
// eslint.config.js
import js from "@eslint/js";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import reactHooks from "eslint-plugin-react-hooks";
import prettier from "eslint-config-prettier";
import globals from "globals";
export default [
{ ignores: ["dist", "build", "vite.config.ts", "setEnvironment.js"] },
js.configs.recommended,
{
files: ["**/*.{ts,tsx}"],
languageOptions: {
parser: tsParser,
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
globals: { ...globals.browser, ...globals.node, ...globals.es2020 }
},
plugins: { "@typescript-eslint": tsPlugin, "react-hooks": reactHooks },
rules: {
...tsPlugin.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
"no-unused-vars": "off",
"no-undef": "off",
"@typescript-eslint/no-unused-vars": "error",
"react-hooks/exhaustive-deps": "warn",
"no-debugger": "error",
"no-console": ["error", { allow: ["warn", "error", "info"] }],
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-empty-interface": "error",
"no-shadow": "off",
"@typescript-eslint/no-shadow": "error",
"no-throw-literal": "error",
"no-return-await": "error",
"no-duplicate-imports": "error",
eqeqeq: ["error", "always"],
"no-var": "error",
"prefer-const": "error",
"use-isnan": "error"
}
},
prettier
];
Requer @eslint/js e globals como devDependencies novas (não existiam no formato antigo).
prettier (de eslint-config-prettier) por último desliga as regras de formatação do ESLint —
Prettier manda no formato, ESLint manda na correção.
no-undef: "off"não é opcional. O@typescript-eslint/recommendeddo formato antigo (.eslintrc.json) desligavano-undefinternamente ao resolver oextends; pegando o objeto de regras direto (tsPlugin.configs.recommended.rules) em flat config, esse desligamento não vem junto — sem repetir explicitamente,no-undefacusa falso positivo em tipos ambient do TS (EventListener,Reactusado só como tipo em.ts). Otscjá cobre isso; deixe o ESLint fora do caminho.
Regras que merecem explicação
| Regra | Efeito prático |
|---|---|
no-console (permite warn/error/info) |
console.log esquecido barra o commit |
@typescript-eslint/no-explicit-any |
any exige eslint-disable justificado |
react-hooks/exhaustive-deps: warn |
Ver abaixo |
eqeqeq |
== proibido |
no-shadow |
Variável interna não pode mascarar externa |
exhaustive-depscomo"warn", não"off". OsuseEffectde abertura de caso de uso (06) dependem de rodar em condições específicas, e a regra reclamaria de arrays intencionalmente incompletos — resolva com// eslint-disable-next-linejustificado nesses poucos casos, não desligando a regra inteira.
eslint-plugin-react-hooks v7 trouxe regras novas — decida o alcance
O recommended da v7 é bem mais amplo que o da v4 (que só tinha rules-of-hooks +
exhaustive-deps). Ele inclui regras no estilo "React Compiler" — por exemplo
react-hooks/set-state-in-effect, que acusa qualquer setStateX(...) chamado direto no corpo de um
useEffect (fora de um callback de evento/subscrição).
Isso pega padrões legítimos de código já existente: estado 100% derivado de outro estado/prop que
era sincronizado via useEffect. A correção correta não é suprimir a regra, é aplicar o padrão que
o React recomienda:
- Se o valor é derivado de outro estado/prop (ex.: uma lista filtrada de uma query), calcule direto
no corpo do componente — não precisa de
useState/useEffectnenhum. - Se precisa resincronizar quando uma prop muda (ex.: um hook que lê de
localStorageporkey), ajuste o estado durante o render (guardando a prop anterior em outrouseStatee comparando), não dentro de umuseEffect.
Adotar o recommended completo é a recomendação — ele pega bug real de cascata de render. Se o
projeto novo preferir adiar, ligue só rules-of-hooks + exhaustive-deps (equivalente ao v4) e trate o
resto depois, mas isso é uma escolha explícita, não o padrão.
--max-warnings 0
eslint . --report-unused-disable-directives --max-warnings 0
Warning é erro. Sem isso, warnings acumulam até ninguém mais ler a saída.
--report-unused-disable-directives acusa eslint-disable que não suprime mais nada — remove
supressão obsoleta. Sem --ext: em flat config o CLI não aceita mais essa flag, o escopo de arquivos
vem do files de cada bloco em eslint.config.js.
Prettier
// .prettierrc
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "none",
"printWidth": 120,
"arrowParens": "always",
"endOfLine": "crlf",
"bracketSpacing": true,
"bracketSameLine": true,
"proseWrap": "preserve"
}
# .prettierignore
node_modules
dist
build
coverage
# Gerado em runtime por setEnvironment.js
public/config.json
package-lock.json
Dois pontos não-óbvios:
-
endOfLine: "crlf"— o time é Windows. Se houver dev em Linux/macOS, troque para"auto"e configure.gitattributes, senão todo commit reescreve o arquivo inteiro. -
bracketSameLine: true— o>de tag multi-linha fica na última prop, não em linha própria. Incomum, mas é o padrão da base.
husky
Instalação
// package.json — repositório de módulo único
"scripts": { "prepare": "husky" }
// package.json — projeto web dentro de um monorepo
"scripts": { "prepare": "cd ../.. && husky caminho/do/modulo/.husky" }
A forma do monorepo instala os hooks apontando para .husky/ dentro do módulo web, enquanto o
core.hooksPath é configurado na raiz do repositório.
Armadilha do monorepo.
core.hooksPathé config local do clone, mas.husky/é versionado. Trocar de branch pode ligar ou desligar os hooks em silêncio. Depois de clonar ou de trocar para uma branch que mexe empackage.json, rodenpm run preparee confirme comgit config core.hooksPath.
pre-commit
# .husky/pre-commit
# git roda o hook da raiz do repo; projeto Node pode estar em subpasta
hookdir="$(cd "$(dirname -- "$0")" && pwd)"
cd "$hookdir/.." || exit 1
# valida Node sem depender de fnm no shell
. "$hookdir/check-node-version.sh"
. "$hookdir/check-debug-flags.sh"
npx lint-staged
Três verificações, nesta ordem: versão de Node → flags de debug → lint-staged.
check-node-version.sh
# .husky/check-node-version.sh
# Sourced pelo hook, ja com cwd no projeto Node. Nao depende de fnm/nvm
# estarem carregados (GUIs de git nao herdam o profile) -- compara o node
# do PATH com .node-version (aceita >=).
expected_version=$(tr -d '[:space:]' < .node-version 2>/dev/null)
actual_version=$(node -v 2>/dev/null | sed 's/^v//')
if [ -z "$actual_version" ]; then
echo "hook: 'node' nao encontrado no PATH. Ative o Node >= ${expected_version:-do projeto} (ex: 'fnm use')."
exit 1
fi
if [ -n "$expected_version" ]; then
lower_version=$(printf '%s\n%s\n' "$expected_version" "$actual_version" | sort -V | head -n1)
if [ "$lower_version" != "$expected_version" ]; then
echo "hook: Node v$actual_version encontrado, necessario >= v$expected_version (.node-version)."
exit 1
fi
fi
Resolve um problema real: commitar por GUI de Git com Node 8 ativo faz o lint-staged falhar com erro
incompreensível.
check-debug-flags.sh
# .husky/check-debug-flags.sh
# Bloqueia commit com enableLogs: true (flag de depuracao de useUseCaseControls).
#
# So chama 'exit' no caminho de erro -- este arquivo e' sourced, entao
# 'exit 0' encerraria o hook antes do lint-staged rodar.
repo_root=$(git rev-parse --show-toplevel)
staged_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.ts' '*.tsx')
if [ -n "$staged_files" ]; then
matches=""
for file in $staged_files; do
file_match=$(grep -n "enableLogs:[[:space:]]*true" "$repo_root/$file" 2>/dev/null || true)
if [ -n "$file_match" ]; then
matches="$matches$file:
$file_match
"
fi
done
if [ -n "$matches" ]; then
echo "hook: 'enableLogs: true' encontrado em arquivo(s) staged - remova antes de commitar:" >&2
printf '%s' "$matches" >&2
exit 1
fi
fi
O comentário sobre exit 0 não é decorativo. O arquivo é carregado com . (source). Um exit 0
no fim encerraria o pre-commit inteiro antes do lint-staged, e o commit passaria sem lint. Ao
escrever um hook novo neste estilo, só chame exit no caminho de erro.
Adapte a lista de flags ao projeto: qualquer flag de depuração que não deva ser commitada entra aqui.
post-merge
# .husky/post-merge
# Instala dependencias quando o merge/pull alterou package.json/lock.
changed=$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)
case "$changed" in
*caminho/do/modulo/package-lock.json*|*caminho/do/modulo/package.json*)
echo "post-merge: dependencias mudaram -> rodando npm install..."
hookdir="$(cd "$(dirname -- "$0")" && pwd)"
cd "$hookdir/.." || exit 1
. "$hookdir/check-node-version.sh"
npm install
;;
esac
Elimina a classe de bug "puxei a branch e o app não sobe". Ajuste os caminhos do case ao layout do
projeto novo.
lint-staged
// package.json
"lint-staged": {
"src/**/*.{ts,tsx}": [
"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"prettier --write ."
],
"**/*.{json,css,scss,md,html,yml,yaml}": ["prettier --write ."]
}
Duas imprecisões comuns nesta configuração, que valem corrigir.
- Os comandos usam
.(projeto inteiro) em vez dos arquivos staged. Olint-stagedpassa a lista de arquivos como argumento, que aqui é ignorada. Funciona, mas fica lento e formata arquivos que você não tocou.prettier --write .faz o hook reescrever e re-stagear arquivos. Um commit pode sair com formatação que você não fez.Forma correta:
"lint-staged": { "src/**/*.{ts,tsx}": [ "eslint --fix --report-unused-disable-directives --max-warnings 0", "prettier --write" ], "**/*.{json,css,scss,md,html,yml,yaml}": ["prettier --write"] }Sem
., olint-stagedanexa só os arquivos staged.
vite-plugin-checker
checker({
typescript: true,
overlay: { initialIsOpen: false, position: "tl" },
terminal: true
});
Type-check contínuo durante npm run dev, em overlay e no terminal. Não substitui tsc no build —
o npm run build roda tsc separadamente, porque o checker só verifica o que foi tocado na sessão.
Contornar os gates
git commit --no-verify pula os hooks. Reserve para emergência real (commit de WIP em branch pessoal).
Nunca em branch compartilhada.
HUSKY=0 desabilita os hooks no ambiente — útil em CI, onde o pipeline já roda lint e build.
No Comments