De CommonJS à ESM, les clés d’une migration réussie

Pebie
ekino-france
Published in
8 min readMay 15, 2024

Introduction

La migration de CommonJS (CJS) vers ECMAScript Modules (ESM) est une question récurrente dans le développement d’applications Node.js. Il est essentiel pour les développeurs de rester à jour avec les dernières normes et pratiques. Certains développeurs font le choix de ne maintenir qu’une seule version de leur librairie (chalk, nanoid). D’autres outils imposent l’adoption des modules ESM. Par exemple Deno qui ne prend pas en charge le développement de module CommonJS.

Dans une première partie, nous aborderons la comparaison entre CommonJS et ESM et leur historique.

Dans un second temps, nous partagerons notre retour d’expérience sur la migration de CJS vers ESM. De la refactorisation du code existant à l’adaptation des outils et des workflows, nous examinerons les différentes facettes de ce processus et nous tenterons de répondre à la question de la migration CJS vers ESM.

Enfin, nous présenterons notre bilan et conclurons sur une ouverture en abordant les nouveautés ESM dans les versions récentes de Node.js ainsi que l’interopérabilité entre CommonJS et ESM (notamment à travers Bun). Nous réfléchirons également aux implications plus larges de cette transition pour la communauté des développeurs et les futurs développements.

Évolution des modules JavaScript : de CJS à ESM

L’évolution des modules JavaScript illustre une transition significative dans la manière dont le langage est structuré et utilisé.

Initialement, le format CommonJS, introduit en 2009, était largement adopté pour organiser le code JavaScript côté serveur, en particulier avec Node.js. CommonJS offre une approche simple et efficace pour l’organisation modulaire du code, facilitant le développement d’applications côté serveur.

Puis en 2015, avec l’évolution des besoins et des standards du web, ECMAScript 2015 (également connu sous le nom d’ES6) a introduit les modules ECMAScript (ESM), offrant une syntaxe plus expressive et une meilleure intégration avec les outils de développement modernes.

Comparaison entre CommonJS et ESM :

Comparaison entre les modules CommonJS et ESM
Comparaison entre les modules CommonJS et ESM

Voici notre retour d’expérience sur la migration de CommonJS vers ESM. Nous rappellerons les objectifs et les enjeux pour chaque typologie de projet sur lesquels nous avons tenté l’exercice.

Migration CJS — ESM : retour d’expérience

Objectifs

La migration d’un module de CommonJS vers ESM peut être motivée par plusieurs objectifs :

- Une meilleure interopérabilité avec d’autres écosystèmes JavaScript, tels que les navigateurs web modernes et les outils de développement frontend. Cela facilite le partage de code entre différents environnements JavaScript.

- Standardisation : Les modules ESM font partie des spécifications ECMAScript, ce qui signifie qu’ils sont devenus un standard dans l’écosystème JavaScript. Migrer vers ESM permet de suivre ces standards et de bénéficier des fonctionnalités et des améliorations introduites dans les versions les plus récentes de JavaScript.

- Meilleure gestion des dépendances : Les modules ESM permettent une résolution statique des dépendances, ce qui simplifie la gestion des dépendances et réduire les risques d’erreurs liées à la résolution dynamique des dépendances dans CommonJS.

- Performances améliorées dans certaines situations, notamment en raison de leur résolution statique des dépendances, ce qui permet une meilleure optimisation lors de la compilation et de l’exécution du code.

- Les modules ESM encouragent une structure de code plus modulaire et déclarative, ce qui rend le code plus facile à lire, à comprendre et à maintenir sur le long terme.

- Compatibilité future : Alors que de plus en plus d’environnements JavaScript adoptent les modules ESM, migrer vers ESM assure une meilleure compatibilité future avec les versions futures de Node.js et d’autres plateformes JavaScript.

Typologies de projets et enjeux

Les ESM offrent d’énormes possibilités en termes de gain de performances à l’image de Daniel Rosenwasser et Jake Bailey chez Microsoft qui détail dans l’article “TypeScript’s Migration to Modules” l’impact des modules ESM dans leur travail sur la version 5.0 de Typescript.

