esm cjs typescript

Tutorial para creadores de paquetes ESM + CommonJS

Hay un intenso debate en la comunidad de JS sobre dejar de lado CommonJS o utilizar paquetes duales. He recopilado enlaces clave y escrito un tutorial sobre la publicación de paquetes duales.

Daniel Gustaw

Daniel Gustaw

7 min read

Tutorial para creadores de paquetes ESM + CommonJS

Comenzaré desde las fuentes y el contexto, luego mostraré la implementación práctica.

ESM puro vs Paquetes duales

Los módulos en JavaScript tienen una historia impresionante y el conocimiento de su evolución es importante para comprender el estado actual y prever la futura configuración del ecosistema JS.

Comprendiendo los módulos ES6 a través de su historia — SitePoint

Hay una opinión ampliamente citada que dice que deberíamos proporcionar un paquete ESM solo en el gist a continuación.

Paquete ESM puro

Pero esto puede llevar a problemas experimentados por los usuarios finales u otros mantenedores.

El valle incómodo hacia ESM: Node.js, Victory y D3

Romper la compatibilidad es una de las formas de introducir cambios, pero es doloroso y conduce a errores como estos:

Error: require() of ES modules is not supported when importing

Error: require() de módulos ES no es compatible al importar node-fetch

Visto 123k veces

Error [ERR_REQUIRE_ESM]: require() de módulo ES no soportado

Visto 379k veces

Es genial que el conocimiento sobre ESM se propague en la comunidad gracias a estos errores, pero CommonJS es actualmente el estándar predeterminado para la inclusión de módulos en el mundo de NodeJS.

CommonJS vs Módulos ES en Node.js - Una Comparación Detallada

No puedo encontrar fuentes oficiales, pero usando GPT-4 podemos estimar que en abril de 2023:

  1. La adopción de ESM alcanza un nivel sustancial, posiblemente alrededor del 30-40% de los paquetes npm.
  2. CommonJS sigue manteniendo una participación significativa, quizás alrededor del 60-70%, debido a su prevalencia histórica y la presencia de muchos proyectos heredados que aún lo utilizan.
  3. Los paquetes duales podrían representar una porción ligeramente mayor del ecosistema, alrededor del 10-15%, ya que los autores de paquetes intentan apoyar ambos sistemas de módulos durante el período de transición.

Entonces, debido a que estamos en un “período de transición”, creo que es mejor asumir la responsabilidad y proporcionar una versión dual para los paquetes existentes.

Si estás creando un nuevo paquete, creo que puedes seleccionar ESM y no preocuparte por CommonJS, pero si tus paquetes fueron publicados anteriormente, este tutorial es para ti.

Peligro del paquete dual

Antes de comenzar, debes estar consciente de la existencia del peligro de los paquetes duales:

Modules: Packages | Node.js v19.8.1 Documentation

Simplificando, si un usuario escribe const pkgInstance = require('pkg') y en otro lugar import pkgInstance from 'pkg', entonces se crearán dos instancias del paquete. Esto puede conducir a problemas difíciles de depurar y comportamientos indefinidos, por lo que hay dos métodos para minimizarlos.

He preparado un diagrama que te ayudará a decidir qué enfoque se adapta mejor a ti:

Si necesitas crear un wrapper ES, entonces consulta directamente la documentación. En el siguiente capítulo asumiré que tienes un paquete sin estado y aplicaré el enfoque de estado aislado.

Estado aislado

Hay una excelente guía práctica que muestra un problema similar a este:

Soporte para CommonJS y ESM con Typescript y Node

Creación de un paquete dual

En este ejemplo vamos a escribir una biblioteca que implementa la función sum. Vamos a crear un proyecto:

npm init -y && tsc --init && mkdir -p src && touch src/index.ts

en el archivo src/index.ts estamos definiendo la función

export function sum(a: number, b: number): number {
    return a+b;
}

en package.json estamos agregando script.build que creará tanto CJS como ESM

"build": "npx tsc --module commonjs --outDir cjs/ && echo '{\"type\": \"commonjs\"}' > cjs/package.json && npx tsc --module es2022 --outDir esm/ && echo '{\"type\": \"module\"}' > esm/package.json"

porque crearemos dos directorios en lugar de un solo dist que añadimos a package.json

  "exports": {
    "require": "./cjs/index.js",
    "import": "./esm/index.js"
  },
  "types": "./src",

Finalmente en package.json necesitamos cambiar main

  "main": "cjs/index.js"

Ahora después de construir

npm run build

podemos probarlo en otro proyecto.

Importar/requerir en paquete dual

