Il y a 6 ans -
Temps de lecture 5 minutes
npm prepublish, le grand détournement
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.
[javascript]{
"name": "test",
"version": "1.0.0-0",
"devDependencies": {
"typescript": "=1.6.2"
}
}[/javascript]
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
).
[javascript]{
"name": "test",
"version": "1.0.0-0",
"scripts": {
"prepublish": "tsc -p src"
}
"devDependencies": {
"typescript": "=1.6.2"
}
}[/javascript]
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 localnpm 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 :
[javascript]{
"name": "test",
"version": "1.0.0-0",
"scripts": {
"prepublish": "npm install && tsc -p src"
}
"devDependencies": {
"typescript": "=1.6.2"
}
}[/javascript]
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
.
[javascript]{
"name": "test",
"version": "1.0.0-0",
"scripts": {
"prepublish": "tsc -p src"
}
"devDependencies": {
"typescript": "=1.6.2"
}
}
[/javascript]
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 publish.
Dans 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.
[javascript]{
"name": "test",
"version": "1.0.0-0",
"scripts": {
"prebuild": "npm install",
"build": "tsc -p src"
}
"devDependencies": {
"typescript": "=1.6.2"
}
}
[/javascript]
Malgré cela, npm
reste un outil très pratique et c’est ce qui rend ce comportement exceptionnel aussi décevant.
Commentaire