Cloudflare a réécrit le registre des modules dans le composant open source workerd, qui constitue le composant central de l’environnement d’exécution Workers, afin d’améliorer la vitesse et la conformité aux normes, et de rapprocher le chargement et la résolution des modules du comportement de Node.js. Cette évolution intervient parallèlement à l’activation par défaut des API stables de Node.js dans Workers et à la possibilité de déployer des applications dont la taille atteint 64 Mio sur tous les forfaits, après la suppression de la limite de taille du paquet compressé.
Mais le changement le plus important ne concerne pas seulement le nombre d’API disponibles. Les applications Node.js dépendent également de la manière dont l’environnement d’exécution identifie les modules, les charge et les met en cache. Cela inclut les modules ESM, CommonJS et WebAssembly, des responsabilités prises en charge par le registre des modules au sein de workerd.
Qu’est-ce qui a changé dans le nouveau registre ?
Les développeurs peuvent essayer la nouvelle implémentation en ajoutant l’indicateur new_module_registry aux paramètres du Worker. Lorsqu’il est activé, Workers prend en charge import.meta.url, import.meta.main et import.meta.resolve(), et traite les spécificateurs de modules comme de véritables URL, y compris leurs chaînes de requête et leurs fragments.
Concrètement, cela signifie que les imports relatifs suivent les mêmes règles que new URL() et que les URL complètes peuvent être utilisées comme spécificateurs de modules. Les modules qui diffèrent par leur chaîne de requête ou leur fragment deviennent également des modules distincts, même s’ils renvoient vers la même source. Ainsi, deux versions d’un même fichier peuvent avoir des états de niveau supérieur distincts, tandis que la réutilisation du même spécificateur renvoie la même instance du module.
Le registre prend également en charge la validation correcte des attributs d’importation. Le type json est actuellement disponible, tandis que des types tels que text et bytes sont définis mais rejetés avec un message explicite, car ils n’ont pas encore été activés. Les attributs inconnus ne sont plus ignorés silencieusement et entraînent désormais une erreur explicite.
Une compatibilité accrue avec Node.js
Lorsqu’il charge un module ESM, require() suit les règles de Node.js pour require(esm). Si le module exporte une valeur nommée module.exports, cette valeur est renvoyée : sinon, l’appel renvoie l’objet d’espace de noms du module. Les composants intégrés node: de workerd font exception, car ils renvoient l’interface CommonJS attendue au lieu de contraindre le développeur à accéder à la propriété default.
Une restriction importante demeure : require() ne peut pas charger un module qui contient, ou dépend d’un module qui contient, un top-level await, car require doit renvoyer le résultat de manière synchrone. Dans ce cas, Workers lève une erreur et import(), asynchrone, constitue la voie appropriée. Cette règle reste valable même si le module lui-même a été préalablement chargé via import().
La nouvelle version harmonise également les catégories d’erreurs et la formulation de leurs messages, quelle que soit la méthode de chargement utilisée, qu’il s’agisse d’un import statique, d’un import() dynamique ou de require(). L’échec de la recherche du module renvoie une erreur standard, tandis qu’un spécificateur qui ne peut pas être analysé comme une URL provoque une TypeError. Cette cohérence est utile aux développeurs qui construisent des chargeurs personnalisés ou une logique de nouvelle tentative.
Qu’est-ce qui change concrètement pour les développeurs ?
Les outils Workers, tels que Wrangler, regroupaient la plupart des fichiers de l’application et de ses dépendances dans un seul module à l’aide d’esbuild, ce qui réduisait la taille du graphe traité par l’environnement d’exécution. En revanche, l’utilisation du Cloudflare Vite plugin repose, dans Vite 8, sur Rolldown pour produire un module d’entrée et des modules supplémentaires lors de la division du code, comme les modules chargés dynamiquement.
Le nouveau registre permet aux outils de compilation d’effectuer moins de transformations et de s’appuyer davantage sur l’environnement d’exécution pour résoudre les modules. Cela est particulièrement important lors du déploiement d’applications sous la forme de plusieurs modules, de l’utilisation de l’option --no-bundle ou de la gestion de fichiers Wasm, texte et binaires comme fichiers indépendants plutôt que de les inclure dans un seul paquet.
La nouvelle implémentation diffère également la compilation jusqu’à la première importation du module, qu’elle soit statique ou dynamique, et permet de partager les caches de code entre plusieurs instances V8 isolate qui exécutent le même Worker. Selon Cloudflare, cela traite en partie la compilation répétée et la présence de plusieurs copies de la source en mémoire dans l’ancienne implémentation.
Limites et points d’attention
Malgré ces changements, le nouveau registre ne sera pas activé automatiquement pour un Worker, ancien ou nouveau, quel que soit le calendrier de compatibilité utilisé. Il faut ajouter explicitement l’indicateur. Cloudflare a également conservé l’ancienne implémentation et indique que les Workers déployés continueront de fonctionner comme auparavant.
La nouvelle implémentation prend également en charge l’importation au stade source des modules WebAssembly, ce qui renvoie directement un objet WebAssembly.Module, mais cette fonctionnalité ne fonctionne actuellement qu’avec WebAssembly et tout autre type provoque une erreur de syntaxe. La mise à jour ne constitue donc pas une transition complète et sans restriction pour tous les chemins de chargement des modules, mais fournit une base plus compatible que les développeurs peuvent tester progressivement.
Lecture éditoriale : la valeur réelle de ce changement réside dans le passage de Workers d’une simulation partielle du comportement des modules à un modèle plus proche des normes JavaScript et de Node.js, ce qui pourrait réduire les transformations imposées par les outils de build et rendre les applications multmodules plus faciles à porter. Toutefois, son impact final dépendra des tests menés par les développeurs avec le nouvel indicateur, en particulier pour les dépendances qui utilisent require(), top-level await ou des attributs d’importation. Le fait qu’il ne soit pas activé par défaut signifie également que la compatibilité améliorée ne constitue pas encore le comportement général de toutes les applications.