Les pouvoirs insoupçonnés de package.json
Dans chacun de nos projets se trouve ce fichier familier que l'on ouvre qu'en de rares occasions avant de le refermer aussitôt : el famoso package.json.
Et c'est fort dommage, car cet incontournable des stacks web recèle quelques secrets et des pouvoirs insoupçonnés par la plupart des devs.
Faisons un point sur toutes les possibilités qu'offre le package.json pour doper votre workflow !
Les commandes personnalisées
Balayons un fait assez connu : la section scripts permet de créer des alias tels que dev, build, typecheck, test servant de raccourci pour exécuter des commandes récurrentes dans notre projet.
Vous êtes-vous déjà demandé comment ces alias pouvait invoquer des librairies comme vite ou next que vous n'avez pourtant pas installées globalement ? Car avant d'exécuter votre commande, votre gestionnaire de paquets ajoute temporairement le node_modules/.bin de votre projet au PATH de votre session afin d'accéder à toutes ses dépendances.
Taper vite échouera probablement mais dans un script cela fonctionne car notre package manager se réfèrera à l'exécutable installé localement dans le projet et défini par votre lockfile.
L'avantage immédiat est que vos scripts sont reproductibles d'une machine à l'autre : tout le monde exécute ainsi la même version du même outil lors du lancement du script. Pas de risque par exemple qu'un typescript 7 installé globalement sur votre machine tourne lors d'un typecheck quand votre collègue utiliserait de son côté un typescript 6. Seules font foi les dépendances explicitement listées dans votre package.json.
Plus méconnus sont en revanche les hooks pre et post qui ajoutent encore plus de flexibilité à ces commandes.
Qu'est-ce donc ? Lorsque vous lancez un script build, par exemple, npm va spontanément chercher deux autres scripts : prebuild et postbuild. S'ils existent, prebuild sera exécuté avant build et postbuild juste après.
Cette convention vaut pour n'importe quel nom de script (on peut ainsi avoir des pretest, postdeploy, prerelease…) et permet d'attacher proprement des étapes de préparation ou de nettoyage autour d'une commande sans bourrer celle-ci de && et perdre en lisibilité.
Une façon bien plus propre d'écrire et de séparer les responsabilités au sein de vos scripts !
Ce comportement automatique est une spécificité de npm : pnpm ne les exécute pas par défaut et exige un enable-pre-post-scripts=true explicite, tandis que Yarn Berry a purement et simplement refusé de les supporter. Derrière cette limitation se trouve une motivation raisonnable : ces étapes implicites peuvent aussi obscurcir le flux d'exécution : un nouvel arrivant sur un projet qui lit "build": "vite build" n'a aucun moyen de deviner que d'autres commandes s'exécutent autour.
D'autres scripts cachés encore...
Il existe une seconde famille de scripts, autrement plus dangereuse : les scripts de cycle de vie que npm va déclencher tout seul à l'installation d'une dépendance. preinstall, install et postinstall s'exécutent en effet non seulement pour votre projet, mais pour chacune des dépendances que vous installez.
Historiquement il s'agissait de permettre à certaines librairies de compiler des modules natifs ou de télécharger un binaire utile à leur bon fonctionnement.
Mais cette facilité permet également à des librairies malveillantes exécuter du code arbitraire sur votre machine à chaque npm install ce qui en a fait le vecteur privilégié d'une série d'attaques de la chaîne d'approvisionnement (supply chain attacks) dans l'écosystème npm ces dernières années.
Conscient du problème et imitant en cela pnpm, Yarn et Bun qui bloquaient déjà ces scripts, npm v12 désactive dorénavant leur exécution par défaut.
Les paquets ayant réellement besoin de ces scripts pre ou post installation doivent désormais être explicitement autorisés avec la commande suivante :
Passer des arguments et lire l'environnement
Un script n'est pas toujours une commande figée et peut parfaitement recevoir des arguments pour en modifier le comportement. Par exemple, tout ce qui est placé après un double dash -- est transmis tel quel à la commande qui le précède :
Vos scripts peuvent également accéder à des variables d'environnement à commencer par les champs du package.json lui-même :
Pratique pour tagger une image Docker ou nommer une archive sans jamais dupliquer le numéro de version !
Attention en revanche à la définition de variables : la syntaxe MA_VAR=valeur ma-commande fonctionne sur macOS et Linux, et échoue sur Windows. C'est là qu'intervient un utilitaire comme cross-env avec une syntaxe unique fonctionnant pour toutes les plateformes :
Orchestrer plusieurs tâches parallèlement
Les projets réels enchaînent rarement une seule action. Le shell offre déjà deux opérateurs pour les articuler, le &et le && :
Le && enchaîne les commandes en séquence qui s'interrompt totalement si l'une des étapes échoue. Le & simple les lance en parallèle…
Le & présente la même limite que celle vue précédemment pour les variables d'environnement personnalisées : il n'existe pas vraiment sous Windows qui lui donne une autre signification *windows fatigue intensifies*.

