Skip to main content

Crear un paquete npm compatible con ESM y CJS en 2024

Escrito por
blog feature multithreading

18 de abril de 2024

0 minutos de lectura
How to Build a Secure NPM Package for ESM and CJS

Publicar paquetes de JavaScript compatibles tanto con ECMAScript Modules (ESM) como con CommonJS (CJS) es una habilidad fundamental para los desarrolladores que buscan integrar una amplia variedad de bibliotecas.

Este artículo se centra en enfoques prácticos y buenas prácticas para mantener la compatibilidad con ESM y CJS. Analizaremos las implicaciones de evitar la declaración ”type: module” en bibliotecas compatibles con ambos formatos e investigaremos el uso de los campos main y module en package.json para diferenciar los puntos de entrada.

El artículo también aclara el propósito y el uso del campo exports en el archivo package.json, que es esencial para controlar la resolución de módulos. Además, para los proyectos de TypeScript, exploraremos la integración de las exportaciones del manifiesto del paquete con un tipo de módulo, para garantizar tanto la compatibilidad como la seguridad de tipos.

Te recomendamos seguir las secciones de código paso a paso que aparecen a continuación. También hay un repositorio de código en GitHub llamado package-json-exports con ejemplos completos para reproducirlos, que depende del proyecto proxy de npm de código abierto Verdaccio.

Evita definir “Type: Module” en bibliotecas compatibles con ESM y CJS

¿Sabías que, si omites el campo ”type” en el archivo package.json, se establece implícitamente como commonjs de forma predeterminada? Por ejemplo: ”type”: “commonjs”.

En cuanto agregas ”type”: “module” al archivo package.json, estableces explícitamente que la biblioteca está dirigida únicamente a proyectos ESM. Aun así, debes definir un campo main en el manifiesto del paquete y actualizarlo para que apunte al módulo exportado compatible con ESM.

Como ejemplo práctico, la siguiente definición de archivo package.json no funciona para los consumidores ESM posteriores, aunque sea ESM “puro”:

{
  "name": "math-add",
  "version": "1.2.0",
  "description": "",
  "module": "src/index.mjs",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0"
}

La definición module corresponde a un módulo ESM y type indica claramente que esta biblioteca está dirigida a consumidores ESM, pero falta el campo main en el archivo package.json. Verás que Node.js genera una excepción con un error que indica que no puede encontrar el paquete, como este:

node:internal/modules/esm/resolve:205
  const resolvedOption = FSLegacyMainResolve(packageJsonUrlString, packageConfig.main, baseStringified);
                         ^

Error: Cannot find package '/~/package-json-exports/consumer-esm/node_modules/math-add/package.json' imported from /~/package-json-exports/consumer-esm/server.js

En conclusión, evita usar la declaración ”type”: “module” en el manifiesto de un paquete npm.

Veamos cómo se usan los campos main y module en el archivo package.json y por qué son mejores directivas.

Compatibilidad con ESM y CJS mediante los campos main y module

Antes de la llegada de ESM, el campo main del archivo package.json se diseñó para indicarle al entorno de ejecución de Node.js cuál era el punto de entrada del paquete. Por lo general, los desarrolladores tenían un archivo index.js o app.js en el directorio raíz y configuraban el campo main para que apuntara a ese archivo, por ejemplo: ”main”: “index.js”.

Si omites el campo main en el archivo package.json, el entorno de ejecución de Node.js intentará resolver el punto de entrada del paquete usando la convención de buscar un archivo server.js en el directorio raíz del paquete.

Para definir un paquete npm compatible tanto con ESM como con CJS, podemos usar una convención en la que main apunte a una exportación CJS y module apunte a una exportación ESM.

Biblioteca:

{
  "name": "math-add",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.cjs",
  "module": "src/index.mjs",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0"
}

Los proyectos consumidores pueden usar CJS o ESM. Los proyectos CJS usarán el archivo src/index.cjs y los proyectos ESM, el archivo src/index.mjs. Ninguno de estos consumidores necesita especificar nada especial sobre la dependencia math-add: simplemente funcionará.

Cómo entender el campo exports de package.json

El uso del campo exports en el archivo package.json ofrece un control aún más preciso sobre las construcciones que se exportan desde tu paquete npm y la forma en que se consumen.

Por ejemplo, puedes especificar la ruta completa al archivo de entrada si el entorno de ejecución de Node.js intenta requerir tu paquete npm con require(‘math-add’) y usar un archivo completamente distinto como entrada si intenta cargarlo con import .. from ‘math-add’).

Aquí tienes un ejemplo de código para un paquete de modo dual CJS y ESM, como el que describimos:

{
  "name": "math-add",
  "version": "1.5.0",
  "description": "",
  "exports": {
    ".": {
      "require": "./src/index.cjs",
      "import": "./src/index.mjs"
    }
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0"
}

Exportaciones de package.json y un tipo de módulo para un proyecto de TypeScript

Si escribes el código ESM de tu paquete con TypeScript y quieres mantener la compatibilidad con versiones anteriores de CJS, también debes declarar types y resolver la compilación de TypeScript y la transpilación para la parte de CJS.

Para la compilación de TypeScript y el proceso de empaquetado, recomiendo tsup. Luego, tendrás que agregar una etapa de scripts de build. No olvides ejecutar esa compilación antes de un trabajo de CI o de publicar el paquete npm manualmente.

Aquí tienes un ejemplo completo:

{
  "name": "math-add",
  "version": "1.5.0",
  "description": "",
 "main": "./dist/index.js",
 "module": "./dist/index.mjs",
 "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsup src/index.ts --format cjs,esm --dts --clean",
    "watch": "npm run build -- --watch src",
    "prepublishOnly": "npm run build"
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0"
}

Observa que también usamos la nueva compatibilidad del entorno de ejecución de Node.js para detectar cambios, mediante la opción de línea de comandos --watch src. Antes esto se hacía con nodemon, que es un excelente paquete, pero es mejor tener menos dependencias.

Próximos pasos: publicación y estructura de paquetes npm modernos en 2024

Este fue un artículo breve y conciso para desarrolladores de JavaScript, con ideas prácticas y fáciles de aplicar para manejar eficazmente los formatos de módulo en sus proyectos.

También debes asegurarte de seguir las buenas prácticas para publicar paquetes npm y crear paquetes npm modernos. Estas abarcan con más detalle la configuración de TypeScript, las pruebas, CI, la seguridad y otras consideraciones.

Mantén seguras tus dependencias de código abierto

Snyk ofrece pull requests de corrección con un clic para dependencias vulnerables de código abierto y sus dependencias transitivas.

Leer más

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

feature insights context
Blog

¿La prevención es, en esencia, un problema ya resuelto?

La prevención en el código generado por agentes está resuelta desde el punto de vista arquitectónico, pero elegir controles que protejan la seguridad sin ralentizar el desarrollo sigue siendo el desafío.

Live Stream

Agentes de remediación, sin misterios: por qué solucionar es mejor que encontrar

Descubre cómo el agente de remediación de Snyk utiliza inteligencia de seguridad, análisis de explotabilidad y validación para convertir las vulnerabilidades en solicitudes de incorporación de cambios listas para fusionarse.