Volver al blog

ESM en 2026: El Fin de CommonJS y la Era del JavaScript Moderno

Hola HaWkers, 2026 es oficialmente el año de la adopción completa de ES Modules (ESM) en el ecosistema JavaScript. Con Node.js 20+ permitiendo importar ESM desde código CommonJS, la última gran barrera cayó.

Vamos a entender esta transición y cómo afecta tu código.

Qué Cambió en 2026

Node.js Unificó los Mundos

// El gran cambio: CJS puede importar ESM

// Antes (hasta 2025): Esto daba error
// archivo.cjs
const esModule = require('./modulo-esm.mjs'); // ❌ No funcionaba

// Ahora (2026): ¡Funciona!
// archivo.cjs
const esModule = require('./modulo-esm.mjs'); // ✅ Funciona

// Node.js 20+ (backportado) permite esto
// Esto elimina el principal dolor de cabeza de migración

Por Qué Esto Es Grande

// El problema histórico

const esmAdoptionBlocker = {
  before: {
    problem: 'Bibliotecas ESM no podían usarse en proyectos CJS',
    symptom: 'ERR_REQUIRE_ESM',
    workaround: 'Dynamic import() - feo y asíncrono',
    result: 'Muchas libs mantenían dual publish'
  },

  after: {
    solution: 'CJS puede require() ESM directamente',
    impact: 'Libs pueden ser ESM-only sin romper a usuarios',
    trend: 'Nuevas libs son ESM-only por defecto'
  },

  timeline: {
    node22: 'Feature añadida',
    node20: 'Backportado',
    node18: 'Backportado (LTS)',
    adoption: 'Todos los Node.js no-EOL lo soportan'
  }
};

CommonJS vs ESM: Las Diferencias

Sintaxis

// CommonJS (la forma antigua)

// Exportar
module.exports = { foo, bar };
// o
exports.foo = foo;

// Importar
const { foo, bar } = require('./module');
const fs = require('fs');

// ESM (la forma moderna)

// Exportar
export { foo, bar };
export default myFunction;

// Importar
import { foo, bar } from './module.js';
import fs from 'node:fs';

Diferencias Importantes

// 1. Timing de carga

// CommonJS: Síncrono, en runtime
const a = require('./a'); // Ejecuta ahora
if (condition) {
  const b = require('./b'); // Ejecuta condicionalmente
}

// ESM: Asíncrono, antes de runtime
import a from './a.js'; // Cargado antes de ejecutar código
// import condicional requiere dynamic import
const b = condition ? await import('./b.js') : null;


// 2. __dirname y __filename

// CommonJS: Disponible automáticamente
console.log(__dirname);  // /path/to/folder
console.log(__filename); // /path/to/folder/file.js

// ESM: Necesita crear
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);


// 3. JSON imports

// CommonJS
const config = require('./config.json'); // Funciona

// ESM
import config from './config.json' with { type: 'json' }; // Sintaxis nueva
// o
import { readFileSync } from 'node:fs';
const config = JSON.parse(readFileSync('./config.json', 'utf8'));


// 4. Top-level await

// CommonJS: No permitido
// await fetch(...); // ❌ SyntaxError

// ESM: ¡Funciona!
const data = await fetch('...'); // ✅ Top-level await
export const result = await processData(data);

Por Qué Migrar a ESM

Beneficios Técnicos

// Ventajas de ESM

const esmBenefits = {
  // 1. Tree shaking
  treeShaking: {
    what: 'Bundlers eliminan código no usado',
    requirement: 'Imports estáticos (ESM)',
    impact: 'Bundles más pequeños',
    example: `
      // Importas solo lo que usas
      import { debounce } from 'lodash-es';
      // Bundler incluye solo debounce, no lodash entero
    `
  },

  // 2. Static analysis
  staticAnalysis: {
    what: 'Herramientas entienden imports antes de ejecutar',
    benefits: [
      'Mejor autocomplete en IDEs',
      'Detección de imports rotos',
      'Refactorización más segura'
    ]
  },

  // 3. Browser-native
  browserNative: {
    what: 'ESM funciona en browser sin bundler',
    use: 'Desarrollo local, apps pequeñas',
    syntax: '<script type="module" src="app.js"></script>'
  },

  // 4. Top-level await
  topLevelAwait: {
    what: 'await fuera de funciones async',
    useful: 'Inicialización de módulos',
    example: 'export const db = await connectDB();'
  },

  // 5. Future-proof
  futureProof: {
    what: 'ESM es el estándar oficial',
    cjs: 'CommonJS es específico de Node.js',
    trend: 'Ecosistema migrando a ESM'
  }
};

