npm vs JSR: publicar ESM-first con tipos nativos
El registro es una capa sustituible detrás de una interfaz estable de nombres con scope, enrutado por .npmrc y tarballs servidos por HTTP. JSR y su diseño ESM-first: TypeScript de origen servido con tipos nativos y transpilado bajo demanda por runtime, la interoperabilidad con clientes npm vía el scope reservado @jsr, la regla de no slow types que fuerza tipos explícitos en la API pública, la procedencia y la puntuación de calidad integradas, y el criterio para decidir cuándo publicar en JSR, cuándo en npm y cuándo en ambos.
Durante años, “publicar un paquete” significó exactamente una cosa: subirlo al registro público de npm. Pero el registro es una capa sustituible, no una ley física. Escribiste npm install mil veces creyendo que hablabas con npm, cuando en realidad hablabas con una interfaz —nombres con scope, enrutado por .npmrc, tarballs por HTTP— que resulta estar implementada por defecto por el registro público. En 2026 JSR ocupa esa misma interfaz con un backend distinto: ESM-first, TypeScript de origen y tipos nativos sin build previo. Conocer el abanico es dejar de estar atado a un solo destino.
- Ver el registro como implementación sustituible de una interfaz estable de nombres, enrutado y tarballs.
- Entender el diseño de JSR: solo ESM, TypeScript de origen con tipos nativos y transpilación por runtime.
- Comprender la interoperabilidad con clientes npm vía el scope
@jsry la regla de los slow types. - Decidir con criterio cuándo publicar en JSR, cuándo en npm y cuándo en ambos a la vez.
El registro como capa sustituible
La abstracción que has usado sin nombrarla se descompone en tres piezas estables, y ninguna presupone quién está detrás:
- Nombres con scope:
@scope/nombrees un identificador que no dice qué servidor lo sirve. - Enrutado por
.npmrc: una línea mapea cada scope a la URL de un registro concreto y su token. - Tarballs por HTTP: el servidor entrega un
.tgzcon su integridad, y cualquier gestor sabe instalarlo.
Esa interfaz es lo estable; el registro detrás es intercambiable. Una vez que lo ves, el mapa se abre, porque JSR no reinventa la interfaz: la reimplementa con un backend diseñado desde cero para el JavaScript moderno, enrutado por el mismo mecanismo de siempre.
# el scope reservado @jsr se enruta al registro de JSR; el resto, al publico
@jsr:registry=https://npm.jsr.io/
JSR es el registro impulsado por el equipo de Deno, y sus decisiones de diseño son deliberadas y rompedoras:
- Solo ESM: no admite el formato CommonJS heredado, lo que simplifica todo el modelo de módulos.
- TypeScript de origen: sirve el código con tipos nativos y transpila bajo demanda para cada runtime; los tipos nunca se desincronizan de la fuente porque no hay un paso de declaración que mantener.
- Solo con scope: todo paquete es
@scope/nombre, sin el espacio plano y disputado del registro clásico. - Procedencia y puntuación integradas: firma vía OIDC de serie, más una puntuación de calidad que premia documentación,
exportscorrectos y compatibilidad cross-runtime.
Interoperabilidad y slow types
Lo decisivo es que JSR no te encierra: interopera con el ecosistema existente. Añades un paquete de JSR a un proyecto Node con npx jsr add, que escribe en tu package.json una dependencia enrutada al scope reservado @jsr, y el registro sirve un tarball compatible —ya transpilado a JavaScript con sus .d.ts— para que npm, pnpm, yarn o bun lo instalen sin enterarse de que el origen era TypeScript puro. Un paquete no necesita package.json: le basta un jsr.json con su name, su version y sus exports apuntando al .ts.
# anade un paquete de JSR desde distintos gestores
npx jsr add @std/encoding # npm
pnpm dlx jsr add @std/encoding # pnpm
bunx jsr add @std/encoding # bun
deno add jsr:@std/encoding # deno, de forma nativa
# publicar a JSR desde el directorio del paquete
npx jsr publish
{
"name": "@acme/parser",
"version": "1.0.0",
"exports": "./mod.ts"
}
El puente con npm se apoya en tres piezas que operan sin que el consumidor las note:
- el scope reservado
@jsr, al que se enruta toda dependencia de JSR en un proyecto npm; - un tarball transpilado con sus
.d.ts, que el registro genera a partir del.tsde origen; - el mismo protocolo de nombres y HTTP, para que npm, pnpm, yarn o bun instalen sin cambios.
La restricción que más sorprende es la regla de los slow types: JSR exige que la API pública lleve tipos explícitos, sin depender de la inferencia para los símbolos exportados. No es un capricho, sino la condición que le permite generar documentación y transpilar por runtime a gran velocidad sin ejecutar el compilador completo.
// slow type: JSR no puede resolver el retorno publico sin compilar todo
export function crear(config) { return { id: config.id, activo: true } }
// rapido: el tipo explicito hace la superficie legible y transpilable
export function crear(config: Config): Instancia { /* ... */ }
El efecto secundario es virtuoso: la regla te empuja a anotar tu superficie pública, lo que de paso mejora la calidad de tu API y la de los tipos que reciben tus consumidores. Una limitación técnica del registro se convierte, sin pretenderlo, en una presión hacia el buen diseño.
Al publicar en JSR no subes solo código: el registro genera la documentación de la API directamente desde tus tipos y tus comentarios, y calcula una puntuación que mide la salud del paquete —si trae procedencia, si documenta sus símbolos, si sus exports son correctos, si funciona en varios runtimes—. Esa presión hacia la calidad es parte del diseño: publicar bien deja de ser un gesto opcional para volverse visible y comparable de un vistazo, un incentivo estructural que el registro plano clásico nunca tuvo.
Cross-runtime y cuándo elegir cada uno
El otro eje del diseño es la ejecución en múltiples runtimes. Como JSR sirve la fuente TypeScript y transpila por objetivo, el mismo paquete puede ejecutarse en Node, Deno, Bun, workers al borde y el navegador, y el registro comprueba y puntúa esa compatibilidad. Es el destino natural de una librería pensada para vivir fuera de un solo entorno.
La compatibilidad no es una promesa vaga, sino una comprobación que el registro ejecuta y refleja en la puntuación:
- Node y Bun: reciben el tarball transpilado con sus
.d.tsy lo instalan como cualquier dependencia npm. - Deno: consume el
.tsde origen de forma nativa, sin transpilación intermedia. - Navegador y workers al borde: solo ESM y APIs estándar, justo lo que el diseño de JSR favorece.
flowchart TD SRC[paquete en TypeScript] --> JSR[registro JSR sirve la fuente] JSR --> DENO[deno consumo nativo] JSR --> NODE[node via scope jsr transpilado] JSR --> BUN[bun] JSR --> EDGE[workers al borde] style SRC fill:#89b4fa,color:#11111b style JSR fill:#cba6f7,color:#11111b style NODE fill:#a6e3a1,color:#11111b
Ni JSR ni npm ganan en abstracto: cada perfil de paquete tiene un destino natural. La decisión se toma por paquete, no por convicción global.
JSR encaja
Librería que nace en TypeScript, pensada cross-runtime, que quiere tipos nativos, docs y procedencia por defecto sin paso de build.
npm sigue mandando
Consumidores que aún dependen de CommonJS, paquetes con binarios o addons nativos, y todo el peso del ecosistema y las herramientas existentes.
Ambos a la vez
Publicación dual: npx jsr publish y npm publish desde el mismo repositorio para máximo alcance sin obligar a nadie a cambiar.
El criterio, afinado, se ordena por la naturaleza del paquete y de su público:
- Nace en TypeScript y vive cross-runtime: JSR, por los tipos nativos y la compatibilidad verificada sin paso de build.
- Consumidores atados a CommonJS o a herramientas que asumen npm: el registro clásico, donde el peso del ecosistema juega a favor.
- Addons nativos o binarios: npm, porque JSR es solo ESM y solo JavaScript o TypeScript.
- Máximo alcance sin forzar a nadie a cambiar: publicación dual en ambos destinos.
La estrategia madura en 2026 es a menudo esa publicación dual: publicas en JSR para quien quiere tipos nativos, docs y cross-runtime, y en npm para no dejar fuera al enorme ecosistema que asume el registro clásico y sus herramientas. Como ambos hablan la misma interfaz de nombres, enrutado y tarballs, sostener los dos destinos desde un solo repositorio cuesta poco y no obliga a ningún consumidor a cambiar su forma de trabajar.
# publicacion dual desde el mismo repositorio
npx jsr publish # a JSR: sirve la fuente TypeScript con tipos nativos
npm publish # a npm: sirve el tarball transpilado clasico
La gran lección de cierre es aprender a ver la arquitectura por debajo del hábito. Escribiste npm install mil veces creyendo que hablabas con npm, pero en realidad hablabas con una interfaz —nombres con scope, enrutado por .npmrc, tarballs servidos por HTTP— que resulta estar implementada, por defecto, por el registro público. Esa interfaz es lo estable; el registro detrás es intercambiable. Una vez que lo ves, JSR deja de parecer un competidor y se revela como lo que es: la misma interfaz con un backend rediseñado para el JavaScript moderno, uno que sirve TypeScript con tipos nativos que nunca se desincronizan porque son la fuente, que transpila por runtime para correr en Node, Deno, Bun y el borde, que trae procedencia y documentación de serie, y que con la regla de los slow types convierte una restricción técnica en una presión hacia APIs mejor tipadas. Y lo hace sin encerrarte, porque el scope @jsr enruta sus paquetes hacia tus clientes de siempre como si fueran tarballs de npm. La consecuencia práctica es que el destino se elige por paquete y no por lealtad: npm para el peso del ecosistema y los consumidores CommonJS, JSR para lo que nace en TypeScript y quiere vivir cross-runtime, y a menudo ambos a la vez mediante publicación dual. Has cerrado el mapa del registro; publicar, versionar, distribuir y firmar ya no son magia, sino capas de una misma abstracción que ahora sabes sustituir con criterio. El resto del camino a producción te espera, pero el arte de exponer una librería al mundo ya es tuyo.
- Añade un paquete de JSR a un proyecto Node con
npx jsr adde inspecciona cómo queda enrutado el scope@jsren tu configuración. - Crea un
jsr.jsonmínimo conname,versionyexportsapuntando a un.ts, y publica connpx jsr publish. - Introduce un slow type en tu API pública, observa el aviso de JSR y corrígelo anotando el tipo explícito.
- Consulta la puntuación de calidad de un paquete en JSR y enumera qué factores la suben o la bajan.
- Diseña una estrategia de publicación dual para una librería TypeScript-first y justifica qué consumidores atiende cada destino.