yarn.lock
a mesma pergunta várias vezes desde que anunciamos que o npm 7 suportaria arquivos . Parecia o seguinte: “Por que, então, deixar o suporte package-lock.json
? Por que não apenas usá-lo yarn.lock
? "
A resposta curta para essa pergunta é: “Porque ela não atende totalmente às necessidades da NPM. Se você confiar apenas nisso, isso diminuirá a capacidade da NPM de criar esquemas ideais de instalação de pacotes e a capacidade de adicionar novas funcionalidades ao projeto. ” A resposta é apresentada em mais detalhes neste material.
yarn.lock
A estrutura básica do arquivo yarn.lock
O arquivo
yarn.lock
é uma descrição da correspondência dos especificadores de dependência do pacote e metadados que descrevem a resolução dessas dependências. Por exemplo:
mkdirp@1.x:
version "1.0.2"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.2.tgz#5ccd93437619ca7050b538573fc918327eba98fb"
integrity sha512-N2REVrJ/X/jGPfit2d7zea2J1pf7EAR5chIUcfHffAZ7gmlam5U65sAm76+o4ntQbSRdTjYf7qZz3chuHlwXEA==
O seguinte é relatado nesta passagem: "Qualquer dependência
mkdirp@1.x
deve ser resolvida exatamente com o que é indicado aqui". Se vários pacotes dependerem mkdirp@1.x
, todas essas dependências serão resolvidas da mesma maneira.
No npm 7, se um arquivo existir no projeto
yarn.lock
, o npm usará os metadados contidos nele. Os valores do campo resolved
dirão ao npm de onde baixar os pacotes e os valores do campo integrity
serão usados para verificar o que é recebido e o que ele espera receber. Se pacotes forem adicionados ou removidos do projeto, o conteúdo será atualizado de acordo yarn.lock
.
Npm neste caso, como antes, cria um arquivo
package-lock.json
. Se esse arquivo estiver presente no projeto, ele será usado como fonte autorizada de informações sobre a estrutura (formulário) da árvore de dependência.
A pergunta aqui é: "Se é
yarn.lock
bom o suficiente para o gerenciador de pacotes do Yarn, por que o npm não pode simplesmente usar esse arquivo?"
Resultados determinísticos da instalação de dependências
Os resultados da instalação de pacotes usando o Yarn são garantidos para serem os mesmos ao usar o mesmo arquivo
yarn.lock
e a mesma versão do Yarn. Usar versões diferentes do Yarn pode fazer com que os arquivos do pacote sejam localizados de maneira diferente no disco.
O arquivo
yarn.lock
garante resolução de dependência determinística. Por exemplo, se for foo@1.x
permitido foo@1.2.3
, dado o uso do mesmo arquivo yarn.lock
, isso sempre acontecerá em todas as versões do Yarn. Mas isso (pelo menos em si) não é equivalente a garantir o determinismo da estrutura da árvore de dependência!
Considere o seguinte gráfico de dependência:
root -> (foo@1, bar@1)
foo -> (baz@1)
bar -> (baz@2)
Aqui estão alguns diagramas de árvore de dependência, cada um dos quais pode ser considerado correto.
Árvore número 1:
root
+-- foo
+-- bar
| +-- baz@2
+-- baz@1
Árvore número 2:
+-- foo
| +-- baz@1
+-- bar
+-- baz@2
O arquivo
yarn.lock
não pode nos dizer qual árvore de dependência usar. Se um root
comando for executado no pacote require(«baz»)
(incorreto, pois essa dependência não é refletida na árvore de dependências), o arquivo yarn.lock
não garante a execução correta desta operação. Essa é uma forma de determinismo que um arquivo pode fornecer package-lock.json
, mas não pode yarn.lock
.
Na prática, é claro, desde Yarn, no arquivo
yarn.lock
, há todas as informações necessárias para selecionar a versão apropriada de uma dependência, a escolha é determinística desde que todos usem a mesma versão do Yarn. Isso significa que a escolha da versão é sempre feita da mesma maneira. O código não muda até que alguém o mude. Deve-se observar que o Yarn é inteligente o suficiente para não ser afetado por discrepâncias em relação ao tempo de carregamento do manifesto do pacote ao criar a árvore de dependência. Caso contrário, o determinismo dos resultados não poderia ser garantido.
Como isso é determinado pelas especificidades dos algoritmos Yarn e não pelas estruturas de dados no disco (não identificando o algoritmo a ser usado), essa garantia de determinismo é fundamentalmente mais fraca que a garantia de que
package-lock.json
Uma descrição completa da estrutura da árvore de dependência armazenada no disco.
Em outras palavras, como o Yarn cria a árvore de dependência é influenciado pelo arquivo
yarn.lock
e pela implementação do próprio Yarn. E no npm, apenas o arquivo afeta a árvore de dependências package-lock.json
. Devido a isso, a estrutura do projeto descrita em package-lock.json
, torna-se mais difícil quebrar acidentalmente, usando diferentes versões do npm. E se forem feitas alterações no arquivo (talvez por engano ou intencionalmente), essas alterações serão claramente visíveis no arquivo ao adicionar sua versão alterada ao repositório do projeto, que usa o sistema de controle de versão.
Dependências aninhadas e deduplicação de dependência
Além disso, existe toda uma classe de situações envolvendo trabalho com dependências aninhadas e desduplicação de dependências, quando um arquivo
yarn.lock
não é capaz de refletir com precisão o resultado da resolução de dependências, que, na prática, será usado pelo npm. Além disso, isso é verdade mesmo nos casos em que o npm usa yarn.lock
metadados como fonte. Enquanto o npm o usa yarn.lock
como uma fonte confiável de informações, o npm não considera esse arquivo a fonte autorizada de informações sobre restrições de versão de dependência.
Em alguns casos, o Yarn gera uma árvore de dependência com um nível muito alto de duplicação de pacotes, e não precisamos dela. Como resultado, acontece que seguir exatamente o algoritmo de Yarn nesses casos está longe de ser o ideal.
Considere o seguinte gráfico de dependência:
root -> (x@1.x, y@1.x, z@1.x)
x@1.1.0 -> ()
x@1.2.0 -> ()
y@1.0.0 -> (x@1.1, z@2.x)
z@1.0.0 -> ()
z@2.0.0 -> (x@1.x)
O projeto
root
depende dos 1.x
pacotes de versões x
, y
e z
. O pacote y
depende x@1.1
e sobre z@2.x
. Um pacote da z
versão 1 não possui dependências, mas o mesmo pacote da versão 2 depende x@1.x
.
Com base nessas informações, o npm gera a seguinte árvore de dependência:
root (x@1.x, y@1.x, z@1.x) <-- x@1.x
+-- x 1.2.0 <-- x@1.x 1.2.0
+-- y (x@1.1, z@2.x)
| +-- x 1.1.0 <-- x@1.x 1.1.0
| +-- z 2.0.0 (x@1.x) <-- x@1.x
+-- z 1.0.0
O pacote
z@2.0.0
depende x@1.x
, o mesmo pode ser dito root
. O arquivo é yarn.lock
mapeado para x@1.x
c 1.2.0
. No entanto, uma dependência de pacote z
onde também foi especificada x@1.x
será resolvida x@1.1.0
.
Como resultado, mesmo que a dependência
x@1.x
seja descrita em yarn.lock
onde é declarado que ela deve ser resolvida para a versão do pacote 1.2.0
, existe um segundo resultado de resolução x@1.x
para a versão do pacote 1.1.0
.
Se você executar o npm com o sinalizador
--prefer-dedupe
, o sistema irá um passo adiante e instalará apenas uma instância da dependência x
, o que levará à formação da seguinte árvore de dependência:
root (x@1.x, y@1.x, z@1.x)
+-- x 1.1.0 <-- x@1.x 1.1.0
+-- y (x@1.1, z@2.x)
| +-- z 2.0.0 (x@1.x)
+-- z 1.0.0
Isso minimiza a duplicação de dependências, a árvore de dependências resultante é confirmada no arquivo
package-lock.json
.
Como o arquivo
yarn.lock
captura apenas a ordem na qual as dependências são resolvidas, não a árvore de pacotes resultante, o Yarn gerará uma árvore de dependências como esta:
root (x@1.x, y@1.x, z@1.x) <-- x@1.x
+-- x 1.2.0 <-- x@1.x 1.2.0
+-- y (x@1.1, z@2.x)
| +-- x 1.1.0 <-- x@1.x 1.1.0
| +-- z 2.0.0 (x@1.x) <-- x@1.1.0 , ...
| +-- x 1.2.0 <-- Yarn , yarn.lock
+-- z 1.0.0
O pacote
x
aparece três vezes na árvore de dependência ao usar o Yarn. Ao usar npm sem configurações adicionais - 2 vezes. E ao usar a flag --prefer-dedupe
- apenas uma vez (embora a árvore de dependência não seja a mais nova e a melhor versão do pacote).
Todas as três árvores de dependência resultantes podem ser consideradas corretas no sentido de que cada pacote receberá as versões de dependências que atendem aos requisitos estabelecidos. Mas não queremos criar árvores de pacotes nas quais há muitas duplicatas. Pense no que aconteceria se
x
- é um grande pacote que possui muitas dependências próprias!
Como resultado, existe apenas uma maneira de o npm otimizar a árvore de pacotes, mantendo a criação de árvores de dependência determinísticas e reproduzíveis. Este método consiste em usar um arquivo de bloqueio, cujo princípio de formação e uso difere em um nível fundamental
yarn.lock
.
Registrando os resultados da implementação da intenção do usuário
Como já mencionado, no npm 7, o usuário pode usar o sinalizador
--prefer-dedupe
para aplicar o algoritmo de geração da árvore de dependência, durante o qual a prioridade é dada à deduplicação de dependência, e não o desejo de sempre instalar as versões mais recentes dos pacotes. O uso de um sinalizador --prefer-dedupe
geralmente é ideal em situações em que a duplicação de pacotes precisa ser minimizada.
Se esse sinalizador for usado, a árvore resultante para o exemplo acima ficará assim:
root (x@1.x, y@1.x, z@1.x) <-- x@1.x
+-- x 1.1.0 <-- x@1.x 1.1.0
+-- y (x@1.1, z@2.x)
| +-- z 2.0.0 (x@1.x) <-- x@1.x
+-- z 1.0.0
Nesse caso, o npm considera que, embora
x@1.2.0
seja a versão mais recente do pacote que atenda aos requisitos x@1.x
, você pode escolher x@1.1.0
. A escolha desta versão resultará em menos duplicação de pacotes na árvore de dependências.
Se não consertamos a estrutura da árvore de dependência em um arquivo de bloqueio, cada programador que trabalha em um projeto em uma equipe precisa configurar seu ambiente de trabalho da mesma maneira que outros membros da equipe o configuram. Só isso permitirá que ele obtenha o mesmo resultado que o resto. Se a "implementação" do mecanismo de criação de árvores de dependência puder ser alterada dessa maneira, isso dará aos usuários do npm uma oportunidade séria de otimizar dependências para suas próprias necessidades específicas. Porém, se os resultados da criação da árvore dependem da implementação do sistema, isso torna impossível a criação de árvores de dependência determinísticas. É exatamente a isso que o uso do arquivo leva
yarn.lock
.
Aqui estão mais alguns exemplos de como as configurações avançadas do npm podem levar à criação de diferentes árvores de dependência:
--legacy-peer-deps
, um sinalizador que força o npm a ignorar completamentepeerDependencies
.--legacy-bundling
, uma bandeira informando à NPM que ele nem deveria tentar tornar a árvore de dependência mais "plana".--global-style
, um sinalizador devido ao qual todas as dependências transitivas são definidas como dependências aninhadas nas pastas de dependência de um nível superior.
Capturar e corrigir os resultados da resolução de dependências e calcular que o mesmo algoritmo será usado ao criar a árvore de dependências não funcionam em condições quando damos aos usuários a oportunidade de configurar o mecanismo para construir a árvore de dependências.
A correção da estrutura da árvore de dependência final permite fornecer aos usuários recursos semelhantes e, ao mesmo tempo, não interromper o processo de criação de árvores de dependência determinísticas e reproduzíveis.
Desempenho e integridade dos dados
Um arquivo é
package-lock.json
útil não apenas quando você precisa garantir o determinismo e a reprodutibilidade das árvores de dependência. Além disso, contamos com esse arquivo para rastrear e armazenar os metadados do pacote, economizando significativamente tempo, que, caso contrário, usando apenas package.json
, seria necessário para trabalhar com o registro npm. Como os recursos do arquivo são yarn.lock
muito limitados, não há metadados que precisamos baixar constantemente.
No npm 7, o arquivo
package-lock.json
contém tudo o que o npm precisa para criar completamente a árvore de dependência do projeto. Na npm 6, esses dados não são tão convenientemente armazenados; portanto, quando encontramos um arquivo de bloqueio antigo, precisamos carregar o sistema com trabalho adicional, mas isso é feito, para um projeto, apenas uma vez.
Como resultado, mesmo que em
yarn.lock
e foram escritas informações sobre a estrutura da árvore de dependências, precisamos usar outro arquivo para armazenar metadados adicionais.
Oportunidades futuras
O que estamos falando aqui pode mudar drasticamente se você levar em consideração as várias novas abordagens para colocar dependências em discos. Estes são pnpm, fio 2 / berry e fio PnP.
Enquanto trabalhamos no npm 8, vamos explorar uma abordagem de sistema de arquivos virtual para árvores de dependência. Essa ideia foi modelada no Tink e o conceito foi confirmado para funcionar em 2019. Também estamos discutindo a idéia de mudar para algo como a estrutura usada pelo pnpm, embora essa seja, em certo sentido, uma mudança ainda mais dramática do que usar um sistema de arquivos virtual.
Se todas as dependências estiverem em algum repositório central e as dependências aninhadas forem representadas apenas por links simbólicos ou um sistema de arquivos virtual, a modelagem da estrutura da árvore de dependências não seria uma questão tão importante para nós. Mas ainda precisamos de mais metadados do que o arquivo pode fornecer
yarn.lock
. Como resultado, faz mais sentido atualizar e racionalizar o formato de arquivo existente, em package-lock.json
vez de uma transição completa para yarn.lock
.
Este não é um artigo que poderia ser chamado de "Sobre os perigos do fio" .lock "
Gostaria de enfatizar que, a julgar pelo que sei, o Yarn cria de maneira confiável as árvores de dependência corretas do projeto. E, para uma versão específica do Yarn (no momento da redação deste documento, isso se aplica a todas as versões novas do Yarn), essas árvores são, como no npm, completamente determinísticas.
Um arquivo é
yarn.lock
suficiente para criar árvores de dependência determinísticas usando a mesma versão do Yarn. Mas não podemos confiar em mecanismos que dependem da implementação do gerenciador de pacotes, dado o uso de tais mecanismos em muitas ferramentas. Isso é ainda mais verdadeiro quando você considera que a implementação do formato de arquivoyarn.lock
não está formalmente documentado em nenhum lugar. (Esse não é um problema exclusivo do Yarn; npm é a mesma situação. Documentar formatos de arquivo é um grande negócio.) A
melhor maneira de garantir a confiabilidade da construção de árvores de dependência altamente determinísticas é, a longo prazo, registrar os resultados da resolução de dependências. Ao mesmo tempo, você não deve confiar que as implementações futuras do gerenciador de pacotes, ao resolver dependências, seguirão o mesmo caminho das implementações anteriores. Essa abordagem limita nossa capacidade de construir árvores de dependência otimizadas.
Desvios da estrutura inicialmente fixa da árvore de dependências devem ser o resultado de um desejo claramente expresso do usuário. Esses desvios devem se documentar, fazendo alterações nos dados gravados anteriormente sobre a estrutura da árvore de dependências.
Somente
package-lock.json
um mecanismo como este é capaz de fornecer esses recursos ao npm.
Qual gerenciador de pacotes você usa em seus projetos JavaScript?