La convention, le package.json et les symboles de version
Je ne compte pas le nombre de fois où je me suis posé la question de ce que faisaient les symboles devant les versions dans un package.json sans en être totalement sûr.
Dans cet article, j'ai regroupé les informations permettant de mieux s'y retrouver : la convention du SemVer, le fichier qui la porte (le package.json) et la syntaxe que npm ajoute par-dessus.
Initialement, j'avais rédigé une première version de ces notes en juillet 2021. Elle comportait une petite erreur sur l'utilisation du
caret, corrigée depuis. Ce guide réunit désormais l'essentiel en un seul endroit, à garder sous la main.
I. Le SemVer, la convention
Le Semantic Versioning, plus connu sous le nom de SemVer, est une convention assez simple : avoir des numéros qui indiquent ce qui change.
L'idée n'est pas d'être parfait mais d'être prévisible pour celles et ceux qui consomment une lib ou un package. Si l'on s'appuie sur le site officiel, on retrouve la forme :
MAJEURE.MINEURE.CORRECTIF
Aux origines
Le Semantic Versioning est introduit en août 2010 par Tom Preston-Werner, cofondateur de GitHub, dans un billet fondateur qui lui donne son nom. Il est ensuite formalisé dans une spécification numérotée : la 1.0.0 en 2011, puis la 2.0.0 en 2013, la version de référence aujourd'hui.
Le problème est simple : les numéros de version sont utilisés de manière incohérente, chacun fait un peu ce qu'il veut et il est difficile de savoir si une mise à jour va casser une application. L'objectif de cette convention va donc être de donner du sens aux numéros de version, afin qu'ils deviennent exploitables.
| Type | Description |
|---|---|
| MAJEURE | Changements incompatibles avec les versions précédentes (rupture de l'API publique) |
| MINEURE | Nouvelles fonctionnalités rétrocompatibles ajoutées à l'API publique |
| CORRECTIF | Corrections de bugs rétrocompatibles |
Cette simplicité est l'une des raisons majeures de son adoption.
Une convention
Un point fondamental : le SemVer est bien une spécification formelle. Elle emploie d'ailleurs le vocabulaire normatif de la RFC 2119 (MUST, SHOULD…) mais elle n'est imposée par aucune autorité de normalisation (ni ISO, ni W3C, ni IETF). Elle parle explicitement de règles, de recommandations et de bonnes pratiques. Elle repose entièrement sur la discipline et la bonne foi des mainteneurs.
Le cœur de la convention : la notion d'API publique
Elle suppose l'existence d'une API publique clairement définie.
Une API est un outil informatique qui permet à un site internet ou à un logiciel de communiquer avec un autre ordinateur et échanger des données.
Source incroyable : https://api.gouv.fr/apropos
C'est cette API qui sert de référence pour décider si un changement est majeur, mineur ou un correctif. En pratique, modifier un comportement interne non documenté n'est pas considéré comme un breaking change au regard de l'API publique, même si cela casse le code de certains utilisateurs. Elle ne protège donc pas les usages implicites, détournés ou non documentés.
C'est un engagement moral, pas une garantie contractuelle.
Le cas particulier des versions 0.x
La spécification est très claire sur ce point : avant la version 1.0.0, l'API est considérée comme instable. On y revient plus bas, car npm traite justement ces versions 0.x de façon particulière avec le caret.
II. Le package.json, le manifeste
Le package.json est le fichier manifeste d'un projet JavaScript. Il raconte ce que le projet est, ce dont il dépend et comment il se construit ou se lance.
L'origine
Très tôt dans l'écosystème Node.js, il a fallu un format pour qu'un package déclare son nom, sa version et ses dépendances afin d'être installé correctement. Le package.json s'est imposé avec npm comme standard pour décrire un package.
C'était un moyen simple et lisible de partager un package et de permettre à npm de résoudre les dépendances.
Il n'y a pas que npm qui utilise le
package.json: yarn, pnpm et d'autres gestionnaires de paquets s'appuient aussi dessus avec une façon différente de gérer les dépendances et surtout, le moyen de les résoudre.
Les attributs
Dans les grandes lignes, on trouve plusieurs catégories de champs dans le package.json :
| Catégorie | Description |
|---|---|
| Identité |
name, version, description, license, author
|
| Dépendances |
dependencies, devDependencies, peerDependencies, optionalDependencies
|
| Scripts | commandes standardisées (npm run dev, build, test, etc.) ou non |
| Compatibilité |
engines, os, cpu pour cadrer l'environnement |
| Publication | ce qui est inclus dans le package, la version, les fichiers d'entrée |
| Customisation | des champs spécifiques aux outils (babel, eslint, jest, etc.) ou tout simplement qu'on souhaite utiliser |
Dans la partie scripts, j'ai récemment découvert un mécanisme sympa :
pre<nomScript>etpost<nomScript>se lancent automatiquement avant/après n'importe quel script (ex :prebuild/postbuildautour debuild).
{
"name": "mon-projet", // identité du package
"version": "1.0.0", // version publiée
"description": "Mon app de démo",
"license": "MIT", // licence du package
"author": "necraidan",
"scripts": {
"dev": "vite", // commande de dev
"build": "vite build", // build de production
"test": "vitest" // tests
},
"dependencies": {
"react": "^18.3.0" // deps runtime
},
"devDependencies": {
"vite": "^5.4.0", // outils de dev
"vitest": "^2.1.0"
},
"peerDependencies": {
"react": ">=18" // compatibilité attendue
},
"optionalDependencies": {
"fsevents": "^2.3.3" // optionnel (ex: macOS)
},
"engines": {
"node": ">=20" // version Node ciblée
},
"eslintConfig": {
"extends": ["eslint:recommended"] // champs custom
}
}
Les types de dépendances
| Type | Description |
|---|---|
dependencies |
indispensables au fonctionnement de l'app en production |
devDependencies |
utiles en développement (tests, lint, build), pas nécessaires en prod |
peerDependencies |
indiquent la compatibilité attendue avec un autre package sans l'embarquer soi‑même (ex. une lib React qui attend React installé) |
optionalDependencies |
optionnelles ; si l'installation échoue, npm continue quand même. C'est au consommateur de gérer l'absence au runtime |
bundleDependencies |
dépendances embarquées lors de la publication du package |
Pour aller plus loin, la documentation officielle du package.json.
Focus sur les peerDependencies
Une peerDependency, c'est une dépendance que le package n'installe pas lui-même : il déclare seulement une compatibilité.
le but : éviter les doublons et les conflits de versions (cas typiques : eslint, vite, react côté librairies).
Depuis npm 7+, les peers sont prises en compte automatiquement pendant la résolution et des conflits de versions peuvent faire échouer l'installation :
{
"name": "eslint-plugin-acme",
"peerDependencies": {
"eslint": "^9.0.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
}
Ici, le plugin n'installe pas eslint : il annonce juste la version attendue. C'est le projet hôte qui doit fournir eslint (en dependencies ou devDependencies selon son usage).
Le bloc peerDependenciesMeta indique que typescript est une peer optionnelle : si typescript n'est pas présent dans le projet hôte, l'installation ne doit pas échouer pour cette raison.
Le lockfile de npm : le package-lock.json
Un lockfile permet de figer les dépendances. Il est conçu pour être versionné dans le repo afin d'assurer des installations prédictibles et rejouables. Cependant, cela peut varier selon l'OS, l'architecture, les dépendances optionnelles ou encore ce qui est spécifié dans les champs os/cpu.
Le package-lock.json décrit l'état exact résolu : l'arbre complet des dépendances (y compris transitives), avec versions précises, sources de téléchargement (resolved) et empreintes (integrity).
Ce qu'on trouve dedans
Concrètement, on va y trouver un lockfileVersion pour la version du format. La partie packages est la description des packages par chemin, avec resolved et integrity (présent avec le package-lock.json v2+, npm 7+ ; aujourd'hui, npm 9+ génère un lockfileVersion: 3).
npm install vs npm ci
La commande npm install va installer les dépendances en s'appuyant sur package.json et le package-lock.json. Il peut le régénérer ou le modifier selon les cas.
La commande npm ci va suivre le package-lock.json, elle échoue si le package.json et le package-lock.json ne correspondent pas, supprime les node_modules (en théorie 😅) avant de lancer l'installation et ne touche pas au package-lock.json.
Cas avancés
npm-shrinkwrap.jsona priorité surpackage-lock.jsonet fige l'arbre des dépendances pour un package publié. En pratique, c'est surtout adapté aux applications/CLI publiées et généralement déconseillé pour les bibliothèques.L'attribut
overridespeut être utilisé pour forcer une version d'une dépendance transitive et surcharger une version particulière.
III. Les symboles de version avec npm
npm va venir enrichir le SemVer avec des symboles. Ce sont des ranges (plage de versions en bon français). Elles indiquent ce que le projet accepte comme mises à jour, notamment lors des npm install.
Le symbole ^ : "jusqu'à la prochaine release majeure"
Le caret autorise les mises à jour tant que le premier chiffre non nul reste identique :
"react": "^1.2.3", // >=1.2.3 <2.0.0
"shiki": "^0.2.3", // >=0.2.3 <0.3.0
"tslib": "^0.0.3", // >=0.0.3 <0.0.4
Sur les versions 0.x, le caret est plus restrictif. Dans notre exemple, ^0.2.3 bloque le passage à tout ce qui est supérieur ou égal à 0.3.0. C'est cohérent avec le statut instable des versions 0.x vu plus haut.
Le symbole ~ : "patchs uniquement"
Le tilde autorise des changements au niveau correctif, tant que la mineure ne bouge pas :
"shiki": "~1.2.3", // >=1.2.3 <1.3.0
"tslib": "~0.2.3", // >=0.2.3 <0.3.0
Les symboles >, >=, <, <=, = : comparateurs
Ces opérateurs fonctionnent comme des comparateurs classiques :
"react": ">1.2.3", // strictement supérieur
"shiki": ">=1.2.3", // supérieur ou égal
"tslib": "<2.0.0", // strictement inférieur
"axios": "<=2.0.0", // inférieur ou égal
"redux": "1.2.3", // (ou `=1.2.3`) version exacte
Le symbole - : inclusif
Un intervalle avec tiret est inclusif sur les bornes :
"tslib": "1.2.3 - 2.3.4", // >=1.2.3 <=2.3.4
"shiki": "0.2.0 - 0.4.2", // accepte 0.4.2 mais pas 0.4.3
Le symbole || : combiner des ensembles
Un intervalle avec le double pipe combine les deux ensembles :
"tslib": "^0.2.0 || >=0.5.0 <1.2.0" // Compatible avec l'ensemble ^0.2.0 ou >=0.5.0 <1.2.0
Le dist-tag : latest
Point important : latest n'est pas une règle SemVer. C'est un dist-tag npm.
Exemple :
"shiki": "latest"
Par défaut, la commande npm install <pkg> installe la version pointée par le tag latest (si on ne met ni @<version> ni @<tag>). Un mainteneur peut alors faire pointer latest vers une version qui n'est pas forcément "la plus récente" au sens strict et utiliser d'autres tags (beta, next, etc.).
Bonus : x, * et les pré-releases -alpha, -beta
Exemples :
"tslib": "1.2.x", // >=1.2.0 <1.3.0
"shiki": "1.2.*", // >=1.2.0 <1.3.0
"redux": ">=1.2.3-0 <1.3.0" // inclut les pré-releases >=1.2.3-*
Les pré-releases (-alpha, -beta) ont des règles spécifiques : par défaut, elles sont exclues des ranges. Pour les inclure, on peut utiliser une borne avec -0 (ou une version pré-release) dans la range.
Conclusion
Le SemVer est né d'un besoin très concret : rendre les mises à jour plus prévisibles dans un monde de dépendances automatisées. Il apporte un langage commun, une structure simple et surtout une convention. Mais attention, ça ne fonctionne que si l'ensemble de la chaîne la respecte : cette convention aide à faire confiance mais ne dispense pas de vérifier.
Le package.json, lui, décrit l'intention du projet (scripts, compatibilité, plages de versions) et le package-lock.json fige l'état exact installé : c'est ce duo qui rend les builds reproductibles et qui évite les "ça marche chez moi". En pratique : on versionne le lockfile, on installe avec npm ci en CI et on relit ses diffs comme du code.
Quant aux symboles, une fois qu'on a en tête le caret, le tilde et les ranges, on lit un package.json sans se reposer la question à chaque fois. C'est tout l'intérêt de ce mémo : l'avoir sous la main et le mettre dans ses favoris.
Sources
Spécification officielle du Semantic Versioning
https://semver.org/Calculateur de ranges
npm
https://semver.npmjs.com/Documentation npm – Semantic Versioning & ranges
https://docs.npmjs.com/about-semantic-versioningDocumentation npm – package.json
https://docs.npmjs.com/cli/v11/configuring-npm/package-jsonDocumentation npm – package-lock.json
https://docs.npmjs.com/cli/v11/configuring-npm/package-lock-json/Documentation npm – npm-shrinkwrap.json
https://docs.npmjs.com/cli/v11/configuring-npm/npm-shrinkwrap-json/Tom Preston-Werner – Semantic Versioning
https://tom.preston-werner.com/2010/08/23/semantic-versioning.htmlHynek Schlawack – Semantic Versioning Will Not Save You
https://hynek.me/articles/semver-will-not-save-you/Discussions communautaires sur les limites du SemVer
https://clojureverse.org/t/stop-using-semantic-versioning-any-writings-on-this/9951
Top comments (0)