Escena Viva es ya 1.0.0, con su etiqueta v1.0.0 en git, y su package.json está casi completo: nombre, versión, main, engines, dependencies, devDependencies. Queda un hueco, y es el que más se usa en el día a día: "scripts": { }. Ese objeto vacío es hoy el motivo de que, para arrancar el servidor, alguien tenga que recordar node src/servidor/servidor.js; para lanzar el informe de ocupación, node src/informes/ocupacion.js; y para la tubería de ventas, una ruta que solo conoce quien la escribió.

En esta lección convertimos scripts en la interfaz única del proyecto. Cuando terminemos, cualquier persona que clone el repositorio podrá escribir npm run y ver la lista completa de cosas que puede hacer, sin abrir un README ni preguntar a nadie. Y de paso resolveremos la pregunta que quedó pendiente en la lección 05-02: por qué "lint": "eslint src" funciona aunque nunca hayas instalado ESLint globalmente.

Contenido

  1. Qué es realmente el campo scripts
  2. Scripts predefinidos frente a scripts personalizados
  3. Los scripts de Escena Viva, uno a uno
  4. El PATH extendido: node_modules/.bin
  5. Paso de argumentos con --
  6. Variables de entorno de los scripts
  7. Ganchos pre y post (y la advertencia de postinstall)
  8. Encadenar comandos y sobrevivir a Windows
  9. ESLint y Prettier: qué resuelve cada uno
  10. Errores comunes y consejos
  11. Ejercicios
  12. Conclusión

  1. Qué es realmente el campo scripts

scripts es un objeto donde cada clave es un nombre y cada valor es una línea de shell. Cuando ejecutas npm run <nombre>, npm:

  1. Busca la clave <nombre> en el package.json del directorio actual.
  2. Prepara un entorno especial (variables npm_* y un PATH ampliado, ya lo veremos).
  3. Lanza esa línea con el shell del sistema (/bin/sh en POSIX, cmd.exe en Windows salvo configuración distinta).
  4. Devuelve el código de salida del comando: 0 es éxito, cualquier otro es fallo.

No hay magia adicional. No es un lenguaje nuevo: es shell. Lo valioso no es el mecanismo, sino la convención: todo el mundo espera encontrar ahí los comandos del proyecto.

Comando Qué hace
npm run Lista todos los scripts disponibles con su contenido
npm run <nombre> Ejecuta el script <nombre>
npm run <nombre> -- <args> Ejecuta el script pasándole argumentos extra
npm start Atajo de npm run start
npm test Atajo de npm run test
npm run <nombre> --silent Ejecuta ocultando el eco de npm

  1. Scripts predefinidos frente a scripts personalizados

npm reconoce un puñado de nombres predefinidos que se pueden invocar sin la palabra run:

Nombre Invocación corta Comportamiento especial
start npm start Si no lo defines, npm intenta node server.js
test npm test Si no lo defines, falla con un aviso
stop npm stop Sin valor por defecto
restart npm restart Ejecuta stop, luego restart, luego start

Todo lo demás es personalizado y exige npm run. Es decir: npm dev no existe (npm intentará interpretarlo como un subcomando suyo y fallará), hay que escribir npm run dev.

Un detalle histórico que sigue vivo: si no defines start, npm busca server.js en la raíz. Escena Viva tiene su servidor en src/servidor/servidor.js, así que el valor por defecto no nos sirve y debemos declararlo explícitamente. Es lo correcto de todos modos: el script explícito documenta el punto de entrada.

  1. Los scripts de Escena Viva, uno a uno

Este es el package.json completo tras esta lección:

