Skip to main content

Criando um pacote npm compatível com ESM e CJS em 2024

Escrito por
blog feature multithreading

18 de abril de 2024

0 minutos de leitura
How to Build a Secure NPM Package for ESM and CJS

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”:

{
  "name": "math-add",
  "version": "1.2.0",
  "description": "",
  "module": "src/index.mjs",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0"
}

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:

node:internal/modules/esm/resolve:205
  const resolvedOption = FSLegacyMainResolve(packageJsonUrlString, packageConfig.main, baseStringified);
                         ^

Error: Cannot find package '/~/package-json-exports/consumer-esm/node_modules/math-add/package.json' imported from /~/package-json-exports/consumer-esm/server.js

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:

{
  "name": "math-add",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.cjs",
  "module": "src/index.mjs",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0"
}

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:

{
  "name": "math-add",
  "version": "1.5.0",
  "description": "",
  "exports": {
    ".": {
      "require": "./src/index.cjs",
      "import": "./src/index.mjs"
    }
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0"
}

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:

{
  "name": "math-add",
  "version": "1.5.0",
  "description": "",
 "main": "./dist/index.js",
 "module": "./dist/index.mjs",
 "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    }
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsup src/index.ts --format cjs,esm --dts --clean",
    "watch": "npm run build -- --watch src",
    "prepublishOnly": "npm run build"
  },
  "keywords": [],
  "author": "",
  "license": "Apache-2.0"
}

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.

Leia mais

feature insights context
Blog

Os ataques autônomos já chegaram. A defesa precisa acompanhar o ritmo.

Os atacantes autônomos estão reduzindo o tempo disponível para a defesa. Saiba como a descoberta, a correção, a validação e a prevenção contínuas ajudam as equipes de segurança a acompanhar esse ritmo.

feature insights context
Blog

A prevenção é essencialmente um problema resolvido?

A prevenção em código gerado por agentes está resolvida do ponto de vista arquitetural — mas escolher controles que protejam a segurança sem desacelerar o desenvolvimento continua sendo um desafio.

Live Stream

Agentes de Remediação, Desmistificados: Por Que Corrigir é Melhor do que Encontrar

Veja como o Remediation Agent da Snyk usa inteligência de segurança, análise de explorabilidade e validação para transformar vulnerabilidades em pull requests prontos para merge.