La lliçó anterior va acabar amb una observació incòmoda: els tres errors que vas resoldre tenien un senyal previ que ningú va veure. Un id que barrejava tipus, un catch que s'empassava un error en silenci, dues línies bessones de les quals només una es va adaptar. Cap no necessitava executar-se per ser sospitós: n'hi havia prou amb llegir el codi amb atenció, i això ho fa una màquina millor que tu, en cent mil·lisegons i sense cansar-se. En aquesta lliçó muntaràs aquesta màquina sobre Nómada Tasques: ESLint per detectar patrons perillosos abans d'executar res, Prettier perquè el format deixi de ser un tema de conversa, un hook de Git perquè res mal formatat entri al repositori i un flux d'integració contínua que ho comprovi un altre cop al servidor. I acabaràs fixant les convencions que cap eina no pot automatitzar: noms, mida de funcions, comentaris que expliquen el perquè i documentació de tipus amb JSDoc.
Contingut
- Què costa no tenir eines de qualitat
- Anàlisi estàtica: què pot saber una màquina sense executar res
- ESLint: instal·lació i primera arrencada
- El fitxer de configuració pla
- Regles, nivells i configuracions recomanades
globals: navegador, Node i proves- Deu regles que eviten bugs reals
- Desactivar una regla sense fer trampes
--fixi els scripts del projecte- Plugins útils
- Prettier: el format deixa de ser una opinió
- ESLint davant de Prettier, i com conviure
- Integració a l'editor
- Hooks de Git amb Husky i lint-staged
- Integració contínua amb GitHub Actions
- Convencions que cap eina no pot imposar
- Documentar tipus amb JSDoc
// @ts-check: comprovació de tipus sense TypeScript- Mètriques de qualitat amb cap
- Nómada Tasques: configurar-ho tot i arreglar els avisos
- Errors Habituals i Consells
- Exercicis
- Conclusió
- Què costa no tenir eines de qualitat
En un projecte sense automatitzar, la qualitat se sosté sobre la revisió humana. I la revisió humana té un problema de pressupost d'atenció: qui revisa disposa d'una quantitat limitada d'energia, i si la gasta en coses que una màquina resoldria sola, no li'n queda per al que només un humà pot veure.
Un comentari típic de revisió sense eines:
«Aquí hi ha cometes dobles i a la resta del fitxer simples. Falta el punt i coma de la línia 34. La sagnia de l'
ifestà a 4 i fem servir 2. I crec queestatno es fa servir.»
Quatre comentaris; els quatre els detecta i arregla una eina en un segon. El que no ha dit ningú, perquè l'atenció se n'ha anat en el format: que aquell if no contempla el cas feta, o que la funció retorna undefined en una branca.
El cost real es reparteix en quatre sumands:
| Cost | Què és | Què l'elimina |
|---|---|---|
| Discutir estil | Cometes, sagnia, punt i coma, longitud de línia | Un formatador determinista |
| Soroll als diffs | Canvis de format barrejats amb canvis de lògica | El mateix formatador, aplicat a tothom |
| Bugs de patró | Variables sense fer servir, == inesperat, promeses sense await |
Un analitzador estàtic |
| Deriva entre persones | Cada fitxer escrit amb un estil diferent | Configuració compartida al repositori |
Els dos primers són de comoditat. El tercer és de correcció, i és el que justifica la lliçó: hi ha una família sencera d'errors que es detecten llegint el codi, sense executar-lo.
- Anàlisi estàtica: què pot saber una màquina sense executar res
Un analitzador estàtic converteix el teu codi en un arbre de sintaxi abstracta (AST) —la mateixa estructura que construeix el motor de JavaScript abans d'executar— i recorre aquest arbre buscant patrons. No executa res; raona sobre la forma.
flowchart LR
A["Codi font<br/>tauler.js"] --> B["Analitzador<br/>→ AST"]
B --> C["Regles<br/>recorren l'arbre"]
C --> D["Avisos i errors<br/>fitxer:línia:columna"]
C --> E["Correccions<br/>automàtiques (--fix)"]
El que un analitzador sí que pot saber:
- Que vas declarar
const visiblesi no la vas fer servir mai. - Que crides
taulr.resum()i aquest identificador no existeix a cap àmbit accessible. - Que una funció
asyncno conté capawait(probablement sobra l'async… o falta unawait). - Que un
cased'unswitchno tébreaki cau al següent (02-03). - Que compares amb
==en lloc de===(01-07). - Que hi ha una assignació dins d'un
if:if (estat = 'feta').
El que no pot saber:
- Si
horesObertesha de sumar sobre les obertes o sobre totes. Això és semàntica, i és exactament el cas 3 de la lliçó anterior. Per a això calen proves. - Si l'aplicació fa el que la Marta necessita.
- Si la teva API retornarà
idcom a cadena o com a número, llevat que l'hi diguis amb tipus.
És important tenir clara aquesta frontera: l'anàlisi estàtica i les proves automatitzades no competeixen, es complementen. La primera és barata, instantània i cobreix una família estreta d'errors; les segones costen d'escriure i cobreixen el comportament. Un projecte seriós té totes dues.
- ESLint: instal·lació i primera arrencada
ESLint és l'analitzador estàtic estàndard de l'ecosistema JavaScript. És configurable fins a l'últim detall i extensible amb plugins, i aquestes dues característiques expliquen tant la seva potència com la seva fama de "configuració complicada".
Nómada Tasques fins ara no tenia package.json: és un projecte de mòduls ES servits directament. El creem ara, perquè a partir d'aquí el projecte té eines:
cd nomada-tasques
npm init -y # crea package.json
npm install --save-dev eslint # ESLint només cal en desenvolupamentUn detall que convé fixar des del principi al package.json:
{
"name": "nomada-tasques",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"lint": "eslint ."
},
"devDependencies": {
"eslint": "^9.0.0"
}
}"type": "module"diu a Node que els.jsd'aquest projecte són mòduls ES (05-04), no CommonJS. Sense aquesta línia, qualsevol eina que executi el teu codi a Node es queixaria delsimport."private": trueevita publicar el paquet per accident a npm.devDependenciesen lloc dedependencies: ESLint no forma part de l'aplicació, només del procés de desenvolupament. El navegador no el descarrega mai.
En executar npm run lint sense configuració, ESLint avisa que no troba fitxer de configuració. Anem a escriure'l.
- El fitxer de configuració pla
L'ESLint modern fa servir la configuració plana (flat config): un fitxer eslint.config.js que exporta un array d'objectes de configuració. Cada objecte diu a quins fitxers s'aplica i quines regles hi regeixen. Els objectes posteriors se superposen als anteriors, com a capes.
// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
export default [
// ── Capa 0 · què NO s'analitza ────────────────────────────────────────
{
ignores: ['coverage/**', 'dist/**', 'node_modules/**']
},
// ── Capa 1 · base per a TOT el JavaScript del projecte ────────────────
js.configs.recommended,
// ── Capa 2 · el codi de l'aplicació: navegador + mòduls ES ────────────
{
files: ['js/**/*.js'],
languageOptions: {
ecmaVersion: 'latest', // sintaxi més recent (camps privats #, ?., ??=)
sourceType: 'module', // import/export, no require
globals: {
...globals.browser // window, document, fetch, localStorage, console…
}
},
rules: {
// aquí van les regles pròpies del projecte (apartat 7)
}
},
// ── Capa 3 · el service worker viu en UN ALTRE entorn global ──────────
{
files: ['sw.js'],
languageOptions: {
globals: { ...globals.serviceworker } // self, caches, clients… però NO document
}
},
// ── Capa 4 · fitxers d'eines que s'executen a Node ────────────────────
{
files: ['*.config.js', 'scripts/**/*.js'],
languageOptions: {
globals: { ...globals.node } // process, __dirname (si escau), console
}
}
];Cinc coses per entendre d'aquest fitxer, perquè són les que causen confusió:
- És JavaScript de debò, no JSON. Pots importar, calcular i compondre. Per això
js.configs.recommendedés un objecte que s'insereix a l'array. - L'ordre importa. Si dues capes defineixen la mateixa regla, guanya l'última. Per això la configuració recomanada va a dalt i els teus ajustos a sota.
filesdecideix l'abast. Un objecte sensefiless'aplica a tot. Això és el que permet la capa 3:sw.jsno tédocumentniwindow, i sí que téselficaches; declarar-ho evita centenars de falsosno-undef.ignoresen un objecte propi (sensefiles) actua com a ignorat global, l'equivalent de l'antic.eslintignore.globalsés un paquet auxiliar amb els catàlegs de variables globals de cada entorn. S'instal·la a part:npm i -D globals.
La capa 3 mereix un comentari més. El service worker que vas escriure a 07-05 s'executa en un fil diferent amb un objecte global diferent. Sense aquesta capa, ESLint marcaria self, caches i clients com a no definits, i —pitjor— no marcaria un ús accidental de document, que en un worker és un error real d'execució. Configurar bé els entorns no és burocràcia: és el que converteix no-undef en un detector d'errors de debò.
- Regles, nivells i configuracions recomanades
Cada regla d'ESLint es configura amb un nivell i, opcionalment, opcions:
rules: {
'no-unused-vars': 'error', // nivell simple
'no-console': ['warn', { allow: ['warn', 'error'] }], // nivell + opcions
'no-alert': 'off' // desactivada
}| Nivell | Valor numèric | Què passa | Codi de sortida |
|---|---|---|---|
'off' |
0 |
La regla no es comprova | — |
'warn' |
1 |
S'informa, però no falla | 0 (èxit) |
'error' |
2 |
S'informa i falla | 1 (fallada) |
La diferència entre warn i error és crítica tan bon punt afegeixes integració contínua: error trenca la construcció, warn no. D'aquí una estratègia molt pràctica:
errorper a tot el que sigui un error real o un risc:no-undef,eqeqeq,no-debugger.warnper al que és preferència o està en migració: regles que acabes d'activar sobre codi existent i que encara produeixen cent avisos.- I una regla d'higiene: els
warnno es poden acumular indefinidament. Si un avís hi és des de fa sis mesos, o s'arregla o s'apaga amb un motiu escrit. Un lint que imprimeix 300 avisos és un lint que ningú llegeix.
js.configs.recommended activa al voltant de seixanta regles que la comunitat considera imprescindibles i cap d'estil. És deliberat: des que Prettier existeix, ESLint ha anat retirant les regles de format de la seva recomanació. Aquesta divisió del treball és el tema de l'apartat 12.
globals: navegador, Node i proves
globals: navegador, Node i provesLa regla no-undef marca qualsevol identificador que no estigui declarat. És una de les més valuoses —hauria caçat un taulr.resum() a l'instant— però només funciona si ESLint sap quines globals són legítimes a cada fitxer.
import globals from 'globals';
// Navegador: window, document, fetch, localStorage, CustomEvent, AbortController…
globals.browser
// Service worker: self, caches, clients, skipWaiting…
globals.serviceworker
// Node: process, console, Buffer, URL…
globals.node
// Jest (el necessitaràs a 08-03): describe, test, expect, beforeEach…
globals.jestI si necessites declarar una global pròpia —per exemple, una constant que injecta el procés de desplegament— es fa amb el seu permís d'escriptura explícit:
Marcar-la com a 'readonly' té un efecte extra: la regla no-global-assign avisarà si algú intenta reassignar-la.
- Deu regles que eviten bugs reals
Aquestes són les que justifiquen tot el muntatge. No són qüestió de gust: cadascuna correspon a una família d'errors que ha costat tardes a algú.
| Regla | Què detecta | Exemple a Nómada Tasques |
|---|---|---|
no-unused-vars |
Variables, paràmetres i importacions sense fer servir | Un import { PESOS } que va quedar després d'una refactorització: codi mort que confon |
no-undef |
Identificadors no declarats | taulr.resum(), un AVUI sense importar |
eqeqeq |
== i != en lloc de === / !== |
Just la coerció de 01-07 que va emmascarar el cas 1 de 08-01 |
no-implicit-globals |
Declaracions que contaminen l'objecte global | Un var estat solt en un script clàssic |
require-await |
Funció async sense cap await a dins |
async function desar() que en realitat és síncrona: promet asincronia que no hi és |
no-return-await |
return await x innecessari dins d'un try… o fora d'ell |
Un fotograma de pila extra sense guany |
no-fallthrough |
Un case que cau al següent sense break |
El switch d'estats de 02-03 |
no-cond-assign |
Assignació dins d'una condició | if (tasca.estat = 'feta') — canvia l'estat i sempre és cert |
no-debugger |
La sentència debugger |
Exactament el que et vas deixar posat a 08-01 |
no-constant-condition |
Condicions sempre certes o sempre falses | `if (tasca.horesEstimades |
I una onzena que mereix explicació a part perquè és la més citada i la més malentesa: no-floating-promises.
Una promesa flotant és una promesa el resultat de la qual no recull ningú:
// Veus l'error?
function enPremerDesar() {
repositori.desar(tauler);
actualitzarTasca(tasca.id, { estat: 'feta' }); // ← retorna una promesa que ningú espera
mostrarAvis('Desat'); // ← menteix: encara no s'ha desat
}Si actualitzarTasca falla, el rebuig no el captura ningú: es converteix en un unhandledrejection (05-06) i l'usuari veu «Desat» sobre un canvi que no va arribar al servidor. És un error silenciós de la pitjor espècie.
El matís important: la regla no-floating-promises real requereix informació de tipus, i aquesta l'aporta TypeScript (apartat 18), no ESLint sobre JavaScript pur. En un projecte només-JavaScript es cobreix amb una combinació d'aproximacions (require-await, no-async-promise-executor, revisió) i, sobretot, amb la convenció d'equip: tota crida que retorni una promesa s'awaiteja o s'encadena amb un .catch() explícit. Escriure-ho a les convencions és la meitat de la feina; l'altra meitat és que TypeScript ho pugui comprovar, i aquí és on // @ts-check comença a resultar atractiu.
- Desactivar una regla sense fer trampes
Hi haurà casos legítims en què una regla s'equivoca. ESLint permet silenciar-la amb precisió quirúrgica:
// Només la línia següent
// eslint-disable-next-line no-console -- registre deliberat de diagnòstic (vegeu 08-01)
console.log('[nomada] mode diagnostic activat');
// Només aquesta línia, al final
const reserva = magatzemEnMemoria(); // eslint-disable-line no-unused-vars
// Un bloc complet
/* eslint-disable no-undef -- aquest fitxer s'executa dins del service worker */
self.addEventListener('install', …);
/* eslint-enable no-undef */Quatre normes d'higiene amb aquests comentaris:
- Anomena sempre la regla. Un
// eslint-disable-next-linea seques apaga totes les regles d'aquella línia, incloses les que encara no existeixen. - Escriu el motiu després de
--. ESLint admet aquesta sintaxi precisament per a això, i evita que d'aquí a un any ningú sàpiga si encara cal. - Prefereix
disable-next-lineadisablede bloc. Un bloc desactivat tendeix a créixer i a tapar coses noves. - Si la desactives en cinc llocs, la regla està mal configurada. Canvia-la a
eslint.config.jso apaga-la globalment amb un comentari que ho justifiqui. Cinc excepcions no són excepcions: són la norma real.
A més, l'opció reportUnusedDisableDirectives (activa per defecte a la configuració recomanada moderna) avisa quan un eslint-disable ja no cal perquè el codi ha canviat. És neteja automàtica de les teves pròpies excepcions.
--fix i els scripts del projecte
--fix i els scripts del projecteMoltes regles són autocorregibles: ESLint sap reescriure el codi per complir-les sense canviar-ne el significat.
npx eslint . # només informa
npx eslint . --fix # arregla el que pot i informa de la resta
npx eslint . --fix-dry-run --format json # simula, sense escriureQuè s'arregla sol i què no:
| Es corregeix automàticament | Requereix criteri humà |
|---|---|
| Cometes, punt i coma, sagnia | no-unused-vars (sobra la variable o falta fer-la servir?) |
== → === quan és segur |
no-undef (falta un import o hi ha una errada?) |
let → const si no es reassigna mai |
require-await (sobra l'async o falta un await?) |
| Ordre d'importacions (amb plugin) | Complexitat excessiva |
La regla mental: --fix resol la forma, mai la intenció. Tot el que impliqui decidir què volies fer es queda per a tu, i això és correcte.
Els scripts que tindrà el projecte a partir d'aquí:
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"verificar": "npm run lint && npm run format:check"
}
}verificar és el guió que executarà la integració contínua, i el que pots llançar abans de cada commit mentre t'hi acostumes.
- Plugins útils
Un plugin d'ESLint aporta regles noves. Aquests tres cobreixen necessitats reals d'un projecte com aquest:
Importacions. Un plugin d'importacions (eslint-plugin-import o el seu successor modern eslint-plugin-import-x) comprova el que el navegador no perdona en mòduls ES: rutes que no existeixen, extensions oblidades, dependències circulars i ordre dels import.
{
files: ['js/**/*.js'],
plugins: { import: importPlugin },
rules: {
'import/no-unresolved': 'error', // la ruta './model/tasca.js' existeix de debò
'import/extensions': ['error', 'always'], // al navegador l'extensió és OBLIGATÒRIA
'import/no-cycle': 'error', // A importa B, B importa A → ordre d'execució impredictible
'import/order': ['warn', { 'newlines-between': 'always' }]
}
}import/extensions en 'always' és especialment valuós aquí: els empaquetadors toleren import { Tasca } from '../model/tasca' sense extensió, però el navegador amb <script type="module"> no. Sense aquesta regla, l'error apareix en temps d'execució i només al navegador. I import/no-cycle protegeix el graf acíclic de dependències que vas dibuixar a 05-04.
Accessibilitat. El plugin d'accessibilitat més conegut (eslint-plugin-jsx-a11y) està pensat per a JSX, que no fas servir. En un projecte d'HTML i DOM la comprovació equivalent es fa amb altres eines: un validador d'HTML, l'auditoria d'accessibilitat de les DevTools, i sobretot axe, que a 08-06 integraràs a les proves d'extrem a extrem. Val la pena saber que existeix la família de regles i per què no encaixa aquí: triar un plugin perquè sona bé i descobrir que no analitza el teu tipus de fitxer és una pèrdua de tarda molt habitual.
Proves. eslint-plugin-jest detecta errors clàssics a les proves que escriuràs a la lliçó següent: un test sense cap asserció, un expect fora d'un test, un test.only oblidat que deixa la resta de la suite sense executar —potser l'error més perillós de tots, perquè la suite continua en verd mentre no comprova res—.
{
files: ['**/*.test.js'],
plugins: { jest: jestPlugin },
languageOptions: { globals: { ...globals.jest } },
rules: {
'jest/no-focused-tests': 'error', // test.only oblidat
'jest/no-disabled-tests': 'warn', // test.skip que fa mesos que hi és
'jest/expect-expect': 'error', // una prova sense assercions no prova res
'jest/valid-expect': 'error'
}
}
- Prettier: el format deixa de ser una opinió
Prettier no és un linter: és un formatador determinista. Descarta completament el format original del teu codi, el reconstrueix des del seu arbre de sintaxi i l'imprimeix seguint les seves pròpies regles. Dues conseqüències enormes:
- El resultat no depèn de com estigués escrit abans. Dues persones amb estils oposats produeixen fitxers idèntics byte a byte.
- Gairebé no cal configurar-lo. Prettier ofereix deliberadament poques opcions, perquè la discussió no es traslladi a la configuració.
// .prettierrc.json — la configuració completa d'un projecte com aquest
{
"semi": true,
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "none",
"arrowParens": "always",
"endOfLine": "lf"
}Comentari de cada opció, perquè són totes les que cal decidir:
semi: punt i coma al final. Posar-lo evita la classe sencera de sorpreses de la inserció automàtica (01-04).singleQuote: cometes simples, coherent amb tot el codi del curs.printWidth: 100: amplada màxima abans de partir. 80 és el clàssic; 100 respira millor en pantalles modernes i evita partir cadenes de.filter().map().reduce().trailingComma: coma final."none"manté l'estil del projecte;"all"produeix diffs més nets en afegir elements. Qualsevol de les dues val: el que no val és que cada fitxer en faci servir una.endOfLine: "lf": final de línia Unix. Sense això, un equip mixt Windows/macOS genera diffs complets per canvis invisibles.
I un fitxer d'exclusions:
Ús:
npx prettier --write . # formata tot el projecte
npx prettier --check . # només comprova; falla si alguna cosa no està formatada (per a CI)El moment d'adoptar-lo. Executar prettier --write . sobre un projecte existent genera un commit gegantí que toca tots els fitxers. Fes-ho en un commit aïllat que no canviï ni una línia de lògica, amb un missatge clar («format: aplicar Prettier a tot el projecte»). I afegeix el seu hash a un fitxer perquè git blame l'ignori:
echo "d4e5f6a7b8c9 # format: Prettier a tot el projecte" >> .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revsAmb això, git blame continuarà atribuint cada línia a qui en va escriure la lògica, no al commit de formatatge. És un detall petit que estalvia molta frustració futura.
- ESLint davant de Prettier, i com conviure
La confusió més habitual de l'ecosistema. Tots dos llegeixen el teu codi i tots dos el poden modificar, però responen preguntes diferents:
| ESLint | Prettier | |
|---|---|---|
| Pregunta que respon | Aquest codi és correcte i segur? | Aquest codi està ben imprès? |
| Tipus d'anàlisi | Semàntica: àmbits, flux, ús de variables | Sintàctica: reimprimeix l'AST |
| Exemple del que detecta | Variable sense fer servir, ==, debugger |
Línia de 180 caràcters, cometes barrejades |
| Configuració | Extensa: centenars de regles | Mínima i deliberadament limitada |
| Pot arreglar? | Algunes regles, amb --fix |
Tot, sempre |
| És opinable? | Sí, cada regla es discuteix | No: s'accepta el seu criteri i es deixa de discutir |
| Si el treus | Apareixen bugs | Apareixen discussions |
El conflicte i la seva solució. ESLint encara inclou algunes regles de format heretades (sagnia, cometes…). Si les actives, ESLint i Prettier poden demanar coses contràries i entraràs en un bucle: deses, Prettier formata, ESLint es queixa; arregles, Prettier ho desfà.
La solució estàndard és eslint-config-prettier: una configuració que apaga totes les regles d'ESLint que xoquen amb Prettier. Es col·loca l'última de l'array, perquè la seva desactivació guanyi:
// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
import prettier from 'eslint-config-prettier';
export default [
js.configs.recommended,
{ files: ['js/**/*.js'], languageOptions: { globals: globals.browser }, rules: { /* … */ } },
prettier // ← SEMPRE l'últim: desactiva el que xocaria
];Existeix també eslint-plugin-prettier, que executa Prettier dins d'ESLint i reporta cada diferència de format com un error. És còmode (una sola eina) però omple la sortida d'errors de format barrejats amb errors reals i alenteix l'anàlisi. La recomanació generalitzada és la separació neta: Prettier formata, ESLint analitza, eslint-config-prettier els manté als seus carrils.
flowchart LR
A["Deses el fitxer"] --> B["Prettier<br/>reimprimeix el format"]
B --> C["ESLint<br/>analitza la correcció"]
C --> D{"Hi ha errors?"}
D -->|No| E["Llest"]
D -->|Sí| F["Els arregles tu<br/>(o eslint --fix)"]
F --> C
- Integració a l'editor
Tot l'anterior es torna invisible —i per tant útil— quan l'editor ho aplica sol.
// .vscode/settings.json — es versiona al repositori: tot l'equip igual
{
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"eslint.useFlatConfig": true,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true
}Què fa cada línia: Prettier formata en desar; ESLint aplica les seves correccions automàtiques també en desar; els finals de línia i els espais sobrants es normalitzen. El resultat pràctic és que deixes de pensar en el format: escrius com et surti, deses i queda correcte.
I un .editorconfig per a qui faci servir un altre editor:
# .editorconfig
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = falseL'última secció importa: en Markdown, dos espais al final de línia signifiquen salt de línia, així que retallar-los trencaria el text.
- Hooks de Git amb Husky i lint-staged
L'editor cobreix qui el té ben configurat. El repositori necessita una garantia que no en depengui. Els hooks de Git són guions que Git executa en moments concrets; el que ens interessa és pre-commit, que corre abans de crear el commit i el pot avortar.
- Husky gestiona els hooks dins del repositori (Git no versiona
.git/hooks, així que sense una eina cada persona hauria d'instal·lar-los a mà). - lint-staged executa les eines només sobre els fitxers a l'àrea de preparació (staged). És el que fa la diferència entre un hook de dos segons i un de dos minuts.
// package.json
{
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.{json,css,md,html}": ["prettier --write"]
}
}El flux complet, pas a pas:
flowchart TD
A["git commit"] --> B["Husky executa<br/>.husky/pre-commit"]
B --> C["lint-staged pren<br/>NOMÉS els fitxers staged"]
C --> D["eslint --fix<br/>prettier --write"]
D --> E{"Queden errors<br/>sense corregir?"}
E -->|Sí| F["Commit AVORTAT<br/>amb el llistat"]
E -->|No| G["Els arranjaments es tornen a afegir<br/>a l'àrea de preparació"]
G --> H["Commit creat"]
Detall clau del pas G: lint-staged torna a afegir els fitxers que les seves eines han modificat, així que el commit conté el codi ja formatat. No has de fer res.
Dos advertiments imprescindibles:
- Un hook lent es desactiva. Si
pre-committriga trenta segons, algú començarà a fer servir--no-verifyi el guardià deixarà d'existir. Per això lint-staged només mira el que està preparat, i per això les proves completes no van apre-commit: van apre-pusho directament a integració contínua. --no-verifyexisteix i de vegades és legítim (un commit d'emergència a producció a les tres de la matinada). Precisament per això el mateix control s'ha de repetir al servidor, on ningú se'l pot saltar.
- Integració contínua amb GitHub Actions
La integració contínua (CI) executa les comprovacions en un servidor net, a cada push i a cada pull request. És l'única capa que ningú pot eludir, i a més elimina el clàssic «a la meva màquina funciona»: el servidor arrenca de zero, instal·la exactament el que diu el fitxer de bloqueig i executa el mateix per a tothom.
# .github/workflows/qualitat.yml
name: Qualitat
# Quan s'executa
on:
push:
branches: [master]
pull_request:
jobs:
verificar:
runs-on: ubuntu-latest # màquina virtual neta per a cada execució
steps:
# 1 · Portar el codi del repositori a la màquina
- name: Descarregar el codi
uses: actions/checkout@v4
# 2 · Instal·lar Node. 'cache: npm' reaprofita les dependències entre execucions
- name: Preparar Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: npm
# 3 · Instal·lació reproduïble: respecta package-lock.json al peu de la lletra
- name: Instal·lar dependències
run: npm ci
# 4 · Anàlisi estàtica. Falla si hi ha errors (nivell 'error')
- name: Analitzar amb ESLint
run: npm run lint
# 5 · Format. No formata: comprova. Falla si alguna cosa no està formatada
- name: Comprovar el format
run: npm run format:checkPunts que convé entendre d'aquest flux:
npm cien lloc denpm install.ciesborranode_modules, instal·la exactament les versions delpackage-lock.jsoni falla si ellockno concorda amb elpackage.json. És determinista;installpot actualitzar ellocksobre la marxa.node-version: lts/*fa servir la versió de suport prolongat vigent, sense fixar un número que quedarà obsolet. Si el teu projecte necessita una versió concreta, declara-la al campenginesdelpackage.jsoni reflecteix-la aquí.format:check, noformat. En CI no es modifica mai el codi: es comprova i es falla. Corregir és feina de l'autor, a la seva màquina.on: pull_requestés el que fa aparèixer la marca verda o vermella a la proposta de canvis, abans de fusionar.
Quan arribis a 08-03 afegiràs un pas npm test a aquest mateix flux, i a 08-06 un altre amb les proves d'extrem a extrem. L'estructura ja està a punt.
Les tres capes de defensa, ordenades per rapidesa de resposta:
| Capa | Quan actua | Es pot eludir | Cost |
|---|---|---|---|
| Editor | En desar | Sí (no instal·lar l'extensió) | 0 s |
| Hook de Git | En fer commit | Sí (--no-verify) |
1-3 s |
| Integració contínua | En fer push / PR | No | 30-90 s |
Les tres són la mateixa comprovació repetida, i aquesta redundància és deliberada: com més aviat falla, més barat és arreglar-ho.
- Convencions que cap eina no pot imposar
Automatitzats el format i l'anàlisi, queda el que exigeix criteri. Aquestes són les convencions que mereixen escriure's en un CONTRIBUTING.md del projecte.
Noms (reprenent 01-04). El nom és la documentació que no es desactualitza mai perquè es llegeix a cada ús.
| Element | Convenció | A Nómada Tasques |
|---|---|---|
| Variables i funcions | camelCase, en català, verb si actua |
horesObertes, crearBacklog(), pintarTargeta() |
| Classes | PascalCase, substantiu |
Tasca, Tauler, RepositoriLocal, ErrorDeApi |
| Constants de mòdul | MAJUSCULES_AMB_GUIO |
AVUI, PESOS, TRANSICIONS, ESDEVENIMENTS |
| Camps privats | # al davant (05-03) |
#estat, #tasques, #magatzem |
| Booleans | Prefix interrogatiu | oberta, estaVencuda(), persistent |
| Fitxers | kebab-case.js, singular si exporta una cosa |
repositori-local.js, tauler-vista.js |
| Gestors | en + esdeveniment, o gestionar + cosa |
enCrear, gestionarClic |
I tres antinoms que cal erradicar: dades, info, gestionar. No diuen res. dades pot ser qualsevol cosa; tasquesPendents diu exactament què és.
Mida de funcions. No hi ha un número màgic, però sí una prova fiable: si necessites un comentari per separar dues parts d'una funció, aquestes dues parts són dues funcions. Com a referència pràctica: per damunt de 30 línies convé mirar-la amb desconfiança, per damunt de 50 gairebé segur que fa dues coses. I el criteri decisiu no és la longitud sinó el nivell d'abstracció: una funció que barreja decidir què renderitzar amb com escriure-ho al DOM ja està malament, tingui deu línies o cent.
Comentaris que expliquen el perquè. El comentari que repeteix el que diu el codi és soroll que a més envelleix malament:
// ❌ Soroll: repeteix el codi
// Incrementa el comptador en un
comptador += 1;
// ❌ Pitjor: menteix, perquè el codi va canviar i el comentari no
// Retorna les tasques ordenades per data
return tasques.sort((a, b) => PESOS[b.prioritat] - PESOS[a.prioritat]);
// ✅ Útil: explica una decisió que el codi no pot contar
// Retornem instàncies noves a cada crida perquè dos consumidors
// (l'aplicació i les proves) no comparteixin estat per accident.
export function crearBacklog() { … }
// ✅ Útil: documenta una restricció externa
// El navegador NO distingeix xarxa caiguda de CORS bloquejat: totes dues arriben com a
// TypeError, deliberadament, per no filtrar informació sobre altres dominis.
throw new ErrorDeApi('No s ha pogut contactar amb el servidor.', { codi: 'xarxa' });
// ✅ Útil: avisa d'un parany
// dataset SEMPRE retorna cadenes; sense Number() el === de cercarPerId falla.
const id = Number(li.dataset.id);La regla: el codi diu què fa; el comentari diu per què és així i no d'una altra manera. Si necessites un comentari per explicar què fa, normalment l'arranjament és reanomenar o extreure una funció, no comentar.
- Documentar tipus amb JSDoc
JSDoc documenta tipus amb comentaris estructurats. En JavaScript pur aporta dues coses immediates: autocompletat i avisos a l'editor (que entén JSDoc de manera nativa), i documentació que es llegeix sense sortir del fitxer.
/**
* Filtra i ordena les tasques per a la seva presentació, sense modificar el tauler.
*
* @param {import('../model/tauler.js').Tauler} tauler Font de dades
* @param {object} opcions
* @param {string|null} [opcions.responsable] Nom exacte, o null per no filtrar
* @param {string} [opcions.text=''] Cerca per títol, sense distingir majúscules
* @param {'prioritat'|'data'|'hores'} [opcions.ordre='prioritat']
* @returns {import('../model/tasca.js').Tasca[]} Array NOU; el tauler no es toca
* @throws {ErrorDeValidacio} Si `ordre` no és un dels valors admesos
*
* @example
* tasquesVisibles(tauler, { responsable: 'Iván' }); // → 3 tasques, 25 h
*/
export function tasquesVisibles(tauler, { responsable = null, text = '', ordre = 'prioritat' } = {}) {
// …
}I per a tipus que es repeteixen, @typedef els defineix una vegada:
/**
* @typedef {object} ResumTauler
* @property {number} total Totes les tasques del tauler
* @property {number} obertes Les que no estan en estat 'feta'
* @property {number} horesTotals Suma d'horesEstimades de totes
* @property {number} horesObertes Suma d'horesEstimades de les obertes
* @property {number} vencudes Obertes amb dataLimit passada (R10)
* @property {number} esforc Suma d'hores × pes de prioritat
*/
/**
* @param {string} avui Data ISO de referència
* @returns {ResumTauler}
*/
resum(avui) { … }Aquest @typedef documenta d'una vegada els números canònics del projecte i fa que l'editor autocompleti resum.horesObertes amb la seva descripció al costat. En un projecte de sis capes, això val més que qualsevol document extern, perquè viu al costat del codi i s'actualitza amb ell.
Consell de dosificació: documenta amb JSDoc les funcions exportades —la superfície pública de cada mòdul— i deixa les internes amb un bon nom. Documentar-ho tot produeix fitxers on hi ha més comentari que codi i ningú no en llegeix cap.
// @ts-check: comprovació de tipus sense TypeScript
// @ts-check: comprovació de tipus sense TypeScriptAquí hi ha el següent esglaó, i es pot pujar sense reescriure res. El compilador de TypeScript sap analitzar fitxers .js fent servir la informació de JSDoc. S'activa amb un comentari a la primera línia:
// @ts-check
import { Tasca } from './tasca.js';
/** @param {number} id */
export function cercar(id) { … }
cercar('7');
// ~~~ Argument of type 'string' is not assignable to parameter of type 'number'.Aquest avís és exactament el cas 1 de la lliçó anterior, detectat a l'editor, sense executar res, sense obrir el navegador i sense que la Marta hagi de reportar res. Un error que va costar una investigació completa hauria estat un subratllat vermell mentre s'escrivia.
Per activar-lo a tot el projecte sense posar el comentari fitxer a fitxer:
// jsconfig.json
{
"compilerOptions": {
"checkJs": true,
"strict": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true
},
"include": ["js/**/*.js"]
}"noEmit": true és l'essencial: TypeScript no genera cap fitxer, només comprova. El teu codi continua sent JavaScript executable tal qual, servit directament al navegador.
La comparació honesta:
JavaScript + JSDoc + @ts-check |
TypeScript | |
|---|---|---|
| Compilació necessària | No | Sí |
| Cobertura de tipus | Bona, una mica limitada en casos avançats | Completa |
| Verbositat | Alta (comentaris llargs) | Baixa (sintaxi nativa) |
| Cost d'adopció | Molt baix, fitxer a fitxer | Mitjà-alt |
Regles com no-floating-promises |
Disponibles amb el plugin de tipus | Disponibles |
Per a Nómada Tasques, @ts-check és l'opció sensata: zero canvis al desplegament i una xarxa que caça tota la família d'errors de tipus. TypeScript sencer és una decisió de projecte que es tracta a Següents Passos, on se situa dins del mapa complet del que ve després d'aquest curs.
- Mètriques de qualitat amb cap
Existeixen mètriques numèriques de qualitat, i convé conèixer-ne dues.
Complexitat ciclomàtica. Compta els camins independents d'execució d'una funció: 1 de base, +1 per cada if, else if, case, for, while, catch, &&, || i ?:. És, gairebé literalment, el nombre mínim de proves necessàries per recórrer totes les branques, i per això interessa aquí.
// Complexitat 1: un sol camí
export function marcaEstat(estat) {
return MARQUES[estat] ?? '?';
}
// Complexitat 5: quatre decisions + base
function validarFormulari(formulari, dades, avui) {
const errors = [];
for (const camp of formulari.elements) { // +1
if (camp.willValidate && !camp.checkValidity()) { // +1 (if) +1 (&&)
errors.push({ camp, missatge: camp.validationMessage });
}
}
if (dades.dataLimit < avui) errors.push(…); // +1
if (dades.etiquetes.length > 5) errors.push(…); // +1
return errors;
}ESLint la mesura amb la regla complexity:
rules: {
complexity: ['warn', { max: 10 }],
'max-depth': ['warn', 4], // imbricació de blocs
'max-lines-per-function': ['warn', { max: 60, skipComments: true, skipBlankLines: true }]
}Com a referència orientativa: per sota de 10, còmoda; entre 10 i 20, mirar si es pot dividir; per damunt de 20, gairebé segur que hi ha dues funcions a dins.
Deute tècnic. És la metàfora, no una mètrica: les dreceres d'avui es paguen amb interessos demà en forma de temps de desenvolupament. Algunes eines l'expressen en hores estimades d'arranjament. Aquest número és una estimació d'una estimació; serveix per comparar l'evolució d'un mateix projecte en el temps, no per comparar projectes ni per presumir.
I l'advertiment que dóna títol a l'apartat: no caiguis en el fetitxisme dels números. Tres patologies reals:
- Perseguir la mètrica en lloc de l'objectiu. Baixar la complexitat partint una funció en quatre trossos incoherents empitjora el codi i millora el número.
- Confondre "sense avisos" amb "ben fet". El cas 3 de 08-01 —
horesObertessumant 48 en lloc de 45— passava totes les regles d'ESLint. Un lint net no diu res sobre si el programa és correcte. - Fer servir les mètriques per avaluar persones. Tan bon punt un número es converteix en objectiu, deixa de ser una bona mesura (llei de Goodhart). Les mètriques són un termòmetre del codi, no una nota.
- Nómada Tasques: configurar-ho tot i arreglar els avisos
Muntem la configuració completa i veiem què troba al codi real.
npm install --save-dev eslint @eslint/js globals eslint-config-prettier prettier husky lint-staged
npx husky init// eslint.config.js — configuració completa del projecte
import js from '@eslint/js';
import globals from 'globals';
import prettier from 'eslint-config-prettier';
export default [
{ ignores: ['node_modules/**', 'coverage/**', 'dist/**'] },
js.configs.recommended,
// L'aplicació: navegador + mòduls ES
{
files: ['js/**/*.js'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: { ...globals.browser }
},
rules: {
// — Correcció —
eqeqeq: ['error', 'always', { null: 'ignore' }], // permet `x == null` (null i undefined alhora)
'no-undef': 'error',
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-implicit-globals': 'error',
'require-await': 'error',
'no-cond-assign': ['error', 'always'],
'no-constant-condition': 'error',
'no-fallthrough': 'error',
// — Higiene —
'no-debugger': 'error', // ← el de 08-01
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'warn',
// — Mida —
complexity: ['warn', { max: 12 }],
'max-depth': ['warn', 4]
}
},
// El service worker: un altre global, altres regles
{
files: ['sw.js'],
languageOptions: { globals: { ...globals.serviceworker } },
rules: { 'no-console': 'off' } // en un SW, la consola és l'única finestra
},
// Eines que corren a Node
{
files: ['*.config.js', 'scripts/**/*.js'],
languageOptions: { sourceType: 'module', globals: { ...globals.node } },
rules: { 'no-console': 'off' }
},
prettier // ← l'últim, sempre
];I la primera execució sobre el codi dels Mòduls 1 a 7:
$ npm run lint
/nomada-tasques/js/vista/tauler-vista.js
14:10 error 'PESOS' is defined but never used no-unused-vars
96:5 warning Unexpected console statement no-console
/nomada-tasques/js/model/tauler.js
52:9 error Unexpected 'debugger' statement no-debugger
/nomada-tasques/js/dades/api-tasques.js
61:9 error Expected '===' and instead saw '==' eqeqeq
88:1 error Async method 'esborrarTasca' has no 'await' require-await
/nomada-tasques/js/vista/controlador.js
38:15 error 'tasca' is not defined no-undef
/nomada-tasques/sw.js
23:3 warning Unexpected console statement no-console
✖ 7 problems (5 errors, 2 warnings)
1 error and 0 warnings potentially fixable with the `--fix` option.Anem un per un, perquè cada arranjament té el seu matís:
1 · 'PESOS' is defined but never used. Import orfe d'una refactorització. S'esborra la línia. Cost: zero. Benefici: qui llegeixi el fitxer no buscarà on es fa servir un pes que ja no es fa servir.
2 · Unexpected 'debugger' statement. El debugger de la lliçó anterior, que anava camí del repositori. Aquest és l'avís que paga tota la configuració ell sol: un debugger a producció congela l'aplicació a qualsevol amb les DevTools obertes.
3 · Expected '===' and instead saw '=='.
// Abans
if (resposta.status == 204) return null;
// Després
if (resposta.status === 204) return null;Aquí no hi havia un error real (status sempre és número), però la regla és de les que no admet excepcions cas a cas: mantenir == al codi obliga a raonar cada vegada si la coerció és segura. --fix ho corregeix sol.
4 · Async method 'esborrarTasca' has no 'await'. Aquest és un error de debò:
// Abans — l'async és una mentida: l'error de xarxa no el captura ningú aquí
export async function esborrarTasca(id) {
fetch(construirUrl(`/tasques/${id}`), { method: 'DELETE' }); // ← promesa flotant
return true; // ← menteix sempre
}
// Després
export async function esborrarTasca(id) {
const resposta = await fetch(construirUrl(`/tasques/${id}`), { method: 'DELETE' });
await comprovar(resposta);
return true;
}La versió original retornava true abans que el servidor contestés. Si l'esborrat fallava amb un 403, la interfície eliminava la targeta igualment i l'error es perdia com a unhandledrejection. Una regla de tres paraules ha trobat un error de coherència de dades.
5 · 'tasca' is not defined. Una variable que es va reanomenar a la meitat de les aparicions:
// Abans
const tascaPremuda = tauler.cercarPerId(id);
if (tasca.estat === 'feta') return; // ← 'tasca' ja no existeix: ReferenceErrorAquest codi llançava ReferenceError en execució, però només a la branca que gairebé mai es recorre. ESLint ho veu sense executar res. És l'exemple perfecte de per què no-undef mereix nivell error.
6 i 7 · no-console. Registres de depuració de 08-01. Els del service worker estan permesos per la capa 3 (allà la consola és l'única finestra). Els de la vista se substitueixen pel registrar() de l'exercici 2 de la lliçó anterior, que respecta nivells i no embruta la consola a producció.
Resultat després dels arranjaments:
$ npm run lint && npm run format:check
Checking formatting...
All matched files use Prettier code style!Cinc errors, dels quals dos eren errors reals que ningú havia notat: un esborrat que mentia sobre el seu resultat i un ReferenceError latent. Cap de les dues hauria aparegut en una revisió ràpida, i totes dues costaven menys d'un minut de configuració.
Errors Habituals i Consells
- Activar centenars de regles de cop en un projecte existent. Surten 400 avisos, ningú se'ls mira i l'eina perd tota la credibilitat. Comença per
recommendedmés les deu regles de l'apartat 7, deixa la resta enwarni puja el llistó quan el terra estigui net. - Oblidar
eslint-config-prettier, o posar-lo abans de les teves regles. Va l'últim de l'array. Si va abans, les teves regles de format el trepitgen i torna el bucle de desar-formatar-queixar-se. - No configurar
globalsper entorn. Sense això,no-undefprodueix falsos positius asw.js(ambselficaches) i als fitxers de proves (ambdescribeiexpect), i la reacció típica —desactivar la regla— desarma el detector més útil que tens. - Oblidar l'extensió als
import. Els empaquetadors ho perdonen; el navegador de 05-04 no. Activa-ho ambimport/extensionsen'always'. // eslint-disable-next-linesense nom de regla ni motiu. Apaga totes les regles d'aquella línia, incloses les futures, i ningú sabrà si encara és necessari.- Barrejar el commit de formatatge amb canvis de lògica. El diff es torna il·legible i la revisió es fa impossible. Format en un commit aïllat, i el seu hash a
.git-blame-ignore-revs. - Hooks de Git tan lents que la gent fa servir
--no-verify. El pre-commit només ha de mirar els fitxers preparats. Les proves completes van apre-pusho a CI. - Creure que un lint net significa que el codi funciona. El cas 3 de 08-01 passava totes les regles. L'anàlisi estàtica comprova la forma; el comportament es comprova amb proves, i això comença a la lliçó següent.
- Consell: fixa les versions i fes servir
npm cia CI. Una actualització automàtica d'ESLint pot activar regles noves i posar en vermell un repositori que ningú ha tocat. - Consell:
npx eslint . --max-warnings=0converteix els avisos en errors per a CI. És la manera d'impedir que elswarns'acumulin sense haver de pujar-los tots aerrora l'editor. - Consell: escriu les convencions a
CONTRIBUTING.md. El que no està escrit es discuteix a cada revisió; el que està escrit se cita en una línia.
Exercicis
Exercici 1 — Configuració completa per entorns.
Escriu l'eslint.config.js de Nómada Tasques contemplant cinc entorns diferents: (a) js/**/*.js com a mòduls ES de navegador; (b) sw.js com a service worker; (c) **/*.test.js amb les globals de Jest i les regles del plugin de proves; (d) cypress/**/*.js amb cy, Cypress, describe i it com a globals de només lectura; (e) els fitxers de configuració a l'arrel, que corren a Node. Justifica en comentaris per què cada entorn necessita la seva capa i quin fals positiu evita.
Exercici 2 — Caçar els errors amb anàlisi estàtica.
Per a cadascun d'aquests fragments, indica quina regla el detecta, si --fix el pot arreglar, i quina és la correcció correcta:
// A
export async function sincronitzar(tauler) {
repositori.desar(tauler);
return { ok: true };
}
// B
function seguentEstat(estat) {
switch (estat) {
case 'pendent':
return 'en-curs';
case 'en-curs':
registrar('info', 'tancant');
case 'feta':
return null;
}
}
// C
if (tasca.horesEstimades = 0) {
throw new ErrorDeValidacio('Hores invalides', 'horesEstimades', 0);
}
// D
const obertes = tauler.obertes;
const tancades = tauler.tasques.filter((t) => t.estat == 'feta');
return tancades.length;Exercici 3 — Documentar el mòdul amb JSDoc i activar @ts-check.
Pren js/model/tauler.js i afegeix-hi documentació JSDoc completa: un @typedef per a ResumTauler amb els sis camps, tipus per a tots els mètodes públics (afegir, canviarEstat, filtrar, resum, horesPerResponsable), @throws on correspongui i un @example amb els números canònics del backlog. Activa // @ts-check al fitxer i descriu quin error assenyalaria l'editor en cadascun d'aquests tres usos incorrectes: tauler.afegir({ id: 7, titol: 'X' }), tauler.canviarEstat('3', 'feta') i tauler.resum().
Solucions
Solució 1
// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
import prettier from 'eslint-config-prettier';
import jest from 'eslint-plugin-jest';
export default [
{ ignores: ['node_modules/**', 'coverage/**', 'dist/**', 'cypress/videos/**', 'cypress/screenshots/**'] },
js.configs.recommended,
// (a) L'aplicació. Globals del navegador: sense elles, `document` i `fetch`
// dispararien no-undef a cada fitxer de vista i de dades.
{
files: ['js/**/*.js'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: { ...globals.browser }
},
rules: {
eqeqeq: ['error', 'always', { null: 'ignore' }],
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'no-implicit-globals': 'error',
'require-await': 'error',
'no-debugger': 'error',
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'no-var': 'error',
complexity: ['warn', { max: 12 }]
}
},
// (b) Service worker. Global diferent: existeix `self`, `caches`, `clients`;
// NO existeix `document`. Declarar-ho aquí fa que un ús accidental de
// `document` al SW sí que es marqui com a error, que és el que volem.
{
files: ['sw.js'],
languageOptions: { sourceType: 'module', globals: { ...globals.serviceworker } },
rules: { 'no-console': 'off' }
},
// (c) Proves. Sense globals.jest, `describe`, `test` i `expect` serien no-undef
// a cada fitxer, i la reacció típica (apagar no-undef) desarmaria la regla.
{
files: ['**/*.test.js', 'proves/**/*.js'],
plugins: { jest },
languageOptions: { globals: { ...globals.jest, ...globals.node } },
rules: {
'jest/no-focused-tests': 'error', // un test.only oblidat deixa la suite sense executar
'jest/no-disabled-tests': 'warn',
'jest/expect-expect': 'error',
'jest/valid-expect': 'error',
'no-console': 'off' // en una prova, un console puntual és acceptable
}
},
// (d) Cypress. `cy` i `Cypress` són globals injectades per l'executor;
// a més el codi de les proves E2E corre al navegador.
{
files: ['cypress/**/*.js'],
languageOptions: {
globals: {
...globals.browser,
cy: 'readonly',
Cypress: 'readonly',
describe: 'readonly',
it: 'readonly',
beforeEach: 'readonly',
expect: 'readonly'
}
},
rules: { 'no-unused-expressions': 'off' } // l'estil .should() el dispara en fals
},
// (e) Configuració i scripts: s'executen a Node, no al navegador.
// `process` i `console` són legítims aquí i no ho són a js/**.
{
files: ['*.config.js', 'scripts/**/*.js'],
languageOptions: { sourceType: 'module', globals: { ...globals.node } },
rules: { 'no-console': 'off' }
},
prettier
];Solució 2
| Cas | Regla | --fix? |
Correcció |
|---|---|---|---|
| A | require-await |
No | La funció és async sense await: o sobra l'async, o falta esperar. Aquí falta: await repositori.desar(tauler) si és asíncron, o treure l'async. L'error real és que { ok: true } es retorna sense garantia de desat |
| B | no-fallthrough |
No | Després de registrar(...) falta un return o un break: 'en-curs' cau a 'feta' i retorna null en lloc de l'estat següent. A més, falta un default (regla default-case) |
| C | no-cond-assign |
No | És = en lloc de ===. La condició assigna 0 a horesEstimades (passant pel setter, que a més llançaria per R3) i avalua a 0, que és fals: la validació no es dispara mai. Correcte: if (tasca.horesEstimades === 0) |
| D | eqeqeq + no-unused-vars |
Sí per a eqeqeq |
t.estat == 'feta' → ===; i obertes està declarada i no es fa servir: s'esborra la línia |
// A · corregit
export async function sincronitzar(tauler) {
await repositori.desar(tauler);
return { ok: true };
}
// B · corregit
function seguentEstat(estat) {
switch (estat) {
case 'pendent':
return 'en-curs';
case 'en-curs':
registrar('info', 'tancant');
return 'feta';
case 'feta':
return null;
default:
throw new ErrorDeValidacio(`Estat desconegut: "${estat}".`, 'estat', estat);
}
}
// C · corregit
if (tasca.horesEstimades === 0) { … }
// D · corregit
return tauler.tasques.filter((t) => t.estat === 'feta').length;Solució 3
// @ts-check
import { Tasca } from './tasca.js';
import { ErrorDeValidacio } from './errors.js';
/**
* Xifres agregades del tauler en una data de referència.
*
* @typedef {object} ResumTauler
* @property {number} total Totes les tasques del tauler
* @property {number} obertes Les que no estan en estat 'feta'
* @property {number} horesTotals Suma d'horesEstimades de totes
* @property {number} horesObertes Suma d'horesEstimades de les obertes
* @property {number} vencudes Obertes amb dataLimit passada (R10)
* @property {number} esforc Suma d'hores × pes de prioritat
*/
export class Tauler {
/** @type {Tasca[]} */
#tasques = [];
/**
* @param {string} nom
* @param {Tasca[]} [tasques=[]] S'afegeixen una a una, aplicant R1
*/
constructor(nom, tasques = []) { … }
/**
* Afegeix una tasca al tauler.
* @param {Tasca} tasca Instància de Tasca, no un objecte pla
* @returns {Tauler} el mateix tauler, per encadenar
* @throws {ErrorDeValidacio} si no és una Tasca, o si l'id ja existeix (R1)
*/
afegir(tasca) { … }
/**
* Aplica una transició d'estat a una tasca del tauler.
* @param {number} id Identificador numèric de la tasca
* @param {'pendent'|'en-curs'|'feta'} nou
* @returns {Tauler}
* @throws {ErrorDeValidacio} si la tasca no existeix o la transició viola R6
*/
canviarEstat(id, nou) { … }
/**
* @param {(tasca: Tasca) => boolean} predicat
* @returns {Tasca[]} array nou; el tauler no es modifica
*/
filtrar(predicat) { … }
/**
* @param {string} avui Data ISO 'yyyy-MM-dd' de referència
* @returns {ResumTauler}
*
* @example
* const tauler = new Tauler('Taller Nómada', crearBacklog());
* tauler.resum('2026-09-20');
* // { total: 6, obertes: 5, horesTotals: 48,
* // horesObertes: 45, vencudes: 1, esforc: 124 }
*/
resum(avui) { … }
/**
* Hores obertes agrupades per persona. Les tasques sense responsable (R8)
* s'agrupen sota la clau 'sense assignar'.
* @returns {Record<string, number>}
*
* @example
* tauler.horesPerResponsable(); // { Iván: 25, Lucía: 14, Marta: 6 }
*/
horesPerResponsable() { … }
}Els tres errors que assenyalaria l'editor amb @ts-check:
1) tauler.afegir({ id: 7, titol: 'X' })
Argument of type '{ id: number; titol: string; }' is not assignable to
parameter of type 'Tasca'. Type is missing the following properties: estat,
oberta, esforc, canviarEstat…
→ És exactament el que comprova R1 en execució, però abans d'executar.
2) tauler.canviarEstat('3', 'feta')
Argument of type 'string' is not assignable to parameter of type 'number'.
→ El cas 1 de 08-01 (l'id que arribava com a cadena), detectat a l'editor.
3) tauler.resum()
Expected 1 arguments, but got 0.
→ Sense data, `estaVencuda` rebria undefined i `vencudes` donaria 0 en silenci:
un error mut, del mateix tipus que el comptador descompensat del cas 3.Conclusió
Nómada Tasques ha deixat de dependre de la disciplina individual. Saps què és un analitzador estàtic i on és la seva frontera: pot afirmar que una variable no es fa servir, que un identificador no existeix, que un async no espera res o que hi ha una assignació dins d'un if; no pot saber si horesObertes ha de sumar 45 o 48. Per això l'anàlisi estàtica i les proves no competeixen: cobreixen famílies diferents d'errors, i un projecte seriós té totes dues.
Tens ESLint configurat de debò: un eslint.config.js pla per capes, amb files delimitant cada entorn —l'aplicació al navegador, el service worker amb el seu global propi, Node per a les eines—, globals ben declarats perquè no-undef sigui un detector real i no una font de falsos positius, els tres nivells (off/warn/error) fets servir amb criteri, i les deu regles que eviten bugs reals: no-unused-vars, no-undef, eqeqeq, no-implicit-globals, require-await, no-fallthrough, no-cond-assign, no-debugger i les altres. Saps silenciar una regla anomenant-la i justificant-la, saps què arregla --fix i què no —la forma sí, la intenció mai—, i coneixes els plugins que aporten valor: importacions (amb import/extensions en 'always', imprescindible al navegador, i import/no-cycle per protegir el graf de 05-04), proves (amb no-focused-tests caçant el test.only oblidat) i per què el plugin d'accessibilitat de JSX no encaixa en un projecte de DOM pur.
Tens Prettier com a formatador determinista i entens per què no competeix amb ESLint sinó que es reparteixen la feina: un respon «està ben imprès?» i l'altre «és correcte?». Saps ajuntar-los sense bucles amb eslint-config-prettier col·locat l'últim, adoptar-lo en un commit aïllat i neutralitzar-lo a git blame. I tens les tres capes de defensa muntades: l'editor formatant i corregint en desar, un hook de pre-commit amb Husky i lint-staged que només mira els fitxers preparats perquè ningú senti la temptació de --no-verify, i un flux de GitHub Actions amb npm ci, npm run lint i npm run format:check que ningú pot eludir, llest per rebre el pas npm test de la propera lliçó.
I tens el que cap eina no et pot donar: les convencions. Noms que documenten (horesObertes, estaVencuda(), RepositoriLocal, #estat) i els antinoms que cal erradicar; el criteri de mida de funció —si necessites un comentari per separar dues parts, són dues funcions—; comentaris que expliquen el perquè i no repeteixen el què; JSDoc documentant la superfície pública de cada mòdul amb @typedef ResumTauler recollint els números canònics; // @ts-check amb jsconfig.json per caçar l'id que arriba com a cadena mentre l'escrius, sense compilar ni canviar el desplegament; i mètriques —complexitat ciclomàtica, deute tècnic— fetes servir com a termòmetre i mai com a objectiu. L'execució real va trobar set problemes i dos eren errors genuïns: un esborrarTasca que retornava true sense esperar el servidor i un ReferenceError latent en una branca poc recorreguda.
Però fixa't en el que cap d'aquelles set línies no va esmentar: que el resum del tauler ha de donar 45 hores obertes de 48, que la transició feta → en-curs està prohibida per R6, que una tasca sense títol ha de llançar ErrorDeValidacio, que el backlog canònic té un esforç ponderat de 124. Això no és forma, és comportament, i cap regla estàtica no ho pot comprovar: cal executar el codi amb entrades conegudes i comparar la sortida amb l'esperada. Això és una prova automatitzada, i cap allà van els tres deutes que vas deixar apuntats a la lliçó anterior. A Proves Unitàries amb Jest muntaràs l'executor, escriuràs la bateria completa de Tasca i Tauler —i descobriràs que aquella insistència de 03-03 en les funcions pures i aquella frontera neta entre model i vista que portes sis mòduls mantenint eren, des del principi, el que faria possible provar-ho tot sense obrir un navegador—.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
