Introducción
Si alguna vez intentaste correr una aplicación Node.js en Cloudflare Workers y te topaste con errores de resolución de módulos, import.meta devolviendo undefined o un require() que funcionaba en tu laptop pero explotaba en producción, el origen del problema era el mismo: el module registry de workerd resolvía especificadores como rutas de filesystem, no como URLs. Esa decisión de diseño, heredada de las primeras versiones del runtime, impedía implementar correctamente import.meta.resolve(), trataba los prefijos node: y cloudflare: como hacks de string, compilaba el bundle completo antes de ejecutar una sola línea y guardaba una copia privada del código por cada aislamiento V8. Con múltiples réplicas de aislamiento corriendo el mismo Worker en distintos núcleos de CPU, eso significaba compilar el mismo fuente varias veces y duplicar memoria sin necesidad.
Cloudflare resolvió este problema reescribiendo el registry desde cero. El resultado, disponible bajo la compatibilidad flag new_module_registry, alinea el comportamiento de Workers con la especificación ESM, los navegadores y Node.js. No es un parche: es un rediseño que trata la resolución por URLs, la compilación perezosa y la compartición de caché como requisitos de arquitectura, no como optimizaciones posteriores.
Qué ocurrió
Logan Gatlin y James Snell, ingenieros del equipo de runtime de Cloudflare, publicaron el detalle de la reescritura del module registry en workerd, el componente open-source que ejecuta todo Worker en la red de Cloudflare. El cambio no elimina el registry anterior: los Workers ya desplegados siguen funcionando sin modificaciones. El nuevo comportamiento se activa habilitando la flag new_module_registry en la configuración de compatibilidad del Worker, lo que permite una migración controlada.
El contexto inmediato es que Workers ya soporta todas las APIs estables de Node.js aplicables a un contexto serverless, y ahora esas APIs están habilitadas por defecto sin necesidad de flags adicionales. Además, Cloudflare eliminó el límite de tamaño del bundle comprimido y elevó el máximo por aplicación a 64 MiB en todos los planes. Sin embargo, soporte de APIs no implica compatibilidad de carga: las aplicaciones Node.js dependen de cómo el runtime resuelve, carga y cachea módulos ESM, CommonJS y WebAssembly. Ahí es donde el nuevo registry cambia el juego.
Impacto para DevOps / Infraestructura / Cloud / Seguridad
Para equipos que migran aplicaciones Node.js a Workers, el cambio reduce drásticamente la superficie de incompatibilidades silenciosas. Antes, un import.meta.resolve() en una librería de terceros fallaba sin contexto claro; ahora devuelve la URL resuelta siguiendo las mismas reglas que new URL(specifier, base). Para infraestructura, la compilación perezosa y la caché compartida entre aislamientos V8 reducen la huella de memoria por Worker y eliminan compilaciones redundantes del mismo fuente en réplicas concurrentes.
Desde la perspectiva de seguridad, el manejo estricto de import attributes cierra una brecha de comportamiento indefinido. El registry anterior ignoraba silenciosamente atributos de importación que no reconocía, en violación de la especificación TC39. El nuevo registry lanza un TypeError ante cualquier atributo desconocido, lo que convierte un error silencioso en una falla explícita en tiempo de despliegue o ejecución. Esto es particularmente relevante cuando dependencias actualizadas introducen atributos de importación que el runtime no soporta.
Detalles técnicos
El registry original resolvía especificadores como rutas de filesystem. El nuevo registry usa URLs como formato nativo. Esto significa que node: y cloudflare: se tratan como protocolos reales, no como prefijos string con lógica especial. Las importaciones relativas se resuelven exactamente como new URL(specifier, base), y los URLs completos funcionan como especificadores válidos.
La API import.meta ahora expone import.meta.url (devuelve algo como file:///bundle/index.js), import.meta.main (booleano, true solo para el módulo de entrada del Worker) y import.meta.resolve(specifier) (transformación de string pura, sin importar el módulo). Un detalle que suele sorprender: resolve() normaliza el percent-encoding como new URL() —colapsa ./a/../b.js— pero no decodifica caracteres ya codificados. import.meta.resolve(‘%66oo.js’) devuelve file:///bundle/%66oo.js, no file:///bundle/foo.js.
En cuanto a identidad de módulos, un query string o fragmento distinto genera una instancia separada: ./counter.js?a y ./counter.js?b cargan el mismo fuente pero se evalúan por separado, cada uno con su propio import.meta.url y su propio estado de top-level. Importar el mismo specifier con el mismo query string devuelve la misma instancia, así que no es un mecanismo para forzar re-evaluación.
Sobre import attributes: json es el único tipo habilitado (llegó a Stage 4 en TC39). text y bytes se reconocen como propuestas en desarrollo pero se rechazan con un error específico. Cualquier clave distinta de type es un error duro. Si el type declarado no coincide con el tipo real del módulo, también se lanza excepción.
Con el plugin Vite de Cloudflare y Vite 8, el bundling pasa de esbuild a Rolldown, que emite un módulo de entrada más chunks adicionales (code splitting, dynamic imports). El runtime recibe un grafo de módulos más pequeño y el nuevo registry puede tomar un rol activo en la resolución, reduciendo las transformaciones en build time.
Qué deberían hacer los administradores y equipos técnicos
Para habilitar el nuevo registry en un Worker existente, agregá la flag en wrangler.toml o wrangler.jsonc:
compatibility_flags = [«new_module_registry»]
Luego ejecutá un despliegue en staging y verificá que no haya regresiones en la resolución de dependencias. Si tu proyecto usa Wrangler con bundling por defecto (esbuild inline), el cambio en el runtime es transparente. Si usás –no-bundle o el plugin Vite con Rolldown, el impacto es mayor porque el registry pasa a resolver un grafo de módulos real.
Revisen dependencias que usen import attributes con type distinto de json. El comportamiento anterior (ignorar silenciosamente) desaparece. Ejecutá los tests de la suite con la flag activada antes de promover a producción. Para aplicaciones que se acercan al límite anterior de bundle, el nuevo máximo de 64 MiB por aplicación elimina la necesidad de particionar en múltiples Workers por restricciones de tamaño.
Si administrás múltiples Workers con configuraciones de compatibilidad heterogéneas, centralizá la flag en una configuración compartida o un script de CI que valide que todos los proyectos la incluyan antes del merge. La documentación de referencia del registry a nivel V8 está disponible en el repositorio de workerd para quien necesite entender la interacción con los módulos nativos del motor.
Conclusión
El rediseño del module registry no es un feature cosmético: corrige una capa estructural de workerd que limitaba la compatibilidad con Node.js, desperdiciaba recursos de compilación y generaba comportamientos indefinidos con import attributes. Al tratar URLs como tipo nativo, implementar import.meta según la especificación, compilar bajo demanda y compartir caché entre aislamientos, Cloudflare elimina una clase entera de bugs de migración que afectaban a equipos que llevan aplicaciones Node.js a serverless. La flag new_module_registry permite adoptarlo sin riesgo de ruptura para los Workers ya en producción, pero postergar la migración significa seguir cargando con las limitaciones del diseño anterior.
Fuentes
https://blog.cloudflare.com/workers-module-registry-nodejs/
https://www.bankinfosecurity.com/
https://www.mandiant.com/resources/blog
