FNSP Web Page

CommonJS ou ESM: o dia em que o require deixa de funcionar

Um dia actualiza-se uma dependência, corre-se o projeto, e aparece isto:

Error [ERR_REQUIRE_ESM]: require() of ES Module
/app/node_modules/chalk/source/index.js from /app/script.js not supported.

A biblioteca passou a ser só ESM, e o projeto ainda usa require. É o ponto em que muita gente descobre que o Node tem dois sistemas de módulos, e que a passagem de um para o outro não é uma questão de escrever import em vez de require.

1. Os dois sistemas

// CommonJS — o do Node desde o princípio
const { readFile } = require('node:fs/promises');
module.exports = { correr };

// ESM — a norma do JavaScript, a mesma do browser
import { readFile } from 'node:fs/promises';
export { correr };

A diferença que interessa não é a sintaxe: é que o require é síncrono e resolvido em execução, enquanto o import é estático e resolvido antes de correr uma linha. É por isso que se pode escrever require dentro de um if, e não se pode fazer o mesmo com import.

2. Como é que o Node decide qual é qual

Esta é a parte que resolve metade das dúvidas, e são três regras:

  • .mjs — sempre ESM, independentemente de tudo o resto.

  • .cjs — sempre CommonJS.

  • .js — depende do "type" do package.json mais próximo. Com "type": "module" é ESM; sem o campo nenhum, é CommonJS.

É por isso que aparecem ficheiros .mjs em projetos que nunca se declararam nada. Este site é um deles: o package.json não tem "type", portanto um .js seria CommonJS — e as configurações do ESLint, do commitlint e do PostCSS precisam de export default. A saída é a extensão:

commitlint.config.mjs
eslint.config.mjs
postcss.config.mjs

Não é estilo nem moda. É a única forma de escrever export default naqueles três ficheiros sem mudar o modo do projeto inteiro.

3. A assimetria, que é o que dói

Um módulo ESM consegue importar CommonJS. O contrário é que era o problema: um ficheiro CommonJS não conseguia fazer require de um módulo ESM, porque o ESM pode ter await ao nível de topo e o require tem de devolver já.

A saída de sempre foi a importação dinâmica, que devolve uma promessa:

// Dentro de um ficheiro CommonJS
async function correr() {
  const { default: chalk } = await import('chalk');
  console.log(chalk.green('feito'));
}

Isto ainda funciona e continua a ser a resposta segura. Mas há novidade: as versões recentes do Node já permitem fazer require de módulos ESM que não usem await no topo. Deixou de ser um erro para passar a ser possível.

Convém confirmar a versão antes de contar com isso — este site, por exemplo, corre em Node 20, onde ainda dá erro:

node -v

4. O que desaparece quando se passa para ESM

Há um punhado de coisas que existiam no CommonJS e que simplesmente não estão lá:

// CommonJS
const caminho = path.join(__dirname, 'dados.json');
const dados = require('./dados.json');

// ESM
const caminho = path.join(import.meta.dirname, 'dados.json');
import dados from './dados.json' with { type: 'json' };

O import.meta.dirname é recente; antes dele escrevia-se fileURLToPath(import.meta.url), que é a linha que aparece em todo o código com alguns anos.

E há uma que apanha toda a gente à primeira: em ESM a extensão é obrigatória nos caminhos relativos.

import { correr } from './tarefas';      // não resolve
import { correr } from './tarefas.js';   // assim sim

Em TypeScript isto fica ainda mais estranho, porque se escreve .js num ficheiro que se chama .ts — o caminho é o do ficheiro compilado, não o do que se está a escrever.

5. O que se ganha

Sobretudo uma coisa: await ao nível de topo, sem embrulhar tudo numa função.

const dados = await fetch(endereco).then((r) => r.json());
console.log(dados);

Parece pouco, e muda a forma como se escreve um script. Os scripts de manutenção deste site são todos assim.

6. O que eu faria

  • Projeto novo: ESM, com "type": "module" desde a primeira linha. É para onde o ecossistema foi, e converter depois custa sempre mais.

  • Projeto que já existe e funciona: não converter por desporto. Converte-se quando uma dependência obrigar — e, mesmo aí, ficheiro a ficheiro, com .mjs, em vez de virar o projeto do avesso num dia.

  • Uma biblioteca: publicar os dois formatos, com o campo exports a apontar cada um. É trabalho, e é o que evita ser a dependência que estraga a tarde de alguém.

O resumo

Quando aparecer o ERR_REQUIRE_ESM, a pergunta a fazer não é «como troco tudo para import». É «que ficheiro é que está a pedir isto, e qual é o modo dele». Quase sempre a resposta é uma extensão — .mjs num sítio só — e não uma migração.

Comentários

Ainda ninguém comentou este artigo.

Voltar ao blog

Gostávamos de saber quantas pessoas visitam o site, com o Google Analytics. Sem a sua autorização não corre nada. Política de Privacidade.