La lección anterior terminó con una observación incómoda: los tres fallos que resolviste tenían una señal previa que nadie vio. Un id que mezclaba tipos, un catch que se tragaba un error en silencio, dos líneas gemelas de las que solo una se adaptó. Ninguno necesitaba ejecutarse para ser sospechoso: bastaba con leer el código con atención, y eso lo hace una máquina mejor que tú, en cien milisegundos y sin cansarse. En esta lección montarás esa máquina sobre Nómada Tareas: ESLint para detectar patrones peligrosos antes de ejecutar nada, Prettier para que el formato deje de ser un tema de conversación, un hook de Git para que nada mal formateado entre en el repositorio y un flujo de integración continua que lo compruebe otra vez en el servidor. Y terminarás fijando las convenciones que ninguna herramienta puede automatizar: nombres, tamaño de funciones, comentarios que explican el porqué y documentación de tipos con JSDoc.
Contenido
- Qué cuesta no tener herramientas de calidad
- Análisis estático: qué puede saber una máquina sin ejecutar nada
- ESLint: instalación y primer arranque
- El fichero de configuración plano
- Reglas, niveles y configuraciones recomendadas
globals: navegador, Node y pruebas- Diez reglas que evitan bugs reales
- Desactivar una regla sin hacer trampas
--fixy los scripts del proyecto- Plugins útiles
- Prettier: el formato deja de ser una opinión
- ESLint frente a Prettier, y cómo convivir
- Integración en el editor
- Hooks de Git con Husky y lint-staged
- Integración continua con GitHub Actions
- Convenciones que ninguna herramienta puede imponer
- Documentar tipos con JSDoc
// @ts-check: comprobación de tipos sin TypeScript- Métricas de calidad con cabeza
- Nómada Tareas: configurarlo todo y arreglar los avisos
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Qué cuesta no tener herramientas de calidad
En un proyecto sin automatizar, la calidad se sostiene sobre la revisión humana. Y la revisión humana tiene un problema de presupuesto de atención: quien revisa dispone de una cantidad limitada de energía, y si la gasta en cosas que una máquina resolvería sola, no le queda para lo que solo un humano puede ver.
Un comentario típico de revisión sin herramientas:
«Aquí hay comillas dobles y en el resto del fichero simples. Falta el punto y coma de la línea 34. La indentación del
ifestá a 4 y usamos 2. Y creo queestadono se usa.»
Cuatro comentarios; los cuatro los detecta y arregla una herramienta en un segundo. Lo que no ha dicho nadie, porque la atención se fue en el formato: que ese if no contempla el caso hecha, o que la función devuelve undefined en una rama.
El coste real se reparte en cuatro sumandos:
| Coste | Qué es | Qué lo elimina |
|---|---|---|
| Discutir estilo | Comillas, sangría, punto y coma, longitud de línea | Un formateador determinista |
| Ruido en los diffs | Cambios de formato mezclados con cambios de lógica | El mismo formateador, aplicado a todos |
| Bugs de patrón | Variables sin usar, == inesperado, promesas sin await |
Un analizador estático |
| Deriva entre personas | Cada fichero escrito con un estilo distinto | Configuración compartida en el repositorio |
Los dos primeros son de comodidad. El tercero es de corrección, y es el que justifica la lección: hay una familia entera de fallos que se detectan leyendo el código, sin ejecutarlo.
- Análisis estático: qué puede saber una máquina sin ejecutar nada
Un analizador estático convierte tu código en un árbol de sintaxis abstracta (AST) —la misma estructura que construye el motor de JavaScript antes de ejecutar— y recorre ese árbol buscando patrones. No ejecuta nada; razona sobre la forma.
flowchart LR
A["Código fuente<br/>tablero.js"] --> B["Analizador<br/>→ AST"]
B --> C["Reglas<br/>recorren el árbol"]
C --> D["Avisos y errores<br/>fichero:línea:columna"]
C --> E["Correcciones<br/>automáticas (--fix)"]
Lo que un analizador sí puede saber:
- Que declaraste
const visiblesy nunca la usaste. - Que llamas a
tablro.resumen()y ese identificador no existe en ningún ámbito accesible. - Que una función
asyncno contiene ningúnawait(probablemente sobra elasync… o falta unawait). - Que un
casede unswitchno tienebreaky cae al siguiente (02-03). - Que comparas con
==en lugar de===(01-07). - Que hay una asignación dentro de un
if:if (estado = 'hecha').
Lo que no puede saber:
- Si
horasAbiertasdebe sumar sobre las abiertas o sobre todas. Eso es semántica, y es exactamente el caso 3 de la lección anterior. Para eso hacen falta pruebas. - Si la aplicación hace lo que Marta necesita.
- Si tu API devolverá
idcomo cadena o como número, salvo que se lo digas con tipos.
Es importante tener clara esa frontera: el análisis estático y las pruebas automatizadas no compiten, se complementan. El primero es barato, instantáneo y cubre una familia estrecha de fallos; las segundas cuestan escribirse y cubren el comportamiento. Un proyecto serio tiene los dos.
- ESLint: instalación y primer arranque
ESLint es el analizador estático estándar del ecosistema JavaScript. Es configurable hasta el último detalle y extensible con plugins, y esas dos características explican tanto su potencia como su fama de "configuración complicada".
Nómada Tareas hasta ahora no tenía package.json: es un proyecto de módulos ES servidos directamente. Lo creamos ahora, porque a partir de aquí el proyecto tiene herramientas:
cd nomada-tareas
npm init -y # crea package.json
npm install --save-dev eslint # ESLint solo hace falta en desarrolloUn detalle que conviene fijar desde el principio en package.json:
{
"name": "nomada-tareas",
"version": "1.0.0",
"type": "module",
"private": true,
"scripts": {
"lint": "eslint ."
},
"devDependencies": {
"eslint": "^9.0.0"
}
}"type": "module"le dice a Node que los.jsde este proyecto son módulos ES (05-04), no CommonJS. Sin esta línea, cualquier herramienta que ejecute tu código en Node se quejaría de losimport."private": trueevita publicar el paquete por accidente en npm.devDependenciesen lugar dedependencies: ESLint no forma parte de la aplicación, solo del proceso de desarrollo. El navegador nunca lo descarga.
Al ejecutar npm run lint sin configuración, ESLint avisa de que no encuentra fichero de configuración. Vamos a escribirlo.
- El fichero de configuración plano
ESLint moderno usa la configuración plana (flat config): un fichero eslint.config.js que exporta un array de objetos de configuración. Cada objeto dice a qué ficheros se aplica y qué reglas rigen ahí. Los objetos posteriores se superponen a los anteriores, como capas.
// eslint.config.js
import js from '@eslint/js';
import globals from 'globals';
export default [
// ── Capa 0 · qué NO se analiza ────────────────────────────────────────
{
ignores: ['coverage/**', 'dist/**', 'node_modules/**']
},
// ── Capa 1 · base para TODO el JavaScript del proyecto ────────────────
js.configs.recommended,
// ── Capa 2 · el código de la aplicación: navegador + módulos ES ───────
{
files: ['js/**/*.js'],
languageOptions: {
ecmaVersion: 'latest', // sintaxis más reciente (campos privados #, ?., ??=)
sourceType: 'module', // import/export, no require
globals: {
...globals.browser // window, document, fetch, localStorage, console…
}
},
rules: {
// aquí van las reglas propias del proyecto (apartado 7)
}
},
// ── Capa 3 · el service worker vive en OTRO entorno global ────────────
{
files: ['sw.js'],
languageOptions: {
globals: { ...globals.serviceworker } // self, caches, clients… pero NO document
}
},
// ── Capa 4 · ficheros de herramientas que se ejecutan en Node ─────────
{
files: ['*.config.js', 'scripts/**/*.js'],
languageOptions: {
globals: { ...globals.node } // process, __dirname (si aplica), console
}
}
];Cinco cosas que entender de este fichero, porque son las que causan confusión:
- Es JavaScript de verdad, no JSON. Puedes importar, calcular y componer. Por eso
js.configs.recommendedes un objeto que se inserta en el array. - El orden importa. Si dos capas definen la misma regla, gana la última. Por eso la configuración recomendada va arriba y tus ajustes debajo.
filesdecide el alcance. Un objeto sinfilesse aplica a todo. Esto es lo que permite la capa 3:sw.jsno tienedocumentniwindow, y sí tieneselfycaches; declararlo evita cientos de falsosno-undef.ignoresen un objeto propio (sinfiles) actúa como ignorado global, el equivalente del antiguo.eslintignore.globalses un paquete auxiliar con los catálogos de variables globales de cada entorno. Se instala aparte:npm i -D globals.
La capa 3 merece un comentario más. El service worker que escribiste en 07-05 se ejecuta en un hilo distinto con un objeto global distinto. Sin esa capa, ESLint marcaría self, caches y clients como no definidos, y —peor— no marcaría un uso accidental de document, que en un worker es un error real de ejecución. Configurar bien los entornos no es burocracia: es lo que convierte a no-undef en un detector de fallos de verdad.
- Reglas, niveles y configuraciones recomendadas
Cada regla de ESLint se configura con un nivel y, opcionalmente, opciones:
rules: {
'no-unused-vars': 'error', // nivel simple
'no-console': ['warn', { allow: ['warn', 'error'] }], // nivel + opciones
'no-alert': 'off' // desactivada
}| Nivel | Valor numérico | Qué ocurre | Código de salida |
|---|---|---|---|
'off' |
0 |
La regla no se comprueba | — |
'warn' |
1 |
Se informa, pero no falla | 0 (éxito) |
'error' |
2 |
Se informa y falla | 1 (fallo) |
La diferencia entre warn y error es crítica en cuanto añadas integración continua: error rompe la construcción, warn no. De ahí una estrategia muy práctica:
errorpara todo lo que sea un fallo real o un riesgo:no-undef,eqeqeq,no-debugger.warnpara lo que es preferencia o está en migración: reglas que acabas de activar sobre código existente y que todavía producen cien avisos.- Y una regla de higiene: los
warnno pueden acumularse indefinidamente. Si un aviso lleva seis meses ahí, o se arregla o se apaga con un motivo escrito. Un lint que imprime 300 avisos es un lint que nadie lee.
js.configs.recommended activa alrededor de sesenta reglas que la comunidad considera imprescindibles y ninguna de estilo. Es deliberado: desde que Prettier existe, ESLint ha ido retirando las reglas de formato de su recomendación. Esa división de trabajo es el tema del apartado 12.
globals: navegador, Node y pruebas
globals: navegador, Node y pruebasLa regla no-undef marca cualquier identificador que no esté declarado. Es una de las más valiosas —habría cazado un tablro.resumen() al instante— pero solo funciona si ESLint sabe qué globales son legítimas en cada fichero.
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 (lo necesitarás en 08-03): describe, test, expect, beforeEach…
globals.jestY si necesitas declarar una global propia —por ejemplo, una constante que inyecta el proceso de despliegue— se hace con su permiso de escritura explícito:
Marcarla como 'readonly' tiene un efecto extra: la regla no-global-assign avisará si alguien intenta reasignarla.
- Diez reglas que evitan bugs reales
Estas son las que justifican todo el montaje. No son cuestión de gusto: cada una corresponde a una familia de fallos que ha costado tardes a alguien.
| Regla | Qué detecta | Ejemplo en Nómada Tareas |
|---|---|---|
no-unused-vars |
Variables, parámetros e importaciones sin usar | Un import { PESOS } que quedó tras un refactor: código muerto que confunde |
no-undef |
Identificadores no declarados | tablro.resumen(), un HOY sin importar |
eqeqeq |
== y != en lugar de === / !== |
Justo la coerción de 01-07 que enmascaró el caso 1 de 08-01 |
no-implicit-globals |
Declaraciones que contaminan el objeto global | Un var estado suelto en un script clásico |
require-await |
Función async sin ningún await dentro |
async function guardar() que en realidad es síncrona: promete asincronía que no hay |
no-return-await |
return await x innecesario dentro de un try… o fuera de él |
Un fotograma de pila extra sin ganancia |
no-fallthrough |
Un case que cae al siguiente sin break |
El switch de estados de 02-03 |
no-cond-assign |
Asignación dentro de una condición | if (tarea.estado = 'hecha') — cambia el estado y siempre es cierto |
no-debugger |
La sentencia debugger |
Exactamente lo que te dejaste puesto en 08-01 |
no-constant-condition |
Condiciones siempre ciertas o siempre falsas | `if (tarea.horasEstimadas |
Y una undécima que merece explicación aparte porque es la más citada y la más malentendida: no-floating-promises.
Una promesa flotante es una promesa cuyo resultado nadie recoge:
// ¿Ves el fallo?
function alPulsarGuardar() {
repositorio.guardar(tablero);
actualizarTarea(tarea.id, { estado: 'hecha' }); // ← devuelve una promesa que nadie espera
mostrarAviso('Guardado'); // ← miente: aún no se ha guardado
}Si actualizarTarea falla, el rechazo no lo captura nadie: se convierte en un unhandledrejection (05-06) y el usuario ve «Guardado» sobre un cambio que no llegó al servidor. Es un fallo silencioso de la peor especie.
El matiz importante: la regla no-floating-promises real requiere información de tipos, y esa la aporta TypeScript (apartado 18), no ESLint sobre JavaScript puro. En un proyecto solo-JavaScript se cubre con una combinación de aproximaciones (require-await, no-async-promise-executor, revisión) y, sobre todo, con la convención de equipo: toda llamada que devuelva una promesa se awaitea o se encadena con un .catch() explícito. Escribirlo en las convenciones es la mitad del trabajo; la otra mitad es que TypeScript pueda comprobarlo, y ahí es donde // @ts-check empieza a resultar atractivo.
- Desactivar una regla sin hacer trampas
Habrá casos legítimos en los que una regla se equivoca. ESLint permite silenciarla con precisión quirúrgica:
// Solo la línea siguiente
// eslint-disable-next-line no-console -- registro deliberado de diagnóstico (ver 08-01)
console.log('[nomada] modo diagnóstico activado');
// Solo esta línea, al final
const respaldo = almacenEnMemoria(); // eslint-disable-line no-unused-vars
// Un bloque completo
/* eslint-disable no-undef -- este fichero se ejecuta dentro del service worker */
self.addEventListener('install', …);
/* eslint-enable no-undef */Cuatro normas de higiene con estos comentarios:
- Nombra siempre la regla. Un
// eslint-disable-next-linea secas apaga todas las reglas de esa línea, incluidas las que aún no existen. - Escribe el motivo tras
--. ESLint admite esa sintaxis precisamente para eso, y evita que dentro de un año nadie sepa si sigue haciendo falta. - Prefiere
disable-next-lineadisablede bloque. Un bloque desactivado tiende a crecer y a tapar cosas nuevas. - Si la desactivas en cinco sitios, la regla está mal configurada. Cámbiala en
eslint.config.jso apágala globalmente con un comentario que lo justifique. Cinco excepciones no son excepciones: son la norma real.
Además, la opción reportUnusedDisableDirectives (activa por defecto en la configuración recomendada moderna) avisa cuando un eslint-disable ya no hace falta porque el código cambió. Es limpieza automática de tus propias excepciones.
--fix y los scripts del proyecto
--fix y los scripts del proyectoMuchas reglas son autocorregibles: ESLint sabe reescribir el código para cumplirlas sin cambiar su significado.
npx eslint . # solo informa
npx eslint . --fix # arregla lo que puede e informa del resto
npx eslint . --fix-dry-run --format json # simula, sin escribirQué se arregla solo y qué no:
| Se corrige automáticamente | Requiere criterio humano |
|---|---|
| Comillas, punto y coma, sangría | no-unused-vars (¿sobra la variable o falta usarla?) |
== → === cuando es seguro |
no-undef (¿falta un import o hay una errata?) |
let → const si nunca se reasigna |
require-await (¿sobra el async o falta un await?) |
| Orden de importaciones (con plugin) | Complejidad excesiva |
La regla mental: --fix resuelve la forma, nunca la intención. Todo lo que implique decidir qué querías hacer se queda para ti, y eso es correcto.
Los scripts que tendrá el proyecto a partir de aquí:
{
"scripts": {
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"format": "prettier --write .",
"format:check": "prettier --check .",
"verificar": "npm run lint && npm run format:check"
}
}verificar es el guion que ejecutará la integración continua, y el que puedes lanzar antes de cada commit mientras te acostumbras.
- Plugins útiles
Un plugin de ESLint aporta reglas nuevas. Estos tres cubren necesidades reales de un proyecto como este:
Importaciones. Un plugin de importaciones (eslint-plugin-import o su sucesor moderno eslint-plugin-import-x) comprueba lo que el navegador no perdona en módulos ES: rutas que no existen, extensiones olvidadas, dependencias circulares y orden de los import.
{
files: ['js/**/*.js'],
plugins: { import: importPlugin },
rules: {
'import/no-unresolved': 'error', // la ruta './modelo/tarea.js' existe de verdad
'import/extensions': ['error', 'always'], // en el navegador la extensión es OBLIGATORIA
'import/no-cycle': 'error', // A importa B, B importa A → orden de ejecución impredecible
'import/order': ['warn', { 'newlines-between': 'always' }]
}
}import/extensions en 'always' es especialmente valioso aquí: los empaquetadores toleran import { Tarea } from '../modelo/tarea' sin extensión, pero el navegador con <script type="module"> no. Sin esta regla, el fallo aparece en tiempo de ejecución y solo en el navegador. Y import/no-cycle protege el grafo acíclico de dependencias que dibujaste en 05-04.
Accesibilidad. El plugin de accesibilidad más conocido (eslint-plugin-jsx-a11y) está pensado para JSX, que no usas. En un proyecto de HTML y DOM la comprobación equivalente se hace con otras herramientas: un validador de HTML, la auditoría de accesibilidad de las DevTools, y sobre todo axe, que en 08-06 integrarás en las pruebas de extremo a extremo. Merece la pena saber que existe la familia de reglas y por qué no encaja aquí: elegir un plugin porque suena bien y descubrir que no analiza tu tipo de fichero es una pérdida de tarde muy común.
Pruebas. eslint-plugin-jest detecta errores clásicos en las pruebas que escribirás en la lección siguiente: un test sin ninguna aserción, un expect fuera de un test, un test.only olvidado que deja el resto de la suite sin ejecutar —quizás el fallo más peligroso de todos, porque la suite sigue en verde mientras no comprueba nada—.
{
files: ['**/*.test.js'],
plugins: { jest: jestPlugin },
languageOptions: { globals: { ...globals.jest } },
rules: {
'jest/no-focused-tests': 'error', // test.only olvidado
'jest/no-disabled-tests': 'warn', // test.skip que lleva meses ahí
'jest/expect-expect': 'error', // una prueba sin aserciones no prueba nada
'jest/valid-expect': 'error'
}
}
- Prettier: el formato deja de ser una opinión
Prettier no es un linter: es un formateador determinista. Descarta por completo el formato original de tu código, lo reconstruye desde su árbol de sintaxis y lo imprime siguiendo sus propias reglas. Dos consecuencias enormes:
- El resultado no depende de cómo estuviera escrito antes. Dos personas con estilos opuestos producen ficheros idénticos byte a byte.
- Casi no hay que configurarlo. Prettier ofrece deliberadamente pocas opciones, para que la discusión no se traslade a la configuración.
// .prettierrc.json — la configuración completa de un proyecto como este
{
"semi": true,
"singleQuote": true,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "none",
"arrowParens": "always",
"endOfLine": "lf"
}Comentario de cada opción, porque son todas las que hay que decidir:
semi: punto y coma al final. Ponerlo evita la clase entera de sorpresas de la inserción automática (01-04).singleQuote: comillas simples, coherente con todo el código del curso.printWidth: 100: ancho máximo antes de partir. 80 es el clásico; 100 respira mejor en pantallas modernas y evita partir cadenas de.filter().map().reduce().trailingComma: coma final."none"mantiene el estilo del proyecto;"all"produce diffs más limpios al añadir elementos. Cualquiera de las dos vale: lo que no vale es que cada fichero use una.endOfLine: "lf": fin de línea Unix. Sin esto, un equipo mixto Windows/macOS genera diffs completos por cambios invisibles.
Y un fichero de exclusiones:
Uso:
npx prettier --write . # formatea todo el proyecto
npx prettier --check . # solo comprueba; falla si algo no está formateado (para CI)El momento de adoptarlo. Ejecutar prettier --write . sobre un proyecto existente genera un commit gigantesco que toca todos los ficheros. Hazlo en un commit aislado que no cambie ni una línea de lógica, con un mensaje claro («formato: aplicar Prettier a todo el proyecto»). Y añade su hash a un fichero para que git blame lo ignore:
echo "d4e5f6a7b8c9 # formato: Prettier en todo el proyecto" >> .git-blame-ignore-revs
git config blame.ignoreRevsFile .git-blame-ignore-revsCon eso, git blame seguirá atribuyendo cada línea a quien escribió su lógica, no al commit de formateo. Es un detalle pequeño que ahorra mucha frustración futura.
- ESLint frente a Prettier, y cómo convivir
La confusión más habitual del ecosistema. Los dos leen tu código y los dos pueden modificarlo, pero responden a preguntas distintas:
| ESLint | Prettier | |
|---|---|---|
| Pregunta que responde | ¿Este código es correcto y seguro? | ¿Este código está bien impreso? |
| Tipo de análisis | Semántico: ámbitos, flujo, uso de variables | Sintáctico: reimprime el AST |
| Ejemplo de lo que detecta | Variable sin usar, ==, debugger |
Línea de 180 caracteres, comillas mezcladas |
| Configuración | Extensa: cientos de reglas | Mínima y deliberadamente limitada |
| ¿Puede arreglar? | Algunas reglas, con --fix |
Todo, siempre |
| ¿Es opinable? | Sí, cada regla se discute | No: se acepta su criterio y se deja de discutir |
| Si lo quitas | Aparecen bugs | Aparecen discusiones |
El conflicto y su solución. ESLint todavía incluye algunas reglas de formato heredadas (sangría, comillas…). Si las activas, ESLint y Prettier pueden pedir cosas contrarias y entrarás en un bucle: guardas, Prettier formatea, ESLint se queja; arreglas, Prettier lo deshace.
La solución estándar es eslint-config-prettier: una configuración que apaga todas las reglas de ESLint que chocan con Prettier. Se coloca la última del array, para que su desactivación gane:
// 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 // ← SIEMPRE el último: desactiva lo que chocaría
];Existe también eslint-plugin-prettier, que ejecuta Prettier dentro de ESLint y reporta cada diferencia de formato como un error. Es cómodo (una sola herramienta) pero llena la salida de errores de formato mezclados con errores reales y ralentiza el análisis. La recomendación generalizada es la separación limpia: Prettier formatea, ESLint analiza, eslint-config-prettier los mantiene en sus carriles.
flowchart LR
A["Guardas el fichero"] --> B["Prettier<br/>reimprime el formato"]
B --> C["ESLint<br/>analiza la corrección"]
C --> D{"¿Errores?"}
D -->|No| E["Listo"]
D -->|Sí| F["Los arreglas tú<br/>(o eslint --fix)"]
F --> C
- Integración en el editor
Todo lo anterior se vuelve invisible —y por tanto útil— cuando el editor lo aplica solo.
// .vscode/settings.json — se versiona en el repositorio: todo el equipo 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é hace cada línea: Prettier formatea al guardar; ESLint aplica sus correcciones automáticas también al guardar; los finales de línea y los espacios sobrantes se normalizan. El resultado práctico es que dejas de pensar en el formato: escribes como te salga, guardas y queda correcto.
Y un .editorconfig para quienes usen otro 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 = falseLa última sección importa: en Markdown, dos espacios al final de línea significan salto de línea, así que recortarlos rompería el texto.
- Hooks de Git con Husky y lint-staged
El editor cubre a quien lo tiene bien configurado. El repositorio necesita una garantía que no dependa de eso. Los hooks de Git son guiones que Git ejecuta en momentos concretos; el que nos interesa es pre-commit, que corre antes de crear el commit y puede abortarlo.
- Husky gestiona los hooks dentro del repositorio (Git no versiona
.git/hooks, así que sin una herramienta cada persona tendría que instalarlos a mano). - lint-staged ejecuta las herramientas solo sobre los ficheros en el área de preparación (staged). Es lo que hace la diferencia entre un hook de dos segundos y uno de dos minutos.
// package.json
{
"lint-staged": {
"*.js": ["eslint --fix", "prettier --write"],
"*.{json,css,md,html}": ["prettier --write"]
}
}El flujo completo, paso a paso:
flowchart TD
A["git commit"] --> B["Husky ejecuta<br/>.husky/pre-commit"]
B --> C["lint-staged toma<br/>SOLO los ficheros staged"]
C --> D["eslint --fix<br/>prettier --write"]
D --> E{"¿Quedan errores<br/>sin corregir?"}
E -->|Sí| F["Commit ABORTADO<br/>con el listado"]
E -->|No| G["Los arreglos se re-añaden<br/>al área de preparación"]
G --> H["Commit creado"]
Detalle clave del paso G: lint-staged vuelve a añadir los ficheros que sus herramientas modificaron, así que el commit contiene el código ya formateado. No tienes que hacer nada.
Dos advertencias imprescindibles:
- Un hook lento se desactiva. Si
pre-committarda treinta segundos, alguien empezará a usar--no-verifyy el guardián dejará de existir. Por eso lint-staged solo mira lo preparado, y por eso las pruebas completas no van enpre-commit: van enpre-pusho directamente en integración continua. --no-verifyexiste y a veces es legítimo (un commit de emergencia en producción a las tres de la mañana). Precisamente por eso el mismo control debe repetirse en el servidor, donde nadie puede saltárselo.
- Integración continua con GitHub Actions
La integración continua (CI) ejecuta las comprobaciones en un servidor limpio, en cada push y en cada pull request. Es la única capa que nadie puede eludir, y además elimina el clásico «en mi máquina funciona»: el servidor arranca desde cero, instala exactamente lo que dice el fichero de bloqueo y ejecuta lo mismo para todo el mundo.
# .github/workflows/calidad.yml
name: Calidad
# Cuándo se ejecuta
on:
push:
branches: [master]
pull_request:
jobs:
verificar:
runs-on: ubuntu-latest # máquina virtual limpia para cada ejecución
steps:
# 1 · Traer el código del repositorio a la máquina
- name: Descargar el código
uses: actions/checkout@v4
# 2 · Instalar Node. 'cache: npm' reutiliza las dependencias entre ejecuciones
- name: Preparar Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: npm
# 3 · Instalación reproducible: respeta package-lock.json al pie de la letra
- name: Instalar dependencias
run: npm ci
# 4 · Análisis estático. Falla si hay errores (nivel 'error')
- name: Analizar con ESLint
run: npm run lint
# 5 · Formato. No formatea: comprueba. Falla si algo no está formateado
- name: Comprobar el formato
run: npm run format:checkPuntos que conviene entender de este flujo:
npm cien lugar denpm install.ciborranode_modules, instala exactamente las versiones delpackage-lock.jsony falla si ellockno concuerda con elpackage.json. Es determinista;installpuede actualizar ellocksobre la marcha.node-version: lts/*usa la versión de soporte prolongado vigente, sin fijar un número que quedará obsoleto. Si tu proyecto necesita una versión concreta, decláralo en el campoenginesdepackage.jsony refléjalo aquí.format:check, noformat. En CI nunca se modifica el código: se comprueba y se falla. Corregir es trabajo del autor, en su máquina.on: pull_requestes lo que hace aparecer la marca verde o roja en la propuesta de cambios, antes de fusionar.
Cuando llegues a 08-03 añadirás un paso npm test a este mismo flujo, y en 08-06 otro con las pruebas de extremo a extremo. La estructura ya está lista.
Las tres capas de defensa, ordenadas por rapidez de respuesta:
| Capa | Cuándo actúa | Se puede eludir | Coste |
|---|---|---|---|
| Editor | Al guardar | Sí (no instalar la extensión) | 0 s |
| Hook de Git | Al hacer commit | Sí (--no-verify) |
1-3 s |
| Integración continua | Al hacer push / PR | No | 30-90 s |
Las tres son la misma comprobación repetida, y esa redundancia es deliberada: cuanto antes falla, más barato es arreglarlo.
- Convenciones que ninguna herramienta puede imponer
Automatizado el formato y el análisis, queda lo que exige criterio. Estas son las convenciones que merecen escribirse en un CONTRIBUTING.md del proyecto.
Nombres (retomando 01-04). El nombre es la documentación que nunca se desactualiza porque se lee en cada uso.
| Elemento | Convención | En Nómada Tareas |
|---|---|---|
| Variables y funciones | camelCase, en español, verbo si actúa |
horasAbiertas, crearBacklog(), pintarTarjeta() |
| Clases | PascalCase, sustantivo |
Tarea, Tablero, RepositorioLocal, ErrorDeApi |
| Constantes de módulo | MAYUSCULAS_CON_GUION |
HOY, PESOS, TRANSICIONES, EVENTOS |
| Campos privados | # delante (05-03) |
#estado, #tareas, #almacen |
| Booleanos | Prefijo interrogativo | abierta, estaVencida(), persistente |
| Ficheros | kebab-case.js, singular si exporta una cosa |
repositorio-local.js, tablero-vista.js |
| Manejadores | al + evento, o manejar + cosa |
alCrear, manejarClic |
Y tres antinombres que hay que erradicar: datos, info, gestionar. No dicen nada. datos puede ser cualquier cosa; tareasPendientes dice exactamente qué es.
Tamaño de funciones. No hay un número mágico, pero sí una prueba fiable: si necesitas un comentario para separar dos partes de una función, esas dos partes son dos funciones. Como referencia práctica: por encima de 30 líneas conviene mirarla con desconfianza, por encima de 50 casi seguro que hace dos cosas. Y el criterio decisivo no es la longitud sino el nivel de abstracción: una función que mezcla decidir qué renderizar con cómo escribirlo en el DOM ya está mal, tenga diez líneas o cien.
Comentarios que explican el porqué. El comentario que repite lo que dice el código es ruido que además envejece mal:
// ❌ Ruido: repite el código
// Incrementa el contador en uno
contador += 1;
// ❌ Peor: miente, porque el código cambió y el comentario no
// Devuelve las tareas ordenadas por fecha
return tareas.sort((a, b) => PESOS[b.prioridad] - PESOS[a.prioridad]);
// ✅ Útil: explica una decisión que el código no puede contar
// Devolvemos instancias nuevas en cada llamada para que dos consumidores
// (la aplicación y las pruebas) no compartan estado por accidente.
export function crearBacklog() { … }
// ✅ Útil: documenta una restricción externa
// El navegador NO distingue red caída de CORS bloqueado: ambos llegan como
// TypeError, deliberadamente, para no filtrar información sobre otros dominios.
throw new ErrorDeApi('No se pudo contactar con el servidor.', { codigo: 'red' });
// ✅ Útil: avisa de una trampa
// dataset SIEMPRE devuelve cadenas; sin Number() el === de buscarPorId falla.
const id = Number(li.dataset.id);La regla: el código dice qué hace; el comentario dice por qué es así y no de otra manera. Si necesitas un comentario para explicar qué hace, normalmente el arreglo es renombrar o extraer una función, no comentar.
- Documentar tipos con JSDoc
JSDoc documenta tipos con comentarios estructurados. En JavaScript puro aporta dos cosas inmediatas: autocompletado y avisos en el editor (que entiende JSDoc de forma nativa), y documentación que se lee sin salir del fichero.
/**
* Filtra y ordena las tareas para su presentación, sin modificar el tablero.
*
* @param {import('../modelo/tablero.js').Tablero} tablero Fuente de datos
* @param {object} opciones
* @param {string|null} [opciones.responsable] Nombre exacto, o null para no filtrar
* @param {string} [opciones.texto=''] Búsqueda por título, sin distinguir mayúsculas
* @param {'prioridad'|'fecha'|'horas'} [opciones.orden='prioridad']
* @returns {import('../modelo/tarea.js').Tarea[]} Array NUEVO; el tablero no se toca
* @throws {ErrorDeValidacion} Si `orden` no es uno de los valores admitidos
*
* @example
* tareasVisibles(tablero, { responsable: 'Iván' }); // → 3 tareas, 25 h
*/
export function tareasVisibles(tablero, { responsable = null, texto = '', orden = 'prioridad' } = {}) {
// …
}Y para tipos que se repiten, @typedef los define una vez:
/**
* @typedef {object} ResumenTablero
* @property {number} total Todas las tareas del tablero
* @property {number} abiertas Las que no están en estado 'hecha'
* @property {number} horasTotales Suma de horasEstimadas de todas
* @property {number} horasAbiertas Suma de horasEstimadas de las abiertas
* @property {number} vencidas Abiertas con fechaLimite pasada (R10)
* @property {number} esfuerzo Suma de horas × peso de prioridad
*/
/**
* @param {string} hoy Fecha ISO de referencia
* @returns {ResumenTablero}
*/
resumen(hoy) { … }Ese @typedef documenta de una vez los números canónicos del proyecto y hace que el editor autocomplete resumen.horasAbiertas con su descripción al lado. En un proyecto de seis capas, eso vale más que cualquier documento externo, porque vive junto al código y se actualiza con él.
Consejo de dosificación: documenta con JSDoc las funciones exportadas —la superficie pública de cada módulo— y deja las internas con un buen nombre. Documentar todo produce ficheros donde hay más comentario que código y nadie lee ninguno.
// @ts-check: comprobación de tipos sin TypeScript
// @ts-check: comprobación de tipos sin TypeScriptAquí está el siguiente escalón, y se puede subir sin reescribir nada. El compilador de TypeScript sabe analizar ficheros .js usando la información de JSDoc. Se activa con un comentario en la primera línea:
// @ts-check
import { Tarea } from './tarea.js';
/** @param {number} id */
export function buscar(id) { … }
buscar('7');
// ~~~ Argument of type 'string' is not assignable to parameter of type 'number'.Ese aviso es exactamente el caso 1 de la lección anterior, detectado en el editor, sin ejecutar nada, sin abrir el navegador y sin que Marta tenga que reportar nada. Un fallo que costó una investigación completa habría sido un subrayado rojo mientras se escribía.
Para activarlo en todo el proyecto sin poner el comentario fichero a fichero:
// jsconfig.json
{
"compilerOptions": {
"checkJs": true,
"strict": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "bundler",
"noEmit": true
},
"include": ["js/**/*.js"]
}"noEmit": true es lo esencial: TypeScript no genera ningún fichero, solo comprueba. Tu código sigue siendo JavaScript ejecutable tal cual, servido directamente al navegador.
La comparación honesta:
JavaScript + JSDoc + @ts-check |
TypeScript | |
|---|---|---|
| Compilación necesaria | No | Sí |
| Cobertura de tipos | Buena, algo limitada en casos avanzados | Completa |
| Verbosidad | Alta (comentarios largos) | Baja (sintaxis nativa) |
| Coste de adopción | Muy bajo, fichero a fichero | Medio-alto |
Reglas como no-floating-promises |
Disponibles con el plugin de tipos | Disponibles |
Para Nómada Tareas, @ts-check es la opción sensata: cero cambios en el despliegue y una red que caza toda la familia de fallos de tipos. TypeScript entero es una decisión de proyecto que se trata en Siguientes Pasos, donde se sitúa dentro del mapa completo de lo que viene después de este curso.
- Métricas de calidad con cabeza
Existen métricas numéricas de calidad, y conviene conocer dos.
Complejidad ciclomática. Cuenta los caminos independientes de ejecución de una función: 1 de base, +1 por cada if, else if, case, for, while, catch, &&, || y ?:. Es, casi literalmente, el número mínimo de pruebas necesarias para recorrer todas las ramas, y por eso interesa aquí.
// Complejidad 1: un solo camino
export function marcaDeEstado(estado) {
return MARCAS[estado] ?? '?';
}
// Complejidad 5: cuatro decisiones + base
function validarFormulario(formulario, datos, hoy) {
const errores = [];
for (const campo of formulario.elements) { // +1
if (campo.willValidate && !campo.checkValidity()) { // +1 (if) +1 (&&)
errores.push({ campo, mensaje: campo.validationMessage });
}
}
if (datos.fechaLimite < hoy) errores.push(…); // +1
if (datos.etiquetas.length > 5) errores.push(…); // +1
return errores;
}ESLint la mide con la regla complexity:
rules: {
complexity: ['warn', { max: 10 }],
'max-depth': ['warn', 4], // anidamiento de bloques
'max-lines-per-function': ['warn', { max: 60, skipComments: true, skipBlankLines: true }]
}Como referencia orientativa: por debajo de 10, cómoda; entre 10 y 20, mirar si se puede dividir; por encima de 20, casi seguro que hay dos funciones dentro.
Deuda técnica. Es la metáfora, no una métrica: los atajos de hoy se pagan con intereses mañana en forma de tiempo de desarrollo. Algunas herramientas la expresan en horas estimadas de arreglo. Ese número es una estimación de una estimación; sirve para comparar la evolución de un mismo proyecto en el tiempo, no para comparar proyectos ni para presumir.
Y la advertencia que da título al apartado: no caigas en el fetichismo de los números. Tres patologías reales:
- Perseguir la métrica en vez del objetivo. Bajar la complejidad partiendo una función en cuatro trozos incoherentes empeora el código y mejora el número.
- Confundir "sin avisos" con "bien hecho". El caso 3 de 08-01 —
horasAbiertassumando 48 en lugar de 45— pasaba todas las reglas de ESLint. Un lint limpio no dice nada sobre si el programa es correcto. - Usar las métricas para evaluar personas. En cuanto un número se convierte en objetivo, deja de ser una buena medida (ley de Goodhart). Las métricas son un termómetro del código, no una nota.
- Nómada Tareas: configurarlo todo y arreglar los avisos
Montamos la configuración completa y vemos qué encuentra en el código real.
npm install --save-dev eslint @eslint/js globals eslint-config-prettier prettier husky lint-staged
npx husky init// eslint.config.js — configuración completa del proyecto
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,
// La aplicación: navegador + módulos ES
{
files: ['js/**/*.js'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: { ...globals.browser }
},
rules: {
// — Corrección —
eqeqeq: ['error', 'always', { null: 'ignore' }], // permite `x == null` (null y undefined a la vez)
'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', // ← lo de 08-01
'no-console': ['warn', { allow: ['warn', 'error'] }],
'prefer-const': 'error',
'no-var': 'error',
'object-shorthand': 'warn',
// — Tamaño —
complexity: ['warn', { max: 12 }],
'max-depth': ['warn', 4]
}
},
// El service worker: otro global, otras reglas
{
files: ['sw.js'],
languageOptions: { globals: { ...globals.serviceworker } },
rules: { 'no-console': 'off' } // en un SW, la consola es la única ventana
},
// Herramientas que corren en Node
{
files: ['*.config.js', 'scripts/**/*.js'],
languageOptions: { sourceType: 'module', globals: { ...globals.node } },
rules: { 'no-console': 'off' }
},
prettier // ← el último, siempre
];Y la primera ejecución sobre el código de los Módulos 1 a 7:
$ npm run lint
/nomada-tareas/js/vista/tablero-vista.js
14:10 error 'PESOS' is defined but never used no-unused-vars
96:5 warning Unexpected console statement no-console
/nomada-tareas/js/modelo/tablero.js
52:9 error Unexpected 'debugger' statement no-debugger
/nomada-tareas/js/datos/api-tareas.js
61:9 error Expected '===' and instead saw '==' eqeqeq
88:1 error Async method 'borrarTarea' has no 'await' require-await
/nomada-tareas/js/vista/controlador.js
38:15 error 'tarea' is not defined no-undef
/nomada-tareas/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.Vamos uno por uno, porque cada arreglo tiene su matiz:
1 · 'PESOS' is defined but never used. Import huérfano de un refactor. Se borra la línea. Coste: cero. Beneficio: quien lea el fichero no buscará dónde se usa un peso que ya no se usa.
2 · Unexpected 'debugger' statement. El debugger de la lección anterior, que iba camino del repositorio. Este es el aviso que paga toda la configuración por sí solo: un debugger en producción congela la aplicación a cualquiera con las DevTools abiertas.
3 · Expected '===' and instead saw '=='.
// Antes
if (respuesta.status == 204) return null;
// Después
if (respuesta.status === 204) return null;Aquí no había un fallo real (status siempre es número), pero la regla es de las que no admite excepciones caso a caso: mantener == en el código obliga a razonar cada vez si la coerción es segura. --fix lo corrige solo.
4 · Async method 'borrarTarea' has no 'await'. Este es un fallo de verdad:
// Antes — el async es una mentira: el error de red no lo captura nadie aquí
export async function borrarTarea(id) {
fetch(construirUrl(`/tareas/${id}`), { method: 'DELETE' }); // ← promesa flotante
return true; // ← miente siempre
}
// Después
export async function borrarTarea(id) {
const respuesta = await fetch(construirUrl(`/tareas/${id}`), { method: 'DELETE' });
await comprobar(respuesta);
return true;
}La versión original devolvía true antes de que el servidor contestara. Si el borrado fallaba con un 403, la interfaz eliminaba la tarjeta igualmente y el error se perdía como unhandledrejection. Una regla de tres palabras ha encontrado un fallo de coherencia de datos.
5 · 'tarea' is not defined. Una variable que se renombró en la mitad de las apariciones:
// Antes
const tareaPulsada = tablero.buscarPorId(id);
if (tarea.estado === 'hecha') return; // ← 'tarea' ya no existe: ReferenceErrorEste código lanzaba ReferenceError en ejecución, pero solo en la rama que casi nunca se recorre. ESLint lo ve sin ejecutar nada. Es el ejemplo perfecto de por qué no-undef merece nivel error.
6 y 7 · no-console. Registros de depuración de 08-01. Los del service worker están permitidos por la capa 3 (allí la consola es la única ventana). Los de la vista se sustituyen por el registrar() del ejercicio 2 de la lección anterior, que respeta niveles y no ensucia la consola en producción.
Resultado tras los arreglos:
$ npm run lint && npm run format:check
Checking formatting...
All matched files use Prettier code style!Cinco errores, de los cuales dos eran fallos reales que nadie había notado: un borrado que mentía sobre su resultado y un ReferenceError latente. Ninguna de las dos habría aparecido en una revisión rápida, y ambas costaban menos de un minuto de configuración.
Errores Comunes y Consejos
- Activar cientos de reglas de golpe en un proyecto existente. Salen 400 avisos, nadie los mira y la herramienta pierde toda credibilidad. Empieza por
recommendedmás las diez reglas del apartado 7, deja el resto enwarny sube el listón cuando el suelo esté limpio. - Olvidar
eslint-config-prettier, o ponerlo antes que tus reglas. Va el último del array. Si va antes, tus reglas de formato lo pisan y vuelve el bucle de guardar-formatear-quejarse. - No configurar
globalspor entorno. Sin ello,no-undefproduce falsos positivos ensw.js(conselfycaches) y en los ficheros de pruebas (condescribeyexpect), y la reacción típica —desactivar la regla— desarma el detector más útil que tienes. - Olvidar la extensión en los
import. Los empaquetadores lo perdonan; el navegador de 05-04 no. Actívalo conimport/extensionsen'always'. // eslint-disable-next-linesin nombre de regla ni motivo. Apaga todas las reglas de esa línea, incluidas las futuras, y nadie sabrá si sigue siendo necesario.- Mezclar el commit de formateo con cambios de lógica. El diff se vuelve ilegible y la revisión se hace imposible. Formato en un commit aislado, y su hash en
.git-blame-ignore-revs. - Hooks de Git tan lentos que la gente usa
--no-verify. El pre-commit solo debe mirar los ficheros preparados. Las pruebas completas van apre-pusho a CI. - Creer que un lint limpio significa que el código funciona. El caso 3 de 08-01 pasaba todas las reglas. El análisis estático comprueba la forma; el comportamiento se comprueba con pruebas, y eso empieza en la lección siguiente.
- Consejo: fija las versiones y usa
npm cien CI. Una actualización automática de ESLint puede activar reglas nuevas y poner en rojo un repositorio que nadie ha tocado. - Consejo:
npx eslint . --max-warnings=0convierte los avisos en errores para CI. Es la forma de impedir que loswarnse acumulen sin tener que subirlos todos aerroren el editor. - Consejo: escribe las convenciones en
CONTRIBUTING.md. Lo que no está escrito se discute en cada revisión; lo que está escrito se cita en una línea.
Ejercicios
Ejercicio 1 — Configuración completa por entornos.
Escribe el eslint.config.js de Nómada Tareas contemplando cinco entornos distintos: (a) js/**/*.js como módulos ES de navegador; (b) sw.js como service worker; (c) **/*.test.js con las globales de Jest y las reglas del plugin de pruebas; (d) cypress/**/*.js con cy, Cypress, describe e it como globales de solo lectura; (e) los ficheros de configuración en la raíz, que corren en Node. Justifica en comentarios por qué cada entorno necesita su capa y qué falso positivo evita.
Ejercicio 2 — Cazar los fallos con análisis estático.
Para cada uno de estos fragmentos, indica qué regla lo detecta, si --fix puede arreglarlo, y cuál es la corrección correcta:
// A
export async function sincronizar(tablero) {
repositorio.guardar(tablero);
return { ok: true };
}
// B
function siguienteEstado(estado) {
switch (estado) {
case 'pendiente':
return 'en-curso';
case 'en-curso':
registrar('info', 'cerrando');
case 'hecha':
return null;
}
}
// C
if (tarea.horasEstimadas = 0) {
throw new ErrorDeValidacion('Horas inválidas', 'horasEstimadas', 0);
}
// D
const abiertas = tablero.abiertas;
const cerradas = tablero.tareas.filter((t) => t.estado == 'hecha');
return cerradas.length;Ejercicio 3 — Documentar el módulo con JSDoc y activar @ts-check.
Toma js/modelo/tablero.js y añade documentación JSDoc completa: un @typedef para ResumenTablero con los seis campos, tipos para todos los métodos públicos (agregar, cambiarEstado, filtrar, resumen, horasPorResponsable), @throws donde corresponda y un @example con los números canónicos del backlog. Activa // @ts-check en el fichero y describe qué error señalaría el editor en cada uno de estos tres usos incorrectos: tablero.agregar({ id: 7, titulo: 'X' }), tablero.cambiarEstado('3', 'hecha') y tablero.resumen().
Soluciones
Solución 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) La aplicación. Globales del navegador: sin ellas, `document` y `fetch`
// dispararían no-undef en cada fichero de vista y de datos.
{
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 distinto: existe `self`, `caches`, `clients`;
// NO existe `document`. Declararlo aquí hace que un uso accidental de
// `document` en el SW sí se marque como error, que es lo que queremos.
{
files: ['sw.js'],
languageOptions: { sourceType: 'module', globals: { ...globals.serviceworker } },
rules: { 'no-console': 'off' }
},
// (c) Pruebas. Sin globals.jest, `describe`, `test` y `expect` serían no-undef
// en cada fichero, y la reacción típica (apagar no-undef) desarmaría la regla.
{
files: ['**/*.test.js', 'pruebas/**/*.js'],
plugins: { jest },
languageOptions: { globals: { ...globals.jest, ...globals.node } },
rules: {
'jest/no-focused-tests': 'error', // un test.only olvidado deja la suite sin ejecutar
'jest/no-disabled-tests': 'warn',
'jest/expect-expect': 'error',
'jest/valid-expect': 'error',
'no-console': 'off' // en una prueba, un console puntual es aceptable
}
},
// (d) Cypress. `cy` y `Cypress` son globales inyectadas por el ejecutor;
// además el código de las pruebas E2E corre en el 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' } // el estilo .should() lo dispara en falso
},
// (e) Configuración y scripts: se ejecutan en Node, no en el navegador.
// `process` y `console` son legítimos aquí y no lo son en js/**.
{
files: ['*.config.js', 'scripts/**/*.js'],
languageOptions: { sourceType: 'module', globals: { ...globals.node } },
rules: { 'no-console': 'off' }
},
prettier
];Solución 2
| Caso | Regla | ¿--fix? |
Corrección |
|---|---|---|---|
| A | require-await |
No | La función es async sin await: o sobra el async, o falta esperar. Aquí falta: await repositorio.guardar(tablero) si es asíncrono, o quitar async. El fallo real es que { ok: true } se devuelve sin garantía de guardado |
| B | no-fallthrough |
No | Tras registrar(...) falta un return o un break: 'en-curso' cae en 'hecha' y devuelve null en vez del estado siguiente. Además, falta un default (regla default-case) |
| C | no-cond-assign |
No | Es = en lugar de ===. La condición asigna 0 a horasEstimadas (pasando por el setter, que además lanzaría por R3) y evalúa a 0, que es falso: la validación nunca se dispara. Correcto: if (tarea.horasEstimadas === 0) |
| D | eqeqeq + no-unused-vars |
Sí para eqeqeq |
t.estado == 'hecha' → ===; y abiertas está declarada y no se usa: se borra la línea |
// A · corregido
export async function sincronizar(tablero) {
await repositorio.guardar(tablero);
return { ok: true };
}
// B · corregido
function siguienteEstado(estado) {
switch (estado) {
case 'pendiente':
return 'en-curso';
case 'en-curso':
registrar('info', 'cerrando');
return 'hecha';
case 'hecha':
return null;
default:
throw new ErrorDeValidacion(`Estado desconocido: "${estado}".`, 'estado', estado);
}
}
// C · corregido
if (tarea.horasEstimadas === 0) { … }
// D · corregido
return tablero.tareas.filter((t) => t.estado === 'hecha').length;Solución 3
// @ts-check
import { Tarea } from './tarea.js';
import { ErrorDeValidacion } from './errores.js';
/**
* Cifras agregadas del tablero en una fecha de referencia.
*
* @typedef {object} ResumenTablero
* @property {number} total Todas las tareas del tablero
* @property {number} abiertas Las que no están en estado 'hecha'
* @property {number} horasTotales Suma de horasEstimadas de todas
* @property {number} horasAbiertas Suma de horasEstimadas de las abiertas
* @property {number} vencidas Abiertas con fechaLimite pasada (R10)
* @property {number} esfuerzo Suma de horas × peso de prioridad
*/
export class Tablero {
/** @type {Tarea[]} */
#tareas = [];
/**
* @param {string} nombre
* @param {Tarea[]} [tareas=[]] Se agregan una a una, aplicando R1
*/
constructor(nombre, tareas = []) { … }
/**
* Añade una tarea al tablero.
* @param {Tarea} tarea Instancia de Tarea, no un objeto plano
* @returns {Tablero} el propio tablero, para encadenar
* @throws {ErrorDeValidacion} si no es una Tarea, o si el id ya existe (R1)
*/
agregar(tarea) { … }
/**
* Aplica una transición de estado a una tarea del tablero.
* @param {number} id Identificador numérico de la tarea
* @param {'pendiente'|'en-curso'|'hecha'} nuevo
* @returns {Tablero}
* @throws {ErrorDeValidacion} si la tarea no existe o la transición viola R6
*/
cambiarEstado(id, nuevo) { … }
/**
* @param {(tarea: Tarea) => boolean} predicado
* @returns {Tarea[]} array nuevo; el tablero no se modifica
*/
filtrar(predicado) { … }
/**
* @param {string} hoy Fecha ISO 'yyyy-MM-dd' de referencia
* @returns {ResumenTablero}
*
* @example
* const tablero = new Tablero('Taller Nómada', crearBacklog());
* tablero.resumen('2026-09-20');
* // { total: 6, abiertas: 5, horasTotales: 48,
* // horasAbiertas: 45, vencidas: 1, esfuerzo: 124 }
*/
resumen(hoy) { … }
/**
* Horas abiertas agrupadas por persona. Las tareas sin responsable (R8)
* se agrupan bajo la clave 'sin asignar'.
* @returns {Record<string, number>}
*
* @example
* tablero.horasPorResponsable(); // { Iván: 25, Lucía: 14, Marta: 6 }
*/
horasPorResponsable() { … }
}Los tres errores que señalaría el editor con @ts-check:
1) tablero.agregar({ id: 7, titulo: 'X' })
Argument of type '{ id: number; titulo: string; }' is not assignable to
parameter of type 'Tarea'. Type is missing the following properties: estado,
abierta, esfuerzo, cambiarEstado…
→ Es exactamente lo que comprueba R1 en ejecución, pero antes de ejecutar.
2) tablero.cambiarEstado('3', 'hecha')
Argument of type 'string' is not assignable to parameter of type 'number'.
→ El caso 1 de 08-01 (el id que llegaba como cadena), detectado en el editor.
3) tablero.resumen()
Expected 1 arguments, but got 0.
→ Sin fecha, `estaVencida` recibiría undefined y `vencidas` daría 0 en silencio:
un fallo mudo, del mismo tipo que el contador descuadrado del caso 3.Conclusión
Nómada Tareas ha dejado de depender de la disciplina individual. Sabes qué es un analizador estático y dónde está su frontera: puede afirmar que una variable no se usa, que un identificador no existe, que un async no espera nada o que hay una asignación dentro de un if; no puede saber si horasAbiertas debe sumar 45 o 48. Por eso el análisis estático y las pruebas no compiten: cubren familias distintas de fallos, y un proyecto serio tiene ambos.
Tienes ESLint configurado de verdad: un eslint.config.js plano por capas, con files delimitando cada entorno —la aplicación en el navegador, el service worker con su global propio, Node para las herramientas—, globals bien declarados para que no-undef sea un detector real y no una fuente de falsos positivos, los tres niveles (off/warn/error) usados con criterio, y las diez reglas que evitan bugs reales: no-unused-vars, no-undef, eqeqeq, no-implicit-globals, require-await, no-fallthrough, no-cond-assign, no-debugger y las demás. Sabes silenciar una regla nombrándola y justificándola, sabes qué arregla --fix y qué no —la forma sí, la intención nunca—, y conoces los plugins que aportan valor: importaciones (con import/extensions en 'always', imprescindible en el navegador, e import/no-cycle para proteger el grafo de 05-04), pruebas (con no-focused-tests cazando el test.only olvidado) y por qué el plugin de accesibilidad de JSX no encaja en un proyecto de DOM puro.
Tienes Prettier como formateador determinista y entiendes por qué no compite con ESLint sino que se reparten el trabajo: uno responde «¿está bien impreso?» y el otro «¿es correcto?». Sabes juntarlos sin bucles con eslint-config-prettier colocado el último, adoptarlo en un commit aislado y neutralizarlo en git blame. Y tienes las tres capas de defensa montadas: el editor formateando y corrigiendo al guardar, un hook de pre-commit con Husky y lint-staged que solo mira los ficheros preparados para que nadie sienta la tentación de --no-verify, y un flujo de GitHub Actions con npm ci, npm run lint y npm run format:check que nadie puede eludir, listo para recibir el paso npm test de la próxima lección.
Y tienes lo que ninguna herramienta puede darte: las convenciones. Nombres que documentan (horasAbiertas, estaVencida(), RepositorioLocal, #estado) y los antinombres que hay que erradicar; el criterio de tamaño de función —si necesitas un comentario para separar dos partes, son dos funciones—; comentarios que explican el porqué y no repiten el qué; JSDoc documentando la superficie pública de cada módulo con @typedef ResumenTablero recogiendo los números canónicos; // @ts-check con jsconfig.json para cazar el id que llega como cadena mientras lo escribes, sin compilar ni cambiar el despliegue; y métricas —complejidad ciclomática, deuda técnica— usadas como termómetro y nunca como objetivo. La ejecución real encontró siete problemas y dos eran fallos genuinos: un borrarTarea que devolvía true sin esperar al servidor y un ReferenceError latente en una rama poco recorrida.
Pero fíjate en lo que ninguna de esas siete líneas mencionó: que el resumen del tablero debe dar 45 horas abiertas de 48, que la transición hecha → en-curso está prohibida por R6, que una tarea sin título debe lanzar ErrorDeValidacion, que el backlog canónico tiene un esfuerzo ponderado de 124. Eso no es forma, es comportamiento, y ninguna regla estática lo puede comprobar: hay que ejecutar el código con entradas conocidas y comparar la salida con lo esperado. Eso es una prueba automatizada, y ahí van las tres deudas que dejaste apuntadas en la lección anterior. En Pruebas Unitarias con Jest montarás el ejecutor, escribirás la batería completa de Tarea y Tablero —y descubrirás que aquella insistencia de 03-03 en las funciones puras y aquella frontera limpia entre modelo y vista que llevas seis módulos manteniendo eran, desde el principio, lo que iba a hacer posible probarlo todo sin abrir un navegador—.
Curso de JavaScript: De Principiante a Avanzado
Módulo 1: Introducción a JavaScript
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
