Voltar para o Blog

ESM em 2026: O Fim do CommonJS e a Era do JavaScript Moderno

Olá HaWkers, 2026 está sendo oficialmente o ano da adoção completa de ES Modules (ESM) no ecossistema JavaScript. Com o Node.js 20+ permitindo importar ESM de código CommonJS, a última grande barreira caiu.

Vamos entender essa transição e como ela afeta seu código.

O Que Mudou em 2026

Node.js Unificou os Mundos

// A grande mudança: CJS pode importar ESM

// Antes (até 2025): Isso dava erro
// arquivo.cjs
const esModule = require('./modulo-esm.mjs'); // ❌ Não funcionava

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

// O Node.js 20+ (backportado) permite isso
// Isso remove a principal dor de cabeça de migração

Por Que Isso É Grande

// O problema histórico

const esmAdoptionBlocker = {
  before: {
    problem: 'Bibliotecas ESM não podiam ser usadas em projetos CJS',
    symptom: 'ERR_REQUIRE_ESM',
    workaround: 'Dynamic import() - feio e assíncrono',
    result: 'Muitas libs mantinham dual publish'
  },

  after: {
    solution: 'CJS pode require() ESM diretamente',
    impact: 'Libs podem ser ESM-only sem quebrar users',
    trend: 'Novas libs são ESM-only por padrão'
  },

  timeline: {
    node22: 'Feature adicionada',
    node20: 'Backportado',
    node18: 'Backportado (LTS)',
    adoption: 'Todos Node.js não-EOL suportam'
  }
};

CommonJS vs ESM: As Diferenças

Sintaxe

// CommonJS (o jeito antigo)

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

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

// ESM (o jeito moderno)

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

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

Diferenças Importantes

// 1. Timing de carregamento

// CommonJS: Síncrono, em runtime
const a = require('./a'); // Executa agora
if (condition) {
  const b = require('./b'); // Executa condicionalmente
}

// ESM: Assíncrono, antes de runtime
import a from './a.js'; // Carregado antes do código executar
// import condicional requer dynamic import
const b = condition ? await import('./b.js') : null;


// 2. __dirname e __filename

// CommonJS: Disponível automaticamente
console.log(__dirname);  // /path/to/folder
console.log(__filename); // /path/to/folder/file.js

// ESM: Precisa criar
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' }; // Sintaxe nova
// ou
import { readFileSync } from 'node:fs';
const config = JSON.parse(readFileSync('./config.json', 'utf8'));


// 4. Top-level await

// CommonJS: Não permitido
// await fetch(...); // ❌ SyntaxError

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

Por Que Migrar Para ESM

Benefícios Técnicos

// Vantagens de ESM

const esmBenefits = {
  // 1. Tree shaking
  treeShaking: {
    what: 'Bundlers removem código não usado',
    requirement: 'Imports estáticos (ESM)',
    impact: 'Bundles menores',
    example: `
      // Você importa só o que usa
      import { debounce } from 'lodash-es';
      // Bundler inclui só debounce, não lodash inteiro
    `
  },

  // 2. Static analysis
  staticAnalysis: {
    what: 'Ferramentas entendem imports antes de executar',
    benefits: [
      'Melhor autocomplete em IDEs',
      'Detecção de imports quebrados',
      'Refatoração mais segura'
    ]
  },

  // 3. Browser-native
  browserNative: {
    what: 'ESM funciona no browser sem bundler',
    use: 'Desenvolvimento local, apps pequenas',
    syntax: '<script type="module" src="app.js"></script>'
  },

  // 4. Top-level await
  topLevelAwait: {
    what: 'await fora de funções async',
    useful: 'Inicialização de módulos',
    example: 'export const db = await connectDB();'
  },

  // 5. Futuro-proof
  futureProof: {
    what: 'ESM é o padrão oficial',
    cjs: 'CommonJS é específico do Node.js',
    trend: 'Ecossistema migrando para ESM'
  }
};

O Ecossistema Está Mudando

// Estado do ecossistema em 2026

const ecosystem2026 = {
  // Libs que são ESM-only agora
  esmOnly: [
    'chalk 5+',
    'node-fetch 3+',
    'execa 6+',
    'got 12+',
    'globby 13+',
    'p-limit 4+',
    'nanoid 4+',
    // E muitas outras...
  ],

  // Frameworks que preferem ESM
  frameworks: {
    vite: 'ESM-first',
    nuxt3: 'ESM por padrão',
    astro: 'ESM por padrão',
    sveltekit: 'ESM por padrão'
  },

  // O que ainda é CommonJS
  stillCJS: {
    express: 'Funciona em ambos',
    lodash: 'Tem lodash-es para ESM',
    moment: 'Use dayjs ou date-fns',
    legacy: 'Projetos não mantidos'
  }
};

Como Migrar Para ESM

Passo 1: Configure o package.json

// Adicione "type": "module"

// package.json
{
  "name": "meu-projeto",
  "version": "1.0.0",
  "type": "module",  // ← Isso faz .js ser tratado como ESM
  "main": "index.js",
  "exports": {
    ".": "./index.js",
    "./utils": "./utils/index.js"
  }
}

// Com "type": "module":
// - arquivos .js são ESM
// - arquivos .cjs são CommonJS (se precisar)

// Sem "type": "module" (ou "type": "commonjs"):
// - arquivos .js são CommonJS
// - arquivos .mjs são ESM

