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

  1. Cuándo extraer un paquete y cuándo no
  2. El caso de Escena Viva: @escena-viva/formato
  3. Estructura de un paquete publicable
  4. La API pública: main, exports y types
  5. Qué se sube: files frente a .npmignore
  6. Paquetes con ámbito y publishConfig
  7. Probar antes de publicar: npm pack y npm link
  8. Publicar: login, --dry-run, 2FA y tokens
  9. Versiones nuevas y etiquetas de distribución
  10. Despublicar casi nunca se puede
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

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

  1. El caso de Escena Viva: @escena-viva/formato

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

mkdir escena-viva-formato
cd escena-viva-formato
npm init --scope=@escena-viva -y

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');
"dependencies": {
  "@escena-viva/formato": "^0.1.0",
  "dotenv": "^17.2.1"
}

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.

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

  1. La API pública: main, exports y types

Histó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.

"exports": {
  ".": "./index.js",
  "./precio": "./lib/precio.js",
  "./package.json": "./package.json"
}

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:

"exports": {
  ".": {
    "require": "./index.js",
    "import": "./index.mjs",
    "default": "./index.js"
  }
}

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.

  1. Qué se sube: files frente a .npmignore

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

  1. Paquetes con ámbito y 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": {
  "access": "public",
  "registry": "https://registry.npmjs.org/"
}

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.

  1. Probar antes de publicar: npm pack y npm link

Publicar 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 pack
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.js o algún fichero de lib/? 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:

cd ../escena-viva
npm install ../escena-viva-formato/escena-viva-formato-0.1.0.tgz
npm run informe

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/formato

El 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 install en el proyecto puede deshacer el enlace y volver a bajar la versión del registro.
  • El enlace no respeta files ni exports con 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.

  1. Publicar: login, --dry-run, 2FA y tokens

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

npm login
npm whoami
npm publish --dry-run
npm publish

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.

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

  1. Comprueba que el árbol de trabajo está limpio (si no, aborta).
  2. Actualiza version en package.json y en package-lock.json.
  3. Crea un commit con el número de versión como mensaje.
  4. Crea una etiqueta de git v0.1.1 apuntando 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 beta

Mover latest es, de hecho, la única forma limpia de "retirar" una versión mala: no la borras, pero dejas de servirla por defecto.

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

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

  1. 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/formato no necesita ninguna.
  • engines declarado y pruebas antes de publicar con prepublishOnly.
  • Repositorio accesible. Los campos repository, bugs y homepage permiten 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 buena

Los 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

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