npm overrides: como forçar a versão de uma dependência transitiva

  • VersionDude
  • guides
  • 7 min de leitura

Um pacote vulnerável três níveis abaixo e um pai que não publicou a correção. O campo overrides do package.json raiz permite escolher a versão, para toda a árvore ou para um único pai. Tem também três regras que explicam quase todos os casos em que parece não fazer nada.

Um relatório de vulnerabilidade refere um pacote que nunca instalou. Está três níveis abaixo, trazido pela dependência de uma dependência, e o pacote intermédio não publicou nenhuma versão que o deixe para trás. Não é possível editar um `package.json` que não é seu. O que pode fazer é declarar um campo `overrides` no seu próprio `package.json` raiz e dizer ao npm que versão instalar em vez daquela.

O override plano: uma versão, em toda a árvore

Um sinal de trânsito retangular cor de laranja com a palavra DETOUR, um pictograma preto de peão por cima e uma seta preta a apontar para a esquerda, preso a um poste preto em frente a uma rede metálica desfocada. Um override faz o mesmo a uma dependência: mesmo destino, outro caminho.
Um sinal de trânsito retangular cor de laranja com a palavra DETOUR, um pictograma preto de peão por cima e uma seta preta a apontar para a esquerda, preso a um poste preto em frente a uma rede metálica desfocada. Um override faz o mesmo a uma dependência: mesmo destino, outro caminho.

A forma mais curta indica o pacote e a versão. Escrever `"overrides": { "foo": "1.2.4" }` faz o npm instalar `foo` na 1.2.4 onde quer que apareça, seja qual for a versão pedida pelos pacotes que dependem dele. A documentação do npm enumera os três usos previstos: substituir uma versão com um problema de segurança conhecido, substituir uma dependência por um fork e garantir que a mesma versão de um pacote é usada em todo o lado.

O valor não tem de ser uma versão exata. Segundo a documentação, aceita qualquer especificador que o npm admita para uma dependência: uma versão exata, um intervalo semver, uma dist-tag ou uma substituição como `npm:`, `file:` ou um URL Git. Para uma correção de segurança, o intervalo é muitas vezes a melhor escolha. `"foo": "^1.2.4"` impõe a versão corrigida como mínimo e deixa entrar as correções seguintes, ao passo que uma versão fixa congela o pacote até alguém se lembrar de retirar a linha. Se a diferença entre os dois operadores não estiver clara, caret ou til explica o que cada um permite.

Antes de escrever o override, descubra quem pede o pacote. `npm explain foo` mostra a cadeia de dependências que provoca a instalação de `foo`, um bloco por cada cópia presente na árvore, cada um seguido até ao projeto raiz. Essa saída dá as duas informações necessárias: que pai é o responsável e se está em causa uma cópia ou várias. Um override plano altera todas, o que nem sempre é o pretendido.

Limitar o override a um único pai

Aninhe a chave para limitar o override a um ramo da árvore. `"overrides": { "bar": { "foo": "1.2.4" } }` substitui `foo` apenas quando é filho de `bar`, neto ou qualquer nível mais fundo abaixo dele. Todos os outros pacotes que dependem de `foo` mantêm a versão que resolveram por si. É a forma indicada quando um único pai continua preso a uma versão vulnerável e o resto da árvore está bem.

  • "foo": "1.2.4" : foo na 1.2.4 em toda a árvore
  • "bar": { "foo": "1.2.4" } : foo na 1.2.4 apenas abaixo de bar, a qualquer profundidade
  • "bar@2.0.0": { "foo": "1.2.4" } : apenas abaixo dessa versão exata de bar
  • "foo": "$foo" : reutilizar a especificação da sua dependência direta foo
  • "foo": "npm:@scope/fork@1.2.4" : substituir foo por outro pacote

As chaves podem levar uma versão e aninhar-se tanto quanto for preciso. `"bar@2.0.0": { "foo": "1.2.4" }` só se aplica se `foo` estiver abaixo dessa versão exata de `bar`, pelo que o override deixa de se aplicar por si no dia em que `bar` for atualizado. As chaves podem ser aninhadas com qualquer comprimento, para descrever um caminho mais longo. E quando é preciso substituir ao mesmo tempo um pacote e um dos seus filhos, a forma de objeto aceita uma chave `.` para o próprio pacote, ao lado das chaves dos filhos.

A dependência direta é o único caso que o npm recusa. Não é permitido substituir um pacote de que depende diretamente, a não ser que o override e a dependência tenham exatamente a mesma especificação; qualquer outra coisa interrompe a instalação com um erro `EOVERRIDE`. A saída documentada é uma referência: `"foo": "$foo"` diz ao npm para reutilizar a especificação declarada pela sua própria entrada de `dependencies` para `foo`, em todas as profundidades. A versão muda num único sítio e toda a árvore acompanha.

