El manifiesto de una librería: exports, types, sideEffects, files y qué NO poner
El package.json de una librería es el documento donde firmas todos los contratos del nivel a la vez: exports dibuja la frontera pública, types apunta al contrato estático, sideEffects autoriza la poda, files decide qué bytes viajan al registro. Pero un manifiesto se define tanto por lo que incluye como por lo que excluye: scripts de desarrollo, devDependencies mal ubicadas, campos de herramientas internas y archivos que no deberían salir de tu repositorio. Cómo leer el package.json como el manifiesto de distribución que es, campo por campo, y por qué la mitad del oficio de publicar consiste en no poner lo que no toca.
Todo lo que has visto en este nivel converge en un solo archivo. Los formatos que ofreces, los externals que declaras, los tipos que publicas, la frontera de tu API: cada decisión termina escrita en el package.json, que deja de ser una lista de dependencias para revelarse como lo que de verdad es —el manifiesto de distribución de tu librería, el documento que el registro, los bundlers y los runtimes leen para saber cómo tratar tu paquete—. Y como todo manifiesto, se define tanto por lo que incluye como por lo que deja fuera. Saber qué campos poner es la mitad del oficio; saber qué campos y qué archivos no poner es la otra mitad, y la que separa un paquete profesional de uno que filtra sus tripas al mundo.
- Leer el
package.jsoncomo el manifiesto de distribución que es, no como un archivo de config. - Cablear los campos que firman el contrato:
exports,types,sideEffects,files. - Decidir qué archivos viajan al registro con
filesy por qué menos es más. - Reconocer qué campos y archivos NO deben publicarse y por qué su ausencia importa.
Los campos que firman el contrato
Un puñado de campos concentra todos los acuerdos del nivel. El exports es la pieza central: dibuja la frontera pública del paquete, mapea cada subpath a su archivo y sella todo lo que no aparece listado, de modo que nadie pueda acoplarse a tus internos. Dentro de él, la condición types apunta al .d.ts de cada entrada —siempre la primera de su rama— y las condiciones import y require sirven el formato correcto a cada consumidor. El sideEffects autoriza al bundler del consumidor a podar módulos enteros al declarar que importarlos no tiene efectos observables. Y type: module fija que tus .js son ESM.
{
"name": "mi-lib",
"version": "2.1.0",
"type": "module",
"sideEffects": false,
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"files": ["dist"]
}
Ese bloque es un manifiesto de librería moderna casi completo, y cada línea es un contrato distinto que ya conoces. Conviene además conservar los campos heredados como red de compatibilidad: main para el consumidor antiguo que no entiende exports, module para el bundler que busca el entry ESM, y types en la raíz para el resolvedor de tipos que no lee la condición. No contradicen a exports —lo respaldan— y su presencia es pura cortesía hacia herramientas que aún no implementan el estándar.
Muchas herramientas leen mi-lib/package.json en tiempo de ejecución para averiguar la versión o alguna configuración. En cuanto defines exports, ese acceso queda bloqueado como cualquier otro subpath no listado, y esas herramientas fallan con ERR_PACKAGE_PATH_NOT_EXPORTED. La solución es una línea que casi todo el mundo olvida: añade "./package.json": "./package.json" a tu exports para reabrir explícitamente ese archivo. Es el subpath que más se olvida y el que más silenciosamente rompe integraciones.
files: qué bytes viajan al registro
El campo files decide qué se publica, y es la primera línea de defensa contra un paquete inflado. Cuando publicas, npm no sube tu repositorio entero: sube una lista blanca definida por files —o, en su ausencia, todo lo que no excluya .npmignore, que es la política peligrosa por defecto—. Declarar files: ["dist"] dice con precisión “solo viaja mi salida compilada”, y todo lo demás —tu src, tus tests, tu configuración— se queda en el repositorio, donde pertenece.
La diferencia entre una lista blanca y una lista negra no es estética: es de seguridad y de peso. Con files enumeras lo que sí sale, de modo que un archivo nuevo que añadas al repo no se publica por accidente. Con .npmignore enumeras lo que no sale, y cualquier cosa que olvides añadir a esa lista se filtra al registro —un .env con secretos, un directorio de fixtures pesado, notas internas—. La regla de 2026 es tajante: usa files como lista blanca y trata todo lo demás como privado por defecto.
{
"files": ["dist", "README.md"],
"publishConfig": {
"access": "public",
"provenance": true
}
}
Algunos archivos viajan siempre, los declares o no —el package.json, el README, el LICENSE—, así que no necesitas listarlos. Lo que sí conviene es verificar en seco qué acabará en el tarball antes de publicar: un empaquetado de prueba te muestra la lista exacta de archivos y su peso, y ahí se cazan de un vistazo el src que no querías subir o los sourcemaps que engordan el paquete sin aportar a quien lo instala.
Antes de cada publicación, un empaquetado en seco te lista exactamente qué archivos van al registro y cuánto pesa el resultado. Es el equivalente a leer el diff antes de un commit: cuesta segundos y caza los errores caros —el dist que no se generó, el src que se cuela, el archivo de secretos que nunca debió salir—. Publicar es difícil de deshacer; revisar el tarball es la última red antes de que tu paquete sea inmutable en el registro para siempre.
Qué NO poner: el manifiesto por sustracción
Un manifiesto de librería se distingue tanto por su disciplina de exclusión como por sus campos. Lo primero que no debe salir son tus dependencias de desarrollo mal ubicadas: devDependencies no las instala el consumidor, así que todo lo que tu código necesite en runtime tiene que estar en dependencies o peerDependencies, nunca en devDependencies; y al revés, tu bundler, tu framework de test y tus linters jamás deben aparecer como dependencies, porque forzarían a cada consumidor a instalarlos.
Después vienen los campos y archivos que son asunto tuyo y de nadie más. Un package.json de aplicación suele acumular configuración de herramientas —bloques de tu bundler, de tu test runner, de tu linter, scripts de desarrollo—, y nada de eso tiene sentido en un paquete publicado: el consumidor no ejecuta tus scripts ni lee tu config. No es que rompan —muchos son inertes— sino que son ruido que ensancha la superficie, revela tu tooling interno y, en el peor caso, filtra rutas o nombres que preferirías no exponer.
Y lo más grave: lo que nunca, bajo ninguna circunstancia, debe viajar al registro.
- Secretos: un
.env, un token en un archivo de config, credenciales en fixtures. El registro es público e inmutable; un secreto publicado está comprometido. - El código fuente sin necesidad: salvo que quieras publicar
srca propósito, tu TypeScript original y tus sourcemaps engordan el paquete sin que el consumidor los use. - Artefactos de test y CI: fixtures pesados, snapshots, reportes de cobertura, configuración de pipelines. Nada de eso es tu librería.
- Los internos que
exportsdebía sellar: si publicasdistentero pero tuexportsno bloquea los subpaths internos, la frontera que dibujaste es de papel.
flowchart TD P[package json de tu libreria] --> C[campos que firman el contrato] C --> C1[exports: la frontera] C --> C2[types: el contrato estatico] C --> C3[sideEffects: autoriza la poda] C --> C4[files: que bytes viajan] P --> N[lo que NO debe salir] N --> N1[secretos y env] N --> N2[devDependencies como runtime] N --> N3[config y scripts internos] N --> N4[fuente y fixtures innecesarios] style C fill:#a6e3a1,color:#11111b style N fill:#f38ba8,color:#11111b
El coste de un package.json mal cableado es un paquete roto para todos sus consumidores, y los errores son sutiles: un exports que apunta a un archivo que files no incluyó, una condición types en el orden equivocado, un main que señala al vacío. En 2026 el estándar de facto antes de publicar es pasar publint —que valida la coherencia del manifiesto y los mapas de exports— y @arethetypeswrong/cli —que confirma que los tipos resuelven bajo cada condición—. Son minutos que ahorran un parche de emergencia sobre un paquete que ya es inmutable.
Los campos que identifican y acreditan
Más allá de los campos que cablean el build, un manifiesto de librería lleva otro grupo cuya función es identificar el paquete y acreditar su procedencia. El name y la version son su identidad en el registro; la license declara bajo qué términos se puede usar —omitirla no lo hace “más libre”, lo hace legalmente ambiguo, y muchas empresas rechazan de plano las dependencias sin licencia clara—; y repository, homepage y bugs conectan el paquete con su código y su soporte, algo que las herramientas de auditoría y los propios consumidores consultan antes de confiar en una dependencia.
{
"name": "@scope/mi-lib",
"version": "2.1.0",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/usuario/mi-lib.git"
},
"keywords": ["esm", "typescript", "tree-shaking"]
}
En 2026 este grupo incluye también la procedencia. Publicar con provenance vincula criptográficamente el tarball del registro con el commit y el pipeline de CI que lo produjeron, de modo que un consumidor puede verificar que el paquete que instala salió de verdad de tu repositorio y no de una máquina comprometida. Tras los ataques a la cadena de suministro de los últimos años, esa acreditación pasó de lujo a expectativa: un paquete sin procedencia verificable es, para muchos equipos, un paquete en el que no se confía.
El hilo que une identidad, licencia y procedencia es la confianza. Un desconocido que evalúa tu librería no puede leer todo tu código, así que se apoya en señales: que tenga licencia clara, que apunte a un repositorio auditable, que acredite su origen. Estos campos no cambian una línea de lo que tu código hace, pero cambian la disposición de alguien a instalarlo —y esa disposición es, al final, lo que decide si tu trabajo se usa o se descarta.
exports
La frontera pública: mapea subpaths, ordena condiciones y sella los internos. El campo que convierte una carpeta en un módulo.
types + sideEffects
Uno apunta al contrato estático; el otro autoriza al consumidor a podar módulos enteros. Dos promesas en dos líneas.
files
Lista blanca de lo que viaja al registro. Menos bytes, cero filtraciones, ningún archivo publicado por accidente.
Lo que NO va
Secretos, devDependencies como runtime, config interna y fuente innecesaria. La disciplina de exclusión es la mitad del oficio.
La síntesis de todo el nivel cabe en una idea que cambia cómo miras un archivo que creías trivial. El package.json de una librería no es metadato que acompaña al código; es el código mismo del contrato, el documento performativo que constituye tu paquete ante el registro, los bundlers y los runtimes. Cuando escribes exports, no describes una frontera: la creas y la haces cumplir, porque a partir de esa línea todo lo que no listaste deja de ser importable. Cuando ordenas la condición types la primera, no documentas tus tipos: decides si el consumidor los encuentra o no. Cuando declaras sideEffects: false, no comentas una propiedad: autorizas a bundlers que jamás verás a borrar módulos enteros de tu código. Cuando acotas files, no anotas una preferencia: determinas qué bytes existen para el mundo y cuáles se quedan en tu máquina para siempre. Cada campo es un acto, no una glosa, y por eso equivocarse en uno no produce un paquete peor documentado, sino un paquete roto. Pero la lección más profunda del manifiesto es la simétrica: se define tanto por sus ausencias como por sus presencias. Un buen package.json de librería es un ejercicio de sustracción tan cuidadoso como de construcción —no filtra secretos, no arrastra la config de tu tooling, no confunde lo que el consumidor instala con lo que tú usas para desarrollar, no publica un src que nadie pidió ni sella una frontera que luego deja abierta por descuido—. Publicar bien es, en el fondo, un acto de restricción: ofrecer exactamente la superficie que tu público necesita y ni un byte más, porque cada exceso es peso muerto, ruido o riesgo. Aquí se cierra el arco del nivel entero. Empezaste entendiendo que publicas para desconocidos que no puedes probar; ahora ves dónde firmas todas las promesas que los harán confiar en ti sin verte. El package.json es esa firma. Trátalo con el rigor de un documento legal, valídalo con las herramientas del ecosistema antes de que sea inmutable, y recuerda que lo que no pones en él protege a tu consumidor tanto como lo que sí pones.
- Escribe el
package.jsonde una librería solo-ESM contype,exports(incluida la reexposición del propiopackage.json),types,sideEffectsyfiles. - Empaqueta en seco y revisa la lista del tarball: confirma que solo viaja
disty que no se cuela nisrcni ningún archivo sensible. - Mueve a propósito una dependencia de runtime a
devDependencies, instala el paquete en un proyecto limpio y observa cómo falla por la dependencia ausente. - Pasa
publinty@arethetypeswrong/clisobre el paquete y corrige cada aviso hasta dejar el manifiesto limpio. - Audita el
package.jsonde una dependencia tuya y haz dos listas: qué campos firman su contrato y qué no debería estar publicado.