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';createWuAdapter(config)
Sección titulada «createWuAdapter(config)»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.
Configuración
Sección titulada «Configuración»| 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 |
sí | (container, ctx) => instance. |
unmount |
function |
sí | (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 functionsQué recibe cada callback
Sección titulada «Qué recibe cada callback»mount(container, { Component, props, appName, options }) // → instanceunmount(container, { appName, instance, options })update(container, { props, prevProps, appName, instance, options })hydrate(container, { Component, props, appName, options }) // → instancepropsson siempre las props actuales fusionadas, no las de registro.instancees lo que devolvieramount(ohydrate). Devuelve un manejador que vayas a necesitar al desmontar: una raíz, una función dispose, un objeto de app.optionses la bolsa completa de opciones que pasó quien llamó aregister(), así que tus opciones propias del adapter son accesibles desde cualquier callback.prevPropsenupdatees el objeto de props de antes de la fusión.
El register() generado
Sección titulada «El register() generado»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.
Un ejemplo completo
Sección titulada «Un ejemplo completo»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 };Anuncia un slot solo si lo dices en serio
Sección titulada «Anuncia un slot solo si lo dices en serio»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.
Contabilidad de props que obtienes gratis
Sección titulada «Contabilidad de props que obtienes gratis»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:
currentPropsse restablece ainitialPropsen un bloquefinally, yinstancese 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.
El plan B standalone
Sección titulada «El plan B standalone»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.
getWuInstance()
Sección titulada «getWuInstance()»const wu = getWuInstance(); // object | nullBusca 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.
waitForWu(timeout)
Sección titulada «waitForWu(timeout)»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:
- Comprobación inmediata con
getWuInstance()— resuelve de forma síncrona si Wu está presente. - Listeners de los eventos de ventana
wu:readyywu:app:ready. - 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(value)
Sección titulada «escapeHtml(value)»escapeHtml('<img src=x onerror=alert(1)>');// '<img src=x onerror=alert(1)>'Escapa &, <, >, " y '. Convierte null / undefined a ''.
Por qué existe
Sección titulada «Por qué existe»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 → postMessage → new 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.
Checklist
Sección titulada «Checklist»-
mountdevuelve un manejador con el que realmente puedas desmontar. -
unmountes idempotente — wu-core puede llamarlo sobre una app ya desmontada. -
updatese pasa solo si el framework vuelve a renderizar en el sitio. -
hydratese pasa solo si el framework adopta el DOM existente. -
ensuredevuelvefalse(en lugar de lanzar) cuando el framework no está. -
standaloneContainerusa por defecto la convención de tu ecosistema. - Todo texto de error que llegue a
innerHTMLpasa porescapeHtml(). - 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.