{
  "name": "escena-viva",
  "version": "1.0.0",
  "description": "Plataforma de venta de entradas para eventos culturales",
  "main": "src/servidor/servidor.js",
  "type": "commonjs",
  "private": true,
  "engines": { "node": ">=24.5.0 <25" },
  "scripts": {
    "start": "node src/servidor/servidor.js",
    "dev": "node --watch src/servidor/servidor.js",
    "catalogo": "node src/catalogo.js",
    "informe": "node src/informes/ocupacion.js",
    "ventas": "node src/informes/tuberia-ventas.js",
    "lint": "eslint src",
    "format": "prettier --write \"src/**/*.js\"",
    "format:check": "prettier --check \"src/**/*.js\"",
    "comprobar": "npm run lint && npm run format:check",
    "test": "node -e \"console.error('Sin pruebas todavia: llegan en el Modulo 9'); process.exit(1)\""
  },
  "dependencies": { "dotenv": "^17.2.1" },
  "devDependencies": { "prettier": "^3.6.2" }
}

Vamos comando a comando.

start

"start": "node src/servidor/servidor.js"

Arranca el servidor HTTP que construimos en el Módulo 4. Coincide con el campo main, y eso no es casualidad: main declara el punto de entrada del paquete, start declara cómo se ejecuta el proceso. Un servidor en producción se levanta con npm start (o directamente con node, ver el aviso más abajo).

dev

"dev": "node --watch src/servidor/servidor.js"

--watch es una bandera nativa de Node (estable desde Node 22) que reinicia el proceso cuando cambia cualquier fichero del que dependa el módulo cargado. Durante años esto exigía instalar nodemon como dependencia de desarrollo; hoy no hace falta ninguna dependencia externa, lo que encaja con la filosofía de Escena Viva: cero dependencias donde la plataforma ya sirve.

Diferencias que conviene conocer:

Aspecto node --watch nodemon
Instalación Ninguna, viene con Node Dependencia de desarrollo
Qué vigila El grafo de módulos cargado Patrones configurables de ficheros
Configuración --watch-path, --watch-preserve-output nodemon.json con reglas ricas
Otros lenguajes No Sí (puede reiniciar cualquier comando)

Para nuestro caso, --watch sobra y basta. Si algún día necesitas vigilar también datos/eventos.json, añade --watch-path=./datos.

catalogo

"catalogo": "node src/catalogo.js"

La CLI del catálogo del Módulo 1: imprime por stdout los tres eventos con sus sesiones. Ponerla en scripts la convierte en parte de la interfaz del proyecto en vez de un fichero suelto que hay que descubrir leyendo el árbol.

informe y ventas

"informe": "node src/informes/ocupacion.js",
"ventas": "node src/informes/tuberia-ventas.js"

informe calcula la ocupación (7 sesiones, aforo 3000, 1811 vendidas). ventas lanza la tubería de streams que lee datos/ventas.csv y produce el agregado por sesión. Los dos escriben datos por stdout y diagnósticos por stderr, la convención del curso, así que esto sigue funcionando:

npm run informe --silent > informe-ocupacion.txt

El --silent es importante aquí: sin él, npm imprime dos líneas de cabecera (> [email protected] informe y el comando) que se colarían en tu fichero. npm las manda a stdout, no a stderr, así que si un script está pensado para redirigirse, acostúmbrate a --silent (o su forma corta -s).

lint

"lint": "eslint src"

Esta es la promesa que hicimos en 05-02. ESLint no está instalado globalmente y sin embargo el script funcionará en cuanto añadas ESLint a devDependencies. La explicación completa está en el apartado 4.

format y format:check

"format": "prettier --write \"src/**/*.js\"",
"format:check": "prettier --check \"src/**/*.js\""

--write reescribe los ficheros; --check solo comprueba y falla si algo no está formateado. El primero es para el desarrollo, el segundo para la integración continua, donde nunca quieres que una máquina modifique ficheros: quieres que te diga que están mal.

Fíjate en las comillas alrededor del patrón. Sin ellas, el shell POSIX expandiría src/**/*.js antes de que Prettier lo viera, y el resultado dependería de la configuración de globstar del shell y del sistema operativo. Con comillas, el patrón llega intacto a Prettier, que lo expande él mismo de forma idéntica en todas las plataformas. Es un detalle pequeño con consecuencias reales.

Los dos puntos en format:check no significan nada para npm: es solo una convención visual para agrupar variantes de un mismo script. npm run format:check es un nombre como cualquier otro.

