Pourquoi npm 7 a-t-il quitté la prise en charge de package-lock.json?

On m'a yarn.lockposé la même question plusieurs fois depuis que nous avons annoncé que npm 7 prendrait en charge les fichiers . Cela ressemblait à ceci: «Pourquoi alors quitter le soutien package-lock.json? Pourquoi ne pas simplement l'utiliser yarn.lock? " La réponse courte à cette question est: «Parce qu'elle ne répond pas pleinement aux besoins de npm. Si vous vous y fiez uniquement, cela nuira à la capacité de npm à former des schémas d'installation de packages optimaux et à ajouter de nouvelles fonctionnalités au projet. " Une réponse plus détaillée est présentée dans ce document.







yarn.lock



Structure de base du fichier yarn.lock



Le fichier yarn.lockest une description de la correspondance des spécificateurs de dépendance de package et des métadonnées décrivant la résolution de ces dépendances. Par exemple:



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==


Cet extrait de code indique ce qui suit: "Toute dépendance sur mkdirp@1.xdoit être résolue exactement comme spécifié ici." Si plusieurs packages dépendent de mkdirp@1.x, toutes ces dépendances seront résolues de la même manière.



Dans npm 7, si un fichier existe dans le projet yarn.lock, npm utilisera les métadonnées qu'il contient. Les valeurs de champ resolvedindiqueront à npm d'où télécharger les packages et les valeurs de champ integrityseront utilisées pour vérifier ce qui est reçu par rapport à ce qui est attendu. Si des packages sont ajoutés ou supprimés du projet, le contenu est mis à jour en conséquence yarn.lock.



En même temps, npm, comme précédemment, crée un fichierpackage-lock.json. Si ce fichier est présent dans le projet, il sera utilisé comme source d'informations faisant autorité sur la structure (forme) de l'arborescence de dépendances.



La question ici est: "Si c'est yarn.lockassez bon pour le gestionnaire de packages Yarn - pourquoi npm ne peut-il pas simplement utiliser ce fichier?"



Résultats déterministes de l'installation des dépendances



Les résultats de l'installation de packages avec Yarn sont garantis identiques lorsque vous utilisez le même fichier yarn.locket la même version de Yarn. L'utilisation de différentes versions de Yarn peut entraîner une localisation différente des fichiers du package sur le disque.



Le fichier yarn.lockgarantit une résolution déterministe des dépendances. Par exemple, si foo@1.xautorisé dans foo@1.2.3, alors, étant donné l'utilisation du même fichier yarn.lock, cela se produira toujours, dans toutes les versions de Yarn. Mais cela (du moins en soi) n'équivaut pas à garantir le déterminisme de la structure de l'arbre de dépendances!



Considérez le graphique de dépendances suivant:



root -> (foo@1, bar@1)
foo -> (baz@1)
bar -> (baz@2)


Voici quelques schémas d'arbre de dépendances, chacun pouvant être considéré comme correct.



Arbre numéro 1:



root
+-- foo
+-- bar
|   +-- baz@2
+-- baz@1


Arbre numéro 2:



+-- foo
|   +-- baz@1
+-- bar
+-- baz@2


Le fichier yarn.lockne peut pas nous dire quel arbre de dépendance utiliser. Si une rootcommande est exécutée dans un package require(«baz»)(ce qui est incorrect, puisque cette dépendance n'est pas reflétée dans l'arborescence de dépendances), le fichier yarn.lockne garantit pas la bonne exécution de cette opération. C'est une forme de déterminisme qu'un fichier peut donner package-lock.json, mais pas yarn.lock.



En pratique, bien sûr, depuis Yarn, dans le fichieryarn.lock, il y a toutes les informations nécessaires pour sélectionner la version appropriée d'une dépendance, le choix est déterministe tant que tout le monde utilise la même version de Yarn. Cela signifie que le choix de la version se fait toujours de la même manière. Le code ne change pas tant que quelqu'un ne le modifie pas. Il convient de noter que Yarn est suffisamment intelligent pour que, lors de la création de l'arborescence de dépendances, il ne dépend pas des écarts concernant le temps de chargement du manifeste du package. Sinon, le déterminisme des résultats ne pourrait être garanti.



Puisque cela est déterminé par les spécificités des algorithmes de Yarn, et non par les structures de données sur le disque (n'identifiant pas l'algorithme à utiliser), cette garantie de déterminisme est intrinsèquement plus faible que la garantie quepackage-lock.jsoncontenant une description complète de la structure de l'arborescence de dépendances stockée sur le disque.



En d'autres termes, la façon dont Yarn construit l'arbre de dépendance est influencée par le fichier yarn.locket l'implémentation de Yarn lui-même. Et dans npm, seul le fichier affecte l'arborescence des dépendances package-lock.json. Cela package-lock.jsonrend plus difficile la rupture accidentelle de la structure du projet comme décrit dans différentes versions de npm. Et si des modifications sont apportées au fichier (peut-être par erreur ou intentionnellement), ces modifications seront clairement visibles dans le fichier lors de l'ajout de sa version modifiée au référentiel du projet, qui utilise le système de contrôle de version.



