Ir al contenido

Escribir un adapter

Wu incluye trece adapters. Si el tuyo no está entre ellos, tienes dos opciones: llamar a wu.define() directamente (perfectamente válido, ver Ciclo de vida), o construir un register() a partir de createWuAdapter() y obtener gratis toda la plomería compartida.

Todo lo de esta página viene de wu-framework/adapters/shared.

import {
createWuAdapter,
getWuInstance,
waitForWu,
escapeHtml,
} from 'wu-framework/adapters/shared';

Toma una pequeña configuración específica del framework y devuelve una función register(appName, Component, options) completa. Centraliza el boilerplate que cada adapter reimplementaba a mano: getWuInstance / waitForWu, el cableado de wu.define(), la contabilidad de instancias por app, el plan B standalone y los slots condicionales update / hydrate.

Clave Tipo Obligatoria Propósito
name string no ('WuAdapter') Etiqueta usada en los mensajes de log, p. ej. 'WuReact'.
ensure async function no () => boolean. Carga o detecta el framework. Devolver false aborta el registro.
mount function (container, ctx) => instance.
unmount function (container, ctx) => void.
update function no (container, ctx) => void. El canal de props vivas.
hydrate function no (container, ctx) => instance. Se engancha al DOM renderizado en servidor.
defineTimeout number no (3000) Milisegundos a esperar por la instancia de Wu.

Si faltan mount o unmount, lanza de inmediato:

[<name>] createWuAdapter requires mount and unmount functions
mount(container, { Component, props, appName, options }) // → instance
unmount(container, { appName, instance, options })
update(container, { props, prevProps, appName, instance, options })
hydrate(container, { Component, props, appName, options }) // → instance
  • props son siempre las props actuales fusionadas, no las de registro.
  • instance es lo que devolviera mount (o hydrate). Devuelve un manejador que vayas a necesitar al desmontar: una raíz, una función dispose, un objeto de app.
  • options es la bolsa completa de opciones que pasó quien llamó a register(), así que tus opciones propias del adapter son accesibles desde cualquier callback.
  • prevProps en update es el objeto de props de antes de la fusión.
register(appName, Component, options)

Opciones universales, desestructuradas por el propio createWuAdapter:

Opción Por defecto
props {}
standalone true
standaloneContainer '#root'
onMount null
onUnmount null

Devuelve Promise<boolean>. onMount(container) se dispara después de montar y después de hidratar; onUnmount(container) se dispara antes de que se ejecute tu unmount.

import { createWuAdapter } from 'wu-framework/adapters/shared';
const instances = new Map();
const register = createWuAdapter({
name: 'WuMithril',
async ensure() {
if (globalThis.m) return true;
try {
globalThis.m = (await import('mithril')).default;
return true;
} catch {
return false; // aborta el registro; register() resuelve false
}
},
mount(container, { Component, props, appName }) {
container.innerHTML = '';
m.mount(container, { view: () => m(Component, props) });
instances.set(appName, { container, props });
return { container };
},
update(container, { props, appName }) {
const inst = instances.get(appName);
if (!inst) return;
inst.props = props;
m.redraw();
},
unmount(container, { appName }) {
const inst = instances.get(appName);
if (inst) m.mount(inst.container, null);
instances.delete(appName);
if (container) container.innerHTML = '';
},
});
export const wuMithril = { register };
export { register };

Esta es la regla más importante del contrato.

update y hydrate solo se añaden al objeto de ciclo de vida cuando los pasas:

if (typeof hydrate === 'function') lifecycle.hydrate = /* … */;
if (typeof update === 'function') lifecycle.update = /* … */;

La razón es que wu.update() debe poder no hacer nada de forma honesta. Si todos los adapters anunciaran update, un shell que llamara a wu.update('reports', { x: 1 }) obtendría una llamada con pinta de exitosa para una app que no puede volver a renderizar: las props se desvanecerían sin error, sin aviso y sin forma de detectarlo. Al omitir el slot, la ausencia es visible para wu-core, que puede no hacer nada de forma deliberada, registrarlo o reportarlo en el diagnóstico.

En concreto: no pases update si tu framework captura las props en tiempo de render (Solid, Qwik), o si la única forma de “actualizar” es desmontar y volver a montar. Un remontaje disfrazado de actualización es peor que no tener actualización: destruye en silencio el estado del componente.

El mismo razonamiento aplica a hydrate. Pásalo solo si tu framework puede adoptar genuinamente el DOM existente (hydrateRoot de React, createSSRApp de Vue). Sin él, una app con markup de servidor simplemente se monta encima: sigue siendo correcto, solo que repintado. Vue es el modelo aquí: construye el callback hydrate solo cuando Vue.createSSRApp existe de verdad en el namespace resuelto.

