> ## Content Index
> Fetch the complete content index at: https://www.sfeir.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# Vendoriser plutôt qu'importer : un comparateur Leaflet sans dépendance
- URL: https://www.sfeir.dev/securite/vendoriser-plutot-quimporter-leaflet/
- Published: 2026-10-19T06:05:20.000Z
- Updated: 2026-10-19T08:41:44.000Z
- Description: 🔓 Besoin d'un comparateur « avant/après » en Leaflet ? leaflet-side-by-side tire underscore, postcss et 17 vulnérabilités pour rogner deux images. Downgrader, forker en v0.0.7 ou vendoriser : l'arbre de décision, et comment 90 lignes de clip-path ont tout réglé.
- Author: Romain Lespinasse
- Tags: Sécurité, JavaScript, best practices, CSS, opensource

> Deux époques d'une même ville, séparées par un rideau que l'on fait glisser, et un `npm audit` qui passe au rouge pour ça.

J'avais besoin d'un classique de la dataviz cartographique : un comparateur « avant / après », ce rideau vertical que l'on glisse pour révéler deux couches côte à côte. Sur [*ChronoMEL*](https://sfeir.dev/afficher-90-ans-orthophotos-metropole?ref=sfeir.dev), ma visionneuse d'orthophotos de la métropole lilloise, il sert à confronter deux millésimes : 1930 à gauche, 2025 à droite, au même endroit, dans le même onglet.

Le réflexe, c'est `npm install`. Il existe un plugin pour ça : `leaflet-side-by-side`. Je l'installe. Et `npm audit` s'allume comme un sapin de Noël. Voici l'enquête sur le coût réel d'une micro-dépendance, et pourquoi j'ai fini par tout réécrire à la main, pour un meilleur résultat.

## Le déclencheur

```text
found 17 vulnerabilities (3 moderate, 9 high, 5 critical)

```

Dix-sept vulnérabilités pour un composant qui, au fond, superpose deux images et les rogne. Le chiffre paraît absurde, il l'est. Mais il ne sort pas de nulle part : il est la somme d'un arbre de dépendances dont je n'avais jamais vu les branches. Et c'est là tout le sujet : on installe un arbre, on hérite d'une forêt.

## L'anatomie de l'arbre

`leaflet-side-by-side`, c'est environ **200 lignes utiles**. Sauf qu'il dépend de `cssify` et `css-img-datauri-stream`, de l'outillage de l'ère *browserify*, dont le seul rôle est d'inliner deux fichiers [CSS](https://www.sfeir.dev/tag/css/) au moment du build. Et ces deux-là tirent à leur tour :

