Comprendre le dossier node_modules

Voir la vidéo

Le dossier node_modules est un dossier un peu mystérieux qui est la source de pas mal de problèmes. Aujourd'hui je vous propose de plonger dans le vif du sujet et d'analyser son rôle dans la résolution des dépendances dans le cadre de NodeJS.

Comment NodeJS résoud

Prenons donc un exemple simple d'import

import {hello} from 'hello'

hello()

Dans le cas d'un import qui ne consitue pas un chemin relatif (non préfixé par ./, ../ ou /) NodeJS dispose de son propre système de résolution qui peut être résumé de la manière suivante :

  • Il cherche un dossier node_modules/NOM_DU_MODULE adjacent au fichier courant
  • Si le dossier n'est pas trouvé il fait la même recherche dans le dossier parent (et cela récursivement)

Si le fichier qui demande le module est lui même dans un dossier node_modules dans ce cas là il cherche directement dans ce dossier (plutôt que de cherche un dossier node_modules adjacent).

Si un dossier portant le nom du module est trouvé il va alors lire le fichier package.json de ce dossier pour connaitre le fichier à importer gràce aux clef main ou exports.

{
  "main": "src/index.js"
}

Cela lui permet de résoudre le fichier qui va donc être importé dans notre fichier original et l'import sera traduit comme cela :

import {hello} from './node_modules/hello/src/index.js'

hello()

Le cas des liens symbolique

Les liens symboliques viennent rajouter une petite couche de complexité dans la résolution des dépendances car NodeJS utilise le chemin réel des fichier dans sa résolution. Prenons cet exemple :

/Projet1
  /node_modules
    /a -> /Projet2/node_modules/a
    /b
  /src
    index.js
  package.json
/Projet2
  /node_modules
    /a
    /b

Le module a dépend du module b et on a à l'intérieur de son fichier un import b from 'b'

Si le fichier index.js essaie de charger le module a, le lien symbolique sera suivi et lors de la recherche du module b NodeJS explorera le dossier Projet2 et ses dossiers parents. Ce comportement peut être modifié à l'aide d'un drapeau --preserve-symlinks (drapeau que l'on utilisera peu souvent car ça crée d'autres problèmes).

Et npm dans tout ça ?

Maintenant que l'on a compris le fonctionnement de la résolution des paquets, on va parler du gestionnaire de dépendance npm et sa manière d'installer les dépendances. Si on installe une dépendance vite par exemple on se retrouve avec la structure suivante.

{appDir}
 ├── src
 │   ├── index.js
 └── node_modules
      |── vite
      |── esbuild
      |── postcss
      |── resolve
      |── rollup
      └── fsevents

On remarque qu'il place toutes les dépendances à la racine du dossier node_modules pour exploiter le système de dossiers parents dans la résolution des dépendances. Ce hoisting permet à plusieurs modules qui partage une même dépendance d'utiliser le même dossier (plutôt que d'installer plusieurs fois la librairie). Mais que se passe-t-il si on installe un paquet a qui a besoin d'une version de esbuild différente par exemple ?

{appDir}
 ├── src
 │   ├── index.js
 └── node_modules
      |── a
      |   |── package.json
      |   |── index.js
      |   └── node_modules
      |       └── esbuild
      |── vite
      |── esbuild
      |── postcss
      |── resolve
      |── rollup
      └── fsevents

Dans ce cas là npm n'a pas d'autres choix que d'installer 2 fois esbuild. Une version sera laissée dans le sous dossier node_modules de a afin que les import fait depuis ce dossier résolve cette version, tandis que les autres fichiers en dehors du module a continuerons à résoudre le dossier esbuild à la racine.

Attention par contre si vous utiliser npm link pour gérer des dépendances croisées car la résolution des liens symboliques s'applique. Aussi, dans le cas d'un monorepo il faudra être vigilant à ce que les dépendances communes à plusieurs modules se retrouvent bien dans un dossier à la racine au risque de dupliquer la dépendance plusieurs fois.

Publié
Technologies utilisées
Auteur :
Grafikart
Partager