wandres.dev
EXPORTS PARA LIBRERÍAS · dual package

Verificar el paquete: publint y attw

Las dos herramientas que auditan un paquete antes de publicarlo: publint valida la coherencia de exports, extensiones y orden de conditions, y arethetypeswrong comprueba que los tipos resuelvan bajo cada estrategia de módulos. Cómo correrlas sobre el tarball y meterlas en CI.

⏱ 16 min

Un exports mal configurado no falla en tu máquina: falla en la de miles de personas, semanas después, de formas silenciosas que ningún test tuyo detecta. Publicar es irreversible, así que la única defensa es auditar el paquete antes de exponerlo. En 2026 dos herramientas se han vuelto el estándar de facto para esa auditoría: publint, que valida la coherencia del empaquetado, y arethetypeswrong —el binario attw—, que valida que los tipos resuelvan de verdad. Correrlas no es opcional: es la diferencia entre publicar con control y publicar a ciegas.

🎯 Al terminar esta lección sabrás
  • Auditar la coherencia de exports, extensiones y conditions con publint.
  • Detectar tipos que no resuelven bajo cada estrategia con attw.
  • Correr ambas sobre el tarball, no sobre el árbol de trabajo.
  • Integrar la verificación en CI como puerta previa a publicar.

publint: la coherencia del empaquetado

publint analiza tu package.json y los archivos que apunta, y verifica que todo el andamiaje del empaquetado sea coherente. No mira si tu código funciona: mira si tu paquete está bien construido para que otros lo consuman. Es rápido, no necesita configuración, y su lista de comprobaciones cubre justo los errores que no se ven a ojo.

# audita el paquete en el directorio actual
npx publint

# audita el tarball exacto que se publicaria
npm pack && npx publint ./acme-core-2.1.0.tgz

Lo que detecta es precisamente el catálogo de fallos de las lecciones anteriores. Que una condition apunte a un archivo con la extensión equivocada para su formato. Que el orden de las conditions esté mal, con types después de import o default antes de otra rama. Que una ruta de exports apunte a un archivo que no existe en el paquete publicado. Que falte la condition types o que el campo main contradiga a exports. Cada aviso viene con la explicación de por qué importa y cómo corregirlo.

Su salida es un informe legible que nombra el punto exacto del package.json y la corrección concreta, no un críptico código de error:

$ publint --strict
Errors:
1. pkg.exports["."] types condition should be the first
2. pkg.main is ./dist/index.cjs but the file does not exist
Warnings:
1. pkg.exports["."].require should be a .cjs file or use "type": "commonjs"

Leído así, cada línea es una tarea accionable: reordena las conditions, comprueba que el build generó el archivo, ajusta la extensión. La herramienta traduce las reglas abstractas del empaquetado en un checklist concreto sobre tu paquete real.

ℹ️
Los avisos se gradúan por severidad

publint clasifica sus hallazgos en errores, avisos y sugerencias. Un error es un fallo que romperá a consumidores reales —una ruta que no existe, una extensión incompatible—; una sugerencia es una mejora de higiene. En CI conviene fallar el pipeline ante los errores y, según la madurez del paquete, también ante los avisos. No todos los hallazgos tienen el mismo peso, pero ninguno se ignora sin haberlo leído.

attw: los tipos que mienten

publint valida el empaquetado; arethetypeswrong valida algo más específico y más traicionero: que las declaraciones de tipos resuelvan correctamente bajo cada estrategia de resolución de módulos que un consumidor pueda usar. Un paquete puede tener un exports que publint aprueba y aun así entregar tipos rotos para quien resuelve con nodenext, o unos tipos que dicen ESM mientras el runtime entrega CommonJS.

# analiza el paquete tal como lo veria cada resolver
npx attw --pack

# sobre un tarball ya generado
attw ./acme-core-2.1.0.tgz

La aportación única de attw es que simula varios resolvers a la vez —node10, node16 en modo ESM y CJS, bundler— y te muestra una matriz de en cuáles tus tipos resuelven bien y en cuáles no. Su valor está en nombrar los desajustes sutiles entre lo que los tipos prometen y lo que el runtime entrega, esos que producen un error de tipos sin error de runtime, o al revés.

🎭

Masquerading ESM o CJS

Los tipos dicen que el módulo es ESM pero el runtime entrega CommonJS, o al revés. El type-checker aprueba código que fallará al ejecutarse.

🕳️

No resolution

Bajo un resolver concreto, TypeScript no encuentra las declaraciones. El consumidor recibe any implícito y pierde todo el tipado.

🔀

Tipos por formato ausentes

El paquete es dual pero sirve el mismo .d.ts a ESM y a CJS, cuando la forma del módulo difiere entre ambos.

🧩

Fallos solo en un resolver

Todo funciona en bundler pero se rompe en node16. La matriz de attw es la única forma de ver ese hueco antes de publicarlo.

