Une bataille a lieu depuis quelques années, et sur le ring, on ne trouve ni Elon ni Mark, mais deux standards Javascript : ESM et CJS.
Note: il existe une version en anglais de cet article.
L'affrontement de ces deux standards entraine régulièrement des débats parfois enflammés. J'ai tenté d'y voir plus clair.
En cause donc, ESM (pour "ECMAScript Modules") et CJS (pour "CommonJs"), deux standards de format pour diviser du code ("code splitting"). Ce qui amène notre première question :
Plusieurs articles reviennent sur l'histoire de cette cohabitation, mais après une lecture attentive de ceux-ci et surtout une plongée passionnante dans le Google group à la fondation de CommonJS, je résumerais les choses ainsi :
Après 12 ans d'existence environ, Javascript prend de l'importance, Javascript a besoin de nouvelles ressources que celles en place pour gérer du code, notamment parce que le besoin d'utiliser Javascript pour faire du serveur se fait sentir. C'est l'appel que lance Kevin Dangoor le 29 janvier 2009 dans "What Server Side Javascript Needs?" et qui permet la mise en place d'un Google groupe pour créer un standard de modularisation : CommonJs.
Le + de Dre Drey
CommonJS permet donc de modulariser son code - aka le séparer en parties qui peuvent être importées et exportées vers et depuis d'autres fichiers. Malgré les envies du début, ce standard sera surtout développé dans l'objectif de faciliter l'utilisation de Javascript comme langage serveur. D'ailleurs, on trouve aussi dans le groupe de travail de CommonJS Ryan Dahl, qui se saisit de cette opportunité pour présenter la première version de Nodejs.
Mais CJS possède quelques désavantages fondamentaux qui ne rendent pas son utilisation optimale du côté des navigateurs, comme le résume Andy Jiyang :
- module loading is synchronous. Each module is loaded and executed one by one, in the order that they are required.
- difficult to tree-shake, which can remove unused modules and minimize bundle size.
- not browser native. You need bundlers and transpilers to make all of this code work client-side. With CommonJS, you are stuck with either big build steps or writing separate code for client and server."
(On trouve encore bien d'autres problèmes à CJS quand on creuse un peu : la sécurité par exemple (comme le rappelle Dan Fabulich "(...)in CJS both module and exports can be replaced on the fly within the module itself."))
Le + de Dre Drey
Du coup, l'instance de standardisation de Javascript, ECMAScript, se penche sur la question. ECMAScript, fondé en 1997 et géré par ECMA (l'European association for standardizing information and communication systems) publie régulièrement de nouvelles normes et en 2015, le ECMAScript 6 (ES6) introduit un nouveau standard pour modulariser son code en JS, l'ESM. Ce standard est plus adapté au côté client que ne l'est CommonJS, et répond à certains problèmes posés par CJS (par exemple faire de l'asynchrone, ce qui est quand même plutôt pratique).
S'il ne fallait retenir qu'une chose de cette historique, c'est que depuis 2015 deux grands standards dominent quand il faut modulariser son code en Javascript : CommonJS et ESM. Ils sont pensés les deux pour répondre à un même besoin, modulariser du code, mais :
Alors on pourrait se dire que l'existence de deux standards, tout aussi étrange qu'elle soit, n'est pas forcément un problème. Les deux pourraient cohabiter, avec chacun leur environnement de prédilection (le navigateur d'un côté et le serveur de l'autre). Ce qui amène notre deuxième question : c'est quoi, le problème au final ?
Le noeud central du problème, c'est que cette cohabitation est difficile, car l'interopérabilité (c'est-à-dire la capacité à fonctionner ensemble) entre ESM et CommonJs est très, très trèèèès faible.
Les problèmes se voient d'ici et ils se découpent grosso modo en deux :
Si CJS et ESM possèdent les deux les mêmes types d'import/export (par défaut et nommés), les syntaxes sont différentes.
Exemples avec deux fichiers, maths.js
et index.js
.
En CSJ, l'import/export se reconnaisst par l'utilisation des mots clé module et require :
//dans mon fichier math.js, j'exporte ma fonction avec module.exports :
module.exports = function sum(x, y) {
return x + y;
};
// puis je l'importe dans mon fichier "index.js" avec require() :
const somme = require("./math.js");
En ESM, lesimport/export se reconnaissent par l'utilisation du mot clé import.
//dans mon fichier maths.js, j'exporte ma fonction avec export.default :
export default function sum(x, y) {
return x + y;
}
//dans mon fichier main.js, je l'importe avec import:
import somme from "./maths.js";
Autre grosse différence : l'utilisation d'ESM ne change pas seulement la façon d'importer et exporter les modules, car c'est tout un standard qui englobe aussi d'autres fonctionnalités.
Ainsi, quand on utilise le standard ESM, ce n'est pas uniquement pour préférer écrire import plutôt que require; par exemple, ESM ne transmet pas les contextes, ce qui implique entre autres un renvoi this
différent (undefined ), ESM possède un StrictMode alors que CommonJS non, etc.
Finalement, on l'a déjà dit, mais CJS est synchrone et ESM est asynchrone ! Ce qui rend les deux très non interopérables est donc le fait qu'avec ESM on peut utiliser le mot clé await
en top level (en dehors d'une fonction async par exemple), alors que CJS ne peut pas lire ce script ! Donc CJS ne peut pas transpiler un script ESM qui utiliserait un await en top level.
Il est bon de savoir que, quand on lance un projet avec un npm init
, c'est une configuration en CJS "par défaut" qui se lance (tous les fichiers .js sont considérés comme étant en cjs). Il faudrait donc utiliser la syntaxe de CJS dans le projet. Si l'on veut utiliser ESM dans un projet en CJS par défaut, il faut le signaler en ajoutant l'extension .mjs au fichier concerné.
Si l'on veut configurer tout le projet en ESM, il faut modifier la configuration dans le package.json
en ajoutant "type":"module"
qui indique que les fichiers .js sont en ESM par défaut. A ce moment, pour utiliser du CJS, il faut le signaler en utilisant l'extension .cjs dans les fichiers concernés.
Imaginon que l'on a un bout de code qui utilise le standard ESM et un autre bout du CJS. Il faut appliquer toute une série de règles:
Franchement j'ai essayé de comprendre ce qui était faisable ou non, notamment l'explication de Dan Falbulich, mais même après plusieurs relectures, ce que je comprends c'est toujours :
Je comprends rien
Super, maintenant qu'on sait ce que c'est CJS et ESM, comment ils en sont arrivés à vivre ensemble et les problèmes qu'ils posent, une dernière question subsiste quand même :
Une fois que l'on s'est bien arraché les cheveux comme moi en restant bloqué.e un jour devant une impossibilité totale de fonctionnement entre deux librairies sur un projet, qu'est-ce qu'on fait concrètement devant ça (j'ai essayé comme Perceval de dire "grelotte ça picote", dans le doute, mais sans surprise ça n'a rien résolu) :
This module can only be referenced with ecmascript
Il y a débat dans la communauté JS sur les avantages et désavantages de chaque standard et sur les bonnes pratiques à adopter. Pour aller plus loin sur ce sujet, Andrea Giammarchi qui a fait partie du NodeJs module working group donne les + et - de chaque position dans un article assez nuancé (ce qui est rare sur ce sujet).
Malheureusement, concrètement, pas de solutions miracles, même si les auteur.trice.s de librairies peuvent s'en sortir en appliquant les quelques règles d'Andrea Giammarchi :
Consider shipping only ESM
If you’re not shipping only ESM, ship CJS only
Test that your CJS works in ESM
If needed, add a thin ESM wrapper for your CJS named exports
Add an
exports
map to yourpackage.json
.
Pour les les dév qui intègrent ces librairies sur leurs projets, plusieurs écoles s'affrontent :
Je vous recommande chaudement de plonger dans les échanges qui donnent naissance à CommonJS et à NodeJS pour comprendre les choix qui ont été faits.
Mais aussi de voir les débats sur différentes communautés et ou plateformes :
Et quelques articles bien résumés, pour entrer davantage dans les détails techniques :