12k
All articles

Comandos npm Para Quando as Coisas Dão Errado

Use npm ls, npm explain, overrides e npm ci para rastrear dependências inesperadas, corrigir versões e evitar drift no lockfile.

OpenReplay Team
OpenReplay Team
Comandos npm Para Quando as Coisas Dão Errado

Quando um pacote aparece em node_modules sem que nada no package.json o tenha solicitado, ou em uma versão que você não fixou, execute npm ls <package> para ver onde ele está e npm explain <package> para descobrir qual dependência o trouxe, antes de mexer em qualquer coisa.

Todo desenvolvedor já viveu o momento de encarar um número de versão em node_modules e pensar: de onde você veio? O reflexo é conhecido: algo parece errado na árvore, então você faz rm -rf node_modules, reinstala e cruza os dedos. Às vezes o problema desaparece. Na maioria das vezes ele volta na hora, porque o instalador reconstruiu a mesma árvore a partir das mesmas entradas, e agora você não tem ideia do que mudou.

Este artigo percorre uma única investigação: um pacote ou versão inesperada, rastreado até a dependência que o solicitou e, então, corrigido na camada correta. Falhas em tempo de instalação como ERESOLVE, EACCES e erros de build nativo são abordadas em outros textos deste blog, nos guias sobre como corrigir conflitos ERESOLVE, erros de permissão EACCES e falhas de build do node-gyp. Este texto é para quando nada lançou um erro ainda.

Pontos Principais

  • npm ls <package> mostra todos os lugares em que um pacote aparece na árvore instalada e a versão em cada local; npm explain <package> mostra a cadeia de dependências que o solicitou.
  • Sem --all, npm ls lista apenas suas dependências diretas; com --all ele imprime a árvore completa, e --depth=<n> define um corte explícito entre esses dois extremos.
  • npm why é um alias de npm explain, então a mesma palavra funciona em npm, pnpm e yarn.
  • O campo overrides no package.json força uma versão específica de uma dependência aninhada independentemente do range que seu pai solicitou, e é por isso que atualizar o pai deve ser tentado primeiro.
  • npm ci exige um package-lock.json existente, apaga node_modules, instala exatamente o que o lockfile especifica e sai com erro se o lockfile e o package.json divergirem.

Por Que Apagar node_modules Destrói as Evidências?

Apagar node_modules e reinstalar remove o único registro de como um pacote inesperado entrou no seu projeto. A árvore instalada e o package-lock.json juntos codificam cada decisão de resolução que o npm tomou: qual pai solicitou qual range, qual versão o satisfez e onde o resultado foi colocado em disco.

Uma reinstalação reexecuta essas decisões a partir do package.json e do lockfile. Se as entradas não mudaram, você obtém a mesma árvore e a mesma surpresa. Se elas mudaram (uma flag de configuração, um registry, uma edição de range), a reinstalação sobrescreve o estado com o qual você precisava comparar. De qualquer forma, leia a árvore antes de reconstruí-la. Os dois comandos que a leem são npm ls e npm explain.

npm ls: Onde Está o Pacote e em Qual Versão?

npm ls <package> filtra a árvore instalada para os caminhos que terminam no pacote indicado, imprimindo cada local como name@version com seus pais indentados acima. Filtre também por um range de versão, como em npm ls semver@^6, quando você só se importa com as cópias de um major específico.

# Every copy of semver, with the path down to each
npm ls semver

# The complete tree, not just direct dependencies
npm ls --all

# Cap the walk at two levels
npm ls --all --depth=2

# Only what ships to production
npm ls --all --omit=dev

A configuração depth tem padrão 0 a menos que --all seja passado, caso em que se torna Infinity. Esse padrão governa um npm ls sem argumento de pacote. Uma vez que você nomeia um pacote, o npm segue o caminho até cada cópia independentemente da profundidade, e é por isso que o próprio exemplo npm ls promzard da documentação mostra uma ocorrência aninhada sem --all; passe --depth=<n> explicitamente se quiser limitar esse percurso.