flowchart LR
A[tarball del paquete] --> B[publint valida exports y formatos]
A --> C[attw valida resolucion de tipos]
B --> D[avisos de conditions y extensiones]
C --> E[matriz de resolvers con fallos]
D --> F[corregir antes de publicar]
E --> F
style B fill:#89b4fa,color:#11111b
style C fill:#cba6f7,color:#11111b
style F fill:#a6e3a1,color:#11111b
📝
La matriz de attw se lee por columnas

Cada columna de la matriz es un resolver —node10 heredado, node16 en sus modos ESM y CJS, bundler— y cada fila un subpath de tu paquete. Una celda verde significa que ese resolver encuentra tipos coherentes; una roja, que no. Lo valioso es el patrón: fallos solo en la columna node16 CJS apuntan a que te falta el .d.cts o que anidaste mal los tipos por formato; fallos en node10 suelen ser aceptables si ya no soportas resolvers antiguos. attw también existe como aplicación web sobre la que pegar el nombre de un paquete publicado, útil para auditar dependencias de terceros antes de adoptarlas.

Auditar el tarball, no el repositorio

Un detalle metodológico que decide si la auditoría vale algo: ambas herramientas deben correr sobre el tarball, no sobre tu árbol de trabajo. La razón es que el tarball rara vez coincide con tu carpeta —el campo files, el .npmignore y los scripts de build deciden qué viaja de verdad—, y un exports impecable que apunte a un dist que no se empaquetó produce un paquete roto que la auditoría del árbol de trabajo jamás detectaría.

Por eso el patrón correcto es empaquetar en seco y auditar el resultado. Tanto publint como attw aceptan la bandera --pack o una ruta a un .tgz, y con ella analizan exactamente los bytes que recibirá quien instale, no los que tú tienes en local:

# audita lo que el mundo veria, no lo que tu tienes
npm pack --dry-run
npx publint --pack
npx attw --pack
💡
Ponlo en CI como puerta previa a publicar

La verificación manual se olvida; la de CI, no. El patrón de 2026 es un job que corre publint y attw sobre el tarball en cada pull request y antes de cada publicación, y falla el pipeline si aparece un error. Combinado con npm publish --dry-run, forma la puerta de calidad que ningún cambio de empaquetado puede cruzar sin pasar. El coste es un minuto de CI; el beneficio es no publicar una versión rota de forma irreversible.

La forma más limpia de fijar esa puerta es un script que encadene las comprobaciones y colgarlo del gancho prepublishOnly, de modo que sea imposible publicar sin haber auditado:

{
  "scripts": {
    "lint:pkg": "publint --strict && attw --pack",
    "prepublishOnly": "npm run build && npm run lint:pkg"
  }
}

Con publint --strict, incluso los avisos de menor severidad detienen la publicación; el attw --pack valida la matriz de tipos sobre el tarball recién construido. Al colgar ambos de prepublishOnly, la verificación deja de ser una buena intención y pasa a ser una precondición mecánica: si algo falla, npm publish aborta antes de tocar el registro.

Publicar sin auditar el tarball es firmar un contrato sin leerlo

El error mental que estas dos herramientas corrigen es creer que, si tu código funciona en tu máquina, tu paquete funciona. No es lo mismo. Tu código y tu paquete son dos cosas distintas: el código es lo que escribes, el paquete es el artefacto que otros resuelven a través de un laberinto de conditions, extensiones, resolvers y estrategias de módulos que tú nunca ejecutas en tu propio desarrollo. Un exports puede estar mal ordenado, apuntar a archivos que no se empaquetaron, servir tipos que mienten sobre el formato, o resolver bien en tu bundler y romperse en node16 —y nada de eso lo revela ejecutar tus tests, porque tus tests importan tu código, no tu tarball—. publint y attw son los primeros consumidores de tu paquete que lo miran como lo mirará el mundo: desde fuera, a través del package.json, bajo cada resolver posible. Correrlas sobre el tarball es leer el contrato antes de firmarlo, y firmarlo es irreversible: una vez publicada, la versión es inmutable y cualquier fallo se paga con un parche de emergencia y la confianza de quien ya la instaló. La disciplina que separa a quien mantiene paquetes de los que depende medio ecosistema del que sube carpetas y reza es exactamente esta: empaquetar en seco, leer el manifiesto como quien revisa una escritura, pasar publint y attw sobre esos bytes, corregir cada aviso, y solo entonces liberar. No es burocracia. Es la única forma de saber lo que estás firmando.

⚔️ Audita antes de firmar
  1. Ejecuta npx publint --pack sobre una librería tuya y corrige cada error de exports, extensión u orden de conditions que reporte.
  2. Corre npx attw --pack y lee la matriz de resolvers; identifica en cuáles tus tipos resuelven y en cuáles no.
  3. Provoca un masquerading: sirve un .d.ts que diga ESM mientras el runtime entrega CJS, y observa cómo lo nombra attw.
  4. Rompe una ruta de exports apuntando a un archivo excluido por files y confirma que solo la auditoría del tarball lo detecta.
  5. Añade un job de CI que corra ambas herramientas sobre el tarball y falle el pipeline ante cualquier error, como puerta previa a publicar.