wandres.dev
PUBLICAR LA LIBRERÍA · versiones y provenance

Controlar qué se publica: files, .npmignore y npm pack

El tarball es el artefacto real, no tu repositorio: un objeto derivado, sellado e inmutable que puede diferir radicalmente de lo que ves en git. El campo files como allowlist de mundo cerrado frente a .npmignore como denylist de mundo abierto, las reglas inamovibles de inclusión y exclusión, cómo npm pack y --dry-run te dejan auditar el manifiesto antes de un acto irreversible, y por qué en 2026 se validan exports y tipos con publint y attw sobre el tarball, no sobre el árbol de trabajo.

⏱ 17 min

El error mental del principiante es creer que publica su proyecto. En realidad publica un artefacto derivado —un tarball comprimido con gzip— cuyo contenido puede diferir radicalmente del repositorio que tiene delante. Ese .tgz es el contrato binario real: lo que cada instalación del planeta descargará, descomprimirá y ejecutará. Controlar con exactitud qué entra en él no es burocracia de empaquetado, sino la diferencia entre quien expone su trabajo con precisión quirúrgica y quien filtra secretos y basura sin enterarse.

🎯 Al terminar esta lección sabrás
  • Ver el tarball como el artefacto publicado real, un derivado sellado distinto del repositorio.
  • Contrastar el campo files como allowlist de mundo cerrado frente a .npmignore como denylist.
  • Conocer las reglas inamovibles de inclusión y exclusión que ignoran ambas listas.
  • Auditar el paquete con npm pack, --dry-run, publint y attw antes de exponerlo de forma irreversible.

El tarball como artefacto, no como carpeta

Cuando ejecutas npm publish, el cliente no “sube tu carpeta”: empaqueta un árbol filtrado en un tarball, calcula su integridad SHA-512 en formato SRI y lo sube con un HTTP PUT. El registro valida que esa versión exacta no exista —la regla de inmutabilidad— y actualiza el packument. A partir de ese instante el artefacto es intocable: nadie puede republicar esa versión, y la ventana para retirarla es de apenas 72 horas antes de que quede congelada para siempre.

Esa irreversibilidad es lo que carga de gravedad la pregunta “¿qué viaja dentro?”. El tarball rara vez coincide con tu carpeta de trabajo, y confundir ambos es el origen de las filtraciones más embarazosas del ecosistema. El artefacto es una caja negra sellada; tu tarea es auditar la caja, no el taller donde la fabricaste.

flowchart LR
REPO[arbol de trabajo en git] --> FILTRO[reglas files o npmignore]
FILTRO --> TAR[tarball tgz sellado]
TAR --> SRI[integridad sha512 SRI]
SRI --> REG[registro inmutable]
style REPO fill:#89b4fa,color:#11111b
style FILTRO fill:#cba6f7,color:#11111b
style REG fill:#a6e3a1,color:#11111b

Allowlist frente a denylist

npm ofrece dos mecanismos opuestos para decidir el contenido del tarball, y la diferencia entre ambos es una diferencia de filosofía de seguridad. El campo files en package.json es una allowlist: solo entra lo que enumeras, un mundo cerrado donde lo no declarado se excluye por defecto. El fichero .npmignore es una denylist con sintaxis de .gitignore: entra todo salvo lo que vetes, un mundo abierto donde lo que olvidas ignorar viaja.

files (allowlist)

Solo viaja lo que enumeras. Mundo cerrado, seguro por defecto: lo recomendado. Un patrón negado con ! afina dentro de la lista.

🚫

.npmignore (denylist)

Viaja todo salvo lo vetado. Mundo abierto. Si falta, npm cae a tu .gitignore. Cómodo, pero propenso a filtrar lo olvidado.

