En un SaaS de generació d'imatges vaig esborrar .prettierrc, eslint.config.mjs i set dependències de package.json en el mateix commit. El lockfile va perdre 545 línies. L'endemà vaig fer el mateix en un altre projecte, i en un altre. A la cinquena vegada ja no estava avaluant res: obria el repo, treia ESLint i Prettier, i escrivia biome.json.
Això ja no és una preferència. És un criteri, i convé poder defensar-lo.
El que em va empènyer la primera vegada
No va ser la velocitat. Va ser l'arbre de dependències. La configuració que vaig treure era aquesta:
const compat = new FlatCompat({ baseDirectory: __dirname });
export default [...compat.extends("next/core-web-vitals", "next/typescript", "prettier")];
Un FlatCompat perquè la flat config entengués configuracions antigues. Un eslint-config-prettier que té com a única feina apagar les regles d'ESLint que xoquen amb Prettier. Un eslint-plugin-tailwindcss i un prettier-plugin-tailwindcss que fan coses semblants des de dos costats diferents. Sis paquets la raó de ser dels quals és que les altres eines no es trepitgin entre elles.
Biome fa format i lint en el mateix binari. Aquell problema desapareix perquè deixa d'haver-hi dues eines a coordinar.
El que es trenca i ningú no avisa
La migració no és esborrar fitxers i seguir. És el repàs de format immediatament després: 78 fitxers tocats en un commit, en què no pots revisar res perquè només hi ha cometes i sagnats. Es fa i s'accepta.
El problema real arriba quan encens el linter de debò. En un bot de recordatoris a Telegram, la primera passada amb les regles recomanades va donar 60 errors al web. Gairebé cap era soroll:
- Labels sense associar. Camps del login sense
htmlFor/id. ESLint amb la config de Next no m'ho havia dit mai. - SVG decoratius. La regla no reconeix la forma abreujada d'
aria-hidden; cal posararia-hidden="true"explícit. - Col·lisió de noms. Un tipus
TaskListimportat que xocava amb un component del mateix nom. Compilava perfectament i estava malament. - Keys per índex. En un component de text rotatori, la key de cada caràcter era l'índex del
map. La solució no va ser silenciar la regla: va ser precalcular l'offset absolut al memo per tenir una key estable.
Aquest és el punt interessant. Dels 60, la majoria eren errors reals que feia mesos que hi eren. La resta es van resoldre amb supressions documentades: un efecte d'una sola execució, bucles d'esquelets de mida fixa, un !important que trepitja react-remove-scroll-bar, <img> d'avatars d'OAuth.
Un biome-ignore sense comentari explicant per què és deute. Amb comentari és una decisió.
La velocitat importa pel lloc on et deixa posar el lint
És el que més es repeteix a les comparatives i el que pitjor s'explica. La xifra no importa. Importa on pots endollar l'ordre quan triga poc.
Amb ESLint el lint vivia a CI, perquè ficar-lo al build local era condemnar-te a esperar. Amb Biome l'script va quedar així:
"build": "bun run lint && next build",
"lint": "biome check .",
"lint:fix": "biome check --write ."
El lint davant del build. Si està vermell, no compiles. Aquesta línia és tota la diferència: l'error deixa de descobrir-se quinze minuts després en un runner i es descobreix abans que el build arrenqui. Amb una eina lenta aquesta línia és insuportable i acabes traient-la.
El que perds
Dues coses, i val la pena anomenar-les.
La primera és l'ecosistema de plugins. Si depens d'un plugin d'ESLint molt concret, no hi és i probablement no hi serà. Ho vaig comprovar en treure eslint-plugin-tailwindcss: l'ordre de classes el resol el formatador de Tailwind, no el linter, així que no el vaig trobar a faltar. Però és un cas afortunat, no una regla general.
La segona és més ximple i dona més guerra: la configuració per defecte no és la mateixa a tot arreu. Un projecte va sortir amb "indentStyle": "tab" i un altre amb "indentStyle": "space" i "indentWidth": 2. Setmanes després vaig haver de fer un commit que només contenia canviar tabuladors per espais a biome.json perquè tot fos igual. Fixa el $schema amb la versió exacta i copia el mateix biome.json entre projectes. La comoditat d'aquesta eina és que gairebé no té configuració; aprofita-ho i tingues-ne una de sola.
El meu stack de qualitat avui
Biome, TypeScript en mode estricte, tests, i knip per al codi mort. Poca cosa més.
Knip és la peça que menys gent fa servir i la que més sorprèn. Una passada al bot de Telegram va esborrar 19 fitxers morts i 21 dependències sense usar: subpaquets @radix-ui/* redundants amb el meta-paquet, @react-three/fiber i three sencers, date-fns, i eslint, que continuava instal·lat mesos després d'haver-lo tret. També en surt un clúster complet d'accions i casos d'ús que ja no cridava ningú.
Però knip es queda fora de CI, a propòsit. En un repo gran la primera passada va donar 107 troballes preexistents: enganxar-lo al gate el posa en vermell el dia u i acabes ignorant el vermell. Va com a eina manual, cada uns quants mesos, amb la poda en el seu propi commit i verificada amb tsc i els tests abans de fer merge.
La configuració ha de ser explícita amb els entrypoints o et buida mitja aplicació. En un monorepo amb Next cal declarar-li a mà quins fitxers són punts d'entrada:
"entry": ["app/**/{page,layout,route,sitemap,robots,icon}.{ts,tsx}"],
"ignore": ["components/ui/**"],
"ignoreDependencies": ["sharp", "tailwindcss", "tw-animate-css"]
Els components generats per shadcn van a ignore perquè sempre n'hi ha més dels que fas servir i això és normal. sharp i tailwindcss van a ignoreDependencies perquè ningú no els importa, es fan servir des del build.
Un linter lent és un linter que acaba en un job a part que ningú no mira. Un linter ràpid entra al build i es converteix en el terra del projecte.