De notre côté, nous avons travaillé sur 2 typologies de projets sur lesquels nous avons tenté de migrer le code. Pour chacune d’entre elles il s’agit de projet Node.js.

Micro-Projet

Pour ce type de projet nous voulions réécrire une simple fonction lambda AWS de CommonJS vers ESM.

La principale modification concerne la syntaxe d’import/export : Les modules ESM utilisant une syntaxe d’import/export différente de celle des modules CommonJS. Les déclarations require() et module.exports doivent être remplacés par des instructions import et export. Cela nécessite souvent des ajustements importants dans le code source du module.

En résumé, sur des projets de petite taille comme une fonction lambda, l’exercice est plutôt simple et peu de modifications sont nécessaires. C’est donc un choix judicieux pour profiter pleinement des avantages d’ESM.

Projet open-source

Concernant une librairie mise à disposition sur NPM, les objectifs sont les mêmes : standardisation, maintenabilité, conformité avec les meilleures pratiques et compatibilité future. Dans certains cas et notamment pour des projets frontend, la performance peut venir s’y greffer.

Enjeux :

Dans notre cas, nous avons travaillé sur un projet open-source ekino/node-logger développé en Typescript. C’est une librairie NPM que l’on utilise sur plusieurs de nos projets. Pour tirer parti des avantages d’ESM, nous devions d’abord nous poser la question de l’interopérabilité : mon code sera-t-il encore compatible dans un environnement CommonJS ? La réponse est simple : non, ou plutôt à travers des solutions très contraignantes expliquées dans la documentation de Node.js “Dual CommonJS/ES module packages”..

Outre les enjeux habituels, nous avons dû envisager le développement hybride qui permettait d’assurer la double compatibilité. Maintenir 2 bases de codes à cause des problèmes liés à la résolution de module ne nous semblait pas opportun. Les modules ESM nécessitent explicitement l’extension .mjs pour être interprétés comme tels.

Heureusement des solutions existent comme l’explique cet article de SenseDeep. Il décrit la création d’un module NPM compatible à la fois avec les modules ESM et CommonJS, sans avoir besoin de maintenir deux bases de code distinctes ni de recourir à Webpack. Il souligne que les solutions existantes sont souvent complexes et fragiles, nécessitant l’utilisation de Webpack, Rollup ou la création de bases de code doubles. L’article critique également l’utilisation des extensions .mjs et .cjs ainsi que les propriétés type et exports du package.json, soulignant leurs limites et leurs incompatibilités. Enfin, l’auteur propose une approche alternative qui utilise TypeScript ou Babel pour transpiler un code source unique en modules ESM et CommonJS, tout en fournissant des directives d’exportation dans package.json pour chaque distribution. Cette approche permet d’obtenir un module hybride facilement consommable à la fois par les applications ESM et CommonJS.

Dans notre cas, d’autres problématiques liées aux dépendances sont apparues :

- La librairie NYC que nous utilisons pour calculer le taux de couverture dans nos tests n’est pas compatible avec ESM. Des solutions de contournement ont dû être envisagées avec l’utilisation d’une autre librairie comme c8.

- Sinon.js, la librairie de test, utilisée pour l’espionnage, le stubbing et le mock, présente des problèmes de compatibilité.

- Dans certains cas si vous utilisez ts-node et tsconfig-path, vous pourrez aussi rencontrer des problèmes d’incompatibilité lors du chargement des modules par le compilateur* (source) :

Pour plus de détails concernant la migration, voici le lien de notre pull request.

*Solutions de contournement :

· https://github.com/TypeStrong/ts-node/discussions/1450#discussion-3563207

· https://github.com/TypeStrong/ts-node/discussions/1450#discussioncomment-1806115

· https://github.com/TypeStrong/ts-node/pull/1585

Conclusion

Pour conclure et déterminer le meilleur choix entre une migration vers des modules ESM (ECMAScript Modules) ou conserver des modules CommonJS, il est important d’évaluer les besoins spécifiques de chaque projet.

Taille du projet :