O que o npm imprime é um mapa de qual pacote depende de qual, então isso não corresponderá a como as pastas realmente estão em disco: um pacote deduplicado aparece sob cada pai que precisa dele, não apenas no único lugar onde seus arquivos vivem. A saída também sinaliza pacotes que são extraneous (instalados mas não declarados), missing, ou em uma versão que não satisfaz o range declarado; pacotes ausentes aparecem com o rótulo UNMET DEPENDENCY. Adicione --package-lock-only e o npm reporta a árvore que o lockfile produziria, ignorando o que node_modules contém atualmente.

Duas observações de nomenclatura. Os filtros atuais são --omit=dev e --include=dev; --production é um alias obsoleto de --omit=dev e --dev um alias obsoleto de --include=dev, enquanto --development não é uma opção documentada de forma alguma. Além disso, npm ls sai com código diferente de zero quando um pacote está ausente ou em uma versão inválida, ou quando um pacote nomeado não corresponde a nada, o que o torna utilizável como verificação de CI; pacotes extraneous por si só não causam falha.

npm explain: Quem Solicitou o Pacote?

npm explain <package> imprime, para cada cópia instalada, a cadeia de declarações de dependência que fez com que ela estivesse ali, subindo até chegar ao projeto raiz. Onde npm ls responde “onde”, npm explain responde “quem”.

npm explain semver
npm why semver              # identical
npm explain semver --json   # for jq

Cada bloco na saída começa com o name@version resolvido e seu caminho em node_modules, e então indenta uma linha por salto: o range que um pai declarou, a versão do próprio pai e o caminho do pai, terminando com uma linha que nomeia o projeto raiz. Leia de baixo para cima para acompanhar seu package.json até a cópia que você não esperava. Pacotes duplicados recebem um bloco por cópia, então ranges conflitantes ficam visíveis lado a lado. Você também pode passar uma pasta, como npm explain node_modules/foo/node_modules/semver, para explicar exatamente uma cópia aninhada.

A sinopse do npm explain lista why como seu alias, e os outros principais gerenciadores usam o mesmo verbo.

Gerenciador de pacotesComandoFormato da saída
npmnpm explain <pkg> ou npm why <pkg>Um bloco por cópia instalada, cadeia até a raiz
pnpmpnpm why <pkg>Uma árvore invertida, com o pacote consultado no topo
Yarnyarn why <pkg>Motivos por workspace, aceita pkg@range

Você Deve Atualizar o Pai ou Adicionar um Override?

Assim que npm explain nomeia o pai que solicitou o range ruim, a primeira correção é mover esse pai para um release que solicite um range melhor. Execute npm outdated <parent> para ver se existe uma versão mais nova, ou leia o package.json do pai no registry com npm view <parent>@latest dependencies. Se um pai mais novo declara um range aceitável, atualize-o e deixe o npm resolver o filho novamente.

Somente quando nenhum release do pai corrige o range é que você deve recorrer a overrides:

{
  "overrides": {
    "semver": "^7.5.4"
  }
}

Um override substitui a versão da dependência aninhada independentemente do range que o pai declarou, então o pai pode agora rodar contra uma versão com a qual nunca foi testado. Esse é o trade-off, e é por isso que overrides é a segunda jogada, e não a primeira. Algumas regras da documentação: overrides são honrados apenas no package.json raiz; um pacote do qual você depende diretamente só pode ser sobrescrito com um spec idêntico ao seu próprio, caso contrário o npm lança EOVERRIDE, e a forma de referência $name existe para esse caso; e os valores podem ser uma versão exata, um range, uma dist-tag ou um especificador npm:, file: ou Git. Coloque o override sob o nome do pai quando quiser que ele se aplique a um ramo da árvore em vez de a todos.

npm config list: Configurações Que Você Esqueceu Que Definiu

npm config list imprime as configurações que você, seu ambiente ou um arquivo .npmrc definiram; npm config list -l também imprime os padrões do npm, e --json retorna os mesmos dados em JSON. Quando uma árvore é resolvida de um jeito que o package.json sozinho não explica, a causa muitas vezes é um valor de configuração que ninguém lembra de ter escrito.

npm config list
npm config list -l