comprobar

"comprobar": "npm run lint && npm run format:check"

Un script de composición: no hace trabajo propio, encadena otros dos. Este es el patrón que conviene interiorizar: scripts pequeños de un solo propósito, más scripts que los combinan. Si mañana añadimos comprobación de tipos, se añade a la cadena sin tocar los demás.

test

"test": "node -e \"console.error('Sin pruebas todavia: llegan en el Modulo 9'); process.exit(1)\""

Un test honesto. Hay dos malas alternativas:

  • Dejar el test por defecto de npm (echo \"Error: no test specified\" && exit 1), que no dice nada útil.
  • Poner "test": "exit 0" para que la CI pase en verde. Esto es mentir: el semáforo verde deja de significar algo.

Nuestro script escribe el motivo por stderr y sale con código 1. La CI fallará, que es exactamente lo que debe pasar en un proyecto 1.0.0 sin pruebas, y el mensaje dice dónde está la solución. En el Módulo 9 este script pasará a ser node --test test/ y el rojo se volverá verde con razón.

  1. El PATH extendido: node_modules/.bin

Aquí está la respuesta prometida. Muchos paquetes declaran ejecutables en su propio package.json:

{
  "name": "prettier",
  "bin": { "prettier": "./bin/prettier.cjs" }
}

Cuando npm instala un paquete con campo bin, crea un enlace en node_modules/.bin/. Tras instalar Prettier, el proyecto tiene:

ls node_modules/.bin
# prettier

Y cuando ejecutas npm run <algo>, npm antepone ese directorio al PATH del proceso hijo. Puedes verlo tú mismo:

"donde": "node -e \"console.log(process.env.PATH.split(':')[0])\""
npm run donde --silent
# /home/tu-usuario/escena-viva/node_modules/.bin

Por eso "format": "prettier --write ..." encuentra prettier sin instalación global y sin escribir ./node_modules/.bin/prettier. Y por eso "lint": "eslint src" funcionará en cuanto ESLint esté en devDependencies.

flowchart LR
  A["npm run format"] --> B["PATH = node_modules/.bin : PATH original"]
  B --> C["shell busca 'prettier'"]
  C --> D["node_modules/.bin/prettier"]
  D --> E["node_modules/prettier/bin/prettier.cjs"]

Tres consecuencias prácticas:

  • La versión que se ejecuta es la del proyecto, no la del sistema. Dos proyectos con Prettier 2 y Prettier 3 conviven sin conflictos.
  • No hace falta npx dentro de los scripts. npx prettier funciona, pero añade una resolución innecesaria. Dentro de scripts, escribe el binario a secas.
  • Si un comando falla con command not found dentro de un script, casi siempre significa que falta el paquete en las dependencias, no que falte una instalación global.

  1. Paso de argumentos con --

Esto no hace lo que parece:

npm run catalogo --sala="Sala Boveda"

npm interpreta --sala como una opción suya (que no conoce) y no la pasa al script. El separador -- marca la frontera:

npm run catalogo -- --sala="Sala Boveda"

Ahora el comando ejecutado es node src/catalogo.js --sala="Sala Boveda" y los argumentos llegan a process.argv. Con un script así:

'use strict';

// Lee --sala del argv; devuelve null si no se ha pasado.
function leerSalaDeArgumentos(argumentos) {
  const encontrado = argumentos.find((arg) => arg.startsWith('--sala='));
  return encontrado ? encontrado.slice('--sala='.length) : null;
}

module.exports = { leerSalaDeArgumentos };

El filtro funciona igual lo lances con node directamente o con npm run ... --. Regla mnemotécnica: todo lo que va antes de -- es para npm; todo lo que va después, para tu programa.

  1. Variables de entorno de los scripts

npm inyecta en el proceso hijo un conjunto de variables derivadas del manifiesto y de la configuración:

Variable Contenido
npm_package_name escena-viva
npm_package_version 1.0.0
npm_lifecycle_event Nombre del script en ejecución
npm_config_* Cualquier opción de configuración de npm
PATH Extendido con node_modules/.bin

Un uso real: que el servidor registre su versión al arrancar sin tener que leer el package.json en tiempo de ejecución.

// Dentro de src/servidor/servidor.js, al arrancar.
const version = process.env.npm_package_version ?? 'desconocida';
process.stderr.write(`Escena Viva ${version} escuchando en ${HOST}:${PUERTO}\n`);

Ojo con la trampa: si alguien arranca con node src/servidor/servidor.js en vez de npm start, esa variable no existe. Por eso el ?? con un valor de reserva. Nunca hagas depender la lógica de negocio de una variable npm_*; úsalas solo para diagnósticos.

npm_lifecycle_event permite que un mismo fichero se comporte distinto según cómo lo hayan llamado, aunque suele ser preferible tener dos scripts explícitos.

Las opciones de configuración también llegan. Si ejecutas npm run informe --formato=csv, npm expone npm_config_formato=csv. Es un mecanismo real, pero prefiere -- y process.argv: es explícito, portable y funciona cuando ejecutas el script sin npm.

  1. Ganchos pre y post

Para cualquier script x, npm ejecuta automáticamente prex antes y postx después, siempre que existan. Funciona con los predefinidos y con los personalizados:

"prestart": "node -e \"require('node:fs').accessSync('datos/eventos.json')\"",
"start": "node src/servidor/servidor.js"

Si el fichero de datos no existe, prestart falla, y npm no ejecuta start. Un fallo temprano y claro en lugar de un servidor arrancado que revienta en la primera petición.

Los ganchos más conocidos son los del ciclo de instalación:

Gancho Cuándo se dispara
preinstall Antes de instalar dependencias
install / postinstall Después de instalarlas
prepare Tras npm install local y antes de npm publish
prepublishOnly Solo antes de publicar

La advertencia de seguridad

postinstall es potente y por eso es peligroso. Cuando instalas un paquete, su postinstall se ejecuta en tu máquina con tus permisos, sin preguntarte nada. Puede leer tu ~/.npmrc (donde vive tu token de npm), tus variables de entorno, tus claves SSH.

Esto no es teoría: es el vector habitual de los ataques a la cadena de suministro. Un mantenedor con la cuenta comprometida publica una versión de parche con un postinstall malicioso, y miles de máquinas lo ejecutan en horas.

Medidas inmediatas, que ampliaremos en la lección 05-06:

  • En CI y producción, npm ci --ignore-scripts cuando el proyecto lo tolere.
  • Revisar los postinstall de las dependencias nuevas antes de aceptarlas.
  • No guardar secretos en el entorno de la máquina que instala paquetes.

En tus propios scripts, postinstall está bien para tareas del proyecto (crear un directorio, generar un fichero derivado). El problema es el código ajeno, no el mecanismo.

  1. Encadenar comandos y sobrevivir a Windows

Los operadores del shell funcionan tal cual:

Operador Significado
a && b Ejecuta b solo si a termina con código 0
a || b Ejecuta b solo si a falla
a ; b Ejecuta b siempre
a & b Ejecuta ambos en paralelo (no en cmd.exe)

&& es el que quieres el 95 % de las veces: encadena y se detiene en el primer fallo. || sirve para valores de reserva: "informe": "node src/informes/ocupacion.js || echo 'informe no disponible' 1>&2".

La portabilidad es el problema real. Estas cosas rompen en Windows:

Construcción POSIX Problema en Windows Solución
rm -rf dist rm no existe node:fs o el paquete rimraf
NODE_ENV=produccion node x.js Sintaxis no válida en cmd cross-env
cmd1 & cmd2 (paralelo) & es secuencial npm-run-all --parallel
cat fichero | node x.js Comportamiento distinto Leer el fichero desde Node
Comillas simples cmd.exe no las interpreta Comillas dobles escapadas

Nuestros scripts de Escena Viva son deliberadamente portables: solo invocan node, eslint y prettier, y usan comillas dobles escapadas. Si algún día necesitas variables de entorno en línea:

"dev:verbose": "cross-env NIVEL_LOG=debug node --watch src/servidor/servidor.js"

Regla general: si un script empieza a parecerse a un programa, escríbelo como un programa en scripts/ y llámalo con node. JavaScript es portable; el shell no.

  1. ESLint y Prettier: qué resuelve cada uno

Se confunden a menudo y no compiten:

ESLint Prettier
Pregunta que responde ¿Este código tiene problemas? ¿Este código está bien presentado?
Detecta Variables sin usar, await en bucles, promesas sin capturar Nada semántico
Modifica Solo lo que sabe arreglar (--fix) Todo el formato, siempre
Discutible Sí, cada regla es una decisión de equipo No, esa es la gracia

La configuración moderna es simple: Prettier manda en el formato, ESLint manda en la corrección, y se desactivan en ESLint las reglas de estilo que chocarían con Prettier. Con el formato fuera de la conversación, las revisiones de código dejan de discutir comillas y hablan de lo que importa.

Para Escena Viva, ESLint entraría así:

npm install --save-dev eslint

Y el script "lint": "eslint src" empieza a funcionar sin más, gracias al PATH del apartado 4. La configuración de reglas es un tema en sí mismo y no la desarrollamos aquí: lo relevante para esta lección es que la herramienta se invoca desde scripts y vive en devDependencies.

Errores Comunes y Consejos

  • Escribir npm dev. Solo start, test, stop y restart prescinden de run. Para el resto, npm run dev.
  • Olvidar el -- al pasar argumentos. npm run catalogo --sala=X no llega a tu programa; npm run catalogo -- --sala=X sí.
  • Redirigir sin --silent. Las cabeceras de npm van a stdout y contaminan el fichero de salida.
  • Poner "test": "exit 0". Un verde falso es peor que un rojo honesto: destruye la confianza en la CI.
  • Scripts gigantes de cinco comandos encadenados. Divide en scripts pequeños y compón con &&. Se depuran mejor y se reutilizan.
  • Asumir bash. sh no es bash y cmd.exe no es ninguno de los dos. Si dudas, mueve la lógica a un fichero .js.
  • Depender de npm_package_version en la lógica. Solo existe si se arrancó con npm. Úsala para diagnósticos, con valor de reserva.
  • Consejo: ejecuta npm run sin argumentos al entrar en un proyecto ajeno. Es la documentación más fiable que vas a encontrar, porque si estuviera desactualizada, no funcionaría.
  • Consejo: nombra los scripts por lo que hacen para el negocio (informe, ventas, catalogo), no por la herramienta que usan. La herramienta cambiará; la intención, no.

Ejercicios

Ejercicio 1: un script verificar-datos con gancho

Añade a Escena Viva un script verificar-datos que compruebe que datos/eventos.json existe, es JSON válido y contiene exactamente 3 eventos con 7 sesiones en total. Debe imprimir el resumen por stdout y salir con código 1 si algo falla. Engánchalo para que se ejecute automáticamente antes de informe.

Ejercicio 2: argumentos y --

Modifica src/catalogo.js para aceptar --sala=<nombre> y filtrar las sesiones de esa sala. Comprueba que funciona tanto con node directo como con npm run. Explica por qué una de las dos formas necesita --.

Ejercicio 3: composición y portabilidad

Diseña un script publicar-informes que ejecute comprobar, luego informe y luego ventas, deteniéndose al primer fallo. Después, revisa esta propuesta de un compañero y di qué tres cosas romperán en Windows:

"publicar-informes": "rm -rf salida && mkdir salida && NODE_ENV=prod node src/informes/ocupacion.js > salida/ocupacion.txt"

Soluciones

Solución 1

// src/utiles/verificar-datos.js
'use strict';

const fs = require('node:fs');
const { RUTA_EVENTOS } = require('../config/rutas.js');

const EVENTOS_ESPERADOS = 3;
const SESIONES_ESPERADAS = 7;

// Lee el fichero semilla y valida su forma; lanza si algo no cuadra.
function verificarDatos() {
  const crudo = fs.readFileSync(RUTA_EVENTOS, 'utf8');
  const eventos = JSON.parse(crudo);

  if (!Array.isArray(eventos)) {
    throw new Error('eventos.json no contiene un array');
  }
  const totalSesiones = eventos.reduce((suma, ev) => suma + ev.sesiones.length, 0);

  if (eventos.length !== EVENTOS_ESPERADOS || totalSesiones !== SESIONES_ESPERADAS) {
    throw new Error(
      `Se esperaban ${EVENTOS_ESPERADOS} eventos y ${SESIONES_ESPERADAS} sesiones; ` +
        `hay ${eventos.length} y ${totalSesiones}`
    );
  }
  return { eventos: eventos.length, sesiones: totalSesiones };
}

if (require.main === module) {
  try {
    const resumen = verificarDatos();
    process.stdout.write(`Datos correctos: ${resumen.eventos} eventos, ${resumen.sesiones} sesiones\n`);
  } catch (error) {
    process.stderr.write(`Datos invalidos: ${error.message}\n`);
    process.exit(1);
  }
}

module.exports = { verificarDatos };
"verificar-datos": "node src/utiles/verificar-datos.js",
"preinforme": "npm run verificar-datos --silent"

El gancho preinforme corta la cadena antes de que informe intente trabajar con datos corruptos.

Solución 2

// Fragmento de src/catalogo.js
const filtroSala = leerSalaDeArgumentos(process.argv.slice(2));
const sesionesVisibles = filtroSala
  ? sesiones.filter((sesion) => sesion.sala === filtroSala)
  : sesiones;
node src/catalogo.js --sala="Teatro Almendra"
npm run catalogo -- --sala="Teatro Almendra"

La forma con npm run necesita -- porque npm consume los argumentos que empiezan por -- como opciones propias. Con node no hay intermediario: todo lo posterior al fichero va directo a process.argv.

Solución 3

"publicar-informes": "npm run comprobar && npm run informe --silent && npm run ventas --silent"

Los tres problemas de la propuesta del compañero en Windows:

  1. rm -rf no existe en cmd.exe; hay que usar node -e "fs.rmSync('salida', { recursive: true, force: true })" o rimraf.
  2. NODE_ENV=prod comando no es sintaxis válida en cmd.exe; requiere cross-env.
  3. mkdir salida falla si el directorio ya existe con distinto comportamiento según el shell; conviene fs.mkdirSync(..., { recursive: true }).

Además, la redirección > sin --silent metería las cabeceras de npm dentro de salida/ocupacion.txt.

Conclusión

El package.json de Escena Viva ya no tiene huecos. scripts es hoy la interfaz única del proyecto: npm start levanta el servidor, npm run dev lo recarga con node --watch sin depender de nadie, npm run catalogo, npm run informe y npm run ventas exponen el trabajo de los módulos 1 y 3, npm run comprobar compone linting y formato, y npm test falla con un mensaje honesto que apunta al Módulo 9.

Por el camino has entendido el mecanismo que lo sostiene: npm ejecuta shell con un PATH que empieza en node_modules/.bin, y por eso las herramientas del proyecto se invocan por su nombre sin instalación global. Sabes separar los argumentos de npm de los tuyos con --, aprovechar npm_package_version para diagnósticos, encadenar con && sin sacrificar la portabilidad, y desconfiar del postinstall del código ajeno.

En la lección siguiente, Creación y Publicación de Paquetes, cambiamos de lado del mostrador. Hasta ahora hemos consumido paquetes; ahora vamos a publicar uno. Extraeremos src/utiles/formato.js —con formatearPrecio, formatearFecha y generarCodigoEntrada— a un paquete propio, @escena-viva/formato, y veremos cómo se decide qué es API pública con exports, qué se sube realmente con files, cómo se comprueba el tarball con npm pack antes de que sea tarde, y por qué despublicar un paquete es casi imposible.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados