DEV Community

Cover image for SemVer & npm : le guide complet
Benjamin Auzanneau
Benjamin Auzanneau

Posted on • Originally published at aratama.dev

SemVer & npm : le guide complet

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> et post<nomScript> se lancent automatiquement avant/après n'importe quel script (ex : prebuild / postbuild autour de build).

{
  "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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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.json a priorité sur package-lock.json et 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 overrides peut ê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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Le dist-tag : latest

Point important : latest n'est pas une règle SemVer. C'est un dist-tag npm.

Exemple :

"shiki": "latest"
Enter fullscreen mode Exit fullscreen mode

Par défaut, la commande npm install &lt;pkg&gt; installe la version pointée par le tag latest (si on ne met ni @&lt;version&gt; ni @&lt;tag&gt;). 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-*
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)