El Ecosistema Está Cambiando

// Estado del ecosistema en 2026

const ecosystem2026 = {
  // Libs que son ESM-only ahora
  esmOnly: [
    'chalk 5+',
    'node-fetch 3+',
    'execa 6+',
    'got 12+',
    'globby 13+',
    'p-limit 4+',
    'nanoid 4+',
    // Y muchas otras...
  ],

  // Frameworks que prefieren ESM
  frameworks: {
    vite: 'ESM-first',
    nuxt3: 'ESM por defecto',
    astro: 'ESM por defecto',
    sveltekit: 'ESM por defecto'
  },

  // Lo que todavía es CommonJS
  stillCJS: {
    express: 'Funciona en ambos',
    lodash: 'Tiene lodash-es para ESM',
    moment: 'Usa dayjs o date-fns',
    legacy: 'Proyectos no mantenidos'
  }
};

Cómo Migrar a ESM

Paso 1: Configura el package.json

// Añade "type": "module"

// package.json
{
  "name": "mi-proyecto",
  "version": "1.0.0",
  "type": "module",  // ← Esto hace que .js sea tratado como ESM
  "main": "index.js",
  "exports": {
    ".": "./index.js",
    "./utils": "./utils/index.js"
  }
}

// Con "type": "module":
// - archivos .js son ESM
// - archivos .cjs son CommonJS (si lo necesitas)

// Sin "type": "module" (o "type": "commonjs"):
// - archivos .js son CommonJS
// - archivos .mjs son ESM

Paso 2: Actualiza los Imports

// Transformaciones necesarias

// De:
const path = require('path');
const { readFile } = require('fs/promises');
const myModule = require('./my-module');

// A:
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import myModule from './my-module.js'; // ¡Nota el .js!

// IMPORTANTE: ESM requiere extensión de archivo
import utils from './utils';     // ❌ No funciona
import utils from './utils.js';  // ✅ Funciona

// Para módulos de Node, usa prefijo node:
import fs from 'node:fs';     // ✅ Recomendado
import fs from 'fs';          // Funciona, pero node: es más claro

Paso 3: Actualiza los Exports

// Transformaciones de export

// De:
module.exports = myFunction;
module.exports = { foo, bar };
exports.something = something;

// A:
export default myFunction;
export { foo, bar };
export const something = something;

// Exports mixtos
export default mainFunction;
export { helper1, helper2 };

Paso 4: Corrige __dirname/__filename

// Crea un helper o añade al inicio de los archivos que lo necesitan

// esm-utils.js
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

export function getDirname(importMetaUrl) {
  return dirname(fileURLToPath(importMetaUrl));
}

// uso en cualquier archivo:
import { getDirname } from './esm-utils.js';
const __dirname = getDirname(import.meta.url);

// O inline (más común):
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Paso 5: Actualiza JSON Imports

// Opción 1: Import assertion (Node 20+)
import config from './config.json' with { type: 'json' };

// Opción 2: Lectura manual (más compatible)
import { readFileSync } from 'node:fs';
const config = JSON.parse(
  readFileSync(new URL('./config.json', import.meta.url), 'utf8')
);

// Opción 3: createRequire (hack útil)
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const config = require('./config.json');

Estrategias de Migración

Migración Gradual

// Para proyectos grandes, migra gradualmente

const migrationStrategy = {
  // Fase 1: Prepara
  phase1: {
    actions: [
      'Identifica dependencias ESM-only',
      'Verifica versión de Node (18+)',
      'Configura ESLint para ESM'
    ]
  },

  // Fase 2: Modo dual
  phase2: {
    how: 'Usa .mjs para nuevos archivos ESM',
    keep: 'Archivos existentes .js como CJS',
    benefit: 'Migración incremental'
  },

  // Fase 3: El cambio
  phase3: {
    action: 'Añade "type": "module"',
    rename: 'Archivos CJS restantes a .cjs',
    test: 'Ejecuta todos los tests'
  },

  // Fase 4: Limpieza
  phase4: {
    actions: [
      'Renombra .mjs a .js',
      'Elimina workarounds',
      'Actualiza documentación'
    ]
  }
};

