Ce qui se passe
Deux murs tombaient déjà cet été côté Workers : la compatibilité Node est activée par défaut depuis le 4 août, et la taille maximale d'un Worker est passée à 64 Mio sur tous les plans le 4 septembre, la limite sur la taille compressée ayant été supprimée.
Restait le troisième : la manière dont le runtime résout, charge et met en
cache les modules. Cloudflare a réécrit le registre de modules de
workerd, et il s'active dès aujourd'hui
par un drapeau :
{
"compatibility_flags": ["new_module_registry"]
}Il n'a pas de date d'activation par défaut : quelle que soit votre
compatibility_date, il faut poser le drapeau explicitement. Les Workers déjà
déployés continuent de fonctionner comme avant.
Ce qui change vraiment
Une seule décision commande tout le reste : un spécificateur d'import n'est plus
un chemin de fichier, c'est une URL. L'ancien registre les traitait comme des
chemins, ce qui rendait import.meta.url impossible à implémenter proprement et
faisait de node: ou cloudflare: des préfixes de chaîne traités à part au lieu
de protocoles.
Il compilait aussi l'intégralité du bundle en amont, qu'un module soit importé ou non, et gardait une copie privée par isolat V8. Or Cloudflare exécute plusieurs répliques d'isolat du même Worker pour répartir la charge sur les cœurs : la même source était donc compilée plusieurs fois et tenue en mémoire autant de fois. Le nouveau registre compile paresseusement au premier import et partage les caches entre répliques.
Trois conséquences à connaître
Une chaîne de requête distingue deux instances. Le registre applique les
règles d'identité de module du navigateur : même source, import.meta.url
différent, état de premier niveau séparé.
import { increment as incA } from './counter.js?a';
import { increment as incB } from './counter.js?b';
incA(); // 1
incA(); // 2
incB(); // 1 — instance distincte, avec sa propre copie de l'étatCe n'est pas un moyen de forcer une réévaluation : le même spécificateur avec la même chaîne de requête rend toujours la même instance.
Les attributs d'import sont validés, là où l'ancien registre les ignorait
silencieusement — en violation de la spécification. json est le seul type
autorisé, le seul dont la proposition TC39 soit en Stage 4 ; text et bytes
sont reconnus mais rejetés avec un message explicite, et toute clé autre que
type devient une erreur franche :
import data from './config.json' with { type: 'json', cache: 'no' };
// TypeError: Unsupported import attribute: "cache"require() sur un module ES suit les règles de Node. Un export nommé
'module.exports' prend la main, sinon vous récupérez l'objet namespace — sauf
pour les modules node: intégrés à workerd, qui rendent directement leur export
par défaut, si bien que require('node:buffer').Buffer se comporte comme prévu.
La restriction qui va avec mérite d'être connue : si le module requis, ou
n'importe quoi dans son graphe, contient un await de premier niveau, require()
lève une erreur au lieu de bloquer — exactement comme ERR_REQUIRE_ASYNC_MODULE
côté Node. Il faut passer par import().
À retenir
S'ajoutent des erreurs enfin cohérentes quel que soit le chemin de chargement —
Module not found est une Error, un spécificateur non analysable une
TypeError —, ce qui rend fiable un branchement sur la classe d'erreur dans un
loader maison, et les imports WebAssembly en source phase (import source wasmModule from './add.wasm'), qui rendent un WebAssembly.Module sans passer
par l'export par défaut.
La marche à suivre tient en trois temps : poser le drapeau sur une préproduction, relancer la suite de tests, puis regarder en priorité le code que vous aviez contourné à coups de polyfills et de bundling forcé. C'est celui-là qui peut disparaître.
Source : How we rebuilt Cloudflare Workers' module registry for Node.js compatibility