Campos modernos: engines, exports y compania
El package.json de 2026: engines y packageManager para clavar el entorno con Corepack, exports como frontera de encapsulacion y resolucion condicional, y files, sideEffects y publishConfig para controlar que publicas, como se poda y a donde va.
Los campos base definen la identidad del paquete; los modernos definen su comportamiento profesional. engines y packageManager clavan el entorno para que nadie construya con la herramienta equivocada; exports reemplaza al viejo main por un mapa que a la vez encapsula el paquete y resuelve la entrada correcta según el consumidor; y files, sideEffects y publishConfig gobiernan qué se publica, cómo se poda y con qué política. Dominar estos campos es la diferencia entre un paquete que funciona y uno que se puede mantener, auditar y optimizar a escala.
- Clavar el entorno de ejecución con
enginesy el gestor exacto conpackageManagery Corepack. - Diseñar un mapa
exportscon resolución condicional y entender su papel como frontera de encapsulación. - Controlar el contenido del tarball publicado con
filesy habilitar el tree shaking consideEffects. - Ajustar la publicación —acceso, registro, procedencia— con
publishConfig.
engines y packageManager: clavar el entorno
El campo engines declara qué versiones de Node o del gestor soporta el paquete. Por defecto es un aviso, pero con engine-strict activo se convierte en una barrera que aborta la instalación si el entorno no cumple. packageManager va un paso más allá: fija el gestor y su versión exacta, y Corepack —incluido en Node— lo lee para aprovisionar automáticamente ese pnpm concreto, de modo que todo el equipo y el CI usen el mismo binario sin instalarlo a mano.
{
"engines": {
"node": ">=20.11",
"pnpm": ">=9"
},
"packageManager": "pnpm@9.12.0"
}
La combinación elimina de raíz una clase entera de bugs: los que solo aparecen porque alguien construyó con una versión distinta del runtime o del gestor. El entorno deja de ser una variable oculta y pasa a ser parte del manifiesto, versionada junto al código.
corepack enable # activa el shim de Corepack, incluido en Node
pnpm install # usa exactamente el pnpm que fija packageManager
Con engine-strict activo, un engines incumplido aborta la instalación en lugar de limitarse a avisar; en una librería pública es prudente declararlo laxo para no excluir consumidores, y en una aplicación interna, estricto para forzar homogeneidad.
engines documenta y opcionalmente exige la versión, pero no la instala. De eso se encargan Corepack para el gestor y ficheros como .nvmrc o .node-version para el runtime, que los gestores de versiones de Node leen para cambiar solos. Declarar ambos —la restricción en engines y la versión concreta en el fichero— cubre a la vez la validación y la conveniencia.
exports: la frontera de encapsulación
exports es el cambio más profundo del package.json moderno. Sustituye a main con un mapa que hace dos cosas a la vez. Primero, encapsula: solo las subrutas que declaras son importables; cualquier intento de acceder a un archivo interno no listado falla con ERR_PACKAGE_PATH_NOT_EXPORTED. Segundo, resuelve condicionalmente: elige un artefacto distinto según quién importe —el compilador de tipos, un entorno ESM, uno CommonJS, el navegador—.
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./plugin": {
"types": "./dist/plugin.d.ts",
"import": "./dist/plugin.js"
},
"./package.json": "./package.json"
}
}
Fíjate en la entrada ./package.json del ejemplo: al encapsular con exports dejarías de poder importar tu propio manifiesto, y muchas herramientas lo necesitan en tiempo de ejecución, así que reexponerlo de forma explícita es una práctica habitual y recomendada.
El orden de las condiciones importa: se evalúan de arriba abajo y gana la primera que coincide, por lo que types debe ir siempre primera y default siempre última como red de seguridad. Las condiciones más habituales, en el orden en que suelen declararse:
types: la ruta al.d.ts; primera para que TypeScript la resuelva antes que nada.nodeybrowser: artefactos distintos según el entorno de ejecución.developmentyproduction: builds con o sin comprobaciones y avisos, que el bundler elige por el modo.importyrequire: el archivo ESM frente al CommonJS según cómo se cargue el paquete.default: la última red de seguridad, que siempre debe existir.
flowchart TD
A[Peticion del consumidor] --> B{subruta declarada en exports}
B -->|no declarada| E[Error PACKAGE PATH NOT EXPORTED]
B -->|declarada| C[Evalua condiciones en orden]
C --> D1[types para el compilador]
D1 --> D2[node o browser segun entorno]
D2 --> D3[import o require segun sistema de modulos]
D3 --> D4[default como ultimo recurso]La contrapartida es una responsabilidad nueva: si expones a la vez una build ESM y una CommonJS de la misma librería con estado, un consumidor podría cargar ambas y acabar con dos instancias —el temido dual package hazard—. La encapsulación de exports ayuda a acotarlo, pero el diseño del paquete debe evitar duplicar estado entre ramas.
El primo interno de exports es imports, un mapa cuyas claves empiezan por # y que solo es visible dentro del propio paquete. Sirve para dar un alias estable a rutas internas —#config, #utils— o para resolver condicionalmente un módulo interno según el entorno, sin exponer nada al exterior. Es la forma canónica y estándar de tener alias de importación sin depender de la configuración del bundler, y por eso sobrevive fuera de él y viaja con el paquete.
files, sideEffects y publishConfig
Estos tres campos gobiernan el paquete que realmente sale por la puerta:
fileses una lista blanca de lo que entra en el tarball publicado. Complementa a.npmignoreinvirtiendo la lógica: en vez de enumerar lo que se excluye, enumeras lo que se incluye. Algunos archivos entran siempre —package.json, el README, la licencia y el punto de entrada—, pero todo lo demás debe estar en la lista o no viajará.sideEffectsle dice al bundler si los módulos del paquete tienen efectos colaterales. Confalse, autorizas un tree shaking agresivo: el bundler puede descartar cualquier módulo cuyas exportaciones no se usen. Si algunos archivos sí tienen efectos —hojas de estilo importadas por su efecto, polyfills—, se declaran como un array de globs para protegerlos.publishConfigsobrescribe ajustes solo en el momento de publicar: elregistryde destino, elaccess(publicpara un paquete con ámbito que quieres abierto), eltagde distribución y laprovenanceque ata el artefacto a su origen en CI.
{
"files": ["dist", "README.md"],
"sideEffects": ["**/*.css"],
"publishConfig": {
"access": "public",
"provenance": true
}
}
El matiz de sideEffects es sutil pero de alto impacto en el peso final: declarar false cuando un módulo sí produce efectos —registra un customElement, aplica un polyfill— hace que el bundler lo elimine y rompe la app en silencio. La lista de globs existe para marcar esas excepciones; el caso más común son los .css que se importan por su efecto de aplicar estilos, no por lo que exportan.
La provenance merece mención aparte: al publicar desde un CI compatible, npm firma una atestación verificable que ata el tarball al commit y al workflow que lo produjeron, siguiendo el estándar SLSA. No evita que un paquete sea malicioso, pero permite comprobar criptográficamente que procede de donde dice: una respuesta directa a los ataques de cadena de suministro.
Recuerda, además, qué entra siempre en el tarball declares lo que declares en files:
package.json, sin excepción posible.- El
READMEy el archivo de licencia. - El punto de entrada señalado por
maino porexports.
Nunca publiques a ciegas. pnpm pack genera el .tgz exacto que subirías, y npm publish --dry-run lista cada archivo incluido sin llegar a publicar. Es la única forma de descubrir a tiempo que estás filtrando un .env, tus tests o medio src/ que files debería haber excluido. Un minuto de inspección evita una fuga irreversible: recuerda que una versión publicada no se puede corregir, solo deprecar.
engines · packageManager
Clavan runtime y gestor. Con Corepack, todo el equipo usa el mismo pnpm sin instalarlo a mano.
exports
Encapsula el paquete y resuelve la entrada por condición. Su superficie es API pública.
files
Lista blanca de lo que entra en el tarball. Lo que no está, no se publica.
sideEffects
false autoriza tree shaking agresivo; un array de globs protege lo que sí tiene efectos.
El giro mental que exige el package.json moderno es dejar de ver exports como un simple puntero al código y empezar a verlo como parte de la superficie pública del paquete, tan vinculante como la firma de tus funciones. Antes de exports, todo archivo publicado era importable: los consumidores llegaban a tus internos por rutas profundas, se acoplaban a detalles de implementación y, sin saberlo, convertían cada refactor tuyo en un breaking change para ellos. Al declarar un mapa de exportaciones cierras esa puerta: lo que no está en el mapa no existe para el exterior, y recuperas la libertad de reorganizar tu dist sin romper a nadie. Pero esa frontera es un contrato: añadir una subruta es una feature, quitarla o renombrarla es un cambio incompatible que obliga a subir la major, exactamente igual que si borraras una función exportada. Lo mismo vale para las condiciones: el orden en que colocas types, import y require decide qué recibe cada consumidor, y una condición mal ordenada puede servir CommonJS a quien pidió ESM y desatar el dual package hazard. Junto a engines y packageManager, que convierten el entorno en algo declarado y no supuesto, y a files, que hace explícito lo que publicas, estos campos marcan la transición de “un paquete que funciona en mi máquina” a “un paquete diseñado para que funcione en las de todos”. Esa intención deliberada, y no la cantidad de campos rellenados, es lo que define a un paquete de nivel Dios.
- Añade
enginesypackageManagera un proyecto y ejecutacorepack enable; comprueba que el gestor se aprovisiona solo con la versión fijada. - Convierte un
mainheredado en un mapaexportscon condicionestypes,importyrequire, y expón una única subruta secundaria bajo./. - Intenta importar un archivo interno no declarado en
exportsy confirma que falla conERR_PACKAGE_PATH_NOT_EXPORTED; entiende esa negativa como encapsulación, no como error. - Ejecuta
pnpm packy abre el tarball resultante: contrasta su contenido con tu campofilesy elimina cualquier archivo que no debería viajar al registro.