Para Bibliotecas

// Si mantienes una lib npm

const libraryMigration = {
  // Opción 1: ESM-only (recomendado en 2026)
  esmOnly: {
    packageJson: {
      "type": "module",
      "exports": {
        ".": "./dist/index.js"
      }
    },
    pros: 'Simple, moderno',
    cons: 'Rompe usuarios CJS en Node < 20',
    when: 'Nueva lib o major version'
  },

  // Opción 2: Dual package (CJS + ESM)
  dual: {
    packageJson: {
      "main": "./dist/index.cjs",
      "module": "./dist/index.js",
      "exports": {
        ".": {
          "import": "./dist/index.js",
          "require": "./dist/index.cjs"
        }
      }
    },
    pros: 'Compatibilidad máxima',
    cons: 'Build más complejo, dual package hazard',
    when: 'Lib popular con usuarios legacy'
  }
};

Herramientas Que Ayudan

Codemods y Linters

// Herramientas para migración

const migrationTools = {
  // ESLint
  eslint: {
    plugin: 'eslint-plugin-import',
    rules: {
      'import/extensions': ['error', 'always'],
      'import/no-commonjs': 'error'
    }
  },

  // Codemod
  codemod: {
    tool: 'jscodeshift',
    transforms: 'cjs-to-esm transforms disponibles',
    command: 'npx cjs-to-esm ./src/**/*.js'
  },

  // TypeScript
  typescript: {
    config: {
      "compilerOptions": {
        "module": "ESNext",
        "moduleResolution": "NodeNext"
      }
    },
    benefit: 'TS genera ESM automáticamente'
  },

  // Bundlers
  bundlers: {
    vite: 'ESM nativo',
    rollup: 'ESM-first',
    esbuild: 'Soporte excelente'
  }
};

Errores Comunes y Soluciones

Troubleshooting

// Errores comunes en la migración

const commonErrors = {
  // Error 1: Cannot use import statement outside a module
  error1: {
    message: 'Cannot use import statement outside a module',
    cause: 'Archivo tratado como CJS',
    solutions: [
      'Añade "type": "module" en package.json',
      'Usa extensión .mjs',
      'Verifica que no haya package.json padre'
    ]
  },

  // Error 2: ERR_MODULE_NOT_FOUND
  error2: {
    message: 'ERR_MODULE_NOT_FOUND',
    cause: 'Falta extensión en el import',
    solution: 'import x from "./file.js" (no "./file")'
  },

  // Error 3: __dirname is not defined
  error3: {
    message: '__dirname is not defined',
    cause: '__dirname no existe en ESM',
    solution: 'Usa import.meta.url + fileURLToPath'
  },

  // Error 4: require is not defined
  error4: {
    message: 'require is not defined',
    cause: 'Usando require en módulo ESM',
    solution: 'Usa import o createRequire'
  },

  // Error 5: JSON import no funciona
  error5: {
    message: 'Unknown file extension ".json"',
    cause: 'JSON necesita assertion',
    solution: 'import x from "./x.json" with { type: "json" }'
  }
};

Conclusión

2026 marca el punto de inflexión para ESM en JavaScript. Con Node.js eliminando la última gran barrera (CJS importando ESM), ya no hay excusa para no migrar.

Acciones recomendadas:

  1. Proyectos nuevos: Usa ESM desde el inicio
  2. Proyectos existentes: Planifica migración gradual
  3. Bibliotecas: Considera ESM-only para la próxima major
  4. Actualiza Node.js: 20+ para mejor experiencia

CommonJS sirvió bien por 15 años, pero su tiempo está llegando al fin. ESM es el futuro - y el futuro es ahora.

Para entender más sobre el ecosistema JavaScript moderno, lee: VoidZero 2026.

¡Vamos con todo! 🦅

Comentarios (0)

Este artículo aún no tiene comentarios 😢. ¡Sé el primero! 🚀🦅

Añadir comentarios