- `underscore` (dans une version porteuse d'une faille critique) ;
- `postcss` (une branche ancienne, plusieurs CVE) ;
- `mime`, `elliptic`, et quelques autres reliques transitives.

```text
leaflet-side-by-side
└─ cssify
   └─ css-img-datauri-stream
      ├─ underscore         ← critique
      ├─ postcss@5          ← ancien
      └─ mime, elliptic…

```

Toute cette dette de sécurité ne sert qu'à transformer deux `.css` en chaînes de caractères. Pour un composant dont le cœur fonctionnel, la logique du rideau, ne dépend de *rien* d'autre que de Leaflet lui-même. Le ratio valeur / surface d'attaque est catastrophique : on importe une dizaine de paquets transitifs pour économiser… une dizaine de lignes de CSS. Quand on ne peut pas couper une branche comme celle-ci (parce qu'elle appartient à une vraie dépendance qu'on garde), le champ `overrides` de npm permet de forcer la version d'un paquet transitif sans attendre que l'amont publie le correctif.

## L'arbre de décision

Devant un `npm audit` rouge, on a rarement une seule porte de sortie. J'en avais trois, par ordre d'effort croissant :

![Trois issues face à un npm audit rouge : downgrader (API 0.7 abandonnée), changer de fork (faux ami en v0.0.7), vendoriser (0 dépendance, 0 vuln)](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/2026/09/arbre-decision-image.webp "Trois issues face à un npm audit rouge")

Trois issues face à un npm audit rouge

1. **Downgrader** vers `leaflet-side-by-side@1.1.0`, antérieur à `cssify`. Échec : cette version vise l'API Leaflet 0.7, abandonnée depuis des années. On troquerait une faille de sécurité contre une incompatibilité d'API.
2. **Changer de fork** pour une version « maintenue » : `leaflet-swipe-mode`, qui ne dépend que de `leaflet`. Sur le papier, l'option idéale.
3. **Vendoriser** : réécrire le composant soi-même, dans le dépôt.

L'option 2 semblait la plus raisonnable : pourquoi réinventer la roue ? C'était un piège.

## Le faux ami

J'ai donc tenté le fork « propre » d'abord. Mauvaise surprise sur toute la ligne. Le readme annonce une API `L.control.sideBySide`, mais le code expose en réalité `L.Control.swipeMode` : une heure perdue à chercher pourquoi rien ne s'affichait. Le paquet dépend aussi implicitement de `window.L`, ce qui devient fragile dès qu'on passe par un bundler ESM où Leaflet n'est pas un global ; il a fallu écrire un shim pour le réexposer. Et la version affichée, `0.0.7`, avec une faible adoption et un dernier commit lointain, correspond exactement au profil de paquet qui finira abandonné à son tour.

Je me suis alors demandé si remplacer une micro-dépendance abandonnée par une autre micro-dépendance en `v0.0.7` était vraiment un gain de robustesse, ou juste un déplacement du risque d'un cran, en attendant la prochaine alerte. Pour un composant aussi central à l'application, je voulais une réponse durable.

## La décision : vendoriser

J'ai écrit le composant de zéro [avec une IA](https://www.sfeir.dev/ia/cadrer-avant-de-prompter-notre-methode-pour-gerer-30-prs-par-semaine/), sans repartir des plugins existants. Le cœur du sujet (superposer deux couches, les rogner, suivre le séparateur) tient en **\~90 lignes**, sans aucune dépendance hors Leaflet. À ce niveau de simplicité, l'écrire soi-même devient plus sûr que d'importer. Le calcul a un coût : ces \~90 lignes, c'est désormais à moi de les maintenir, de les tester, de suivre les évolutions de l'API Leaflet. Il ne tient que parce que le composant est petit, stable et bien compris. Voyons comment.

### Comprendre `clip-path` (le ressort du composant)

Tout repose sur `clip-path`. Elle rogne la zone visible d'un élément selon une forme, ici un simple rectangle exprimé comme un polygone à quatre points :

```css
.couche { clip-path: polygon(x1 y1, x2 y2, x3 y3, x4 y4); }

```

Tout ce qui sort du polygone devient invisible, mais l'élément, lui, garde sa taille et sa position. C'est exactement ce qu'il faut ici : afficher une moitié d'une couche de carte sans la déplacer ni la redimensionner. On applique deux `clip-path` complémentaires (gauche et droite) qui se rejoignent pile sur le séparateur. C'est simple, et surtout performant : le navigateur ne recompose pas la géométrie, il masque.

![Deux couches carte superposées, rognées par deux clip-path: polygon() complémentaires qui se rejoignent sur le séparateur vertical à la position X](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/2026/09/clip-image.webp "Le rognage des deux couches par clip-path: polygon()")

Le rognage des deux couches par clip-path: polygon()

`clip-path` remplace l'ancienne propriété `clip` (dépréciée, retirée des spécifications modernes, et qui n'acceptait qu'un rectangle sur un élément positionné en absolu). `clip-path` gère des formes arbitraires : cercles, polygones, et un équivalent rectangulaire dédié, `inset()`. Pour un rectangle aligné sur les axes de l'écran, deux syntaxes sont possibles. Elles ne mesurent pas leur rectangle de la même façon : `polygon()` prend des positions absolues pour chaque coin, alors qu'`inset()` prend des distances de retrait depuis chacun des bords opposés. Comme `containerPointToLayerPoint` (plus bas) fournit justement des positions absolues, `polygon()` les consomme directement, alors qu'`inset()` demanderait de les convertir en soustrayant à la largeur et à la hauteur du conteneur, à chaque déplacement. `polygon()` reste donc l'écriture la plus directe ici, tout en restant l'outil moderne et parfaitement supporté par les navigateurs.

### Le principe : `clip-path` \+ `containerPointToLayerPoint`

Pour rester aligné pendant les déplacements et les zooms, on convertit les coins de la fenêtre en *layer points* (le repère interne de Leaflet, qui suit la carte), puis on recalcule les deux polygones à chaque mouvement :

```js
_update() {
  const nw = this._map.containerPointToLayerPoint([0, 0]);
  const se = this._map.containerPointToLayerPoint(this._map.getSize());
  const clipX = nw.x + this._dividerX();
  this._divider.style.left = `${this._dividerX()}px`;
  this._left.getContainer().style.clipPath  = `polygon(${nw.x}px ${nw.y}px, ${clipX}px ${nw.y}px, ${clipX}px ${se.y}px, ${nw.x}px ${se.y}px)`;
  this._right.getContainer().style.clipPath = `polygon(${clipX}px ${nw.y}px, ${se.x}px ${nw.y}px, ${se.x}px ${se.y}px, ${clipX}px ${se.y}px)`;
}

```

### Le séparateur déplaçable

Astuce qui évite de réinventer le drag : un `<input type="range">` plein écran, piste transparente, seule la poignée reste visible. On écoute l'événement `input` pour redessiner, et (détail crucial) on désactive le drag de la carte pendant la manipulation, sinon glisser le rideau déplace aussi le fond :

```js
this._range.addEventListener('mousedown',  () => this._map.dragging.disable());
this._range.addEventListener('touchstart', () => this._map.dragging.disable());
document.addEventListener('mouseup',  () => this._map.dragging.enable());

```

### Le CSS de la poignée

La poignée circulaire et ses chevrons sont injectés en `data:image/svg+xml` : zéro asset externe, donc rien à inliner, donc plus aucune raison d'avoir `cssify` dans l'arbre. La cause même des 17 vulnérabilités disparaît. Penser à styliser `::-webkit-slider-thumb` et `::-moz-range-thumb` : les deux moteurs ne partagent pas le même pseudo-élément.

### Brancher / débrancher proprement

Un composant Leaflet propre se nettoie après lui :

- sur l'événement `move` de la carte → appeler `_update()` ;
- dans `remove()` → annuler les `clip-path`, retirer les écouteurs et le DOM ajouté.

Sans ce ménage, on laisse des écouteurs actifs à chaque branchement/débranchement de la carte, le genre de fuite invisible qui finit par ramer après quelques navigations. Sur ChronoMEL, ce composant se branche via le hook `onReady` de [leaflet-atlas](https://sfeir.dev/leaflet-atlas-carte-geojson-config?ref=sfeir.dev), la carto pilotée par la config qui porte le reste de l'application.

## Les pièges à connaître

- Même couche des deux côtés : le second `clip-path` écrase le premier, on ne voit qu'une moitié. Il faut bien deux objets de couche distincts.
- z-index et panes : le rideau doit passer au-dessus des tuiles, mais sous les contrôles Leaflet, sinon on masque les boutons de zoom.
- Ordre des points de `polygon()` : les quatre coins doivent être donnés dans le même sens de parcours (ici horaire, en partant du coin haut-gauche). Un point inversé et le polygone se croise au lieu de dessiner un rectangle net.

## Conclusion

![Une balance : plateau gauche, un fichier unique et deux compteurs à zéro sous le label vendoriser ; plateau droit, trois caisses de vraies dépendances sous le label installer.](https://storage.ghost.io/c/0b/31/0b31478e-a66a-46f1-8c34-d44388e21192/content/images/2026/09/bilan-image-2.webp "Vendoriser ou installer : un arbitrage composant par composant")

Vendoriser ou installer : un arbitrage composant par composant

Le vrai problème, c'est l'arbre que `leaflet-side-by-side` traîne avec lui : une dépendance embarque tout ce qu'elle tire derrière, au-delà de son seul `index.js`. Vendoriser reste un choix ponctuel, à juger composant par composant plutôt qu'à appliquer en principe général. Pour une vraie dépendance (un parseur de projections, un moteur de tuiles vectorielles), recopier serait de l'inconscience : on hériterait de la complexité sans l'expertise ni les correctifs de l'amont. Pour un composant minuscule, stable, qu'on comprend entièrement, la question à se poser est simple : suis-je capable de tenir ce code seul ? Si oui, recopier bat souvent installer, pour \~90 lignes qui deviennent **0 dépendance, 0 vulnérabilité**.

Rien n'oblige non plus à trancher dès le premier `npm install`. On peut très bien commencer par la dépendance, et ne vendoriser que le jour où le besoin se fait sentir, comme ici avec `npm audit` qui s'allume. Vendoriser n'est pas une décision qu'on prend au premier commit : c'est une option qui reste disponible tant que le composant reste petit.

Le reste de l'arbre garde ses règles habituelles : lire le résultat de `npm audit` au lieu de le subir, committer le lockfile, [grouper les mises à jour Dependabot](https://www.sfeir.dev/automatiser-dependabot-sans-perdre-le-controle-de-vos-mises-a-jour/) pour ne pas se noyer sous les PR. Une autre histoire de CVE héritée, celle de [github-slug-action](https://www.sfeir.dev/5-ans-de-github-slug-action-une-aventure-open-source/), pour une prochaine fois.

La suite revient à la carte elle-même : [donner une date à chaque objet](https://sfeir.dev/projeter-donnees-datees-carte-temporelle?ref=sfeir.dev) pour qu'il n'apparaisse qu'à partir de son millésime.