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"dopackage.jsonmais 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.mjsNã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 -v4. 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 simEm 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
exportsa 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.