Publié par
Il y a 2 années · 5 minutes · Back, Craft, Front

npm prepublish, le grand détournement

npm

Le gestionnaire de paquets npm permet de gérer un projet de développement : dépendances, construction et publication d’un paquet.

Ce gestionnaire propose des scripts bien identifiés qui permettent de réaliser les étapes courantes de la gestion d’un projet.

L’utilisation de l’un de ces scripts : prepublish, a été détournée et peut être très déroutante si l’on n’y prend pas garde.
Nous allons montrer dans cet article, les problèmes qui surgissent à l’utilisation de ce script précis.

Avant d’aborder le problème lié au script de pré-publication, faisons tout d’abord un petit rappel du script qui permet la construction et qui précède, en toute logique, la publication : install.

Comprendre npm install

npm install récupère les dépendances du projet, les construit localement si besoin et construit ensuite le projet lui-même.
Lors des étapes de construction, npm peut être amené à compiler les dépendances qui dépendent de l’architecture locale de la machine (si la version binaire n’est pas disponible).

Prenons le cas d’une application test, gérée par npm et qui dépend du compilateur typescript.

{
  "name": "test",
  "version": "1.0.0-0",
  "devDependencies": {
    "typescript": "=1.6.2"
  }
}

Lors de l’exécution de npm install, npm installe la dépendance typescript.
La construction de l’application ne consiste, quant à elle, qu’à compiler – à l’aide du compilateur typescript fraîchement installé – les fichiers *.ts en *.js.

Dans notre exemple, cette étape de construction ne dépend pas de l’architecture locale de la machine : un fichier *.js, une fois généré, peut être utilisé sur n’importe quelle machine.
Ces fichiers peuvent donc être directement intégrés dans la distribution de notre application qui sera publiée par npm.
De cette manière, les applications qui dépendront de la nôtre, pourront directement récupérer les fichiers *.js fournis dans la distribution sans avoir besoin de faire la compilation localement.
Pour ce faire, nous devons indiquer à npm comment et surtout avec quel script compiler les fichiers *.ts en *.js.

Comprendre npm prepublish

Dans ce cas précis, npm préconise de ne pas utiliser le script install pour effectuer la tâche de compilation.
Il est en effet préconisé d’utiliser le script prepublish qui sera exécuté avant chaque publish (analogiquement postpublish sera exécuté après chaque publish).

{
  "name": "test",
  "version": "1.0.0-0",
  "scripts": {
    "prepublish": "tsc -p src"
  }
  "devDependencies": {
    "typescript": "=1.6.2"
  }
}

Ceci paraît assez logique : on ne prépare le contenu de l’application qu’une seule fois, avant de la publier.

Comprendre le problème

Ce qui en revanche défie la logique, tient dans l’une des premières lignes de la documentation en question :

prepublish: Run BEFORE the package is published. (Also run on local npm install without any arguments.)

En conséquence, l’exécution de npm install se traduit par l’exécution des scripts : preinstall, install, postinstall (par convention) puis par prepublish (peu intuitif, mais c’est le comportement documenté et observé). Par ailleurs, l’exécution de npm publish se traduit par l’exécution des scripts : prepublish, publish et postpublish.

Revenons à notre exemple désormais.
Avant de pouvoir correctement déclencher un npm prepublish, il faut tout de même avoir pris soin d’installer le compilateur typescript dont notre projet dépend.
On doit donc exécuter npm install avant de pouvoir exécuter le script prepublish.
Afin d’automatiser la construction de l’application on pense donc, en première approche, à configurer son fichier package.json de cette manière :

{
  "name": "test",
  "version": "1.0.0-0",
  "scripts": {
    "prepublish": "npm install && tsc -p src"
  }
  "devDependencies": {
    "typescript": "=1.6.2"
  }
}

Dans ce premier cas, vous l’aurez compris, nous obtenons une boucle infinie. En effet npm publish déclenche npm prepublish, qui – comme configuré – déclenche npm install, qui – contre-intuitivement – déclenche npm prepublish, qui déclenche npm install, qui déclenche…

Une fois que l’on a fait cette erreur, on revient à la configuration précédente de son package.json.

{
  "name": "test",
  "version": "1.0.0-0",
  "scripts": {
    "prepublish": "tsc -p src"
  }
  "devDependencies": {
    "typescript": "=1.6.2"
  }
}

On chaine ensuite les appels dans des commandes externes au package.json de cette manière : npm install && npm publish.
Cet enchaînement se traduit malheureusement par l’exécution de npm install qui – contre-intuitivement – déclenche npm prepublish directement suivi par un autre npm prepublish qui est lui déclenché par le npm publishDans ce cas, on obtient donc une double exécution du script prepublish, ce qui n’est pas vraiment optimal.

On peut raisonnablement vouloir exécuter des tests avant de publier l’application, ce qui dans le cadre d’une intégration continue et de tests IHM, peut être assez pénalisant s’ils sont exécutés en double.

Conclusion

Ce comportement, qui fait exception à tous les scripts définis par npm, est véritablement contre-intuitif. Il est pourtant toujours présent même s’il est très controversé par les utilisateurs sur github.
Il y a bien sûr des méthodes pour éviter les deux problèmes que l’on vient d’évoquer : boucle infinie et double exécution.
On peut notamment définir des scripts personnalisés, comme suit, mais on sort dès lors du cadre standard que npm semble pourtant proposer.

{
  "name": "test",
  "version": "1.0.0-0",
  "scripts": {
 "prebuild": "npm install",
    "build": "tsc -p src"
  }
  "devDependencies": {
    "typescript": "=1.6.2"
  }
}

Malgré cela, npm reste un outil très pratique et c’est ce qui rend ce comportement exceptionnel aussi décevant.

Sergio Dos Santos
8 xp / Craft / DevOps / Back / Front

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *