Ir al contenido

Arquitectura

Wu tiene cuatro piezas móviles y una regla: las apps nunca se importan entre sí.

┌─ Shell (la raíz de confianza) ────────────────────────────────────────┐
│ wu.init({ apps }) ──► obtiene <url>/wu.json de cada app │
│ │
│ wu.mount('cart', '#cart') │
│ │ │
│ ▼ │
│ ┌─ Contenedor anfitrión #cart ─────────────────────────────────────┐ │
│ │ shadowRoot (attachShadow, delegatesFocus) │ │
│ │ ├─ <style> estilos base del sandbox │ │
│ │ ├─ estilos inyectados (según styleMode) │ │
│ │ └─ <div class="wu-app-root"> ← el contenedor de la app │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ El contexto de ejecución JS depende del modo de sandbox: │
│ module → ventana principal │
│ strict/eval → iframe oculto del mismo origen │
│ │
│ ┌─ Sustrato (compartido por todas las apps y todos los modos) ─────┐ │
│ │ eventBus · store · contratos de capacidades · timeline · hooks │ │
│ └──────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────┘

La página que llama a wu.init() y wu.mount(). Es dueña del layout, el enrutado y la autenticación. Es la raíz de confianza: todo lo que hace Wu ocurre dentro del origen del shell, por invitación del shell.

<appUrl>/wu.json — la declaración que hace la app de su propio nombre, punto de entrada, modo de estilos, exports, imports y roles requeridos. Se obtiene durante init(), se valida y se cachea en memoria. Los campos que Wu no reconoce se descartan. Mira El manifiesto.

Dos capas independientes, siempre presentes las dos:

  • Un shadow root para el DOM y el CSS. Todas las apps reciben uno, en todos los modos, sin forma de desactivarlo (salvo que el Shadow DOM no esté disponible, lo que activa un contenedor de respaldo degradado).
  • Un contexto de ejecución JS, elegido por el modo sandbox: la ventana principal (module) o un iframe oculto del mismo origen (strict, eval).

La capa de CSS es encapsulación de verdad. La capa de JS es contención para tu propio código: aísla globales y garantiza la limpieza, pero no es una barrera frente a código que intenta escapar. Mira el modelo de amenazas.

Un bus de eventos, un store compartido y contratos de capacidades versionados, todos viviendo en la ventana del shell y accesibles desde cualquier app sin importar el modo de sandbox. Esta es la única forma sancionada de que las apps interactúen. Mira Comunicación.

wu.mount('cart', '#cart')
├─ 1. Comprobación RBAC — ¿can(appName)? ───► lanza WU_ACCESS_DENIED
├─ 2. Incrementa el contador de referencias; cancela cualquier desmontaje
│ pendiente
├─ 3. Crea el sandbox
│ ├─ ¿hay marcado del servidor? → lo adopta (ruta de hidratación)
│ └─ si no → attachShadow + <div class="wu-app-root">
├─ 4. Inyecta estilos según styleMode; espera a sandbox.stylesReady
├─ 5. Carga la app (solo si aún no se ha registrado)
│ ├─ resuelve la URL de entry (directa, o sondeo HEAD 8× en paralelo)
│ ├─ module → import() en la ventana principal
│ ├─ strict → import() dentro de un iframe oculto
│ └─ eval → obtiene el HTML, lo parsea, ejecuta los <script> clásicos
│ en el iframe
├─ 6. Espera a wu.define('cart', …) (timeout de 10 s)
├─ 7. Llama a lifecycle.hydrate(...) si se adoptó marcado del servidor,
│ y si no a lifecycle.mount(container)
└─ 8. Registra el montaje, emite app:mounted, ejecuta los hooks afterMount

Los pasos 5 y 6 solo ocurren en el primer montaje. Una vez que una app ha llamado a wu.define(), su ciclo de vida queda cacheado y los remontajes saltan directamente al paso 7 — porque, de todas formas, un navegador no vuelve a ejecutar un módulo ES ya cacheado.

Si algún paso lanza un error, decide el error boundary: reintentar (hasta tres intentos, con backoff y una limpieza completa del sandbox entre ellos), recuperarse en silencio o relanzar.

Tres cosas se configuran por separado y se combinan libremente:

Eje Dónde se define Valores
Sandbox JS configuración de init() o config por app module, strict, eval
Aislamiento CSS solo en wu.json shared, isolated, fully-isolated
Comunicación siempre disponible, idéntica en todos los modos

La asimetría de la fila central es deliberada y sorprende a todo el mundo una vez: styleMode es una propiedad de la app, así que la app lo declara en su propio manifiesto. Definirlo en la configuración de init() del shell no hace nada.

No hay negociación de versiones para los módulos de framework. Wu sí reutiliza el runtime cuando el shell lo pone en el global — seis adapters miran window.React y equivalentes antes de importar el suyo — pero nada compara versiones, así que una major distinta se adopta en silencio. No hay registro de módulos ni share scope al estilo de Module Federation. Lo que ofrece en su lugar son contratos de capacidades: las apps publican objetos vivos bajo un rango semver, y un desajuste falla de forma ruidosa en el consume(). Eso sí es negociación de verdad, pero para capacidades entre apps, no para módulos de framework. Ver comunicación.

No se integra con el build. No hay plugin de webpack para Wu, ni plugin de Vite, ni configuración de bundler compartida. Una app es una URL que sirve un manifiesto y un bundle. El acoplamiento entre shell y app es HTTP, no el grafo de build.

No hay imports entre apps. Los campos exports/imports de wu.json describen rutas de componentes para wu.use(), no son un enlazador. Una app no puede meterse en el scope de módulos de otra.

Asunto Módulo
Orquestación, mount/unmount, selección de sandbox wu-core.js
Creación del shadow root, inyección de estilos wu-sandbox.js, wu-style-bridge.js
Rastreo de efectos secundarios en modo module wu-proxy-sandbox.js
Realm de iframe para strict / eval wu-iframe-sandbox.js (perezoso)
Parseo de HTML para eval wu-html-parser.js (perezoso)
Obtención y validación del manifiesto wu-manifest.js
Sustrato wu-event-bus.js, wu-store.js, wu-contracts.js
Puntos de extensión wu-hooks.js, wu-plugin.js
Renderizado en servidor src/server/

Todo lo marcado como perezoso es un chunk aparte en el build ESM y solo se carga cuando se usa realmente esa funcionalidad.