A saída é agrupada por origem (linha de comando, ambiente, .npmrc do projeto, .npmrc do usuário, global), o que indica qual arquivo editar. Duas chaves merecem atenção primeiro. Um registry fora do padrão significa que as versões foram resolvidas contra um mirror ou registry privado cujo conteúdo pode estar atrasado em relação ao público. Uma configuração legacy-peer-deps salva diz ao npm para construir a árvore sem consultar peerDependencies de forma alguma, como ele se comportava até a versão 6, então você pode acabar com combinações que o resolvedor atual teria recusado. Há um efeito colateral: uma vez que um lockfile foi construído com essa flag, todo npm ci posterior também precisa dela, ou a instalação quebra. Uma linha esquecida em um .npmrc de projeto pode explicar tanto uma árvore local estranha quanto uma execução de CI vermelha.

npm ci vs npm install: O Que Acontece Quando o Lockfile Divergе?

Quando o lockfile satisfaz o package.json, o npm install usa as versões exatas do lockfile; quando não satisfaz, o npm install resolve novamente e atualiza o package-lock.json. O npm ci, em vez disso, dá erro.

Comportamentonpm installnpm ci
Exige package-lock.jsonNãoSim
Lockfile e package.json divergemResolve novamente, reescreve o lockfileSai com erro
node_modules existenteReutilizadoRemovido primeiro
Escreve package.json ou lockfileSimNunca
Adiciona um único pacoteSimNão

A documentação do npm install é explícita sobre a hierarquia: os ranges no package.json são a fonte da verdade, e o lockfile mantém suas versões fixadas apenas enquanto elas ainda couberem dentro desses ranges. Esse é exatamente o comportamento que você não quer em CI, onde um lockfile silenciosamente reescrito esconde o desvio que você está tentando detectar. O npm ci se recusa a reconciliar os dois arquivos e falha de forma ruidosa, então use-o em pipelines e reserve o npm install para a máquina onde você pretende alterar dependências.

Conclusão

Um pacote inesperado na árvore é uma decisão de resolução com rastro documental, e npm ls mais npm explain leem esse rastro sem perturbá-lo. Rastreie a cadeia até o pai que declarou o range, corrija o pai se existir um release melhor, faça override apenas quando não existir, e então verifique npm config list em busca de configurações que distorceram a resolução em primeiro lugar. Execute npm ci no CI para que a próxima divergência faça o build falhar em vez de reescrever silenciosamente o lockfile.

Perguntas Frequentes

O que significa 'deduped' ao lado de um pacote na saída do npm ls?

Um rótulo 'deduped' significa que o npm ls está mostrando o pacote naquele ponto do grafo lógico de dependências, mas não existe uma cópia separada ali: uma única cópia instalada mais acima em node_modules satisfaz o range daquele pai. Não é um erro. Como o npm ls imprime a árvore lógica, o mesmo pacote aparece sob cada pai que o exige, e apenas a linha sem rótulo corresponde a uma pasta física.

Como remover pacotes que o npm ls reporta como extraneous?

Execute npm prune. Ele apaga qualquer coisa em node_modules da qual nada mais dependa; nomeie um ou mais pacotes para limitar a ação a eles. Adicione --omit=dev, ou defina NODE_ENV como production, e suas devDependencies vão embora também. Use --dry-run para ver o plano primeiro, e --json para obter as mudanças em JSON. As instalações já removem pacotes extraneous por conta própria, então você geralmente só precisa disso depois de uma queda ou de uma instalação incompleta.

O npm dedupe corrige versões duplicadas que o npm ls mostra, ou preciso de overrides?

O npm dedupe apenas consolida cópias que os ranges declarados já permitem. Ele percorre a árvore e eleva cada dependência o mais alto que consegue, de modo que pais com ranges sobrepostos acabam compartilhando uma única cópia, e ele nunca busca nada novo no registry. Se dois pais pedem ranges sem nenhuma versão em comum, ambas as cópias permanecem, e a correção é atualizar um pai ou adicionar uma entrada em overrides. O npm find-dupes executa a mesma passagem em modo dry run, então você pode ver o resultado antes.

Como listar pacotes npm instalados globalmente?

Execute npm ls -g. A flag --global aponta o npm ls para o prefixo global, listando os pacotes instalados lá em vez dos do projeto atual. As mesmas regras de profundidade se aplicam: sem --all ele imprime apenas os pacotes globais de primeiro nível, e npm ls -g --all expande cada um em sua árvore completa de dependências. Adicione um valor explícito de --depth para limitar o percurso, ou --json para saída legível por máquina.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — self-hosted, with full data ownership.

Star on GitHub

We use cookies to improve your experience. By using our site, you accept cookies.