La asimetría es la clave: en un allowlist, un fichero nuevo y sensible que aparezca mañana en tu repo —un .env, un volcado de depuración, una nota interna— no viaja porque no está en la lista; en un denylist, viaja salvo que te acuerdes de vetarlo. El fallo por omisión de uno es no publicar algo; el del otro es publicar un secreto. Por encima de ambos hay reglas inamovibles que ninguna lista altera:

  • Siempre viajan, los listes o no: package.json, el README, la LICENSE y los ficheros apuntados por main y por bin.
  • Siempre quedan fuera, aunque los incluyas: node_modules y .git por peso y seguridad, .npmrc porque podría llevar tu token, y package-lock.json porque no tiene sentido dentro de una dependencia.
{
  "name": "@acme/widget",
  "version": "2.1.0",
  "type": "module",
  "files": ["dist", "!dist/**/*.test.js"],
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js"
    }
  }
}

El patrón profesional es un files mínimo que apunte al dist y refine con patrones negados —aquí, excluir los tests transpilados que se colaron—. La regla mental es simple: si dudas de si un fichero viajará, no lo dejes al azar de un denylist; decláralo en files y verifícalo.

Cuando el paquete expone un ejecutable, el campo bin lo mapea a un script con su línea shebang; ese fichero viaja siempre, lo listes o no, y conviene confirmar en el manifiesto que llega y con permisos de ejecución.

{
  "name": "@acme/cli",
  "version": "1.0.0",
  "bin": { "acme": "./dist/cli.js" },
  "files": ["dist"]
}
⚠️
El allowlist te salva de filtrar secretos

Sin un files explícito, un npm publish distraído sube el árbol entero: fuentes sin transpilar, notas internas, ficheros .env con claves, capturas de depuración. Ha habido filtraciones sonadas de credenciales por exactamente esto, y como la versión es inmutable, rotar la clave filtrada es la única salida, porque el tarball con el secreto queda publicado para siempre. Un allowlist estrecho convierte “todo salvo lo que recuerde ignorar” en “nada salvo lo que decida enviar”: la única postura correcta cuando el resultado es público e irreversible.

La distancia entre el repositorio y el tarball

Entender por qué repo y tarball difieren es lo que da sentido a toda la disciplina. No son el mismo objeto, y las razones son concretas y cotidianas:

  • El dist transpilado: publicas JavaScript compilado y .d.ts, no tu TypeScript fuente; el consumidor recibe lo derivado, no lo original.
  • Ficheros generados fuera de git: el dist suele estar en .gitignore y sin embargo viaja en el tarball, justo al revés de lo que sugiere el repositorio.
  • Symlinks del monorepo: en un workspace las dependencias internas se resuelven por enlace en desarrollo, pero el tarball debe llevar referencias publicables.
  • Artefactos a medio construir: si el dist no se regeneró antes de empaquetar, viaja una versión vieja o vacía sin que nada avise.

El último punto es el más traicionero, y por eso el empaquetado se ancla a los scripts de ciclo de vida: prepare compila el dist antes de empaquetar —también al instalar desde git—, y prepublishOnly corre solo en publish para las verificaciones de release. Confiar en que el dist está fresco sin un script que lo garantice es la vía rápida a publicar el fantasma de una build anterior.

Inspeccionar antes del acto irreversible

La herramienta que cierra el bucle es npm pack: genera el mismo tarball que subiría publish, pero lo deja en disco sin exponer nada. Con --dry-run ni siquiera lo escribe; solo imprime el manifiesto de lo que contendría. Y npm publish --dry-run simula la publicación completa —scripts, reglas de inclusión, tamaño final— sin tocar el registro. Es la red de seguridad obligatoria antes de un acto que no se deshace.

# lista el contenido exacto del tarball sin escribir nada
npm pack --dry-run

# simula la publicacion completa: scripts, reglas, tamano final
npm publish --dry-run

# inspecciona un tarball ya generado
tar -tzf acme-widget-2.1.0.tgz

