npm overrides : forcer la version d'une dépendance transitive

  • VersionDude
  • guides
  • 7 min de lecture

Un paquet vulnérable trois niveaux plus bas, et un parent qui n'a pas publié de correctif. Le champ overrides du package.json racine permet de choisir soi-même la version, pour tout l'arbre ou pour un seul parent. Il obéit aussi à trois règles qui expliquent la plupart des cas où il semble sans effet.

Un rapport de vulnérabilité cite un paquet que vous n'avez jamais installé. Il se trouve trois niveaux plus bas, amené par la dépendance d'une dépendance, et le paquet intermédiaire n'a publié aucune version qui s'en écarte. Impossible de modifier un `package.json` qui n'est pas le vôtre. En revanche, vous pouvez déclarer un champ `overrides` dans votre propre `package.json` racine et indiquer à npm quelle version installer à la place.

L'override à plat : une version, partout dans l'arbre

Un panneau routier orange rectangulaire portant le mot DETOUR, un pictogramme de piéton noir au-dessus et une flèche noire pointant vers la gauche, fixé à un poteau noir devant un grillage flou. Un override fait la même chose à une dépendance : même destination, autre chemin.
Un panneau routier orange rectangulaire portant le mot DETOUR, un pictogramme de piéton noir au-dessus et une flèche noire pointant vers la gauche, fixé à un poteau noir devant un grillage flou. Un override fait la même chose à une dépendance : même destination, autre chemin.

La forme la plus courte nomme le paquet et la version. Écrire `"overrides": { "foo": "1.2.4" }` fait installer `foo` en 1.2.4 partout où il apparaît, quelle que soit la version demandée par les paquets qui en dépendent. La documentation de npm cite trois usages prévus : remplacer une version touchée par une faille connue, remplacer une dépendance par un fork, et garantir qu'une même version d'un paquet est utilisée partout.

La valeur n'a pas à être une version exacte. D'après la documentation, elle accepte tout spécificateur que npm admet pour une dépendance : une version exacte, un intervalle semver, un dist-tag, ou un remplacement du type `npm:`, `file:` ou URL Git. Pour un correctif de sécurité, l'intervalle est souvent le meilleur choix. `"foo": "^1.2.4"` impose la version corrigée comme minimum et laisse passer les correctifs suivants, là où une version figée bloque le paquet jusqu'à ce que quelqu'un pense à retirer la ligne. Si la différence entre les deux opérateurs reste floue, caret ou tilde détaille ce que chacun autorise.

Avant d'écrire l'override, cherchez qui réclame le paquet. `npm explain foo` affiche la chaîne de dépendances qui provoque l'installation de `foo`, un bloc par exemplaire présent dans l'arbre, chacun remonté jusqu'au projet racine. Cette sortie donne les deux informations utiles : quel parent est responsable, et s'il y a un exemplaire ou plusieurs. Un override à plat les modifie tous, ce qui n'est pas toujours souhaitable.

Limiter l'override à un seul parent

Imbriquez la clé pour limiter l'override à une branche de l'arbre. `"overrides": { "bar": { "foo": "1.2.4" } }` ne remplace `foo` que lorsqu'il est enfant de `bar`, petit-enfant, ou plus profond encore sous lui. Tous les autres paquets qui dépendent de `foo` gardent la version qu'ils ont résolue seuls. C'est la forme à choisir quand un seul parent reste accroché à une version vulnérable et que le reste de l'arbre est sain.

  • "foo": "1.2.4" : foo en 1.2.4 partout dans l'arbre
  • "bar": { "foo": "1.2.4" } : foo en 1.2.4 uniquement sous bar, à toute profondeur
  • "bar@2.0.0": { "foo": "1.2.4" } : uniquement sous cette version précise de bar
  • "foo": "$foo" : reprendre la spécification de votre dépendance directe foo
  • "foo": "npm:@scope/fork@1.2.4" : remplacer foo par un autre paquet

Les clés peuvent porter une version et s'imbriquer aussi profondément que nécessaire. `"bar@2.0.0": { "foo": "1.2.4" }` ne s'applique que si `foo` se trouve sous cette version précise de `bar` : l'override cesse donc de lui-même le jour où `bar` est mis à jour. Les clés s'imbriquent sur n'importe quelle longueur pour décrire un chemin plus long. Et pour remplacer à la fois un paquet et l'un de ses enfants, la forme objet accepte une clé `.` qui désigne le paquet lui-même, à côté des clés de ses enfants.

La dépendance directe est le seul cas que npm refuse. Il est interdit de remplacer un paquet dont vous dépendez directement, sauf si l'override et la dépendance portent exactement la même spécification ; sinon l'installation s'arrête sur une erreur `EOVERRIDE`. La parade documentée est une référence : `"foo": "$foo"` demande à npm de reprendre la spécification que déclare votre propre entrée `dependencies` pour `foo`, à toutes les profondeurs. La version se change à un seul endroit et tout l'arbre suit.

