Peut-on vraiment écrire un module JavaScript (ESM) compatible back et front-end ?

Posté le 08-09-2024 par Hélène Jonin – lecture en 5 minutes (≈ 1040 mots)
Partie 1/1 d'une série de posts sur Exposés Invités

Node.js à sa sortie faisait la promesse d’un langage et d’un paradigme unique pour le développement back et front-end. Un écosystème complexe s’est construit autour depuis : constitué de célèbres frameworks front-end, mais aussi autres outils à tout faire un peu plus obscurs tels que Webpack, Gulp.js ou Babel.

Une planche CommitStrip pour illustrer le florilège des bibliothèques Nodes.js/JavaScript

Cela a contribué, à mon sens, à l’édification d’usines à gaz (en plus de déprimer toute une génération de développeur·euses), et à rendre les sites web de plus en plus difficiles à utiliser. Mais le débat ne se situe pas sur l’utilité et le bon usage de ces technologies, ni sur leur empreinte à l’exécution par ailleurs, mais sur leur avenir.

Les modules en JavaScript

Sans vouloir réécrire la très bonne documentation de Mozilla sur le sujet, on peut rappeler que :

  • Pour pallier le manque d’un système de module en JavaScript, plusieurs systèmes ont vu le jour, dont le système originel de Node.js, CommonJS, et ceux-ci coexistent désormais.
  • Un système natif aux navigateurs a vu le jour récemment, les ESM ou modules EcmaScript/JavaScript.

On peut imaginer sans prendre de grands risques que ce dernier système devrait remplacer tous les autres dans les prochaines années. Cependant, on peut aussi se demander, dans l’état actuel des technologies, est-il aisé (c’est-à-dire, en un minimum de technologies et langages intermédiaires) d’écrire un tel module TypeScript/JavaScript compatible Node.js et navigateur ?

Je vais essayer d’y répondre progressivement, d’abord en jouant avec un module JavaScript dans Node.js, puis d’en faire lui-même un module JavaScript compatible navigateur, et enfin compatible Node.js.

Je testerai aussi, en parallèle et en complément, la nouvelle fonctionnalité d’import JSON.

Un projet Node.js qui utilise un module JavaScript (ESM), en TypeScript

Pour le moment, c’est encore simple. La documentation de Node.js sur TypeScript recommande d’installer Typescript pour la compilation (et le contrôle de type), puis d’utiliser une librairie tierce telle que ts-node pour l’exécution.

Le projet contient les fichiers suivants :

  • Un module greeting.ts :
export const sayHello = () => "Hello World!";
  • Un fichier principal index.ts qui appelle ce module :
import {sayHello} from "./greetings";

console.log(sayHello());
  • Le fichier de description du paquet package.json :
{
  "name": "simple-blog-project",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "build": "npx tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "ts-node": "^10.9.2",
    "typescript": "^5.6.2"
  }
}
  • Le fichier de configuration du compilateur TypeScript tsconfig.json (initialisé via tsc --init)
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",  
    "esModuleInterop": true, 
    "forceConsistentCasingInFileNames": true,   
    "strict": true,     
    "skipLibCheck": true
  }
}

Résultats :

  • Exécuter ce code via la commande npx ts-node index.ts affiche bel et bien “Hello World!” dans la console.
  • L’exécuter avec node en le compilant préalablement (npx tsc && node index.js) fonctionne également.

et qui compile lui-même vers un module JavaScript (ESM), compatible navigateur

La configuration précédente permet d’obtenir un module au format CommonJS, le format de module originel de Node.js. D’après la documentation de TypeScript sur les modules, ce n’est plus recommandé.

TypeScript fournit un guide pour aider à choisir les bonnes options de compilateur (notamment module) en fonction du format de module de sortie désiré. En réalité, je n’ai pas compris ce guide, ni la documentation à propos de l’option module de manière générale.

En testant différentes configurations, j’ai réussi à obtenir un module JavaScript que j’ai pu importer et utiliser dans un navigateur :

  • Dans tsconfig.json, en modifiant la valeur de module :
- "module": "commonjs",  
+ "module": "esnext",  
  • Dans un nouveau fichier index.html, en important le module (compilé préalablement avec npx tsc) greetings.js :
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Testing modules</title>
</head>
<body>
<h1></h1>
<script type="module">
  import { sayHello } from "./greetings.js";
  document.querySelector("h1").textContent = sayHello();
</script>
</body>
</html>

Exécuter ce dernier fichier dans un navigateur affiche une page avec pour titre “Hello World!”.

Pour continuer à faire fonctionner ts-node et node (avec les commandes respectives npx ts-node index.ts et npx tsc && node index.js vues dans la section précédente), il faut aussi :

  • Dans package.json, spécifier le type :
+ "type": "module",
  • Plus étonnant, dans index.ts, importer le module greetings avec son extension .js :
- import {sayHello} from "./greetings";
+ import {sayHello} from "./greetings.js

C’est en fait indiqué dans la documentation :

Node.js ESM import declarations use a strict module resolution algorithm that requires relative paths to include file extensions.

Et discuté dans une issue de TypeScript.

  • Et il faut ajouter un loader ESM en option de la commande ts-node, soit NODE_OPTIONS="$NODE_OPTIONS --loader ts-node/esm" npx ts-node index.ts.

Le README du projet ts-node résume la situation.

et utilisable dans un projet Node.js

Incroyable mais vrai, ce module peut être importé tel quel dans un projet Node.js !

Pour tester, dans un nouveau projet Node.js :

  • Dans package.json, spécifier "type": "module".
  • Installer le module précédent local via npm install --save CHEMIN_VERS_LE_MODULE.
  • Créer un fichier index.js qui utilise ce module et l’exécuter : cela fonctionne !

Bonus : En utilisant l’import JSON

Dans le cas de l’application que je développe, j’avais besoin d’utiliser la fonctionnalité d’import JSON.

Créer un fichier name.json, que l’on importe dansgreetings.ts :

{
  "first": "Jane",
  "last": "Doe"
}
+ import name from "./name.json" with { type: "json" };

Il faut explicitement spécifier le type pour Typescript. Mais cela rend le module incompatible avec Firefox.

Il faut aussi activer l’import JSON dans la configuration Typescript, au moyen de l’option resolveJsonModule. Cela nécessite de spécifier également l’option moduleResolution :

- "resolveJsonModule": false
- "moduleResolution": "classic"
+ "resolveJsonModule": true
+ "moduleResolution": "bundler"

Conclusion

Cette méthode nous permet de créer un module JavaScript compatible back et front-end. Elle a cependant encore des limites. Notamment, si le module fait appel à des dépendances externes, il faut alors l’empaqueter, dans un package npm pour une utilisation dans un project Node.js, dans un “bundle” (le jar du JavaScript) pour une utilisation dans un navigateur. Dans le cas d’un package npm, il ne manque qu’à publier module. Dans le cas d’un bundle, il n’y a, de ce que j’ai retiré de mes recherches, pas d’autre choix que de passer par un outil obscur à tout faire comme Webpack. Je suis à l’écoute de retours sur cette méthode et cet article directement dans le dépôt du projet qui m’a donné du fil à retordre.

BTW I use Node >= 22