Conditional Exports no Node.js permite que um pacote entregue arquivos diferentes conforme o ambiente ou a forma de carregamento. A configuração fica no campo exports do package.json e pode distinguir ESM, CommonJS, Node.js, desenvolvimento e outras condições reconhecidas.
Esse recurso é útil para bibliotecas que precisam oferecer compatibilidade sem expor toda a estrutura interna. Ao mesmo tempo, configurações incorretas podem criar duas instâncias do pacote, tipos divergentes ou comportamento diferente entre testes e produção.
Neste guia, você aprenderá a montar condições, ordenar chaves, publicar ESM e CommonJS, evitar o dual package hazard, testar consumidores e versionar mudanças com segurança.
Estrutura básica
{
"name": "minha-lib",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}Um consumidor ESM recebe index.js, enquanto um consumidor CommonJS recebe index.cjs.
A ordem das condições importa
As condições são avaliadas na ordem em que aparecem. Coloque casos mais específicos antes de fallbacks amplos:
{
"exports": {
".": {
"node": {
"import": "./dist/node.js",
"require": "./dist/node.cjs"
},
"default": "./dist/browser.js"
}
}
}Evite colocar default antes das condições específicas, pois ele pode capturar a resolução cedo demais.
Condição import
A condição import é usada quando a entrada é carregada por ESM ou import dinâmico:
import { criarCliente } from 'minha-lib';O arquivo alvo deve ser compatível com o formato do pacote e com a extensão utilizada.
Condição require
A condição require atende consumidores CommonJS:
const { criarCliente } = require('minha-lib');Se o pacote usa type: module, a entrada CommonJS normalmente recebe extensão .cjs.
Condição node
A condição node permite fornecer implementação específica do runtime:
{
"exports": {
".": {
"node": "./dist/node.js",
"default": "./dist/universal.js"
}
}
}Use quando a versão Node depende de módulos internos, streams ou sistema de arquivos.
Condição default
default funciona como fallback para ambientes que não correspondem às condições anteriores. Ela melhora compatibilidade com ferramentas que não anunciam uma condição específica.
Development e production
{
"exports": {
".": {
"development": "./dist/index.dev.js",
"production": "./dist/index.prod.js",
"default": "./dist/index.js"
}
}
}Essas condições dependem de como o runtime ou ferramenta é iniciado. Não presuma que serão selecionadas automaticamente apenas pelo valor de NODE_ENV.
Condições personalizadas
Aplicações podem ativar condições por opção de execução. Isso é útil em ambientes controlados, mas bibliotecas públicas devem priorizar condições amplamente compreendidas.
Condições customizadas sem documentação tornam o pacote difícil de consumir e testar.
Subpaths condicionais
{
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./errors": {
"import": "./dist/errors.js",
"require": "./dist/errors.cjs"
}
}
}Cada subpath precisa manter paridade de API entre os formatos.
Tipos TypeScript
{
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}Os tipos devem descrever as duas entradas. Se ESM e CommonJS expõem estruturas diferentes, o problema está no design do pacote, não apenas nos arquivos de declaração.
Dual package hazard
Uma aplicação pode carregar a entrada ESM e a CommonJS ao mesmo tempo. Isso cria duas instâncias independentes:
// módulo A
import { cache } from 'minha-lib';
// módulo B
const { cache } = require('minha-lib');Se a biblioteca mantém estado global, classes, símbolos ou registries, os consumidores podem observar inconsistências.
Como reduzir o risco
- evite estado singleton no módulo;
- mantenha uma implementação central compartilhada quando possível;
- exporte fábricas em vez de instâncias globais;
- teste carregamento misto;
- documente o formato recomendado.
Wrapper CommonJS sobre implementação ESM
Dependendo do design, uma entrada pode adaptar a outra. Porém, como require() é síncrono e import ESM é assíncrono, nem toda API pode ser embrulhada sem alterar comportamento.
Evite soluções que bloqueiem ou retornem Promises inesperadas.
Implementações separadas
Gerar dois builds é comum, mas exige testes de paridade:
dist/
index.js
index.cjs
index.d.tsExecute a mesma suíte contra os dois arquivos para impedir divergência.
Encapsulamento com exports
Conditional exports também bloqueia caminhos não declarados. Veja Package Exports no Node.js para estruturar uma API pública pequena.
ESM como entrada principal
Projetos modernos podem oferecer ESM em import e manter CommonJS apenas como compatibilidade temporária. O guia ESM no Node.js explica o formato moderno.
Import dinâmico
A condição import também pode ser usada quando CommonJS chama import():
const modulo = await import('minha-lib');Veja Dynamic Import no Node.js.
Testando com projeto consumidor
Não teste apenas arquivos dentro do repositório. Gere um tarball:
npm pack --dry-run
npm packInstale em projetos temporários ESM e CommonJS e execute imports reais.
Teste ESM
import test from 'node:test';
import assert from 'node:assert/strict';
import { somar } from 'minha-lib';
test('entrada ESM', () => {
assert.equal(somar(2, 3), 5);
});Teste CommonJS
const test = require('node:test');
const assert = require('node:assert/strict');
const { somar } = require('minha-lib');
test('entrada CommonJS', () => {
assert.equal(somar(2, 3), 5);
});Teste de identidade
Para bibliotecas com classes ou símbolos, valide comportamento quando ambos os formatos aparecem na mesma aplicação. O ideal é não depender de identidade global entre entradas.
Compatibilidade com ferramentas
Bundlers, test runners, TypeScript e gerenciadores de pacotes podem resolver condições de maneiras diferentes. Mantenha a configuração simples e teste a matriz realmente suportada.
Versionamento
Alterar a ordem de condições ou trocar o arquivo selecionado pode ser uma mudança incompatível, mesmo sem mudar a assinatura aparente. Avalie impacto em efeitos colaterais, formato, tipos e estado compartilhado.
Erros comuns
- colocar default antes de condições específicas;
- publicar arquivo que não está no tarball;
- expor APIs diferentes em import e require;
- manter singletons duplicados;
- esquecer tipos de subpaths;
- presumir que NODE_ENV seleciona condição;
- testar apenas dentro do monorepo;
- usar condições customizadas sem fallback.
Checklist para produção
- mantenha poucas condições;
- ordene do específico ao geral;
- inclua default quando necessário;
- teste ESM e CommonJS;
- teste pacote empacotado;
- garanta paridade de API;
- evite estado global;
- publique tipos corretos;
- documente versões suportadas.
Conclusão
Conditional Exports no Node.js oferece compatibilidade controlada entre formatos e ambientes. Ele é poderoso quando a biblioteca possui uma API pública estável e builds equivalentes.
Use condições com parcimônia, teste consumidores reais e evite estado compartilhado implícito. A configuração deve facilitar o uso do pacote, não transformar a resolução em um quebra-cabeça.
Consulte a documentação oficial de conditional exports do Node.js e a referência de condições aninhadas.