Quand l'override semble ne rien faire

Les overrides ne sont lus que dans le `package.json` racine. La documentation de npm est explicite : ceux des dépendances installées, workspaces compris, ne sont pas pris en compte dans la résolution de l'arbre. Deux conséquences en découlent. Dans un monorepo, le champ va dans le manifeste racine, pas dans le paquet où le problème se manifeste. Et un bloc `overrides` dans une bibliothèque que vous publiez ne change rien pour ceux qui l'installent ; la documentation renvoie les auteurs de bibliothèques vers l'épinglage de la dépendance ou vers `bundleDependencies`.

Vérifiez ensuite le résultat au lieu de le supposer. Lancez `npm install` pour que l'arbre soit résolu à nouveau, puis relancez `npm explain foo` : la version affichée en tête de chaque bloc est celle qui est désormais installée. Versionnez `package.json` et `package-lock.json` ensemble, car c'est le lockfile qui transporte la version résolue vers toutes les autres machines. package-lock.json contre package.json explique pourquoi les deux fichiers avancent toujours par paire.

Un override installe une version avec laquelle le paquet parent n'a jamais déclaré fonctionner. C'est tout l'intérêt quand l'intervalle déclaré ne contient qu'une version vulnérable, et c'est aussi le risque : npm ne vous empêchera pas d'imposer une version majeure pour laquelle le parent n'a pas été écrit. Considérez cette ligne comme un pansement avec une date de péremption. Quand le parent publie une version qui accepte le correctif, supprimez l'override.

Un override installe une version avec laquelle le paquet parent n'a jamais déclaré fonctionner. C'est tout l'intérêt quand l'intervalle déclaré ne contient qu'une version vulnérable, et c'est aussi le risque : npm ne vous empêchera pas d'imposer une version majeure pour laquelle le parent n'a pas été écrit. Considérez cette ligne comme un pansement avec une date de péremption. Quand le parent publie une version qui accepte le correctif, supprimez l'override.

- VersionDude

Yarn et pnpm utilisent un autre champ

La même idée existe dans les deux autres gestionnaires de paquets, sous d'autres noms. Yarn lit un champ `resolutions`, décrit dans la documentation de son manifeste comme un moyen d'utiliser une résolution précise à la place de ce que le résolveur choisirait normalement ; ses clés admettent un seul niveau de précision, écrit `parent/enfant`. pnpm nomme le réglage `overrides` comme npm, le place dans `pnpm-workspace.yaml` dans sa documentation actuelle, et le cible avec un séparateur `>`, comme dans `bar@1>foo`. Les deux réservent le champ à la racine du projet, comme npm. La syntaxe ne se transpose pas d'un outil à l'autre : un projet qui change de gestionnaire de paquets doit réécrire le bloc.

L'ordre de décision est court. Si le parent dispose déjà d'une version qui accepte le correctif, mettez le parent à jour et n'écrivez aucun override. Sinon, limitez l'override à ce parent plutôt qu'à tout l'arbre. Si le même paquet doit être identique partout, utilisez la forme à plat, avec un intervalle quand un minimum suffit. Et si l'installation bute sur un conflit de dépendance peer et non sur une version vulnérable, le problème est d'une autre nature : les dépendances peer le traitent.

FAQ

Les overrides npm fonctionnent-ils dans le package.json d'un workspace ?

Non. npm ne prend en compte que les overrides déclarés dans le package.json racine du projet. Sa documentation précise que ceux des dépendances installées, workspaces compris, ne sont pas considérés dans la résolution de l'arbre : le champ va donc dans le manifeste racine.

Que signifie l'erreur EOVERRIDE ?

Elle signale qu'un override vise un paquet dont vous dépendez directement, avec une spécification différente de celle de vos dependencies. npm n'autorise cet override que si les deux spécifications sont identiques. Alignez-les, ou écrivez l'override sous forme de référence, par exemple "foo": "$foo", qui reprend la spécification de votre dépendance directe.

Un override peut-il utiliser un intervalle au lieu d'une version exacte ?

Oui. La documentation de npm indique que la valeur d'un override accepte tout spécificateur admis pour une dépendance : version exacte, intervalle semver, dist-tag, ou remplacement du type npm:, file: ou URL Git. Un intervalle comme ^1.2.4 impose une version corrigée minimale sans figer le paquet.

Comment savoir quel paquet amène une dépendance transitive ?

Lancez npm explain suivi du nom du paquet. La commande affiche la chaîne de dépendances qui provoque son installation, un bloc par exemplaire présent dans l'arbre, chacun remonté jusqu'au projet racine.

npm overrides et Yarn resolutions, est-ce la même chose ?

Le but est le même, la syntaxe non. npm utilise un champ overrides fait d'objets imbriqués, Yarn un champ resolutions dont les clés s'écrivent parent/enfant, et pnpm un réglage overrides avec un séparateur >. Les trois réservent le champ à la racine du projet.

Projet lié