Por que o npm 7 deixou o suporte ao package-lock.json?

Fiz yarn.locka 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.xdeve 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 resolveddirão ao npm de onde baixar os pacotes e os valores do campo integrityserã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 arquivopackage-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.lockbom 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.locke 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.lockgarante resolução de dependência determinística. Por exemplo, se for foo@1.xpermitido 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.locknão pode nos dizer qual árvore de dependência usar. Se um rootcomando for executado no pacote require(«baz»)(incorreto, pois essa dependência não é refletida na árvore de dependências), o arquivo yarn.locknã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 arquivoyarn.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 quepackage-lock.jsonUma 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.locke 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.locknã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.lockmetadados como fonte. Enquanto o npm o usa yarn.lockcomo 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 rootdepende dos 1.xpacotes de versões x, ye z. O pacote ydepende x@1.1e sobre z@2.x. Um pacote da zversã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.0depende x@1.x, o mesmo pode ser dito root. O arquivo é yarn.lockmapeado para x@1.xc 1.2.0. No entanto, uma dependência de pacote zonde também foi especificada x@1.xserá resolvida x@1.1.0.



Como resultado, mesmo que a dependência x@1.xseja descrita em yarn.lockonde é declarado que ela deve ser resolvida para a versão do pacote 1.2.0, existe um segundo resultado de resolução x@1.xpara 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.lockcaptura 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 xaparece 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-dedupepara 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-dedupegeralmente é 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.0seja 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 completamente peerDependencies.
  • --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.lockmuito limitados, não há metadados que precisamos baixar constantemente.



No npm 7, o arquivo package-lock.jsonconté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 emyarn.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.jsonvez 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.locksuficiente 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.locknã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.jsonum mecanismo como este é capaz de fornecer esses recursos ao npm.



Qual gerenciador de pacotes você usa em seus projetos JavaScript?






All Articles