Dépendances imbriquées et déduplication des dépendances



De plus, il existe toute une classe de situations impliquant l'utilisation de dépendances imbriquées et la déduplication des dépendances, lorsque le fichier n'est yarn.lockpas en mesure de refléter avec précision le résultat de la résolution des dépendances, qui, en pratique, sera utilisé par npm. De plus, cela est vrai même pour les cas où npm utilise des yarn.lockmétadonnées comme source. Bien que npm l'utilise yarn.lockcomme source fiable d'informations, npm ne considère pas ce fichier comme la source faisant autorité d'informations sur les restrictions de version des dépendances.



Dans certains cas, Yarn génère un arbre de dépendance avec un niveau très élevé de duplication de packages, et nous n'en avons pas besoin. En conséquence, il s'avère que suivre exactement l'algorithme de Yarn dans de tels cas est loin d'être idéal.



Considérez le graphique de dépendance suivant:



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)


Le projet rootdépend du 1.xpaquet versions x, yet z. Le package ydépend de x@1.1et de z@2.x. Le package de la zversion 1 n'a pas de dépendances, contrairement au package de la version 2 x@1.x.



Sur la base de ces informations, npm génère l'arborescence de dépendances suivante:



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


Le paquet z@2.0.0dépend de x@1.x, on peut en dire autant root. Le fichier yarn.lockcorrespond à x@1.xc 1.2.0. Cependant, la dépendance du package z, où elle est également indiquée x@1.x, sera résolue à la place x@1.1.0.



Par conséquent, même si la dépendance x@1.xest décrite yarn.locklà où il est indiqué qu'elle doit être résolue vers la version du package 1.2.0, il existe un deuxième résultat de résolution x@1.xvers la version du package 1.1.0.



Si vous exécutez npm avec l'indicateur --prefer-dedupe, le système ira encore plus loin et n'installera qu'une seule instance de la dépendance x, ce qui conduira à la formation de l'arborescence de dépendances suivante:



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


Cela minimise la duplication des dépendances, l'arborescence de dépendances résultante est validée dans le fichier package-lock.json.



Étant donné que le fichier yarn.lockcapture uniquement l'ordre dans lequel les dépendances sont résolues, et non l'arborescence de packages résultante, Yarn générera l'arborescence de dépendances suivante:



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