Quando o override parece não fazer nada

Os overrides só são lidos no `package.json` raiz. A documentação do npm é explícita: os das dependências instaladas, workspaces incluídos, não são considerados na resolução da árvore. Daqui resultam duas consequências. Num monorepo, o campo pertence ao manifesto raiz, não ao pacote onde o problema aparece. E um bloco `overrides` numa biblioteca que publica não faz nada por quem a instala; a documentação remete os autores de bibliotecas para a fixação da dependência ou para `bundleDependencies`.

Depois, confirme o resultado em vez de o presumir. Execute `npm install` para que a árvore seja resolvida de novo e volte a correr `npm explain foo`: a versão no topo de cada bloco é a que está agora instalada. Faça commit de `package.json` e `package-lock.json` em conjunto, porque é o lockfile que leva a versão resolvida a todas as outras máquinas. package-lock.json contra package.json explica por que razão os dois ficheiros têm de andar aos pares.

Um override instala uma versão com a qual o pacote pai nunca declarou funcionar. É esse o objetivo quando o intervalo declarado só contém uma versão vulnerável, e é também o risco: o npm não o impedirá de forçar uma versão major para a qual o pai não foi escrito. Trate essa linha como um penso com prazo de validade. Quando o pai publicar uma versão que aceite a correção, apague o override.

Um override instala uma versão com a qual o pacote pai nunca declarou funcionar. É esse o objetivo quando o intervalo declarado só contém uma versão vulnerável, e é também o risco: o npm não o impedirá de forçar uma versão major para a qual o pai não foi escrito. Trate essa linha como um penso com prazo de validade. Quando o pai publicar uma versão que aceite a correção, apague o override.

- VersionDude

O Yarn e o pnpm usam outro campo

A mesma ideia existe nos outros dois gestores de pacotes, com outros nomes. O Yarn lê um campo `resolutions`, descrito na documentação do seu manifesto como uma forma de usar uma resolução específica em vez da que o resolvedor escolheria normalmente; as suas chaves admitem um único nível de precisão, escrito `pai/filho`. O pnpm chama à definição `overrides`, tal como o npm, coloca-a em `pnpm-workspace.yaml` na sua documentação atual e delimita-a com um separador `>`, como em `bar@1>foo`. Ambos reservam o campo à raiz do projeto, tal como o npm. A sintaxe não se transfere de uma ferramenta para outra: um projeto que muda de gestor de pacotes tem de reescrever o bloco.

A ordem de decisão é curta. Se o pai já tiver uma versão que aceite a correção, atualize o pai e não escreva nenhum override. Se não tiver, limite o override a esse pai e não a toda a árvore. Se o mesmo pacote tiver de ser idêntico em todo o lado, use a forma plana, com um intervalo quando um mínimo bastar. E se a instalação parar num conflito de dependência peer e não numa versão vulnerável, o problema é outro: as dependências peer tratam dele.

FAQ

Os overrides do npm funcionam no package.json de um workspace?

Não. O npm só considera os overrides declarados no package.json raiz do projeto. A documentação indica que os das dependências instaladas, workspaces incluídos, não são considerados na resolução da árvore, pelo que o campo pertence ao manifesto raiz.

O que significa o erro EOVERRIDE?

Significa que um override visa um pacote de que depende diretamente, com uma especificação diferente da que consta nas suas dependencies. O npm só permite esse override quando as duas especificações são idênticas. Alinhe-as, ou escreva o override como referência, por exemplo "foo": "$foo", que reutiliza a especificação da sua dependência direta.

Um override pode usar um intervalo em vez de uma versão exata?

Sim. A documentação do npm diz que o valor de um override pode ser qualquer especificador aceite para uma dependência: versão exata, intervalo semver, dist-tag ou uma substituição como npm:, file: ou um URL Git. Um intervalo como ^1.2.4 impõe uma versão corrigida mínima sem congelar o pacote.

Como descobrir que pacote traz uma dependência transitiva?

Execute npm explain seguido do nome do pacote. O comando mostra a cadeia de dependências que provoca a sua instalação, um bloco por cada cópia presente na árvore, cada um seguido até ao projeto raiz.

npm overrides é o mesmo que Yarn resolutions?

O objetivo é o mesmo, a sintaxe não. O npm usa um campo overrides com objetos aninhados, o Yarn um campo resolutions com chaves escritas pai/filho e o pnpm uma definição overrides com um separador >. Os três reservam o campo à raiz do projeto.

Projeto relacionado