Rassembler dans un même livrable NPM du code serveur bigoût (parfums CommonJS require et ESM import), plus une version minifiée pour le browser, et des déclarations de types TypeScript, c'est possible. Objectifs : centraliser le développement et unifier le cycle des releases. Guide pratique...
Node.js (en v19 à l’écriture de cet article) dispose de deux systèmes de chargement de modules, chacun ayant une syntaxe spécifique. Un programme JavaScript doit choisir entre le CommonJS (CJS) historique ou le moderne ECMAScript Modules (ESM), et ses dépendances devront également suivre le standard élu.
Lorsqu'on produit une bibliothèque à incorporer dans d'autres programmes, il peut être judicieux d'offrir les deux formats afin d'assurer la compatibilité avec le plus grand nombre de projets.
En outre, certaines bibliothèques, comme un module de manipulation de dates ou de fonctions cryptographiques, ciblent indifféremment le côté serveur et le navigateur ; les exemples et cas d'usage ne manquent pas. Pour une page web dans un navigateur, si ESM est devenu une possibilité depuis quelques années, bien souvent l'option du fichier unique, minifié, lui est préférée pour diverses raisons (notamment la maîtrise du nombre de requêtes HTTP).
Pour éliminer la duplication de code et faciliter l'importation dans tous les milieux, nous verrons dans cet article comment générer un module qui expose les trois saveurs.
Épargnons tout de suite au lecteur impatient un suspense insoutenable : il existe des outils pour obtenir le résultat facilement et avec très peu de paramétrage, comme notamment l'excellent programme tsup [1]. Cet article a pour ambition d'expliquer, pour mieux les comprendre, les mécanismes qui sont à l'œuvre.
Mais d'abord, un bref rappel chronologique s'impose.
1. Histoire
1.1 Il était une fois : ECMAScript
Le langage JavaScript naquit en 1995 comme solution ad hoc de l'éditeur Netscape pour un usage précis : ajouter un peu d'interactivité après affichage d'un document HTML dans le logiciel Navigator. Cette idée originale de Brendan Eich (écrite en dix jours [2]...) s'est propagée chez les éditeurs de butineurs concurrents. Chacun apporta son lot d'évolutions syntaxiques et de fonctionnalités particulières (généralement délibérément différentes ; après tout, pourquoi choisir le même nom que le concurrent pour la propriété qui indique la touche frappée lors d'un événement clavier...).
Il en a résulté une pénible segmentation des interpréteurs de ce langage et un accroissement important de la complexité à élaborer des programmes compatibles d'une plateforme à l'autre.
Pendant que le consortium W3C [3], dirigé par Tim Berners-Lee, s’attelle à l'uniformisation du modèle HTML dynamique (DOM et API) et des aspects visuels (CSS), le groupe ECMA [4] (European Computer Manufacturers Association) se fixe pour mission d'établir une standardisation pour le JavaScript, appelée ECMAScript, qui n'a cessé d'évoluer (ajout des Promises, programmation fonctionnelle, déstructuration et spread, etc.).
1.2 Il était deux fois : Node.js
Les choses se sont encore compliquées lorsqu'en 2009 Ryan Dahl a présenté [5] Node.js (c'est le terme officiel, prononcé "naudjéyesse", mais dans la suite j'écrirai aussi Node ou node). Il s'agit d'un framework pour exécuter côté serveur des programmes event driven avec le moteur JavaScript V8 de Google, en dehors de tout navigateur HTML.
L'argumentaire de popularisation vantait qu'avec un seul et même langage, les développeurs pourraient plus facilement s'occuper des deux côtés front et back ends, ou, à défaut, communiqueraient plus aisément entre eux.
Or, des différences fondamentales séparent ces deux paradigmes. Le serveur effectue des traitements parallèles, écoute des ports réseau, communique avec des bases de données, accède au matériel et dispose localement des fichiers de programme à exécuter. De son côté, le navigateur est dans un sandbox, déroule un programme à vocation visuelle pour le compte d'un utilisateur humain isolé, à partir de scripts qui lui proviennent à distance par HTTP.
Ceci a poussé les deux écosystèmes à suivre leurs propres évolutions [6], Node étant souvent tardif dans l'adoption ECMA par rapport aux navigateurs. Une des principales dissidences concerne la modularisation des programmes et les systèmes de chargement de dépendances.
2. Systèmes de chargement
Lorsque la taille d'un programme devient supérieure à quelques dizaines de lignes, ou quand il a besoin de réutiliser des travaux tiers, il est nécessaire de le découper en petites unités plus ou moins hermétiques. La terminologie varie selon les langages, on parlera de modules, fichiers ou autres packages, que l'on imbriquera ici par un #include, là avec un import, etc. À son tour, une unité de code sera potentiellement amenée à exporter des éléments utilisables par d'autres unités, ou à publier des points d'entrée directement déclenchables par l'environnement extérieur.
Avant Node et du temps où le standard JavaScript n'avait pas encore prévu le coup, les développeurs ont dû se montrer inventifs pour mettre en œuvre un mécanisme d'inclusion de fichiers et d'encapsulation de modules sur une page web. Dans le navigateur, où le contrôle du chargement des <script> se trouve intégralement confié au document HTML, un module ne pouvait pas de lui-même se déclarer dépendant d'un autre. C'était au code HTML de prendre soin de lister la succession de scripts dans le bon ordre, pour que le suivant puisse s'exécuter après le chargement du précédent.
Entre-temps, les travaux du W3C et des éditeurs de navigateurs ont rendu possible le chargement de nouveaux scripts au cours de l’exécution du script « dur » initial. Mais les problématiques d’interdépendances demeurent. Plusieurs approches ont émergé. Sans entrer en profondeur, nous allons mentionner les plus courantes.
2.1 Asynchronous Module Definition
Si tous les programmes de la page sont de mon cru, je peux encore me débrouiller. Mais si je charge un module tiers, qui s'appuie lui aussi sur diverses dépendances ordonnées, il me faudra laborieusement maintenir un code HTML rendu excessivement complexe et verbeux pour de mauvaises raisons.
Si bien qu'un outil nommé AMD [7] a vu le jour : un microchargeur (define) grâce auquel on diffère l'exécution d'un script, dont l'enveloppe stipule le nom de ses dépendances et autodéclare son propre nom. Une fois tous les scripts chargés, le loader résout l'arbre des dépendances et exécute les modules dans le bon ordre.
Dans l'exemple suivant, nous déclarons un module my-module, qui dépend de deux autres (dep1 et dep2), et qui exporte deux symboles myVal et myFunc.
En rencontrant ce code, même si les modules dep1 ou dep2 ne sont pas encore chargés, rien ne se produit, si ce n'est que le système AMD prend note que ce module my-module en dépend. La factory sera exécutée dès que les deux dépendances deviendront disponibles (c'est-à-dire, lorsque la page HTML les aura chargées).
Lorsque cela se produit, my-module peut être résolu, et tout autre module qui s'était déclaré dépendre de lui verra sa factory s'exécuter automatiquement.
D'autres implémentations de ce concept existent, les plus connues étant RequireJS et Dojo (2005).
2.2 CommonJS
L'approche RequireJS fait le travail pour l'écosystème du navigateur, mais elle accroît artificiellement la complexité du code. Node.js quant à lui, ne souffre pas de la limitation des balises <script> du HTML, et les fichiers sources lui sont directement disponibles localement (par opposition au chargement distant via HTTP). Aussi a-t-on opté dès sa création pour une notation connue dans l'écosystème des navigateurs sous le nom de CommonJS, un projet de l'ingénieur Kevin Dangoor chez Mozilla en 2009, c'est-à-dire l'année de sortie de Node.
La syntaxe se présente comme ceci :
Pour Node, ça fonctionne tel quel. Pour un navigateur, la syntaxe s'y prend différemment d'AMD, mais l'esprit reste le même : l'environnement d'exécution (le navigateur) a besoin d'un chargeur qui apporte les primitives require et module.exports.
Plusieurs implémentations existent, plus ou moins populaires, mais la segmentation n'en a pas été résolue pour autant. L'écosystème du client web s'est alors tourné vers une autre famille d'outils : les bundlers.
2.3 Bundlers
Il s'agit d'un préprocesseur qui prend les fichiers sources JavaScript, séparés et modularisés selon une approche ou une autre, pour les réunir en un seul fichier contenant toutes les dépendances.
Parmi les plus célèbres, on peut citer Browserify [8], Wepback et Rollup.
Le bundler prend en entrée un ensemble de fichiers tel que celui vu plus haut, et les transforme en un unique fichier JavaScript résultant, dans lequel ne figure plus de pseudodirectives require, exports ou define. Ce code unifié, appelé bundle, généralement encapsulé dans une fonction anonyme où toutes les dépendances sont résolues et incorporées à la closure. Il ne reste plus qu'à charger ce bundle dans une page web avec une balise <script> unique.
IIFE : Immediately Invoked Function Expression
En JavaScript, on utilise couramment le principe des fonctions anonymes immédiatement invoquées (IIFE) : il s'agit de déclarer, puis d’aussitôt exécuter une fonction qui se verra donc recevoir un scope encapsulé. C'est un moyen de construire un bloc de code avec des variables locales, des fonctions imbriquées, classes et autres symboles qui resteront privés et ne pollueront pas le scope appelant.
La syntaxe est de la forme : (function( ) { /* bloc de code étanche */ })()
2.4 ECMAScript Modules
En 2015, face à la divergence des approches et aux difficultés subies par les applications dans les navigateurs, ECMA a fixé le standard des ECMAScript Modules (ESM). Aujourd'hui, la prise en charge par les navigateurs est assez générale.
La syntaxe adoptée est la suivante :
Cela évoque CommonJS avec juste une autre notation, mais les différences ne sont pas que syntaxiques : notamment, le modèle CommonJS présente des faiblesses de sécurité [9] que la norme ESM rectifie. Les détails sortiraient du cadre de cet article, mais disons en quelques mots qu'il est possible, depuis n'importe où dans le programme, d'altérer un autre module programmatiquement, ce qui représente un danger potentiel, surtout pour des modules sensibles auxquels on confie des mots de passe, par exemple.
Le chargement dans une page HTML se fait comme ceci :
Dès lors, le browser interprétera correctement les instructions d'import et d'export, et chargera les modules référencés dans le fichier, récursivement. Si la clause import ne stipule pas un chemin relatif (c'est-à-dire qu'on importe donc un autre module externe), il faut également fournir une Import Map [10] dans le document HTML, mais ceci dépasse le sujet présent.
Les navigateurs ont adopté ESM dès 2017 [11] (beaucoup plus tard sur mobile) ; mais cette magie n'arrange pas obligatoirement nos affaires, au vu du nombre (parfois très élevé) de requêtes HTTP que le navigateur doit faire pour charger tous les fichiers séparément. On peut prédire sans risque que l'ESM dans le navigateur conservera encore un certain temps un simple statut de curiosité. En fin de compte, le bundler reste encore la solution la plus répandue.
Côté serveur, ce n'est que dans Node 13, en 2019, que l'ESM est intégré sans nécessiter de feature-flags expérimentaux. Tout se joue dans le fichier package.json. C'est dans ce manifeste, qui rassemble les métadonnées du programme à exécuter (ou du module à distribuer), que l'on précise le standard de chargement dans la propriété "type" : sans surprise, ce sera soit "commonjs" (par défaut) soit "module" si l'on souhaite de l'ESM. Néanmoins, compte tenu de la quantité faramineuse de paquets distribués sur la plateforme npmjs.org, de très loin le registre le plus vaste par rapport à tous les autres langages (voir Figure 1), l'adoption ne sera jamais totale.
3. TypeScript
Pour qu'il soit complet, il faut ajouter TypeScript à notre tour d'horizon.
Est-il besoin de le présenter ? Cet additif au langage JavaScript qui apporte typage et confiance est presque devenu indispensable au développement de services web autant que d'applications HTML clientes. Il nous faut donc prévoir une étape de transpilation dans notre chaîne de build.
Transpilation
La transpilation, ou transcompilation, est en quelque sorte une compilation source vers source. C'est l'opération qui permet de transformer du code d'un certain langage vers un autre code source dans un autre langage.
Ni Node.js ni les navigateurs ne sont capables de comprendre le TypeScript, et l’on a donc besoin de le transpiler d'abord.
La transpilation diffère de la compilation, qui produit théoriquement du code binaire directement exécutable par la machine (qu'elle soit matérielle ou virtuelle). Le résultat de la transpilation est supposé subir encore d'autres transformations sur son chemin, avant (ou au moment) d'être exécuté.
On retrouve de la transpilation par exemple avec les outils qui automatisent la traduction de programmes Python 2 vers Python 3 (ou le contraire), ou bien depuis Objective-C vers Swift, etc.
Lorsqu'on publie un package NPM (Node Package Manager), c'est-à-dire un module à incorporer comme dépendance dans un projet hôte, c'est du code JavaScript que l'on distribue, même si on l'a rédigé en TypeScript à l'origine. Mais l'un des intérêts de ce langage est de pouvoir embarquer avec le module les définitions de types qui seront indispensables au développeur utilisant la dépendance dans un projet TypeScript.
Le compilateur tsc est capable de se référer aux définitions de types d'une dépendance si son package.json stipule un champ "types" qui pointe vers le fichier, au sein du module, contenant les déclarations.
4. Mise en pratique
Tous ces rappels étant faits (un grand merci pour votre patience !), nous pouvons maintenant entrer dans l'expérimentation.
Nous allons construire un package NPM qui conviendra aussi bien à un projet CommonJS qu'ESM, et qui embarquera un bundle unique et minifié chargeable dans un navigateur. Bien entendu, le module est écrit en TypeScript et nous empaquetons les définitions de types pour un usage propre et professionnel par des développeurs tiers.
On appelle parfois ce type de livrable un module hybride (CJS, ESM, bundled).
L'intérêt est de pouvoir se concentrer sur le code métier, sans se soucier des spécificités des différentes cibles.
4.1 Forme du projet
Pour la démonstration, supposons que nous fabriquons une bibliothèque appelée @ed-diamond/glmf-hybrid composée des fichiers src/file1.ts et src/file2.ts, l'un important l'autre (ou l'inverse, c'est vous qui voyez).
Le manifeste package.json commence comme ceci :
Nous rédigeons le projet à la mode ESM (l'avenir), c'est-à-dire en utilisant la syntaxe import et export ; mais le fruit de notre travail conviendra aussi bien à un projet CommonJS, comme nous allons le voir.
Le fichier index.ts réexporte les symboles publics de la bibliothèque, de sorte qu'un projet consommateur se contentera de les importer depuis la racine du module :
Un projet hôte ESM pourra écrire ceci :
tandis qu'un projet CJS fera :
4.2 Configuration
Occupons-nous dans un premier temps des deux saveurs pour Node.js. Nous les générerons dans deux sous-répertoires du dossier dist.
Nous utilisons tsc pour transpiler nos sources ESM TypeScript en deux résultantes JavaScript : ESM et CJS.
Dans le fichier package.json, nous allons déclarer une cible pour chaque build, et une cible qui fait les deux (voir 4.3). Chaque tour de compilation nécessite une configuration différente, et nous allons donc définir deux fichiers, qui étendent une racine commune. Nous placerons ces trois fichiers dans un répertoire config (nous verrons le quatrième au 4.6).
Le fichier parent tsconfig.json comporte les directives suivantes (en plus de celles à votre goût et suivant les besoins de votre projet) :
La documentation de TypeScript précise que esModuleInterop résout certains problèmes (subtils), dont celui sur la sécurité mentionné en 2.4.
Le fichier config/tsconfig.esm.json pour la transpilation ESM contient :
Nous y précisons la configuration parente avec extends, le type de système de modules : "module": "ESNext" (pour obtenir de l'ESM) et le répertoire outDir où générer les fichiers transpilés à la norme ESM.
Voyons à présent le contenu de config/tsconfig.cjs.json pour produire du CJS :
Dans celui-ci, nous précisons un système "module": "CommonJS", un outDir différent, et nous décidons (arbitrairement) que ce sera ce build qui servira d'occasion à produire les définitions de types (voir 3).
On aurait pu choisir le build ESM pour prendre en charge cette tâche, voire la confier à une troisième cible qui n'aurait fait que ça (avec emitDeclarationOnly). Mais quoi qu'il en soit, il est inutile de le faire deux fois : les définitions de types sont les mêmes, et sont insensibles au système de modules choisi, car les d.ts TypeScript utilisent toujours la notation ESM.
4.3 package.json
Voici les cibles que nous pouvons préciser dans la rubrique scripts de notre package.json :
La commande npm run build aura pour effet de dérouler le build ESM suivi du build CJS, chacun par son propre fichier de configuration TypeScript, et le dernier produira également les définitions de types.
La résultante se retrouve sur le disque comme ceci :
Les fichiers sous dist/cjs utilisent le require, les fichiers sous dist/ems utilisent l'import, et les types .d.ts sont présents.
4.4 Préparation des modes
Mais ce n'est pas encore suffisant pour permettre à un programme d'importer notre bibliothèque. En effet, il nous faut ajouter quelques méta-informations dans le manifeste du projet, et préciser dans chaque répertoire le format correspondant au moyen d'un autre petit fichier package.json minimaliste.
Commençons par eux. Dans dist/cjs/package.json :
et dans dist/esm/package.json :
Et maintenant, ajoutons ceci dans le package.json principal de notre projet :
Ces champs servent à préciser à un importateur où se trouvent les définitions de type TypeScript, et où est le code que Node doit considérer [12] pour chacun des deux modes (import (ESM) et require (CommonJS)).
Pour ce qui est de la création des deux petits manifestes pour ESM et CJS dans les sous-dossiers de dist, nous pouvons automatiser en ajoutant un postbuild pour chaque cible dans la rubrique scripts (un script dont le préfixe est post, comme dans postquelqueChose, est automatiquement exécuté à la suite du lancement de script correspondant quelqueChose) :
Lorsqu'un projet incorpore notre dépendance, npm télécharge notre paquet qui contient tout. Puis, à l'exécution, selon que le type du projet (dans son propre package.json) est ESM ou CJS, Node consultera nos package.json pour décider de charger telle ou telle saveur (nous verrons en 4.8 que celui du ESM pourrait être remplacé par l’utilisation de l’extension .mjs, mais j’ai choisi ici de conserver la symétrie à des fins didactiques).
4.5 Correction des extensions pour ESM
Il reste une dernière étape pour débloquer l'utilisation des sources en mode ESM, à cause d'une exigence du standard concernant les extensions de fichiers [13]. La spécification précise qu'en mode ESM, les chemins relatifs des imports doivent obligatoirement indiquer une extension de fichier. Or, nous avons développé le module en TypeScript (fichiers .ts), et nous avons omis l'extension dans nos sources (voir index.ts en 4.1), de façon à laisser TS et JS être beaux joueurs ensemble (au vu d'un chemin relatif, le compilateur TypeScript devine qu'on charge un ts, et Node (CJS) devine qu'on charge un js ; l'extension peut être sous-entendue).
Mais en mode ESM, ça ne fonctionne plus : le runtime exige que les imports stipulent l'extension. Nous ne pouvons pas la préciser dans nos sources, car ce serait TypeScript qui coincerait.
Nous allons devoir modifier les fichiers JavaScript après transpilation, pour ajouter l'extension .js au bout des imports de fichiers relatifs (import ... from "./...";). Les commandes grep et sed feront l'affaire dans un simple script shell que je place dans config/fix-esm-ext.sh, mais toute autre approche est possible :
Ce programme est à exécuter après la transpilation ESM. Nous pouvons modifier la cible postbuild:esm du package.json :
4.6 Le navigateur et son bundle
Que se passe-t-il dans une page HTML ?
Nous l'avons déjà dit, le document peut opter pour charger un <script> au standard type="module". La mécanique sera alors similaire à celle évoquée ci-dessus.
Si nous optons pour un chargement ficelé (bundle), nous produirons un livrable sous la forme d'un fichier JavaScript unique, par exemple dans dist/cdn/lib.min.js, qui contient tout (aussi bien nos fichiers sources que les dépendances sur lesquelles notre bibliothèque s'appuie).
Je propose l'utilisation du bundler rollup, simple et répandu, qui se configure de la façon suivante au moyen du fichier config/rollup.config.js :
Quelques explications :
- on utilise le plugin @rollup/plugin-typescript pour indiquer au bundler de commencer par la transpilation ;
- on retrouve le chemin du résultat à produire : file: "dist/cdn/lib.min.js" ;
- on choisit le schéma IIFE (voir encadré) qui enregistrera tous les symboles exportés dans le namespace window["@ed-diamond/glmf-hybrid"] ;
- et l’on obscurcit et minifie le fichier JS final, grâce au plugin Terser.
Si l'on héberge les fichiers du module sur un serveur HTTP, l'intégration dans la page HTML devient :
Nous pouvons ajouter une cible build:browser dans le package.json pour déclencher la construction du bundle.
Le répertoire dist après la commande npm run build aura maintenant cette allure :
4.7 Pack
Notre module est prêt à être distribué. La commande qui se charge de la préparation du paquetage est npm pack. Elle génère une archive GZIP à partir des fichiers stipulés dans la propriété files du package.json. Dans notre cas, il s'agit de tout le répertoire dist :
Voici sa sortie :
À ce stade, on pourrait publier le paquet sur la registry npmjs.org, avec la commande npm publish.
Mais avant, écrivons un petit smoke test pour nous assurer que l'archive fonctionne bien comme on le croit.
4.8 Smoke test
L'exercice consiste à fabriquer un projet temporaire consommateur, qui ait comme dépendance notre petit module sans le faire venir de la registry, mais directement depuis notre archive locale. C'est possible avec la commande npm install en précisant un chemin de fichier GZIP.
Préparons dans notre projet des fichiers de test comme ceci :
Notez l'extension .mjs. Par défaut, Node exécute un fichier js en mode CommonJS, et un fichier mjs en mode ECMAScript Modules.
Voyons d'abord quoi mettre dans le fichier index.js de test pour CJS :
Le fichier index.mjs pour ESM lui ressemble, mais bien sûr, il utilise l'import :
Quant au fichier browser.js, nous nous plaçons au plus près de ce que ferait le tag <script src...> en HTML qui exécute la IIFE préparée par Rollup, et qui intègre nos symboles dans l'objet global window["@ed-diamond/glmf-hybrid"] :
Les lignes de tests en elles-mêmes peuvent être identiques, vous saurez trouver un moyen de mutualiser.
À présent, dans un répertoire temporaire, installons notre pack :
La commande a créé un package.json sur place, et a déballé l'archive dans node_modules. Maintenant, on copie le contenu du répertoire test/smoke au même endroit.
Nous allons exécuter node dans trois modes différents, pour lui faire goûter alternativement toutes nos saveurs.
Dans la pratique, vous voudrez certainement arranger ces opérations dans un script shell. L'idée n'est pas seulement de vérifier cette seule fois que les choses sont en ordre, mais de se prémunir des régressions à mesure que le projet avance, car une mauvaise configuration accidentelle dans une des nombreuses options est vite arrivée.
Pour CJS et ESM, lancer les tests est aussi simple que :
Enfin, concernant la validation du bundle minifié, nous devons prévoir dans le scope global un objet window, qui n'est pas défini dans Node (mais qui est fourni par les navigateurs). Nous voulons ensuite exécuter directement le fichier minifié (lib.min.js), sans passer par un mécanisme d'importation, et enfin nous jouons les instructions de test de browser.js.
Tout ceci se fait en alimentant l'entrée standard de la commande node :
Victoire, on peut effectuer le npm publish pour déployer le package dans la registry.
4.9 Cadeau bonus : le CDN
Dans le snippet HTML vu plus haut pour illustrer le chargement du module IIFE avec une balise <script>, j'avais indiqué le chemin complet du fichier minifié https://.../dist/cdn/lib.min.js.
Il y a plus élégant. Si nous déployons une bibliothèque publique sur npmjs.org, nous bénéficions gracieusement d'un hébergement web statique pour le module, via le service https://www.jsdelivr.com/.
Dans le package.json de @ed-diamond/glmf-hybrid, on peut ajouter un champ "browser" [14] qui stipule le fichier du bundle. Dans notre cas, il s'agit de "browser": "dist/cdn/lib.min/js".
La section complète des points d'entrée devient maintenant :
Par la suite, après publication, l'adresse du CDN automatique a la forme :
https://cdn.jsdelivr.net/npm/@ed-diamond/glmf-hybrid@1.0.0
Cette URL fournit directement le fichier JavaScript minifié, généré par notre bundler, et prêt à être incorporé dans le HTML.
Conclusion
Le lecteur doit se sentir maintenant un peu mieux éclairé, en principe, sur les tenants et aboutissants des systèmes de chargement des programmes JavaScript.
Comme indiqué dès le début, la distribution d'un module hybride qui prend en charge la norme ECMAScript Modules, la compatibilité CommonJS, et qui s'accompagne d'un IIFE minifié peut se faire très simplement au moyen des bons outils. En plus de celui évoqué en introduction, citons le projet modulestrap [15] qui fournit un point de départ confortable.
Références
[1] L'outil d'empaquetage de modules hybrides TSUP : https://tsup.egoist.dev
[2] JavaScript créé en 10 jours :
https://thenewstack.io/brendan-eich-on-creating-javascript-in-10-days-and-what-hed-do-differently-today/
[3] Le World Wild Web Consortium : https://www.w3.org/
[4] ECMAScript : https://262.ecma-international.org
[5] Naissance de Node.js :
https://www.protechtraining.com/blog/post/introduction-to-nodejs-with-ryan-dahl-182
[6] Les versions de JavaScript : https://www.w3schools.com/js/js_versions.asp
[7] Asynchronous Module Definition : https://requirejs.org/docs/whyamd.html
[8] Le bundler Browserify pour CommonJS dans le navigateur : https://github.com/browserify/browserify
[9] Billet de blog d'Andrea Giammarchi sur la sécurité dans CommonJS :
https://webreflection.medium.com/cjs-vs-esm-5f8b90a4511a
[10] Déclaration d'une Import Map dans une page HTML :
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_modules_using_import_maps
[11] Can I Use ESM : https://caniuse.com/?search=ESM
[12] Documentation package.json, Conditional Exports :
https://nodejs.org/api/packages.html#conditional-exports
[13] La spécification du Loader ESM : https://nodejs.org/api/packages.html#modules-loaders
[14] Le fichier par défaut pour le CDN JsDelivr :
https://www.jsdelivr.com/documentation#id-configuring-a-default-file-in-packagejson
[15] Le kit de démarrage Modulestrap : https://github.com/WebReflection/modulestrap