Cover Photo by Paul Esch-Laurent on Unsplash
Préambule : Contrairement aux croyances, npm ne veut pas dire Node Package Manager
, c’est une rétroacronymie, même si concrètement, c’est exactement ce qu’il fait 😅. Il est développé et maintenu par la société npm
(site officiel), qui s’occupe aussi du registry npm publique.
Vous ne comprenez pas ce qu’il se passe lorsque vous faites un npm install
?
D’où sort le package-lock.json
?
À quoi servent toutes les commandes autres que npm install
ou npm run start
?
Eh bien moi non plus je ne comprenais pas ! Mais ne vous inquiétez pas, je vais vous aider à y voir plus clair.
Dans cette série d’articles, je vous propose de faire un tour sur les différentes fonctionnalités de base de npm.
La première chose que je fais quand je récupère un projet node, c’est de lancer la commande npm install
.
Je vois plein d’informations, et au début, voilà ce que je me disais :
node_modules
??? Si c’est un dossier ça doit être important, je commit !package-lock.json
, deux versions possibles :
Pour essayer de mieux comprendre toutes ces informations et alertes, commençons par regarder le fichier qui défini notre projet, le package.json
.
{
"name": "my-project",
"version": "1.0.0",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test"
},
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"@testing-library/jest-dom": "^5.15.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"react-scripts": "^4.0.3"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
}
}
Un exemple issu de la génération d’un nouveau projet react
Les premières entrées name
et version
donnent des informations générales sur votre projet. Il en existe plein d’autres que vous pouvez retrouver sur la documentation officielle.
Le block scripts
contient une série de commandes à utiliser pour faciliter le développement dans votre projet. Chaque commande est définie par un nom et la commande à exécuter.
Pour utiliser un script, il suffit d’utiliser la commande npm run <nom du script>
.
Dans tout projet de qualité, vous devriez retrouver la commande npm run test
(ou le raccourci npm test
).
Le block dependencies
contient l’ensemble des packages utilisés dans votre projet. Vous pouvez ajouter des dépendances via la commande npm install <package_name>
.
Dans notre exemple, nous retrouvons le package react
, mais vous pouvez aussi retrouver des packages de composants graphiques (antd
, @mui/material
, chartjs
, …) ou tout autre package utilisé au runtime de votre application (lodash
, axios
, …).
Le block devDependencies
contient l’ensemble des packages utilisés lors du cycle de développement de votre projet.
Vous pouvez ajouter des dépendances via la commande npm install <package_name> -D
.
C’est une bonne pratique de séparer les dépendances entre dependencies
et devDependencies
. Dans le cas d’un package, seule les dependencies
seront installées et le poids du package parent en sera allégé.
Dans notre exemple, nous retrouvons les packages react-scripts
et @testing-library
, qui vous permettent de compiler, tester ou exécuter votre projet. Vous pouvez aussi retrouver d’autres librairies de testing (jest
, mocha
, chai
, cypress
, …), des librairies de types dans des projets en typescript (typescript
, @types/node
, @types/react
, …).
Il existe aussi d’autres types de dépendances, que je n’aborderais pas ici par soucis de simplicité. Ces dernières sont peu utilisées dans le développement d’application, mais peuvent servir dans le développement de packages : peerDependencies
, bundledDependencies
, optionalDependencies
. (plus d’info dans la doc officielle).
Concernant le versioning des dépendances, c’est la norme semver
qui est de rigueur. Ce système de notation vous permet de sélectionner de façon intelligente la version du package à utiliser.
Une version s’écrit sous le format suivant : major
.minor
.patch
major
: est incrémenté en cas de breaking change
;minor
: nouvelle feature, rétrocompatible;patch
: bug fix, rétrocompatible.En général, il est possible de monter de version minor
et patch
sans risquer un changement de comportement, et en récupérant les correctifs de sécurité et bug, ainsi que les nouvelles fonctionnalités. Un changement dans la version major
indique souvent un effort supplémentaire de réécriture de votre code pour s’adapter aux changements apportés.
Il existe plusieurs façons d’écrire un ensemble de versions visées, je vous en détaille quelques-unes :
1.2.3
: version figée, on souhaite avoir exactement cette version^1.2.3
: première version non 0
figée. Ici version major
figée => 1.2.3
, 1.3.5
…~1.2.3
: version major
et minor
figées => 1.2.3
, 1.2.4
, 1.2.5
…>=1.2.3
: toute version supérieure ou égale => 1.2.3
, 1.2.5
, 2.5.8
latest
: la dernière versionDe manière générale, j’utilise principalement la notation ~1.2.3
, qui me permet de bénéficier des nouveautés et des correctifs sans introduire d’incompatibilité.
Je vous invite à tester via ce petit site pour mieux appréhender les différentes notations et visualiser les versions qui correspondent à votre notation.
Plus d’info dans la doc officielle
Il est courant de voir dans le package.json
des blocs spécifiques permettant de configurer les outils de développement. En fonction des outils que vous utilisez, vous avez le choix d’un fichier spécifique à la racine, de variables d’environnement ou d’un bloc dans le package.json
.
Un exemple de config dans un package.json
Dans notre cas, la configuration d'eslint
a été mise dans le package.json
via le bloc eslintConfig
, mais il était aussi possible d’avoir un fichier .eslintrc.js
à la racine du projet.
npm install
Maintenant que nous avons éclairci le contenu du package.json
, passons à la toute première étape lorsque l’on clone un nouveau projet : npm install
.
Cette commande permet de télécharger toutes les dépendances et sous-dépendances du projet, en accord avec les versions définies dans le package.json
.
À chaque npm install
, une résolution des versions des dépendances est calculée, les sources des dépendances sont téléchargées dans le dossier node_modules
(plus de détail ici), et un fichier package-lock.json
est généré/mis à jour avec l’arbre des dépendances et leur version résolue.
Une fois cette commande lancée, vous pouvez démarrer votre projet et commencer à coder !
⚠️ Si deux développeurs installent un même projet, il est possible de ne pas avoir le même arbre de dépendances (comme expliqué très brièvement dans la doc).
Il est alors important de mettre en place un mécanisme afin de toujours obtenir le même résultat après installation. Ce qui nous permettra d’éviter des bugs aléatoires non reproductibles, et d’être plus serein quant au déploiement de notre projet.
package-lock.json
Une petite minute, on parle de résolution de version
, package-lock.json
, mais ça correspond à quoi tout ça ?
Pour mieux comprendre, prenons l’exemple d’un petit projet, défini par ce package.json
:
{
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"X": "^1.0.1"
}
}
Nous avons vu qu’à chaque npm install
, les versions des dépendances sont résolues. On risque donc d’avoir des versions différentes en fonction de la date d’installation du projet :
Date | Package X |
---|---|
D | 1.0.2 |
D+5 | 1.0.5 |
D+X | 1.2.1 |
Dans le monde parfait de semver
, cela ne devrait pas poser de problèmes, mais comment éviter que 2 ans plus tard, on soit incapable d’installer le projet ?
J’ai longtemps eu la superbe idée de figer la version dans le package.json
en utilisant "X": "1.0.2"
. Après un npm install
, j’aurai en effet toujours la même version :
Date | Package X |
---|---|
D | 1.0.2 |
D+5 | 1.0.2 |
D+X | 1.0.2 |
Mais j’oublie quelque chose d’important ! Et les sous-dépendances alors ?
En considérant "X": "1.0.2"
et une sous-dépendance "Y": "^2.0.3"
définie dans le package.json
de X
, après de multiples npm install
, on obtient le tableau de version suivant :
Date | Package X | Package Y |
---|---|---|
D | 1.0.2 | 2.0.3 |
D+5 | 1.0.2 | 2.0.7 |
D+X | 1.0.2 | 2.1.4 |
Bon on fait comment alors ? Je ne vais pas tout figer non plus, j’en aurais pour mille ans ! 🤨
package-lock.json
Ce fichier permet de conserver l’arbre de résolution des dépendances et de leur version.
En considérant :
package.json
avec "X": "^1.0.1"
, X dépend de "Y": "^2.0.0"
package-lock.json
qui a résolu les versions "X": "1.0.2"
et "Y": "2.0.2"
{
"name": "my-project",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "my-project",
"version": "1.0.0",
"dependencies": {
"X": "^1.0.1"
}
},
"node_modules/X": {
"name": "X",
"version": "1.0.2",
"dependencies": {
"Y": "^2.0.0"
}
},
"node_modules/Y": {
"name": "Y",
"version": "2.0.2"
}
}
}
package-lock.json généré, plus d’informations sur la structure dans la doc officielle
Je lance ma commande npm install
et je me prépare à admirer la puissance de npm
Date | Package X | Package Y |
---|---|---|
D | 1.0.2 | 2.0.3 |
D+5 | 1.0.5 | 2.0.7 |
D+X | 1.2.1 | 2.1.4 |
Quoi ?? Toutes mes versions changent ?? Mais je ne comprends plus rien ça sert à rien ce fichier !!? En plus il est modifié à chaque npm install
! 😡
npm ci
Et oui, pour dire à npm d’installer les dépendances à partir du fichier package-lock.json
, il faut utiliser une autre commande : npm ci
.
Date | Package X | Package Y |
---|---|---|
D | 1.0.2 | 2.0.3 |
D+5 | 1.0.2 | 2.0.3 |
D+X | 1.0.2 | 2.0.3 |
Ouf ! On a enfin figé nos versions, si on doit faire un patch dans 2 ans on ne cassera pas tout ! 🤩
semver
pour maitriser les versions de nos dépendances;npm install
résout les versions et ajoute/met à jour le package-lock.json
;npm ci
:
package-lock.json
pour partager la résolution de version avec ses collègues et pour l’intégration
continuenode_modules
, qui est construit par les commandes d’installationDans les articles suivants, nous découvrirons des fonctionnalités npm un peu plus poussées, et je vous présenterai des outils pour booster votre expérience développeur sur Node !