En la lección anterior convertimos el campo scripts en la interfaz única del proyecto: cualquier persona que llegue a Escena Viva sabe que npm start arranca el servidor y npm run comprobar valida el código, sin necesidad de conocer las rutas internas. Hasta ahora hemos estado siempre del mismo lado del mostrador: éramos consumidores de paquetes. En esta lección cambiamos de lado y publicamos uno.
El caso concreto es real y modesto, que es exactamente como deben empezar estas cosas. src/utiles/formato.js contiene tres funciones —formatearPrecio, formatearFecha y generarCodigoEntrada— que ya estamos copiando y pegando en otros dos proyectos internos: el panel de taquilla y el generador de informes contables. Ese es el síntoma que justifica extraer un paquete. Vamos a convertir ese fichero en @escena-viva/formato, decidiendo con precisión qué se puede importar desde fuera, qué se sube al registro y qué pasa cuando publicas algo que luego quieres retirar.
Contenido
- Cuándo extraer un paquete y cuándo no
- El caso de Escena Viva:
@escena-viva/formato - Estructura de un paquete publicable
- La API pública:
main,exportsytypes - Qué se sube:
filesfrente a.npmignore - Paquetes con ámbito y
publishConfig - Probar antes de publicar:
npm packynpm link - Publicar: login,
--dry-run, 2FA y tokens - Versiones nuevas y etiquetas de distribución
- Despublicar casi nunca se puede
- Errores Comunes y Consejos
- Ejercicios
- Conclusión
- Cuándo extraer un paquete y cuándo no
Extraer código a un paquete tiene un coste permanente: un repositorio más, un ciclo de versiones más, y la obligación de no romper a quien lo consume. Ese coste solo se paga cuando hay reutilización real.
| Señal | ¿Extraer? | Motivo |
|---|---|---|
| El mismo fichero copiado en dos o más proyectos | Sí | Las copias divergen y los errores se arreglan una sola vez |
| Código estable, con poca deriva funcional | Sí | Un paquete que cambia cada semana es un lastre para sus consumidores |
| API pequeña y bien definida (3-6 funciones) | Sí | La superficie que hay que mantener compatible es asumible |
| Se usa solo en este proyecto | No | Basta con un módulo interno bien colocado |
| Depende del estado global de la aplicación | No | Un paquete debe ser una función de sus entradas |
| Se extrae "por si acaso" o porque queda profesional | No | Es coste sin beneficio |
La regla práctica es la de las tres copias: cuando vas a hacer la tercera copia manual de un fragmento, ese fragmento pide un paquete.
Nuestro formato.js cumple: está copiado en dos proyectos, tiene tres funciones puras, no toca disco ni red, y su comportamiento no ha cambiado desde el módulo 1.
- El caso de Escena Viva:
@escena-viva/formato
@escena-viva/formatoEste es el código que vamos a extraer, tal como estaba en el proyecto:
// src/utiles/formato.js (versión que había en Escena Viva)
function formatearPrecio(precioCentimos) {
const euros = (precioCentimos / 100).toFixed(2);
return `${euros.replace('.', ',')} €`;
}
function formatearFecha(fechaIso) {
const fecha = new Date(fechaIso);
const dia = String(fecha.getUTCDate()).padStart(2, '0');
const mes = String(fecha.getUTCMonth() + 1).padStart(2, '0');
return `${dia}/${mes}/${fecha.getUTCFullYear()}`;
}
function generarCodigoEntrada(anyo, secuencia) {
const numero = String(secuencia).padStart(6, '0');
return `EV-${anyo}-${numero}`;
}
module.exports = { formatearPrecio, formatearFecha, generarCodigoEntrada };Creamos un directorio hermano del proyecto (no dentro de él) para el paquete:
El flag --scope hace que el nombre inicial sea @escena-viva/escena-viva-formato; lo ajustamos a mano en el package.json. El manifiesto final del paquete queda así:
{
"name": "@escena-viva/formato",
"version": "0.1.0",
"description": "Formato de precios, fechas y codigos de entrada de Escena Viva",
"license": "MIT",
"type": "commonjs",
"main": "./index.js",
"exports": {
".": "./index.js",
"./precio": "./lib/precio.js",
"./package.json": "./package.json"
},
"files": ["index.js", "lib/", "README.md", "LICENSE"],
"engines": { "node": ">=20" },
"publishConfig": { "access": "public" },
"scripts": {
"test": "node --test",
"prepublishOnly": "npm test"
}
}Fíjate en tres cosas que ya no aparecen: private: true (un paquete privado no se puede publicar), main apuntando a un fichero de servidor, y dependencies. Este paquete no tiene ninguna dependencia, y eso es una virtud, no una carencia.
El código se reorganiza en módulos pequeños dentro de lib/ y un index.js que reexporta:
// lib/precio.js — convierte centimos enteros en una cadena legible en euros.
function formatearPrecio(precioCentimos) {
if (!Number.isInteger(precioCentimos)) {
throw new TypeError('El precio debe ser un entero de centimos');
}
const euros = (precioCentimos / 100).toFixed(2);
return `${euros.replace('.', ',')} €`;
}
module.exports = { formatearPrecio };// index.js — punto de entrada unico del paquete: reexporta la API publica.
const { formatearPrecio } = require('./lib/precio.js');
const { formatearFecha } = require('./lib/fecha.js');
const { generarCodigoEntrada } = require('./lib/codigo.js');
module.exports = { formatearPrecio, formatearFecha, generarCodigoEntrada };En Escena Viva, el cambio del lado consumidor es mínimo. Se borra src/utiles/formato.js, se sustituye el require en los ficheros que lo usaban y se declara la dependencia:
// Antes, en src/informes/ocupacion.js
const { formatearPrecio, formatearFecha } = require('../utiles/formato.js');
// Despues
const { formatearPrecio, formatearFecha } = require('@escena-viva/formato');Empezamos en 0.1.0 a propósito. Como vimos en 05-03, en el rango ^0.x.y el acento circunflejo solo permite cambios de parche, así que un 0.x nos deja margen para reorganizar la API sin romper a nadie por accidente. Cuando se estabilice, saltaremos a 1.0.0 como acto deliberado.
- Estructura de un paquete publicable
Un paquete pequeño y responsable cabe en muy pocos ficheros:
escena-viva-formato/ ├── package.json Manifiesto: nombre, version, exports, files ├── index.js Punto de entrada de la API publica ├── lib/ precio.js, fecha.js, codigo.js ├── test/formato.test.js ├── README.md Que hace, como se instala, ejemplos ├── CHANGELOG.md Que cambio en cada version └── LICENSE Texto completo de la licencia
El README.md no es documentación decorativa: es la página que la gente ve en el registro y, en la práctica, lo primero que decide si tu paquete se usa. Debe contener el ejemplo mínimo que funciona copiado y pegado: instalación en una línea y tres líneas de uso con su salida (formatearPrecio(2450) // '24,50 €').
El fichero LICENSE con el texto completo importa más de lo que parece: sin él, legalmente tu código es "todos los derechos reservados" y una empresa con abogados no podrá usarlo aunque esté publicado en abierto. El campo license del package.json debe coincidir con ese fichero.
- La API pública:
main, exports y types
main, exports y typesHistóricamente, main era el único control: indicaba el fichero que se cargaba al hacer require('paquete'). Su problema es que no impedía nada. Cualquiera podía escribir require('@escena-viva/formato/lib/precio.js') y acoplarse a tu estructura interna; el día que renombrases lib/ habrías roto a un consumidor sin haber cambiado ninguna función pública.
El campo exports resuelve eso. Es hoy la forma correcta de declarar la API pública porque hace dos cosas a la vez: mapea nombres públicos a ficheros reales y bloquea todo lo que no esté listado.
Con ese mapa, el comportamiento desde fuera del paquete es:
| Importación | Resultado | Motivo |
|---|---|---|
require('@escena-viva/formato') |
Funciona | La clave . apunta a index.js |
require('@escena-viva/formato/precio') |
Funciona | Subruta declarada explícitamente |
require('@escena-viva/formato/lib/precio.js') |
ERR_PACKAGE_PATH_NOT_EXPORTED |
La ruta interna no está en el mapa |
require('@escena-viva/formato/lib/fecha.js') |
ERR_PACKAGE_PATH_NOT_EXPORTED |
Ídem, aunque el fichero exista |
require('@escena-viva/formato/package.json') |
Funciona | Declarada a propósito |
Ese bloqueo es una libertad para ti: mientras index.js y ./precio sigan devolviendo lo mismo, puedes reorganizar lib/ sin publicar una versión mayor. Observa que ./package.json se declara explícitamente porque varias herramientas de análisis lo leen; si no lo pones, también queda bloqueado.
exports admite además condiciones, útiles cuando un paquete ofrece CommonJS y ESM. Node elige la rama según cómo se cargue el paquete, y default debe ir siempre la última porque se evalúan en orden:
Sobre types: aunque no uses TypeScript, muchos de tus consumidores sí. Declarar "types": "./index.d.ts" y escribir un fichero de veinte líneas con las firmas hace que tu paquete se autocomplete en sus editores. Es opcional, pero es de las cosas con mejor relación entre esfuerzo y agradecimiento.
- Qué se sube:
files frente a .npmignore
files frente a .npmignoreCuando publicas, npm empaqueta un directorio entero en un tarball. Decidir qué entra tiene dos mecanismos posibles y no son equivalentes.
| Mecanismo | Cómo funciona | Riesgo |
|---|---|---|
.npmignore |
Lista negra: se sube todo salvo lo listado | Un fichero nuevo se sube por defecto |
files en package.json |
Lista blanca: solo se sube lo listado | Un fichero nuevo queda fuera por defecto |
files es más seguro por la misma razón que un cortafuegos que deniega por defecto es más seguro que uno que permite por defecto. Con .npmignore, el día que alguien añada notas-internas.md o un .env.produccion al repositorio, ese fichero viajará al registro público salvo que alguien se acuerde de actualizar la lista negra. Con files, ese mismo fichero simplemente no existe para npm.
Hay un detalle que confunde: si no hay files ni .npmignore, npm usa el .gitignore como lista negra; y si hay .npmignore, el .gitignore deja de aplicarse por completo, lo que produce sorpresas desagradables.
Algunos ficheros se incluyen siempre (package.json, README, LICENSE y el fichero de main) y otros se excluyen siempre (node_modules/, .git/, package-lock.json, .npmrc). Que .npmrc esté siempre excluido es una salvaguarda importante, porque ahí viven los tokens. Pero no confíes en salvaguardas: usa files.
- Paquetes con ámbito y
publishConfig
publishConfig@escena-viva/formato es un paquete con ámbito (scoped): el @escena-viva es un espacio de nombres asociado a un usuario u organización del registro. Evita colisiones de nombres —formato a secas está tomado desde hace años—, agrupa los paquetes de una misma organización y permite políticas de acceso comunes.
El detalle que sorprende a todo el mundo la primera vez es que los paquetes con ámbito son privados por defecto, y publicar privado requiere un plan de pago. Si tu paquete es abierto hay que decirlo con npm publish --access public, pero escribir ese flag cada vez es una invitación al olvido. Mejor dejarlo en el manifiesto:
publishConfig también sirve para el caso contrario, muy habitual en empresa: un paquete interno que debe ir al registro privado de la compañía y nunca al público. Fijar ahí el registry evita una fuga de código por un npm publish distraído.
- Probar antes de publicar:
npm pack y npm link
npm pack y npm linkPublicar es irreversible en la práctica, así que la comprobación previa no es opcional.
npm pack construye exactamente el mismo tarball que se subiría, pero lo deja en tu disco:
npm notice 📦 @escena-viva/[email protected] npm notice Tarball Contents npm notice 1.1kB LICENSE npm notice 842B README.md npm notice 318B index.js npm notice 402B lib/codigo.js npm notice 380B lib/fecha.js npm notice 455B lib/precio.js npm notice 621B package.json npm notice total files: 7 npm notice filename: escena-viva-formato-0.1.0.tgz
Esa lista es el momento de la verdad. Lo que hay que buscar activamente:
- ¿Aparece algún
.env? Sería una filtración de credenciales publicada en abierto. - ¿Aparece
datos/con las ventas de prueba? Podría contener correos reales. - ¿Aparece
test/? No es peligroso, pero infla la descarga de todos tus consumidores. - ¿Falta
index.jso algún fichero delib/? El paquete se instalaría roto.
npm pack --dry-run muestra la misma lista sin escribir nada, y tar -tzf fichero.tgz inspecciona un tarball ya creado. La prueba definitiva es instalar ese tarball en el proyecto real:
Si el informe de ocupación sigue imprimiendo 24,50 €, el paquete funciona tal y como lo recibirá cualquiera.
La alternativa para el desarrollo diario es npm link, que crea enlaces simbólicos:
# En el directorio del paquete
npm link
# En el directorio de Escena Viva
npm link @escena-viva/formatoEl primer comando enlaza el paquete a la carpeta global de npm; el segundo crea dentro de node_modules/@escena-viva/formato un enlace simbólico a tu carpeta de trabajo. Cada cambio que guardes en el paquete lo ve el proyecto al instante, sin reinstalar. Sus rarezas conviene conocerlas antes de perder una tarde:
npm installen el proyecto puede deshacer el enlace y volver a bajar la versión del registro.- El enlace no respeta
filesniexportscon la misma fidelidad que un tarball: puedes estar usando un fichero que luego no se publicará. - Si paquete y proyecto dependen del mismo módulo, pueden cargarse dos copias distintas (problema clásico con librerías que mantienen estado interno).
Regla práctica: npm link para iterar rápido, npm pack más instalación del tarball para la comprobación final. Para deshacerlo, npm unlink @escena-viva/formato y npm install.
- Publicar: login,
--dry-run, 2FA y tokens
--dry-run, 2FA y tokensEl primer paso es autenticarse. npm login abre el navegador y guarda el token resultante en tu .npmrc de usuario (~/.npmrc), nunca en el del proyecto. Después, un ensayo que hace todo el proceso —construir el tarball, validar el manifiesto, comprobar permisos— salvo la subida, y solo entonces la publicación real:
El último comando ejecuta antes el gancho prepublishOnly que definimos, que a su vez lanza npm test. Es la aplicación práctica de lo que vimos en 05-04: un gancho que impide publicar una versión con las pruebas rojas.
Sobre la seguridad de la cuenta, dos medidas que no son negociables si tu paquete lo usa alguien más. La primera, 2FA (doble factor): se activa con npm profile enable-2fa auth-and-writes y hace que cada publicación pida un código temporal; la mayoría de los secuestros de paquetes conocidos han sido secuestros de la cuenta del mantenedor, no fallos del registro. La segunda, tokens de acceso granulares para CI: un servidor de integración continua no puede introducir un código de 2FA, así que necesita un token, y no debe ser uno clásico con permisos totales, sino uno granular, limitado a los paquetes que debe publicar y con fecha de caducidad, guardado como secreto del sistema de CI y jamás en el repositorio. Veremos la configuración completa de secretos y despliegue automático en el módulo 11.
- Versiones nuevas y etiquetas de distribución
Nunca edites el campo version a mano. npm version lo hace, además de dejar el repositorio coherente:
npm version patch # 0.1.0 -> 0.1.1 (corrección compatible)
npm version minor # 0.1.1 -> 0.2.0 (funcionalidad nueva)
npm version major # 0.2.0 -> 1.0.0 (ruptura)Cada uno de esos comandos, en un directorio con git:
- Comprueba que el árbol de trabajo está limpio (si no, aborta).
- Actualiza
versionenpackage.jsony enpackage-lock.json. - Crea un commit con el número de versión como mensaje.
- Crea una etiqueta de git
v0.1.1apuntando a ese commit.
Esa etiqueta es lo que te permitirá, dentro de dos años, recuperar el código exacto de una versión publicada. Recuerda subirla: git push --follow-tags. Para prelanzamientos, npm version prerelease --preid=beta lleva de 1.0.0 a 1.0.1-beta.0.
Y aquí entran las etiquetas de distribución, que son alias móviles hacia versiones concretas. Cuando alguien escribe npm install @escena-viva/formato, npm resuelve la etiqueta latest. Si publicas una beta sin más, se convierte en latest y todo el mundo se la lleva sin pedirla. La forma correcta es npm publish --tag beta: así latest sigue apuntando a la última estable y quien quiera la beta debe pedirla con npm install @escena-viva/formato@beta.
Las etiquetas se gestionan después de publicar sin necesidad de republicar nada:
npm dist-tag ls @escena-viva/formato
npm dist-tag add @escena-viva/[email protected] next
npm dist-tag add @escena-viva/[email protected] latest
npm dist-tag rm @escena-viva/formato betaMover latest es, de hecho, la única forma limpia de "retirar" una versión mala: no la borras, pero dejas de servirla por defecto.
- Despublicar casi nunca se puede
Aquí conviene bajar el ritmo, porque la intuición engaña. Publicar no es como subir un fichero a un servidor propio: es un compromiso público. La política del registro es aproximadamente esta:
| Situación | ¿Se puede despublicar? |
|---|---|
| Menos de 72 horas desde la publicación | Sí, con npm unpublish |
| Más de 72 horas, sin dependientes y con una sola versión | Solo el paquete entero, en condiciones estrictas |
| Más de 72 horas y con otros paquetes dependiendo | No |
Versión concreta que alguien tiene en su package-lock.json |
No |
La razón de esta rigidez tiene nombre propio. En 2016, el autor del paquete left-pad —once líneas de código que rellenaban una cadena por la izquierda— lo despublicó tras una disputa sobre el nombre de otro paquete. left-pad era dependencia transitiva de herramientas usadas por medio mundo, así que durante horas fallaron compilaciones en miles de organizaciones. La consecuencia fue el endurecimiento de la política de despublicación: la estabilidad del ecosistema pesa más que el derecho a retirar el propio código.
La alternativa correcta cuando una versión es mala o un paquete queda obsoleto es npm deprecate, que no borra nada pero avisa en cada instalación:
npm deprecate @escena-viva/[email protected] "Error de redondeo; usa 0.1.1"
npm deprecate @escena-viva/formato@"<0.2.0" "Sin mantenimiento; migra a 0.2.x"
npm deprecate @escena-viva/formato "Sustituido por @escena-viva/utiles"
npm deprecate @escena-viva/[email protected] "" # retirar el avisoEl mensaje aparece como advertencia al instalar, no rompe nada y da tiempo a migrar. Es lo que hace un mantenedor responsable.
Un caso especial y urgente: si has publicado credenciales por accidente, despublicar no es la solución. Aunque consigas retirarlo, el tarball ya se ha replicado en cachés y espejos. Lo que hay que hacer es rotar inmediatamente esas credenciales, y después publicar una versión limpia y marcar la mala como obsoleta.
De ahí que el orden correcto de trabajo sea siempre: npm pack → revisar la lista → npm publish --dry-run → npm publish.
- Buenas prácticas de un paquete responsable
- README con ejemplos ejecutables. Un bloque que se pueda copiar y funcione a la primera vale más que tres párrafos de descripción.
- CHANGELOG.md. Una entrada por versión, con las rupturas destacadas. Es lo primero que lee quien va a actualizar.
- SemVer honesto. La versión no la decide tu percepción del tamaño del cambio, sino su efecto en el consumidor. Si renombras un parámetro, es mayor aunque sean dos líneas.
- Cero dependencias si es posible. Cada dependencia tuya se convierte en transitiva de todos tus consumidores, con su superficie de riesgo.
@escena-viva/formatono necesita ninguna. enginesdeclarado y pruebas antes de publicar conprepublishOnly.- Repositorio accesible. Los campos
repository,bugsyhomepagepermiten encontrar el código y abrir incidencias.
Errores Comunes y Consejos
Publicar con private: true en el manifiesto. npm se niega con This package has been marked as private. Es una protección deliberada: quítala solo cuando el paquete es realmente publicable.
Olvidar --access public en un paquete con ámbito. El error 402 Payment Required no significa que npm quiera cobrarte por publicar código abierto; significa que está intentando publicarlo como privado. Añade publishConfig.access.
Usar .npmignore y descubrir un .env en el tarball. La lista negra falla en silencio ante ficheros nuevos. Cambia a files y verifica siempre con npm pack.
Publicar la carpeta equivocada. npm publish empaqueta el directorio actual. Comprueba pwd antes; el --dry-run te lo confirma mostrando el nombre del paquete.
Editar version a mano y olvidar la etiqueta de git. Al cabo de unos meses tendrás versiones publicadas sin ningún commit identificable detrás. Usa siempre npm version.
Publicar una beta sin --tag. Se convierte en latest y toda tu base de usuarios se la instala. Es de los errores más caros y más fáciles de cometer.
Confiar en npm link como prueba final. El enlace simbólico no ejercita files ni exports igual que un tarball. Prueba el .tgz antes de publicar, y trabaja siempre como si la publicación fuese definitiva, porque a efectos prácticos lo es.
Ejercicios
Ejercicio 1: bloquear una ruta interna
Un compañero, en otro proyecto, ha escrito require('@escena-viva/formato/lib/codigo.js') para usar generarCodigoEntrada sin cargar el resto. Explica qué error obtendrá con el exports que hemos definido, y escribe el exports que le daría acceso a esa función mediante la subruta pública @escena-viva/formato/codigo sin exponer la estructura de lib/.
Ejercicio 2: revisar el tarball
En el directorio del paquete has añadido, sin darte cuenta, un fichero .env con NPM_TOKEN=... y una carpeta datos/ventas-teatro-almendra.json con correos de prueba. El package.json tiene files: ["index.js", "lib/", "README.md", "LICENSE"]. Responde: ¿se subirían esos ficheros? ¿Qué comando lo comprueba? ¿Cambiaría la respuesta si en vez de files hubiera un .npmignore con la línea datos/?
Ejercicio 3: una versión mala publicada
Has publicado @escena-viva/[email protected] y cuatro días después detectas que formatearPrecio(2450) devuelve '24.50 €' con punto en vez de coma. Ya hay dos proyectos internos que la tienen en su package-lock.json. Escribe la secuencia completa de comandos para resolverlo correctamente y explica por qué no empiezas por npm unpublish.
Soluciones
Solución 1. Obtendrá ERR_PACKAGE_PATH_NOT_EXPORTED, porque el mapa de exports deniega por defecto toda ruta no declarada, aunque el fichero exista físicamente dentro del paquete. El exports corregido añade una subruta pública que apunta al fichero interno:
"exports": {
".": "./index.js",
"./precio": "./lib/precio.js",
"./codigo": "./lib/codigo.js",
"./package.json": "./package.json"
}Ahora require('@escena-viva/formato/codigo') funciona, y la ruta con lib/ sigue bloqueada. La ventaja es que el nombre público ./codigo y el fichero real ./lib/codigo.js quedan desacoplados: si mañana mueves el fichero, basta con cambiar el mapa, sin versión mayor.
Solución 2. No, no se subirían. files es una lista blanca: solo entra en el tarball lo enumerado, y ni .env ni datos/ están. Además, npm excluye .npmrc siempre, aunque eso no cubre a un .env.
Se comprueba con npm pack --dry-run (o npm pack y luego tar -tzf), leyendo la lista de ficheros del tarball.
Con .npmignore la respuesta cambia peligrosamente: datos/ sí estaría excluido porque figura en la lista, pero .env se subiría, ya que no está listado y .npmignore desactiva por completo el .gitignore. Este es exactamente el motivo por el que files es más seguro: falla hacia el lado de no publicar.
Solución 3. No se empieza por npm unpublish porque han pasado más de 72 horas y además hay proyectos que ya dependen de esa versión; el registro lo rechazará, y aunque lo permitiera, romperías esas instalaciones. La secuencia correcta es publicar el arreglo y señalar la versión mala:
npm test # 1. Corregir el codigo y cubrirlo con una prueba
npm version patch # 2. 0.2.0 -> 0.2.1
git push --follow-tags
npm pack --dry-run # 3. Revisar contenido y ensayar
npm publish --dry-run
npm publish # 4. Publicar el arreglo
npm deprecate @escena-viva/[email protected] "Separador decimal incorrecto; actualiza a 0.2.1"
npm dist-tag ls @escena-viva/formato # 5. Confirmar que latest es la version buenaLos proyectos afectados recibirán el aviso de obsolescencia en su próxima instalación y, como su rango es ^0.2.0, un npm update les traerá 0.2.1 sin más intervención. Conviene además añadir la entrada correspondiente al CHANGELOG.md.
Conclusión
Publicar un paquete es sobre todo un ejercicio de decidir fronteras. Hemos visto que la extracción se justifica por reutilización real —la regla de las tres copias— y no por elegancia, y hemos convertido src/utiles/formato.js en @escena-viva/formato, un paquete con ámbito, sin dependencias y con una API de tres funciones.
Las decisiones importantes se concentran en cuatro campos del manifiesto: main como punto de entrada clásico, exports como declaración estricta de lo que se puede importar y muro frente a las rutas internas, files como lista blanca de lo que viaja al registro, y publishConfig.access para que un paquete con ámbito se publique en abierto. Alrededor de ellos, un flujo de comprobación que no conviene saltarse: npm pack para leer el tarball con los ojos abiertos, instalación del .tgz para la prueba real, npm publish --dry-run como ensayo y solo entonces npm publish, con 2FA en la cuenta y tokens granulares en la integración continua.
Y una lección que conviene interiorizar antes de necesitarla: lo publicado es prácticamente definitivo. Fuera de la ventana de 72 horas no hay marcha atrás, como enseñó el caso de left-pad; lo que sí hay es npm deprecate, versiones nuevas y etiquetas de distribución para dirigir a la gente hacia el código bueno.
En la lección siguiente, Seguridad y Mantenimiento de Dependencias, damos la vuelta al argumento. Si publicar significa que otros ejecutarán tu código, instalar significa que tú ejecutas el de desconocidos: veremos npm audit y cómo leerlo de verdad, npm ls para localizar quién arrastra una dependencia vulnerable, overrides para forzar una versión parcheada, los ataques que hay que saber reconocer —typosquatting, cuentas secuestradas, paquetes abandonados— y la rutina de mantenimiento que dejaremos escrita en el README de Escena Viva.
Curso de Node.js: De Principiante a Avanzado
Módulo 1: Introducción a Node.js
- ¿Qué es Node.js?
- Instalación y Configuración del Entorno
- Tu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Moderno para Node.js
- El Proyecto del Curso: la Plataforma Escena Viva
Módulo 2: Conceptos Básicos
- Arquitectura de Node.js
- El Bucle de Eventos (Event Loop)
- Callbacks y Programación Asíncrona
- Promesas y async/await
- Eventos y EventEmitter
- Módulos CommonJS y require()
- Módulos ES e Interoperabilidad
Módulo 3: Sistema de Archivos y E/S
- Lectura y Escritura de Archivos
- El Módulo fs a Fondo
- Rutas Multiplataforma con el Módulo path
- Trabajando con Streams
- Streams de Transformación y pipeline
- Buffers y Datos Binarios
Módulo 4: HTTP y Servidores Web
- Creando un Servidor HTTP Simple
- Manejo de Solicitudes y Respuestas
- Enrutamiento Manual
- Sirviendo Archivos Estáticos
- Recibiendo Datos: Cuerpos de Petición y JSON
- Consumiendo APIs Externas desde Node.js
Módulo 5: NPM y Gestión de Paquetes
- Introducción a NPM y package.json
- Instalación y Uso de Paquetes
- Versionado Semántico y package-lock
- Scripts de npm y Automatización del Proyecto
- Creación y Publicación de Paquetes
- Seguridad y Mantenimiento de Dependencias
Módulo 6: Framework Express.js
- Introducción a Express.js
- Configuración de una Aplicación Express
- Enrutamiento en Express
- Middleware
- Middleware de Terceros Esenciales
- Validación de Datos de Entrada
- Manejo de Errores
Módulo 7: Bases de Datos y ORMs
- Introducción a las Bases de Datos
- Usando MongoDB con Mongoose
- Operaciones CRUD
- Relaciones, Poblado y Consultas Avanzadas
- Usando Bases de Datos SQL con Sequelize
- Migraciones, Transacciones y Datos de Prueba
Módulo 8: Autenticación y Autorización
- Introducción a la Autenticación
- Registro de Usuarios y Hash de Contraseñas
- Sesiones y Cookies con Passport.js
- Autenticación con JWT
- Control de Acceso Basado en Roles
- Buenas Prácticas de Seguridad en APIs
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con Mocha y Chai
- Dobles de Prueba con Sinon
- Pruebas de Integración
- Cobertura y Automatización de las Pruebas
- Depuración de Aplicaciones Node.js
Módulo 10: Temas Avanzados
- El Módulo Cluster
- Hilos de Trabajo (Worker Threads)
- Caché y Colas de Trabajo con Redis
- Optimización del Rendimiento
- Construcción de APIs RESTful
- GraphQL con Node.js
Módulo 11: Despliegue y DevOps
- Configuración y Variables de Entorno
- Registro y Monitorización en Producción
- Usando PM2 para la Gestión de Procesos
- Empaquetado con Docker
- Desplegando en Heroku y Otras PaaS
- Integración y Despliegue Continuos