Crear otro proyecto

npm init -y

y añadir dependencia con parche a nuestro proyecto original

    "sumesm": "file:./../dual"

y aquí en index.js podemos escribir

const s = require('sumesm');

console.log(s.sum(1, 2));

así como

(async () => {
    const s = await import('sumesm');
    console.log(s.sum(1, 2));
})()

ambos funcionarán.

Prueba para paquete dual en jest

Volvamos a nuestro paquete y escribamos pruebas.

npm install --save-dev jest @types/jest ts-jest
npx ts-jest config:init

o si no puedes recordar todos estos comandos, puedes usar

gpt-cli add and config jest for typescript to node project

usando este programa https://github.com/gustawdaniel/gpt-cli. Vamos a crear una prueba.

mkdir -p test && touch test/sum.test.ts

con contenido

import {sum} from "../src";

it('sum', () => {
    expect(sum(1, 2)).toEqual(3)
})

y actualizar script en package.json

    "test": "jest",

la prueba funciona

Tests:       1 passed, 1 total
Time:        1.185 s

podemos reemplazar ts-node por esbuild-jest en package.json y preset: 'ts-jest', en jest.config.js por

"transform": {
    "^.+\\.tsx?$": "esbuild-jest"
  },

para acelerar las pruebas 8 veces

Tests:       1 passed, 1 total
Time:        0.152 s, estimated 2 s

y también funciona.

Desafortunadamente, las pruebas rompen nuestra compilación, así que tenemos dos opciones.

La primera es lenta, pero parece ser estable. Es inclusión:

 "include": [
    "src/**/*"
  ]

a tsconfig.json. El segundo es dos veces más rápido y es una migración simple de tsc a esbuild. Puedes reemplazar el antiguo build en package.json por

    "build": "npx esbuild --bundle src/index.ts --outdir=cjs --platform=node --format=cjs && echo '{\"type\": \"commonjs\"}' > cjs/package.json && npx esbuild --bundle src/index.ts --outdir=esm --platform=neutral --format=esm && echo '{\"type\": \"module\"}' > esm/package.json"

Verificar la autocompletación de tipos

Gracias a "types": "./src", en package.json funciona. Es una práctica común reemplazar el código fuente por archivos que contienen solo tipos, porque los fuentes completos son más pesados. Pero prefiero este método porque es más fácil de depurar.

Para el paquete final necesitas añadir:

package.json
esm
cjs
src

Construyendo con swc

Intenté reemplazar esbuild por swc, pero aún no está listo.

Vamos a profundizar en los problemas

Supongamos ahora que necesitamos usar el paquete humanize-string. Seleccioné este paquete porque es un ejemplo de un paquete que dejó cjs, causando problemas. Su versión 2.1.0 es cjs, pero 3.0.0 es puro esm.

Si añadimos este paquete en la versión 2.1.0 a nuestro proyecto, entonces cjs puede construirse correctamente, pero para esm hay un error:

el paquete xregexp que es dependencia de decamelize tenía una exportación predeterminada en la versión 4, por lo que era imposible convertirlo fácilmente a esm.

podemos leer sobre este problema aquí:

La importación ya no funciona desde la versión 4.4.0 · Problema #305 · slevithan/xregexp

Por otro lado, cuando instalamos humanize-string en 3.0.0, la construcción funciona pero las pruebas están rotas:

afortunadamente en este caso encontré una solución sobrescribiendo la versión de decamelize:

  "dependencies": {
    "humanize-string": "^2.1.0"
  },
  "overrides": {
    "decamelize": "4.0.0"
  }

porque se eliminó la dependencia xregexp

Release v4.0.0 · sindresorhus/decamelize

pero si no encontrara esta opción, probablemente me mudaría a pnpm para pnpm patch o usaría npm patch-package. Este escenario es típico si intentas hacer algo con esm.

Futuro de los Paquetes JS

Ahora estamos en un momento de transición. Está bastante claro que en el futuro los módulos cjs serán llamados legacy y usaremos más bien ESM. Espero que al ofrecer paquetes duales en lugar de solo ESM, los usuarios pasen menos tiempo lidiando con errores. Mientras tanto, una nueva ola de herramientas para desarrolladores como SWC, esbuild, Rome y otras seguirán mejorando el soporte de ESM. Eventualmente, podremos eliminar el soporte para CommonJS en el futuro cuando su impacto en los usuarios finales se vuelva insignificante.

Gracias a todos los usuarios de Reddit que me ayudaron a entender este tema en la discusión:

ESM vs Dual Package?

Other articles

You can find interesting also.