Node.js
Módulos: ESM, CommonJS y resolución
Compara ES Modules y CommonJS en Node.js, sus reglas de carga, package.json, extensiones, exports, imports y resolución de dependencias.
- Última actualización
- Actualizada
- Nivel
- Fundamentos
Node.js
Compara ES Modules y CommonJS en Node.js, sus reglas de carga, package.json, extensiones, exports, imports y resolución de dependencias.
| CommonJS | ESM |
|---|---|
| `require` y `module.exports` | `import` y `export` |
| Carga tradicionalmente síncrona | Enlazado estático y evaluación del grafo |
| Exports como valores del objeto exportado | Exports como live bindings |
| `__filename` y `__dirname` | `import.meta.url` y APIs URL |
{
"type": "module"
}Con type: module, los archivos .js se interpretan como ESM dentro del package scope. .mjs fuerza ESM y .cjs fuerza CommonJS.
Sin una decisión explícita, la interpretación depende de extensión, package scope y versión. Evita proyectos ambiguos.
import { readFile } from "node:fs/promises";
import path from "node:path";El prefijo node: comunica que el módulo pertenece al runtime y evita confusión con paquetes del registry.
import { fileURLToPath } from "node:url";
import path from "node:path";
const filename = fileURLToPath(import.meta.url);
const dirname = path.dirname(filename);En ESM, la identidad del módulo es una URL. Convierte a path solo al llamar una API que lo necesite.
Node debe convertir un specifier en una ubicación y un formato.
import "node:fs";
import "./config.js";
import "my-package";
import "my-package/feature";La resolución considera built-ins, URLs relativas, package scopes, exports, condiciones y extensiones. ESM exige specifiers más explícitos que CommonJS en varios casos.
Un módulo normalmente se evalúa una vez por identidad resuelta y luego se reutiliza.
// counter.js
let count = 0;
export const next = () => ++count;La caché puede convertir un módulo en estado compartido. Eso es útil para singletons controlados, pero perjudica aislamiento si el estado es accidental.
a.js → b.js → a.jsEn un ciclo, un módulo puede observar otro parcialmente inicializado. ESM conserva live bindings, pero acceder antes de inicialización puede fallar. Los ciclos suelen señalar límites mal definidos.
const adapter = await import(`./adapters/${name}.js`);Úsalo para carga condicional real. Valida el origen del specifier; construir rutas desde entrada no confiable puede exponer módulos inesperados.
Importar CommonJS desde ESM y ESM desde CommonJS tiene reglas asimétricas. Un default import desde CommonJS suele representar module.exports; named exports pueden depender del análisis de Node y no equivalen a live bindings reales.
No publiques un dual package sin probar ambos grafos. Una misma librería cargada como CJS y ESM puede crear instancias duplicadas.
{
"exports": {
".": "./dist/index.js",
"./cli": "./dist/cli.js"
},
"imports": {
"#config": "./src/config.js"
}
}exports define la API pública del paquete. Oculta rutas internas no declaradas y permite condiciones. imports crea aliases privados dentro del package.
node: identifica built-ins.exports es un contrato público, no decoración.¿Por qué un paquete puede comportarse como dos singletons distintos en una aplicación?
Porque puede cargarse mediante identidades o sistemas distintos, por ejemplo una copia CJS y otra ESM. Cada grafo conserva su propia caché e instancia.
npm, package.json y gestión de dependencias explica cómo se construye y reproduce el grafo externo.