Le package xapparaît trois fois dans l'arborescence des dépendances lors de l'utilisation de Yarn. Lors de l'utilisation de npm sans paramètres supplémentaires - 2 fois. Et lorsque vous utilisez l'indicateur --prefer-dedupe- une seule fois (bien que l'arborescence des dépendances ne soit ni la plus récente ni la meilleure version du package).



Les trois arborescences de dépendances résultantes peuvent être considérées comme correctes dans le sens où chaque package recevra les versions des dépendances qui répondent aux exigences énoncées. Mais nous ne voudrions pas créer des arborescences de packages dans lesquelles il y a trop de doublons. Pensez à ce qui se passera si x- c'est un gros paquet qui a de nombreuses dépendances!



En conséquence, il n'y a qu'une seule façon pour npm d'optimiser l'arborescence des packages tout en prenant en charge la création d'arbres de dépendances déterministes et reproductibles. Cette méthode consiste à utiliser un fichier de verrouillage dont le principe de formation et d'utilisation diffère à un niveau fondamental yarn.lock.



Fixer les résultats de la mise en œuvre des intentions des utilisateurs



Comme déjà mentionné, dans npm 7, l'utilisateur peut utiliser l'indicateur --prefer-dedupepour appliquer l' algorithme de génération d'arbre de dépendance, au cours duquel la priorité est donnée à la déduplication des dépendances, et non le désir de toujours installer les dernières versions des packages. L'indicateur est --prefer-dedupegénéralement idéal dans les situations où les paquets en double doivent être minimisés.



Si cet indicateur est utilisé, l'arborescence résultante pour l'exemple ci-dessus ressemblera à ceci:



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


Dans ce cas, npm voit que même si c'est x@1.2.0la version la plus récente du package qui satisfait l'exigence x@1.x, il est tout à fait possible de choisir à la place x@1.1.0. Le choix de cette version réduira la duplication des packages dans l'arborescence des dépendances.



Si nous ne corrigeions pas la structure de l'arborescence des dépendances dans un fichier de verrouillage, alors chaque programmeur travaillant sur un projet dans une équipe devrait configurer son environnement de travail de la même manière que les autres membres de l'équipe l'ont configuré. Seul cela lui permettra d'obtenir le même résultat que les autres. Si "l'implémentation" du mécanisme de construction de l'arborescence des dépendances peut être modifiée de cette manière, cela donne aux utilisateurs de npm de sérieuses options pour optimiser les dépendances pour leurs propres besoins spécifiques. Mais, si les résultats de la création d'un arbre dépendent de l'implémentation du système, cela rend impossible la création d'arbres de dépendances déterministes. C'est exactement ce à quoi conduit l'utilisation du fichier yarn.lock.



Voici quelques exemples supplémentaires de la manière dont les paramètres npm avancés peuvent conduire à la création de différentes arborescences de dépendances:



  • --legacy-peer-deps, un indicateur qui fait complètement ignorer npm peerDependencies.
  • --legacy-bundling, un indicateur indiquant à npm qu'il ne devrait même pas essayer de rendre l'arbre de dépendance plus «plat».
  • --global-style, l'indicateur par lequel toutes les dépendances transitives sont installées en tant que dépendances imbriquées dans les dossiers de dépendances de niveau supérieur.


La capture et la correction des résultats de la résolution des dépendances et l'attente que le même algorithme sera utilisé pour générer l'arborescence des dépendances ne fonctionnent pas dans des conditions lorsque nous donnons aux utilisateurs la possibilité de personnaliser le mécanisme de construction de l'arborescence des dépendances.



La fixation de la structure de l'arbre de dépendances fini nous permet de mettre à la disposition des utilisateurs de telles opportunités et en même temps de ne pas perturber le processus de construction d'arbres de dépendances déterministes et reproductibles.



Performances et exhaustivité des données



Le fichier est package-lock.jsonutile non seulement lorsque vous devez garantir le déterminisme et la reproductibilité des arbres de dépendance. De plus, nous nous appuyons sur ce fichier pour suivre et stocker les métadonnées des packages, ce qui permet de gagner un temps considérable qui autrement, en utilisant uniquement package.json, aurait été nécessaire pour fonctionner avec le registre npm. Étant donné que les capacités du fichier sont yarn.locktrès limitées, il ne contient aucune métadonnée que nous devons constamment télécharger.



Dans npm 7, le fichier package-lock.jsoncontient tout ce dont npm a besoin pour créer entièrement l'arborescence de dépendances d'un projet. Dans npm 6, ces données ne sont pas stockées de manière aussi pratique, donc lorsque nous rencontrons un ancien fichier de verrouillage, nous devons charger le système avec du travail supplémentaire, mais cela est fait, pour un projet, une seule fois.



En conséquence, même siyarn.lock et les informations sur la structure de l'arborescence de dépendances ont été enregistrées, nous devons utiliser un autre fichier pour stocker des métadonnées supplémentaires.



Opportunités futures



Ce dont nous avons parlé ici peut changer radicalement si vous prenez en compte les différentes nouvelles approches pour placer les dépendances sur les disques. Ce sont pnpm, yarn 2 / berry et PnP Yarn.



Nous, en travaillant sur npm 8, allons explorer une approche pour construire des arbres de dépendances basés sur un système de fichiers virtuel. Cette idée a été modélisée dans Tink, le concept a été validé en 2019. Nous discutons également de l'idée de passer à quelque chose comme la structure utilisée par pnpm, bien qu'il s'agisse, dans un sens, d'un changement encore plus radical que l'utilisation d'un système de fichiers virtuel.



Si toutes les dépendances se trouvent dans une sorte de référentiel central et que les dépendances imbriquées ne sont représentées que par des liens symboliques ou un système de fichiers virtuel, la modélisation de la structure de l'arborescence de dépendances ne serait pas un problème si important pour nous. Mais nous avons encore besoin de plus de métadonnées que ce que le fichier peut fournir yarn.lock. En conséquence, la mise à jour et la rationalisation du format de fichier existant ont plus de sens package-lock.json, plutôt qu'une transition complète vers yarn.lock.



Ce n'est pas un article qui pourrait s'appeler "Sur les dangers de yarn.lock"



Je tiens à souligner que d'après ce que je sais, Yarn génère de manière fiable des arbres de dépendance de projet corrects. Et, pour une version spécifique de Yarn (au moment d'écrire ces lignes, cela s'applique à toutes les versions fraîches de Yarn), ces arbres sont, comme avec npm, complètement déterministes.



Le fichier est yarn.locksuffisant pour créer des arbres de dépendance déterministes en utilisant la même version de Yarn. Mais nous ne pouvons pas nous fier à des mécanismes qui dépendent de l'implémentation du gestionnaire de paquets étant donné l'utilisation de mécanismes similaires dans de nombreux outils. C'est encore plus vrai si l'on considère que l'implémentation du format de fichieryarn.lockn'est formellement documenté nulle part. (Ce n'est pas un problème propre à Yarn, la même situation s'est produite dans npm. La documentation des formats de fichiers est un travail assez sérieux.) La



meilleure façon de garantir la fiabilité de la construction d'arbres de dépendances strictement déterminés est, à long terme, de corriger les résultats de la résolution des dépendances. Dans le même temps, vous ne devez pas vous fier à la conviction que les futures implémentations du gestionnaire de packages suivront, lors de la résolution des dépendances, le même chemin que les implémentations précédentes. Cette approche limite notre capacité à concevoir des arbres de dépendance optimisés.



Les écarts par rapport à la structure initialement fixée de l'arbre de dépendances devraient être le résultat d'un désir clairement exprimé de l'utilisateur. Ces écarts doivent se documenter, en apportant des modifications aux données précédemment enregistrées sur la structure de l'arborescence de dépendances.



Seul package-lock.json, ou un mécanisme comme ce fichier est capable de donner à npm de telles capacités.



Quel gestionnaire de packages utilisez-vous dans vos projets JavaScript?






All Articles