Pour les micro-projets, la migration vers ESM est relativement simple avec peu de modifications nécessaires, offrant ainsi une meilleure standardisation et maintenabilité.

Pour les projets plus importants, comme les librairies open-source, la décision nécessite une analyse plus approfondie en raison de la complexité accrue et des implications sur la compatibilité.

Interopérabilité :

Si le projet nécessite une compatibilité avec des environnements CommonJS existants, il est préférable de conserver une approche hybride (avoir une base de code qui transpile vers 2 sources distinctes) ou de rester entièrement sur CommonJS.

Pour de nouveaux projets ou ceux n’ayant pas besoin de compatibilité rétroactive, l’adoption d’ESM est justifiée.

Complexité de la solution :

Il est crucial d’évaluer la complexité des solutions disponibles pour garantir la compatibilité entre ESM et CommonJS. Par exemple, l’utilisation de Webpack ou de Rollup introduit une complexité supplémentaire.

Si des solutions simples et élégantes, telles que celles utilisant TypeScript ou Babel, peuvent être mises en œuvre avec succès, cela peut influencer en faveur de l’adoption d’ESM.

Écosystème et dépendances :

Il est essentiel d’examiner l’écosystème de dépendances du projet. Certaines dépendances ne prennent pas en charge ESM et des solutions de contournement vont s’imposer, ce qui rend la migration plus compliquée.

L’impact sur les outils de test, comme dans le cas de Sinon.js, et sur d’autres aspects du développement, tels que les compilateurs TypeScript, doit également être pris en compte.

Objectifs à long terme :

Enfin, les objectifs à long terme du projet doivent être pris en considération. Si l’adoption d’ESM s’aligne avec la vision future du projet et offre des avantages significatifs en termes de maintenabilité, de performances ou de compatibilité, cela peut justifier les efforts supplémentaires nécessaires à la migration.

Le bon choix dépend d’une évaluation approfondie des besoins spécifiques du projet, de ses contraintes et de ses objectifs à long terme.

Bilan, ouverture et implications

Si Node.js cherche à concilier au mieux les deux gestionnaires de module, à travers des flags expérimentaux par exemple (“Automatically detect and run ESM syntax”) :

--experimental-detect-module

il est peu probable que Node.js abandonne le support CommonJS, compte tenu qu’il reste le gestionnaire de module par défaut.

Néanmoins, nous recommandons d’utiliser ESM dès que vous le pouvez. Par exemple, sur de nouvelles applications ou sur des petits projets nécessitants peu d’effort. De même, lorsque vous en avez la possibilité.

D’autres plateformes d’exécution javascript comme Deno affirme très clairement leur position : l’article “Building modules with Deno”, explique que l’écriture et la publication de modules JavaScript modernes peut être difficile en raison de la nécessité de prendre en charge différents formats et cibles. Il présente Deno comme une solution offrant une approche simple et immédiate pour écrire des modules qui fonctionnent dans divers environnements sans nécessiter de configuration complexe ou de TypeScript.

Bun représente une autre alternative, qui se distingue en offrant une alternative moderne et performante, axée sur la rapidité, la compatibilité avec TypeScript et JSX, ainsi que la prise en charge complète des standards Web. Plus qu’un simple runtime, Bun aspire à devenir un ensemble complet d’outils pour le développement JavaScript/TypeScript, facilitant la migration vers ESM tout en offrant une expérience de développement cohérente et efficace :

“ESM & CommonJS compatibility. The world is moving towards ES modules (ESM), but millions of packages on npm still require CommonJS. Bun recommends ES modules, but supports CommonJS.”

Source : “What is Bun ?”

Au passage, nous vous invitons à lire l’article de nos ingénieurs Node.js chez ekino, “Bun: New, fast, but is it stable and complete enough?”.

Bien que la transition vers les modules ECMAScript (ESM) offre des avantages indéniables, le choix entre ESM et CommonJS demeure dépendant des besoins spécifiques de chaque projet, soulignant ainsi l’importance d’une évaluation minutieuse pour une transition en toute fluidité.

--

--