Criando um pacote npm compatível com ESM e CJS em 2024
18 de abril de 2024
0 minutos de leitura
Publicar pacotes JavaScript compatíveis com ECMAScript Modules (ESM) e CommonJS (CJS) é uma habilidade essencial para desenvolvedores que precisam integrar bibliotecas variadas.
Este artigo aborda métodos práticos e boas práticas para manter o suporte a ESM e CJS. Vamos analisar as implicações de não usar a declaração ”type: module” em bibliotecas compatíveis com os dois formatos e investigar o uso dos campos main e module em package.json para diferenciar os pontos de entrada.
O artigo também esclarece a finalidade e o uso do campo exports no arquivo package.json, essencial para controlar a resolução de módulos. Além disso, em projetos TypeScript, vamos explorar a integração das exports do manifesto do pacote com um tipo de módulo, garantindo compatibilidade e segurança de tipos.
Você pode acompanhar as etapas de código abaixo, mas também há um repositório de código no GitHub chamado package-json-exports, com exemplos completos para reprodução que usam o projeto proxy npm de código aberto Verdaccio.
Evite definir “Type: Module” em bibliotecas compatíveis com ESM e CJS
Você sabia que, quando o campo ”type” é omitido do arquivo package.json, ele é definido implicitamente como commonjs por padrão? Por exemplo: ”type”: “commonjs”.
Quando você adiciona ”type”: “module” ao arquivo package.json, define explicitamente que a biblioteca se destina apenas a projetos ESM. Ainda é necessário definir um campo main no manifesto do pacote, que então deve ser atualizado para apontar para o módulo exportado compatível com ESM.
Para dar um exemplo prático, a definição a seguir de um arquivo package.json não funciona para consumidores ESM upstream, embora seja ESM “puro”:
A definição module é um módulo ESM e o campo type deixa claro que a biblioteca se destina a consumidores ESM, mas falta o campo main no arquivo package.json. Você verá o Node.js lançar uma exceção informando que não foi possível localizar o pacote, por exemplo:
Em resumo, evite usar a declaração ”type”: “module” no manifesto de um pacote npm.
Vamos analisar o uso dos campos main e module no arquivo package.json e por que eles são melhores indicações.
Compatibilidade com ESM e CJS usando os campos main e module
Antes do surgimento do ESM, o campo main no arquivo package.json servia para indicar ao ambiente de execução Node.js qual era o ponto de entrada do pacote. Em geral, os desenvolvedores tinham um arquivo index.js ou app.js no diretório raiz e configuravam o campo main para apontar para ele, como em ”main”: “index.js”.
Se você omitir o campo main do arquivo package.json, o ambiente de execução Node.js tentará localizar o ponto de entrada do pacote usando a convenção de arquivo server.js no diretório raiz do pacote.
Para tornar um pacote npm compatível com ESM e CJS, podemos usar uma convenção em que main aponta para uma exportação CJS e module aponta para uma exportação ESM.
Biblioteca:
Os projetos consumidores podem usar CJS ou ESM. Projetos CJS vão consumir o arquivo src/index.cjs, enquanto projetos ESM vão consumir o arquivo src/index.mjs. Nenhum dos dois tipos de projeto precisa especificar nada de especial sobre a dependência math-add: ela simplesmente funciona.
Entenda o campo exports do package.json
O uso do campo exports no arquivo package.json oferece um controle ainda mais detalhado sobre quais elementos são exportados pelo seu pacote npm e como eles são consumidos.
Por exemplo, você pode fornecer o caminho completo para o arquivo de entrada quando o ambiente de execução Node.js tentar carregar seu pacote npm com require(‘math-add’) e outro arquivo como ponto de entrada quando o Node.js tentar carregar o pacote com import .. from ‘math-add’).
Veja um exemplo de código para um pacote de modo duplo, CJS e ESM, conforme descrito:
Exports do package.json e um tipo de módulo em um projeto TypeScript
Se você estiver escrevendo o código ESM do seu pacote em TypeScript e quiser manter a compatibilidade com CJS, também precisará declarar types e configurar a compilação e a transpilação do TypeScript para a parte CJS.
Para compilar e empacotar o TypeScript, recomendo tsup. Em seguida, você precisará de uma etapa de script build — e não se esqueça de executar essa compilação antes de um job de CI ou de publicar o pacote npm manualmente.
Veja um exemplo completo:
Você vai notar que também usamos o suporte recente do ambiente de execução Node.js para monitorar alterações, com a opção de linha de comando --watch src. Antes, isso era feito com o nodemon, que é um ótimo pacote, mas quanto menos dependências, melhor.
Próximos passos: publicação e estrutura modernas de pacotes npm em 2024
Este foi um artigo curto e objetivo para desenvolvedores JavaScript, com ideias práticas e acionáveis para lidar com formatos de módulos nos seus projetos.
Também é importante seguir as boas práticas para publicar pacotes npm e criar pacotes modernos, que abordam com mais profundidade a configuração do TypeScript, testes, CI, segurança e outros aspectos.
Mantenha suas dependências de código aberto seguras
A Snyk cria PRs de correção com um clique para dependências vulneráveis de código aberto e suas dependências transitivas.