Pour du parallélisme fiable entre toutes les plateformes, deux outils dédiés ont fait leurs preuves : npm-run-all2 (le fork maintenu de l'historique npm-run-all) et concurrently :
run-p pour parallel, run-s pour sequential, et un dev:* qui ramasse automatiquement tous les scripts partageant le même préfixe.
Exécuter du javascript avec une commande
Les alias de commande peuvent vite devenir limités dès que vous avez besoin d'une condition (« supprimer ce dossier seulement s'il existe »), d'une boucle ou d'une gestion d'erreur. Non seulement la commande en une ligne devient un enfer illisible mais elle peut ne pas fonctionner à l'identique d'un système d'exploitation à l'autre.
Dans ce cas, il est préférable de faire pointer votre alias vers un vrai fichier JavaScript contenant toute la logique d'exécution.
J'utilise par exemple souvent un script personnalisé de lint qui vient toiletter mes fichiers de traduction en les réordonnant, vérifier que chaque fichier de langue est exhaustif et qui repère les clefs de traduction mortes lorsque la librairie que j'utilise n'offre pas de telles fonctionnalités.
Ajouter sa propre CLI
Et là on rentre carrément dans les arcanes du package.json là où peu s'aventurent. Avec un champ bin, vous pouvez associer un nom de commande à un fichier exécutable.
Le fichier ciblé doit commencer par un shebang pour préciser le runtime.
Une fois le lien établi, la commande devient disponible comme n'importe quel outil installé :
C'est de cette manière que Vite, Angular, ESLint ou Prisma mettent à disposition leur propre CLI sans nécessiter un npm run.
Rien ne vous empêche donc d'en faire autant : vos collègues récupèreront les mêmes commandes sans rien devoir installer de plus. Votre codebase peut inclure sa propre CLI pour uniformiser le workflow entre ses contributeurs.
Les champs oubliés
La section scripts monopolise l'attention, mais quelques autres champs méritent largement le détour :
importsdéfinit des alias d'import résolus nativement par Node, sans configuration de bundler ni de TypeScript. Les clés doivent commencer par#:
enginesetpackageManagerverrouillent respectivement la version de Node et celle du gestionnaire de paquets attendues par le projet. De quoi mettre fin aux écarts de version entre deux machines qui donnent des résultats différents.
overrides(ou son équivalentresolutionschez Yarn) force la version d'une dépendance transitive, ce qui reste le seul moyen de corriger une faille de sécurité dans un paquet que vous n'installez même pas directement.
filesliste ce qui sera réellement embarqué lors d'une publication sur npm.
workspaces, enfin, transforme votre dépôt en monorepo et fait le lien avec les avantages d'un monorepo.
Faut-il jeter les gros orchestrateurs pour autant ?
Il serait malhonnête de conclure que le package.json remplace tout. Sur un monorepo conséquent, la mise en cache des tâches, leur parallélisation intelligente et l'exploitation du graphe de dépendances — ce que font Turborepo ou Nx — restent hors de portée d'une section scripts, et le sont de plus en plus à mesure que le projet grossit.
Mais il faut reconnaître que l'écrasante majorité des projets frontend n'en arrive jamais là, et que la tentation est grande d'installer l'orchestrateur avant d'en avoir le besoin. Or une dizaine de scripts bien nommés que vous comprenez intégralement vieilliront toujours mieux qu'un fichier de configuration que vous n'osez plus toucher, dont la moitié des options ont été copiées d'un article de blog et dont une mise à jour surprise peut casser la CI du jour au lendemain.