Campos base: la identidad del paquete
El manifiesto que todo lo gobierna: name y version como clave inmutable en el registro, el interruptor type que decide entre ESM y CommonJS, y los descriptores heredados main, module y types que aun sostienen la interoperabilidad.
El package.json es el documento constitucional de un paquete de JavaScript: un objeto JSON que actores tan dispares como Node, pnpm, Vite, TypeScript y el registro npm leen para responder, cada uno, a una pregunta distinta. Antes de dominar dependencias, scripts o exports, conviene dominar el puñado de campos que definen qué es el paquete —su nombre, su versión, su naturaleza de módulos y los punteros a su código—. Equivocarse aquí rara vez produce un error ruidoso: produce comportamientos silenciosos y desconcertantes muchas capas más abajo.
- Fijar
nameyversioncomo la clave primaria e inmutable del paquete en el registro. - Comprender cómo el campo
typedecide la interpretación de cada archivo.jsentre ESM y CommonJS. - Distinguir el papel de
main,moduleytypes, y por qué conviven pese a estar casi obsoletos. - Redactar un encabezado de manifiesto correcto y sin ambigüedades para un paquete de 2026.
name y version: la clave primaria del registro
El par formado por name y version no es meramente descriptivo: es la coordenada única e inmutable con la que un registro identifica cada artefacto. En el momento en que publicas @acme/motor@2.4.1, ese tarball queda congelado para siempre; el registro no permite reutilizar la combinación exacta, y la retirada (unpublish) está severamente restringida para no romper a quien ya dependía de ella. Interiorizar esto cambia cómo versionas: una versión no es una etiqueta editable, es una promesa pública.
El campo name obedece reglas estrictas heredadas de su uso como identificador en URLs y sistemas de ficheros:
- Todo en minúsculas, sin espacios; solo caracteres seguros para una URL.
- No puede empezar por punto ni por guion bajo, ni contener mayúsculas.
- Longitud máxima de 214 caracteres, incluido el ámbito o scope.
- Puede llevar ámbito con la forma
@acme/motor, que agrupa paquetes y habilita permisos y publicación restringida.
El ámbito no es mera cosmética organizativa: un paquete con ámbito nace privado por defecto en el registro y necesita una autorización explícita para hacerse público, lo que reduce el riesgo de publicar por error algo interno. Además, agrupa la propiedad y los permisos de todo un conjunto de paquetes bajo una misma cuenta u organización.
{
"name": "@acme/motor",
"version": "2.4.1",
"description": "Motor de reglas para el pipeline de Acme.",
"license": "MIT",
"private": false
}
El campo version debe ser un semver estricto —MAJOR.MINOR.PATCH— que el parser node-semver acepte sin ambigüedad; cualquier desviación rompe la publicación. Si el proyecto es una aplicación interna o un paquete de un monorepo que nunca se sube al registro, marca "private": true: es el pestillo que impide una publicación accidental y, de paso, autoriza campos de workspace que el registro rechazaría.
El registro trata cada versión publicada como inmutable: no puedes sobrescribir 2.4.1 con otro contenido, y la retirada (unpublish) solo se permite en una ventana muy corta tras publicar y bajo condiciones estrictas, precisamente para no romper a quien ya dependía de ella. La herramienta correcta para decir “esta versión no debe usarse” no es borrar, sino npm deprecate, que deja un aviso sin retirar el artefacto. Trata cada pnpm publish como definitivo, porque en la práctica lo es.
type: el interruptor entre ESM y CommonJS
Ningún campo base tiene consecuencias tan profundas como type. Determina cómo interpreta Node —y, por extensión, casi todo el toolchain— cada archivo con extensión .js dentro del paquete. Con "type": "module", esos archivos son módulos ES: usan import y export, y carecen de require, __dirname o module.exports. Con "type": "commonjs" o en ausencia del campo, son CommonJS clásico. Las extensiones explícitas .mjs y .cjs fuerzan su naturaleza por archivo y ganan siempre, sin importar lo que diga type.
flowchart TD
JS[Archivo .js] --> T{campo type}
T -->|module| E[Interpretado como ESM]
T -->|commonjs o ausente| C[Interpretado como CommonJS]
M[Archivo .mjs] --> E
X[Archivo .cjs] --> CEn 2026, "type": "module" es la elección por defecto sensata para todo trabajo nuevo: el ecosistema asume ESM, los bundlers lo prefieren y las herramientas modernas nacen sin CommonJS. La antigua fricción del dual package se ha suavizado —Node ya permite require() de un grafo ESM síncrono sin flag experimental—, pero el campo type sigue gobernando la interpretación de cada .js y sigue siendo la primera causa de errores como require is not defined o Cannot use import statement outside a module. Cuando algo no cuadra, type es el primer sospechoso.
Fijar type cubre el caso normal, pero a veces necesitas un archivo que contradiga al paquete: un script de configuración en CommonJS dentro de un paquete ESM, o un módulo ESM aislado en una base heredada. Para eso están las extensiones explícitas .mjs —siempre ESM— y .cjs —siempre CommonJS—: ganan a type sin discusión y permiten migrar archivo a archivo, sin voltear el paquete entero de golpe. Son la palanca que hace posible una transición gradual a ESM en bases de código grandes, donde un cambio atómico sería inviable.
El campo condiciona también a las herramientas que generan .js: un compilador de TypeScript o un bundler consultan type para decidir si emiten import o require, de modo que una discrepancia entre lo que declaras y lo que tu build produce se paga en tiempo de ejecución, no de compilación —justo cuando menos lo esperas—.
Los tres síntomas que delatan un type mal ajustado y que conviene reconocer al vuelo:
require is not definedoexports is not defined: código CommonJS interpretado como ESM.Cannot use import statement outside a module: lo contrario, ESM interpretado como CommonJS.__dirname is not defined: un archivo ESM esperando globales que solo existen en CommonJS.
main, module, types y el resto del encabezado
Antes de que existiera el mapa exports, tres campos apuntaban al código del paquete, y todavía sobreviven como red de compatibilidad:
maines el punto de entrada clásico: lo que se resuelve al importar el paquete sin subruta cuando no hayexports. Su valor por defecto esindex.js.modulenunca fue estandarizado por Node, que lo ignora por completo; es una convención que los bundlers (Rollup, webpack, Vite) honran para elegir una build ESM apta para tree shaking.types(otypings) indica a TypeScript dónde está la declaración.d.tsde entrada.
{
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts"
}
Un paquete robusto acompaña cada punto de entrada de su declaración de tipos y evita que main, module y exports cuenten historias distintas sobre dónde está el código. Cuando conviven, exports manda para el Node moderno y main queda como respaldo para tooling antiguo; mantenerlos coherentes es lo que impide que un consumidor cargue una build mientras su verificador de tipos analiza otra. Un quinto descriptor heredado, browser, permitía sustituir módulos por variantes aptas para el navegador; hoy su función la absorbe la condición browser dentro de exports, pero aún lo encontrarás vivo en paquetes veteranos.
El campo description es prosa libre que alimenta la búsqueda del registro, y license debe ser un identificador SPDX válido —MIT, Apache-2.0, BSD-3-Clause— o UNLICENSED para código privado; no es una frase, sino un código que las auditorías de licencias leen automáticamente. Estos punteros heredados no desaparecen, pero en un paquete moderno quedan subordinados al mapa exports, que veremos en la última lección de este nivel y que se impone a main cuando ambos existen.
Metadatos para el registro y las auditorías
Más allá del núcleo funcional, un puñado de campos describe la procedencia y el contexto del paquete. No cambian cómo se ejecuta el código, pero alimentan la ficha del registro, las herramientas de descubrimiento y las auditorías de seguridad y licencias que hoy son parte inseparable de cualquier cadena de suministro seria.
{
"keywords": ["reglas", "pipeline", "validacion"],
"author": "Acme Engineering <dev@acme.dev>",
"homepage": "https://acme.dev/motor",
"repository": {
"type": "git",
"url": "git+https://github.com/acme/motor.git"
},
"bugs": {
"url": "https://github.com/acme/motor/issues"
}
}
keywordsmejora la búsqueda dentro del registro; una lista corta y precisa vale más que un saco de términos.repositoryes cada vez más crítico: habilita la provenance y permite a las auditorías verificar que el tarball publicado procede del código que dice proceder.homepageybugsorientan a quien consume el paquete hacia la documentación y el canal de incidencias correctos.authorycontributorsatribuyen la autoría y, junto alicense, cierran la ficha legal del paquete.
En conjunto, estos metadatos convierten un paquete anónimo en uno auditable y confiable, algo que en 2026 no es un adorno sino un requisito para que muchas organizaciones acepten siquiera instalarlo.
Para publicar, npm exige realmente poco: bastan un name y una version válidos. Pero un paquete profesional no se queda en el mínimo legal: declara type, license, description, exports, repository y engines, porque cada uno de esos campos ausentes es una pregunta que trasladas a quien te consume o a la herramienta que te audita.
name
Minúsculas, seguro para URL, hasta 214 caracteres, con ámbito opcional. Junto a version, la clave inmutable del registro.
version
Semver estricto MAJOR.MINOR.PATCH. Una vez publicada, congelada: es una promesa, no una etiqueta.
type
module o commonjs. Decide cómo se interpreta cada .js; .mjs y .cjs mandan por encima.
license
Un identificador SPDX, no una frase. Lo leen herramientas de auditoría, no solo humanos.
La clave para entender package.json es que no tiene un solo lector, sino varios, y cada campo cuenta su historia a un público diferente. El registro npm lee name y version para archivar el tarball; Node lee type para decidir el lenguaje de cada archivo; el bundler lee module para elegir la build que mejor se poda; el compilador de TypeScript lee types para encontrar las declaraciones; y una auditoría de dependencias lee license para aprobar o vetar el paquete. La inmensa mayoría de los bugs de empaquetado —esos que se manifiestan como works on my machine— nacen de una contradicción entre las historias que cuentan estos campos: un type: module cuyo main apunta a un artefacto CommonJS, una version que promete compatibilidad que el código ya rompió, un types que describe una API distinta de la que exporta el runtime. Diseñar bien un paquete no es rellenar campos: es garantizar que todos los lectores del manifiesto reciban una versión coherente de la misma verdad. Esa coherencia, y no la mera validez sintáctica del JSON, es lo que distingue a un paquete profesional de uno que funciona por casualidad.
- Abre el
package.jsonde una dependencia instalada ennode_modulesy localizaname,version,type,main,moduleytypes. - Determina si el paquete es ESM, CommonJS o dual, y contrasta si
mainymoduleapuntan a artefactos coherentes con el valor detype. - En un proyecto tuyo, cambia
typede ausente amoduley observa qué archivos.jsdejan de compilar; entiende cada error como una pista sobre qué asumía CommonJS. - Redacta desde cero el encabezado de un paquete publicable: nombre con ámbito, versión inicial
0.1.0,typeexplícito,licenseSPDX ydescriptionde una línea.