createWuAdapter mantiene dos objetos en el closure de register(): initialProps (congeladas en el registro) y currentProps (vivas).

  • update: currentProps = { ...currentProps, ...nextProps } antes de que se ejecute tu callback.
  • hydrate: currentProps = { ...currentProps, ...ctx.props }ganan las props del servidor, porque son las que produjeron el markup que estás a punto de adoptar.
  • unmount: currentProps se restablece a initialProps en un bloque finally, y instance se pone a null.

Ese restablecimiento importa más de lo que parece. Cuando wu-core reubica una app en otro contenedor —una pestaña keep-alive que se mueve, un cambio de layout—, el nuevo montaje debe partir de las props de registro, no de lo último que envió el shell antes de que la app desapareciera.

Tras construir el ciclo de vida, el register() generado hace:

try {
const wu = await waitForWu(defineTimeout);
wu.define(appName, lifecycle);
return true;
} catch {
// no hay shell dentro de defineTimeout
if (standalone) {
const el = document.querySelector(standaloneContainer);
if (el) { await mountApp(el); return true; }
}
return false;
}

Sin Shadow DOM, sin bus de eventos, sin aislamiento: solo tu componente en la página. Esto es lo que permite que un mismo bundle sea a la vez un micro-frontend y una app normal.

Elige el valor por defecto de standaloneContainer acorde a la convención de tu ecosistema (#root para el tooling de la familia React, #app para la familia Vue/Svelte) y deja que quien lo use lo sobrescriba.

const wu = getWuInstance(); // object | null

Busca la instancia de Wu en window, luego en window.parent y luego en window.top. Devuelve null en un entorno que no sea navegador.

Cada lectura de parent/top va envuelta en su propio try:

try { if (window.parent?.wu) return window.parent.wu; } catch { /* cross-origin */ }

Leer .wu en un Window cross-origin lanza un SecurityError síncrono, y el encadenamiento opcional no te salva: window.parent siempre es un objeto Window, así que ?. nunca cortocircuita. Sin el try, un adapter que corra en un iframe cross-origin lanzaría antes de poder recurrir a window.top.

const wu = await waitForWu(3000); // rechaza: "Wu Framework not found after 3000ms"

El timeout por defecto es 5000 ms; todos los adapters incluidos lo llaman con 3000, que es también el valor por defecto de defineTimeout en createWuAdapter.

Estrategia de resolución:

  1. Comprobación inmediata con getWuInstance() — resuelve de forma síncrona si Wu está presente.
  2. Listeners de los eventos de ventana wu:ready y wu:app:ready.
  3. Un sondeo conservador cada 200 ms como respaldo, que además impone el timeout.

Los tres se desmontan en un cleanup() compartido, así que no queda ningún intervalo ni listener filtrado por ninguna de las vías.

escapeHtml('<img src=x onerror=alert(1)>');
// '&lt;img src=x onerror=alert(1)&gt;'

Escapa &, <, >, " y '. Convierte null / undefined a ''.

No es una utilidad de propósito general: cierra una escalada real que cruza límites de confianza.

Los slots renderizan su estado de error con innerHTML e interpolan err.message. Ese mensaje no siempre lo produce el framework. En modo sandbox strict, un throw new Error('<img src=x onerror=…>') dentro de un módulo remoto viaja literal —iframe → postMessagenew Error(data.error)— hasta el slot. Y el statusText de una respuesta HTTP/1.1 lo controla el servidor remoto.

Sin escapar, esa carga se ejecutaba en el shell. Dicho de otro modo: la micro-app escapaba de su iframe por el canal de reporte de errores, la única vía que nadie ve como una entrada.

Consulta Modelo de amenazas para el límite que rodea todo esto.

  • mount devuelve un manejador con el que realmente puedas desmontar.
  • unmount es idempotente — wu-core puede llamarlo sobre una app ya desmontada.
  • update se pasa solo si el framework vuelve a renderizar en el sitio.
  • hydrate se pasa solo si el framework adopta el DOM existente.
  • ensure devuelve false (en lugar de lanzar) cuando el framework no está.
  • standaloneContainer usa por defecto la convención de tu ecosistema.
  • Todo texto de error que llegue a innerHTML pasa por escapeHtml().
  • Quien lo consuma importa desde un subpath, no desde un barrel.

El adapter de Vanilla es la implementación de referencia: es el único adapter incluido construido sobre createWuAdapter(), y su código fuente es lo bastante corto como para leerlo de una sentada.