El informe condensa lo que importa auditar, y cada campo dice algo distinto:

  • package size: los bytes comprimidos que descarga quien instala; el coste de red de tu paquete.
  • unpacked size: lo que ocupa ya instalado en disco; el coste real dentro del node_modules ajeno.
  • total files: cuántos ficheros viajan; un salto inesperado delata un intruso o una regla mal puesta.
  • shasum e integrity: las huellas SHA que cada instalación futura verificará contra su lockfile.

En 2026 la inspección va más allá del tamaño, y crucialmente se hace sobre el tarball, no sobre el árbol de trabajo, porque solo el tarball es lo que verá quien instale:

  • publint: valida que tu campo exports y tus extensiones sean coherentes con ESM y CJS, y detecta errores de empaquetado que no se ven en desarrollo.
  • arethetypeswrong (el binario attw): comprueba que los tipos se resuelvan bien bajo cada estrategia de módulos y cada condición de exports.
  • instalar el .tgz: npm i ./paquete.tgz en un proyecto limpio ejercita el artefacto real, no tu carpeta con sus symlinks y su caché.

Un manifiesto sano se reconoce por un puñado de señales que conviene recorrer una a una antes de liberar:

  • Ningún fichero fuente sin transpilar ni ningún .map que no quisieras exponer.
  • Ningún secreto: nada de .env, .npmrc, claves ni volcados de depuración.
  • El dist presente y fresco, con su .d.ts junto a cada módulo emitido.
  • Un total de ficheros esperado, sin saltos que delaten una regla mal puesta o un intruso.
  • La LICENSE y el README presentes, que viajan siempre pero conviene confirmar íntegros.
💡
Audita el artefacto, no el repositorio

La disciplina que distingue al mantenedor serio es leer el manifiesto de npm pack --dry-run como quien revisa una escritura antes de firmarla: recorrer cada fichero, confirmar que el dist está recién compilado, que no se cuela ninguna fuente ni ningún secreto, y que publint y attw pasan sin avisos. Solo entonces se libera. Auditar el árbol de trabajo no basta, porque el tarball puede diferir; se audita exactamente lo que el mundo va a consumir.

El tarball es el contrato real, no tu repositorio

Todo lo demás de este nivel se sigue de una sola inversión mental: no publicas tu proyecto, publicas un artefacto derivado cuyo contenido puede no parecerse a tu repositorio. Las reglas de files y .npmignore dejan de ser detalles de configuración y se revelan como la definición precisa de la frontera entre tu taller y lo que el mundo consume, y la elección entre allowlist y denylist es una elección entre mundo cerrado —seguro por defecto, lo que no declaras no sale— y mundo abierto —cómodo, pero donde lo que olvidas vetar se filtra—. La integridad SRI no es un adorno, sino lo que permite a cada instalación futura verificar que recibió exactamente esos bytes y no otros. Y la inmutabilidad no es una limitación molesta, sino la razón por la que un lockfile de hace tres años sigue resolviendo hoy al mismo código y un secreto filtrado no se puede borrar. Cuando interiorizas que npm publish construye y sella un contrato binario, dejas de publicar a ciegas: corres npm pack --dry-run, lees el manifiesto entero, pasas publint y attw sobre el tarball —nunca sobre el árbol—, instalas el .tgz en un proyecto limpio para ejercitar el artefacto real, y solo entonces liberas. Esa costumbre de auditar la caja y no el taller es la que separa a quien mantiene paquetes de los que depende medio ecosistema de quien sube carpetas y reza.

⚔️ Audita tu primer tarball
  1. Añade un campo files mínimo a un package.json de práctica y ejecuta npm pack --dry-run; lee el manifiesto completo, fichero a fichero.
  2. Crea un .npmignore y compara cómo cambia el contenido frente a usar solo files; observa cuál falla por omisión de forma más segura.
  3. Cuela a propósito un fichero .env en la carpeta y comprueba si tu configuración lo dejaría viajar.
  4. Corre npm publish --dry-run e identifica package size, unpacked size y total files en el informe.
  5. Instala y pasa publint y attw sobre el tarball, luego instala el .tgz en un proyecto limpio y corrige cualquier aviso de exports o de tipos.