Attention aux subtilités de versions dans votre package.json

Ce ne sont pas les modules que vous recherchez.

Parfois, je ne comprends pas quelque chose, donc je cherche la réponse et il est naturel de la partager. D’autres fois, je pense être suffisamment en retard pour que ce partage soit inutile, jusqu’à ce que des membres de mon équipe aient le même souci. Ce billet figure dans la seconde catégorie.

Si vous utilisez régulièrement NPM, vous avez dû remarquer les tildes (~) et les chevrons (^) devant les numéros de version dans le package.json. Vous vous êtes peut-être interrogé·e sur le fichier package-lock.json. Ce billet va s’attarder un peu sur tout ça.

Une parabole #

Début 2016, j’ai rejoint une équipe de développement web et j’ai découvert les joies1 de Node, NPM et Webpack. La pile m’a passionné et j’ai fait un petit projet perso pour me mettre à niveau.

Et est arrivée une bizarrerie : j’ai assez rapidement détecté un bogue que personne d’autre dans l’équipe n’avait. Puisque j’étais le seul à le voir, nous l’avons écarté comme étant un problème avec mon navigateur.

Avance rapide de quelques mois. L’application est maintenant en production et l’équipe de support nous transmet un ticket avec exactement le même bogue que celui que nous avions vu sur mon ordinateur. Ce n’était donc pas un problème de configuration, mais il était spécifique à mon ordinateur, car j’avais rejoint l’équipe un mois après qu’ils aient commencé à travailler.

Quel est le rapport avec tout cela ? Eh bien, avec le NPM, le moment auquel vous installez vos dépendances n’est pas si anodin. Vous voyez, NPM enregistre vos dépendances dans le fichier package.json, mais d’une manière qui lui permet de télécharger des versions en aval de celle qui est déclarée.

C’est ce qui m’est arrivé : pour l’une des dépendances, j’avais téléchargé une version plus récente que celle que mes collègues utilisaient. Cette version avait introduit un bogue — pas évident à détecter, car c’était un effet secondaire pour des changements qui n’avaient a priori rien à voir. Ça n’était pas un souci tant qu’on se limitait à mon poste, mais notre intégration continue, lorsqu’elle a construit la version de production, a téléchargé exactement la même version.

Imaginez-vous trouver un bogue entre deux bases de code identiques. Je me souviens avoir compris le problème, mais je suis incapable de dire comment j’ai pensé à comparer les versions des dépendances avec mon coéquipier.

Voici un exemple de pourquoi il vaut mieux comprendre le fonctionnement de NPM si vous devez vous en servir. Je vous propose quelques conseils de base pour commencer.

Comment vous pouvez déclarer vos dépendances avec NPM #

Jetons un œil aux déclarations de versions valides dans un fichier package.json. D’après la documentation de NPM, tous les exemples suivants sont valides :

 1{ "dependencies" :
 2  { "foo" : "1.0.0 - 2.9999.9999"
 3  , "bar" : ">=1.0.2 <2.1.2"
 4  , "baz" : ">1.0.2 <=2.3.4"
 5  , "boo" : "2.0.1"
 6  , "qux" : "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0"
 7  , "asd" : "http://asdf.com/asdf.tar.gz"
 8  , "til" : "~1.2"
 9  , "elf" : "~1.2.3"
10  , "two" : "2.x"
11  , "thr" : "3.3.x"
12  , "lat" : "latest"
13  , "dyl" : "file:../dyl"
14  }
15}

La plupart de ces déclarations est assez explicite, mais j’aimerais m’attarder une minute sur les deux suivantes :

  • ~version « Approximativement équivalent à la version »
  • ^version « Compatible avec la version »

Des exemples seront plus clairs que du texte. S’il vous faut davantage d’informations, je vous invite à consulter la documentation de semver.

Les tildes #

Ils permettent les changements du numéro de patch si vous spécifiez la mineure. Si vous ne la précisez pas, les changements peuvent s’appliquer à la mineure également.

  • ~1.2.3 := >=1.2.3 < 1.3.0
  • ~1.2 := >=1.2.0 < 1.3.0 := 1.2.x
  • ~1 := >=1.0.0 < 2.0.0 := 1.x

Les chevrons #

Ils permettent les changements du numéro non nul le plus à gauche.

  • ^1.2.3 := >=1.2.3 < 2.0.0
  • ^0.2.3 := >=0.2.3 < 0.3.0
  • ^0.0.3 := >=0.0.3 < 0.0.4

Comment vous devriez déclarer vos dépendances avec NPM #

Je ne voudrais pas passer pour un gourou, mais je peux vous donner mon opinion bâtie sur [mon expérience][#parabole]. Dans mon anecdote, la différence portait sur le patch, me semble-t-il, et le bogue n’était lié à aucun des changements de la release note — en tout cas, pas directement.

Ma conclusion2 est que vous devriez être extrêmement prudent quand vous choisissez vos dépendances. Pour tout projet sensible ou destiné à la production, je recommanderais de fixer la version en retirant les préfixes. Ne faites pas confiance aux versions descendantes tant que vous ne les avez pas testées, et ne limitez pas ces tests à ce qu’indiquent les release notes.

Dans le monde Java, Maven permet de déclarer des plages de versions pour les dépendances, mais la plupart des développeurs que je connais (moi compris) considèrent qu’il s’agit généralement d’une mauvaise pratique.

Vos builds doivent être répétables. Laisser votre outil de build déterminer les versions au moment de la compilation est juste incompatible avec cet objectif.

Si vous ne voulez pas qu’NPM préfixe les versions, vous pouvez utiliser la commande suivante : npm config set save-prefix=''.

Une note sur le package-lock.json #

En travaillant sur mon projet perso à l’époque de la [parabole][#parabole], j’ai découvert Yarn. C’est une alternative à NPM made in Facebook pour contrer des problèmes qu’ils ont rencontrés. Beaucoup ont été séduit·es par sa vitesse incomparable.

Mais ce qui a retenu mon attention en priorité a été le fichier yarn.lock : lors la première installation des dépendances, Yarn crée un fichier de verrouillage qui indique les versions effectivement installées. Ainsi, lorsqu’un autre membre de l’équipe récupère le projet et installe les modules, Yarn va regarder le fichier de verrouillage plutôt que le package.json.

Les années ont passé et NPM a intégré des fonctionnalités similaires à celle de Yarn, dont la ligne suivante lorsque vous installez vos dépendances pour la première fois :

$ npm i
npm notice created a lockfile as package-lock.json. You should commit this file.

Mon voisin de bureau m’a récemment dit qu’il ajoutait ce fichier au .gitignore. C’est une bonne idée pour la plupart des fichiers générés, mais pas pour celui-ci : vous devriez le versionner et le partager avec votre équipe pour garantir des installations identiques sur tous les postes.

Non, n’essayez pas de le modifier à la main. Il est écrit pour NPM, pas pour des humains. Modifiez package.json et exécutez npm i pour mettre à jour le fichier de verrouillage.

Un chouette outil pour expérimenter #

Il est temps de conclure. Je vais donc vous quitter sur un lien sympathique : semver vous aide à comprendre et tester votre spécification de version grâce à une interface dynamique et claire. Je ne vais pas faire plus de fioritures que le site en question, cliquez juste ici pour voir : https://semver.npmjs.com/


  1. Aucune ironie ici, j’y ai vraiment pris plaisir. ↩︎

  2. Les commentaires et conseils que j’ai pu voir sur StackOverflow en cherchant pour ce billet me confortent dans mon opinion. ↩︎