Passo 2: Atualize os Imports

// Transformações necessárias

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

// Para:
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import myModule from './my-module.js'; // Note o .js!

// IMPORTANTE: ESM requer extensão de arquivo
import utils from './utils';     // ❌ Não funciona
import utils from './utils.js';  // ✅ Funciona

// Para módulos do Node, use prefixo node:
import fs from 'node:fs';     // ✅ Recomendado
import fs from 'fs';          // Funciona, mas node: é mais claro

Passo 3: Atualize os Exports

// Transformações de export

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

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

// Mixed exports
export default mainFunction;
export { helper1, helper2 };

Passo 4: Corrija __dirname/__filename

// Crie um helper ou adicione no topo dos arquivos que precisam

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

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

// uso em qualquer arquivo:
import { getDirname } from './esm-utils.js';
const __dirname = getDirname(import.meta.url);

// Ou inline (mais comum):
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Passo 5: Atualize JSON Imports

// Opção 1: Import assertion (Node 20+)
import config from './config.json' with { type: 'json' };

// Opção 2: Leitura manual (mais compatível)
import { readFileSync } from 'node:fs';
const config = JSON.parse(
  readFileSync(new URL('./config.json', import.meta.url), 'utf8')
);

// Opção 3: createRequire (hack útil)
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const config = require('./config.json');

Estratégias de Migração

Migração Gradual

// Para projetos grandes, migre gradualmente

const migrationStrategy = {
  // Fase 1: Prepare
  phase1: {
    actions: [
      'Identifique dependências ESM-only',
      'Verifique versão do Node (18+)',
      'Configure ESLint para ESM'
    ]
  },

  // Fase 2: Dual mode
  phase2: {
    how: 'Use .mjs para novos arquivos ESM',
    keep: 'Arquivos existentes .js como CJS',
    benefit: 'Migração incremental'
  },

  // Fase 3: Flip the switch
  phase3: {
    action: 'Adicione "type": "module"',
    rename: 'Arquivos CJS restantes para .cjs',
    test: 'Rode todos os testes'
  },

  // Fase 4: Cleanup
  phase4: {
    actions: [
      'Renomeie .mjs para .js',
      'Remova workarounds',
      'Atualize documentação'
    ]
  }
};

Para Bibliotecas

// Se você mantém uma lib npm

const libraryMigration = {
  // Opção 1: ESM-only (recomendado em 2026)
  esmOnly: {
    packageJson: {
      "type": "module",
      "exports": {
        ".": "./dist/index.js"
      }
    },
    pros: 'Simples, moderno',
    cons: 'Quebra users CJS em Node < 20',
    when: 'Nova lib ou major version'
  },

  // Opção 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: 'Compatibilidade máxima',
    cons: 'Build mais complexo, dual package hazard',
    when: 'Lib popular com users legados'
  }
};

Ferramentas Que Ajudam

Codemods e Linters

// Ferramentas para migração

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 disponíveis',
    command: 'npx cjs-to-esm ./src/**/*.js'
  },

  // TypeScript
  typescript: {
    config: {
      "compilerOptions": {
        "module": "ESNext",
        "moduleResolution": "NodeNext"
      }
    },
    benefit: 'TS gera ESM automaticamente'
  },

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

Erros Comuns e Soluções

Troubleshooting

// Erros comuns na migração

const commonErrors = {
  // Erro 1: Cannot use import statement outside a module
  error1: {
    message: 'Cannot use import statement outside a module',
    cause: 'Arquivo tratado como CJS',
    solutions: [
      'Adicione "type": "module" no package.json',
      'Use extensão .mjs',
      'Verifique se não há package.json pai'
    ]
  },

  // Erro 2: ERR_MODULE_NOT_FOUND
  error2: {
    message: 'ERR_MODULE_NOT_FOUND',
    cause: 'Falta extensão no import',
    solution: 'import x from "./file.js" (não "./file")'
  },

  // Erro 3: __dirname is not defined
  error3: {
    message: '__dirname is not defined',
    cause: '__dirname não existe em ESM',
    solution: 'Use import.meta.url + fileURLToPath'
  },

  // Erro 4: require is not defined
  error4: {
    message: 'require is not defined',
    cause: 'Usando require em módulo ESM',
    solution: 'Use import ou createRequire'
  },

  // Erro 5: JSON import não funciona
  error5: {
    message: 'Unknown file extension ".json"',
    cause: 'JSON precisa de assertion',
    solution: 'import x from "./x.json" with { type: "json" }'
  }
};

Conclusão

2026 marca o ponto de virada para ESM no JavaScript. Com o Node.js removendo a última grande barreira (CJS importando ESM), não há mais desculpa para não migrar.

Ações recomendadas:

  1. Novos projetos: Use ESM desde o início
  2. Projetos existentes: Planeje migração gradual
  3. Bibliotecas: Considere ESM-only para próxima major
  4. Atualize Node.js: 20+ para melhor experiência

O CommonJS serviu bem por 15 anos, mas seu tempo está chegando ao fim. ESM é o futuro - e o futuro é agora.

Para entender mais sobre o ecossistema JavaScript moderno, leia: VoidZero 2026.

Bora pra cima! 🦅

Comentários (0)

Esse artigo ainda não possui comentários 😢. Seja o primeiro! 🚀🦅

Adicionar comentário