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

  1. Què costa no tenir eines de qualitat
  2. Anàlisi estàtica: què pot saber una màquina sense executar res
  3. ESLint: instal·lació i primera arrencada
  4. El fitxer de configuració pla
  5. Regles, nivells i configuracions recomanades
  6. globals: navegador, Node i proves
  7. Deu regles que eviten bugs reals
  8. Desactivar una regla sense fer trampes
  9. --fix i els scripts del projecte
  10. Plugins útils
  11. Prettier: el format deixa de ser una opinió
  12. ESLint davant de Prettier, i com conviure
  13. Integració a l'editor
  14. Hooks de Git amb Husky i lint-staged
  15. Integració contínua amb GitHub Actions
  16. Convencions que cap eina no pot imposar
  17. Documentar tipus amb JSDoc
  18. // @ts-check: comprovació de tipus sense TypeScript
  19. Mètriques de qualitat amb cap
  20. Nómada Tasques: configurar-ho tot i arreglar els avisos
  21. Errors Habituals i Consells
  22. Exercicis
  23. Conclusió

  1. 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'if està a 4 i fem servir 2. I crec que estat no 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.

  1. 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 que pot saber:

  • Que vas declarar const visibles i no la vas fer servir mai.
  • Que crides taulr.resum() i aquest identificador no existeix a cap àmbit accessible.
  • Que una funció async no conté cap await (probablement sobra l'async… o falta un await).
  • Que un case d'un switch no té break i 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 horesObertes ha 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à id com 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.

  1. 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 desenvolupament

Un 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 .js d'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 dels import.
  • "private": true evita publicar el paquet per accident a npm.
  • devDependencies en lloc de dependencies: 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.

  1. 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.
  • files decideix l'abast. Un objecte sense files s'aplica a tot. Això és el que permet la capa 3: sw.js no té document ni window, i sí que té self i caches; declarar-ho evita centenars de falsos no-undef.
  • ignores en un objecte propi (sense files) 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ò.

  1. 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:

  • error per a tot el que sigui un error real o un risc: no-undef, eqeqeq, no-debugger.
  • warn per 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 warn no 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.

  1. globals: navegador, Node i proves

La 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.jest

I 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:

globals: {
  ...globals.browser,
  __VERSIO_APP__: 'readonly'       // 'readonly' | 'writable' | 'off'
}

Marcar-la com a 'readonly' té un efecte extra: la regla no-global-assign avisarà si algú intenta reassignar-la.

  1. 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.

  1. 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-line a 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-line a disable de 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.js o 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.

  1. --fix i els scripts del projecte

Moltes 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 escriure

Què 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?)
letconst 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.

  1. 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'
  }
}

  1. 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ó.
npm install --save-dev prettier
// .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:

# .prettierignore
node_modules/
coverage/
dist/
*.min.js

Ú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-revs

Amb 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.

  1. 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

  1. 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 = false

L'última secció importa: en Markdown, dos espais al final de línia signifiquen salt de línia, així que retallar-los trencaria el text.

  1. 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.
npm install --save-dev husky lint-staged
npx husky init                       # crea .husky/ i el hook pre-commit
# .husky/pre-commit
npx lint-staged
// 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-commit triga trenta segons, algú començarà a fer servir --no-verify i el guardià deixarà d'existir. Per això lint-staged només mira el que està preparat, i per això les proves completes no van a pre-commit: van a pre-push o directament a integració contínua.
  • --no-verify existeix 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.

  1. 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:check

Punts que convé entendre d'aquest flux:

  • npm ci en lloc de npm install. ci esborra node_modules, instal·la exactament les versions del package-lock.json i falla si el lock no concorda amb el package.json. És determinista; install pot actualitzar el lock sobre 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 camp engines del package.json i reflecteix-la aquí.
  • format:check, no format. 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.

  1. 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.

  1. 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.

  1. // @ts-check: comprovació de tipus sense TypeScript

Aquí 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
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.

  1. 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 —horesObertes sumant 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.

  1. 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: ReferenceError

Aquest 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 recommended més les deu regles de l'apartat 7, deixa la resta en warn i 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 globals per entorn. Sense això, no-undef produeix falsos positius a sw.js (amb self i caches) i als fitxers de proves (amb describe i expect), 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 amb import/extensions en 'always'.
  • // eslint-disable-next-line sense 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 a pre-push o 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 ci a 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=0 converteix els avisos en errors per a CI. És la manera d'impedir que els warn s'acumulin sense haver de pujar-los tots a error a 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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats