Commandes npm pour quand les choses tournent mal
Utilisez npm ls, npm explain, overrides et npm ci pour tracer les dépendances inattendues, corriger les versions et éviter les écarts de lockfile.
Lorsqu’un paquet apparaît dans node_modules sans que rien dans package.json ne l’ait demandé, ou dans une version que vous n’avez pas épinglée, exécutez npm ls <package> pour voir où il se situe et npm explain <package> pour identifier la dépendance qui l’a tiré, avant de toucher à quoi que ce soit.
Tout développeur a déjà connu ce moment où l’on fixe un numéro de version dans node_modules en se demandant : mais d’où sors-tu ? Le réflexe est bien connu : quelque chose semble anormal dans l’arbre, alors on fait rm -rf node_modules, on réinstalle, et on croise les doigts. Parfois le problème disparaît. Le plus souvent il revient aussitôt, parce que l’installateur a reconstruit le même arbre à partir des mêmes entrées, et vous n’avez désormais aucune idée de ce qui a changé.
Cet article déroule une seule investigation : un paquet ou une version inattendus, dont on remonte la trace jusqu’à la dépendance qui l’a demandé, puis que l’on corrige à la bonne couche. Les échecs à l’installation comme ERESOLVE, EACCES et les erreurs de compilation native sont traités ailleurs sur ce blog, dans les guides sur la résolution des conflits ERESOLVE, les erreurs de permission EACCES et les échecs de build node-gyp. Ce texte-ci s’adresse aux cas où aucune erreur n’a encore été levée.
Points clés à retenir
npm ls <package>montre chaque endroit où un paquet apparaît dans l’arbre installé ainsi que la version présente à chaque emplacement ;npm explain <package>montre la chaîne de dépendances qui l’a demandé.- Sans
--all,npm lsne liste que vos dépendances directes ; avec--all, il affiche l’arbre complet, et--depth=<n>fixe une limite explicite entre ces deux extrêmes. npm whyest un alias denpm explain, si bien que le même mot fonctionne avec npm, pnpm et yarn.- Le champ
overridesdepackage.jsonforce une version précise d’une dépendance imbriquée, indépendamment de l’intervalle demandé par son parent : c’est pourquoi il faut d’abord essayer de mettre à jour le parent. npm ciexige unpackage-lock.jsonexistant, supprimenode_modules, installe exactement ce que le lockfile spécifie, et se termine en erreur si le lockfile etpackage.jsonsont en désaccord.
Pourquoi supprimer node_modules détruit-il les preuves ?
Supprimer node_modules puis réinstaller efface la seule trace de la manière dont un paquet inattendu s’est retrouvé dans votre projet. L’arbre installé et package-lock.json encodent ensemble chaque décision de résolution prise par npm : quel parent a demandé quel intervalle, quelle version l’a satisfait, et où le résultat a atterri sur le disque.
Une réinstallation rejoue ces décisions à partir de package.json et du lockfile. Si les entrées n’ont pas changé, vous obtenez le même arbre et la même surprise. Si elles ont changé (un indicateur de configuration, un registre, une modification d’intervalle), la réinstallation écrase l’état auquel vous aviez besoin de comparer. Dans les deux cas, lisez l’arbre avant de le reconstruire. Les deux commandes qui le lisent sont npm ls et npm explain.
npm ls : où se trouve le paquet et dans quelle version ?
npm ls <package> filtre l’arbre installé pour ne garder que les chemins aboutissant au paquet nommé, en affichant chaque emplacement sous la forme name@version avec ses parents indentés au-dessus. Il est également possible de filtrer par intervalle de versions, comme dans npm ls semver@^6, lorsque seules les copies d’une version majeure donnée vous intéressent.
# Every copy of semver, with the path down to each
npm ls semver
# The complete tree, not just direct dependencies
npm ls --all
# Cap the walk at two levels
npm ls --all --depth=2
# Only what ships to production
npm ls --all --omit=dev
Le réglage depth vaut 0 par défaut, sauf si --all est passé, auquel cas il devient Infinity. Cette valeur par défaut régit un npm ls nu, sans argument de paquet. Dès que vous nommez un paquet, npm suit le chemin vers chaque copie quelle que soit la profondeur, ce qui explique que l’exemple npm ls promzard de la documentation officielle montre un résultat imbriqué sans --all ; passez explicitement --depth=<n> si vous souhaitez limiter ce parcours.
Ce que npm affiche est une carte des relations de dépendance entre paquets, elle ne correspondra donc pas à la disposition réelle des dossiers sur le disque : un paquet dédupliqué apparaît sous chaque parent qui en a besoin, et non uniquement à l’unique endroit où ses fichiers résident. La sortie signale aussi les paquets superflus (extraneous : installés mais non déclarés), manquants, ou dans une version qui ne satisfait pas l’intervalle déclaré ; les paquets manquants apparaissent avec la mention UNMET DEPENDENCY. Ajoutez --package-lock-only et npm rapporte l’arbre que produirait le lockfile, en ignorant le contenu actuel de node_modules.
Deux remarques de syntaxe. Les filtres actuels sont --omit=dev et --include=dev ; --production est un alias déprécié de --omit=dev et --dev un alias déprécié de --include=dev, tandis que --development n’est pas du tout une option documentée. Par ailleurs, npm ls se termine avec un code de sortie non nul lorsqu’un paquet est manquant ou dans une version invalide, ou lorsqu’un paquet nommé ne correspond à rien, ce qui le rend utilisable comme contrôle en CI ; les paquets superflus, à eux seuls, ne le font pas échouer.
npm explain : qui a demandé ce paquet ?
npm explain <package> affiche, pour chaque copie installée, la chaîne de déclarations de dépendances qui a conduit à sa présence, en remontant jusqu’au projet racine. Là où npm ls répond à la question « où », npm explain répond à la question « qui ».
npm explain semver
npm why semver # identical
npm explain semver --json # for jq
Chaque bloc de la sortie commence par le name@version résolu et son chemin dans node_modules, puis indente une ligne par saut : l’intervalle déclaré par un parent, la version du parent lui-même et son chemin, pour finir par une ligne nommant le projet racine. Lisez-la de bas en haut pour suivre le trajet de votre package.json jusqu’à la copie que vous n’attendiez pas. Les paquets dupliqués obtiennent un bloc par copie, ce qui rend les intervalles contradictoires visibles côte à côte. Vous pouvez aussi passer un dossier, comme npm explain node_modules/foo/node_modules/semver, pour n’expliquer qu’une seule copie imbriquée.
Le synopsis de npm explain mentionne why comme alias, et les autres gestionnaires majeurs utilisent le même verbe.
| Gestionnaire de paquets | Commande | Forme de la sortie |
|---|---|---|
| npm | npm explain <pkg> ou npm why <pkg> | Un bloc par copie installée, chaîne remontant jusqu’à la racine |
| pnpm | pnpm why <pkg> | Un arbre inversé, avec le paquet demandé en haut |
| Yarn | yarn why <pkg> | Les raisons par workspace, accepte pkg@range |
Faut-il mettre à jour le parent ou ajouter un override ?
Une fois que npm explain a désigné le parent qui a demandé le mauvais intervalle, le premier correctif consiste à faire passer ce parent à une version qui en demande un meilleur. Exécutez npm outdated <parent> pour voir s’il existe une version plus récente, ou consultez le package.json du parent dans le registre avec npm view <parent>@latest dependencies. Si un parent plus récent déclare un intervalle acceptable, mettez-le à jour et laissez npm résoudre à nouveau l’enfant.
Ce n’est que lorsqu’aucune version du parent ne corrige l’intervalle qu’il faut recourir à overrides :
{
"overrides": {
"semver": "^7.5.4"
}
}
Un override remplace la version de la dépendance imbriquée indépendamment de l’intervalle déclaré par le parent : celui-ci peut donc désormais s’exécuter avec une version avec laquelle il n’a jamais été testé. C’est le compromis à accepter, et c’est pourquoi overrides constitue le second recours et non le premier. Quelques règles issues de la documentation : les overrides ne sont pris en compte que dans le package.json racine ; un paquet dont vous dépendez directement ne peut être surchargé qu’avec une spécification identique à la sienne, sinon npm lève EOVERRIDE, et la forme de référence $name existe précisément pour ce cas ; enfin, les valeurs peuvent être une version exacte, un intervalle, un dist-tag, ou un spécificateur npm:, file: ou Git. Imbriquez l’override sous le nom du parent lorsque vous voulez qu’il ne s’applique qu’à une branche de l’arbre plutôt que partout.
npm config list : les réglages que vous avez oublié avoir définis
npm config list affiche les réglages définis par vous, par votre environnement ou par un fichier .npmrc ; npm config list -l affiche en plus les valeurs par défaut de npm, et --json renvoie les mêmes données en JSON. Quand un arbre se résout d’une manière que package.json seul ne peut expliquer, la cause est souvent une valeur de configuration dont personne ne se souvient avoir écrite.
npm config list
npm config list -l
La sortie est regroupée par source (ligne de commande, environnement, .npmrc du projet, .npmrc utilisateur, global), ce qui vous indique quel fichier modifier. Deux clés méritent d’être examinées en priorité. Un registry autre que celui par défaut signifie que les versions ont été résolues contre un miroir ou un registre privé dont le contenu peut être en retard sur le registre public. Un réglage legacy-peer-deps enregistré indique à npm de construire l’arbre sans consulter du tout les peerDependencies, comme il se comportait jusqu’à la version 6 : vous pouvez donc obtenir des appariements que le résolveur actuel aurait refusés. Il y a un effet de bord : dès qu’un lockfile a été construit avec cet indicateur, chaque npm ci ultérieur en a besoin aussi, sinon l’installation casse. Une seule ligne oubliée dans un .npmrc de projet peut expliquer à la fois un arbre local étrange et une CI au rouge.
npm ci contre npm install : que se passe-t-il quand le lockfile est en désaccord ?
Lorsque le lockfile satisfait package.json, npm install utilise les versions exactes du lockfile ; dans le cas contraire, npm install résout à nouveau et met à jour package-lock.json. npm ci, lui, échoue en erreur.
| Comportement | npm install | npm ci |
|---|---|---|
Exige package-lock.json | Non | Oui |
Lockfile et package.json en désaccord | Résout à nouveau, réécrit le lockfile | Se termine en erreur |
node_modules existant | Réutilisé | Supprimé d’abord |
Écrit dans package.json ou le lockfile | Oui | Jamais |
| Ajout d’un paquet unique | Oui | Non |
La documentation de npm install est explicite sur l’ordre de préséance : les intervalles déclarés dans package.json font foi, et le lockfile ne conserve ses versions épinglées que tant qu’elles rentrent encore dans ces intervalles. C’est précisément le comportement que vous ne voulez pas en CI, où un lockfile réécrit silencieusement masque la dérive que vous cherchez justement à détecter. npm ci refuse de réconcilier les deux fichiers et échoue bruyamment : utilisez-le donc dans les pipelines et réservez npm install à la machine sur laquelle vous avez l’intention de modifier les dépendances.
Conclusion
Un paquet inattendu dans l’arbre est une décision de résolution qui laisse une trace écrite, et npm ls associé à npm explain lit cette trace sans l’altérer. Remontez la chaîne jusqu’au parent qui a déclaré l’intervalle, corrigez le parent s’il existe une meilleure version, n’utilisez un override que dans le cas contraire, puis vérifiez avec npm config list s’il n’existe pas des réglages qui ont faussé la résolution dès le départ. Exécutez npm ci en CI pour que la prochaine incohérence fasse échouer le build au lieu de réécrire discrètement le lockfile.
FAQ
Que signifie « deduped » à côté d'un paquet dans la sortie de npm ls ?
La mention « deduped » indique que npm ls affiche le paquet à cet endroit du graphe logique de dépendances, mais qu'aucune copie distincte n'y existe : une unique copie installée plus haut dans node_modules satisfait l'intervalle de ce parent. Ce n'est pas une erreur. Comme npm ls affiche l'arbre logique, le même paquet apparaît sous chaque parent qui le requiert, et seule la ligne sans mention correspond à un dossier physique.
Comment supprimer les paquets que npm ls signale comme superflus (extraneous) ?
Exécutez npm prune. Cette commande supprime tout ce qui se trouve dans node_modules dont rien d'autre ne dépend ; nommez un ou plusieurs paquets pour restreindre l'opération à ceux-ci. Ajoutez --omit=dev, ou définissez NODE_ENV à production, et vos devDependencies partent aussi. Utilisez --dry-run pour voir d'abord le plan d'action, et --json pour récupérer les changements en JSON. Les installations éliminent déjà d'elles-mêmes les paquets superflus, vous n'aurez donc généralement besoin de cette commande qu'après un plantage ou une installation interrompue.
npm dedupe corrige-t-il les versions dupliquées que montre npm ls, ou faut-il recourir aux overrides ?
npm dedupe ne regroupe que les copies que les intervalles déclarés autorisent déjà. Il parcourt l'arbre et remonte chaque dépendance aussi haut que possible, de sorte que les parents aux intervalles qui se recoupent finissent par partager une seule copie, et il ne récupère jamais quoi que ce soit de nouveau depuis le registre. Si deux parents demandent des intervalles sans aucune version commune, les deux copies subsistent, et la solution consiste à mettre à jour un parent ou à ajouter une entrée overrides. npm find-dupes effectue la même passe à blanc, ce qui vous permet de voir le résultat à l'avance.
Comment lister les paquets npm installés globalement ?
Exécutez npm ls -g. L'indicateur --global oriente npm ls vers le préfixe global, listant les paquets qui y sont installés plutôt que ceux du projet courant. Les mêmes règles de profondeur s'appliquent : sans --all, seuls les paquets globaux de premier niveau sont affichés, tandis que npm ls -g --all déploie chacun d'eux en son arbre de dépendances complet. Ajoutez une valeur --depth explicite pour limiter le parcours, ou --json pour une sortie exploitable par une machine.