Plugins y hooks
Wu tiene dos mecanismos de extensión y no son lo mismo.
Los hooks son middleware. Registras una función en una de las 12 fases del ciclo de vida; se ejecuta en línea, puede mutar el contexto y puede cancelar la operación. Acceso total, sin sandbox: es tu propio código en tu propio shell.
Los plugins son extensiones empaquetadas, con aire de terceros. Declaran permisos, reciben una superficie de API congelada y limitada a esos permisos, y sus hooks están protegidos por timeout y aislados de errores, de modo que un plugin malo degrada en vez de romper la página.
Usa hooks para la lógica propia de tu shell. Usa plugins para comportamiento reutilizable y distribuible.
import { useHook } from 'wu-framework';// o wu.hooks.useuse(phase: string, middleware: (context, next) => Promise<void>, options?: { name?: string; priority?: number; failOpen?: boolean;}): () => voidRegistra middleware en una fase del ciclo de vida y devuelve una función para darlo de baja.
| Opción | Por defecto | Significado |
|---|---|---|
name |
autogenerado | Identificador que usa remove(). |
priority |
0 |
El más alto se ejecuta primero. A igual prioridad se mantiene el orden de registro. |
failOpen |
false |
Si es true, un lanzamiento se registra y la cadena continúa en lugar de cancelar. |
const off = wu.hooks.use('beforeMount', async (ctx, next) => { console.log('a punto de montar', ctx.appName); await next(); console.log('montado');}, { priority: 10, name: 'mount-logger' });
off(); // dar de bajaCasos límite. Lanza [WuHooks] Unknown lifecycle phase: <phase> para un
nombre que no esté entre los 12, y [WuHooks] Middleware must be a function en
otro caso. Ninguno de los dos errores lleva un code. No hay opción once ni
opción appName. priority usa ||, así que un priority: 0 explícito y NaN
colapsan ambos a 0. La función de baja se puede llamar dos veces sin problema.
Las 12 fases
Sección titulada «Las 12 fases»| Fase | Contexto | Cancelable |
|---|---|---|
beforeInit |
{ config } |
Sí |
afterInit |
{ config } |
No |
beforeLoad |
{ appName, containerSelector, attempt } |
Sí |
afterLoad |
{ appName, containerSelector, sandbox } |
No |
beforeMount |
{ appName, containerSelector, sandbox, lifecycle } |
Sí |
afterMount |
{ appName, containerSelector, sandbox, mountTime } |
No |
beforeUpdate |
{ appName, props, mounted } |
Sí |
afterUpdate |
{ appName, props } |
No |
beforeUnmount |
{ appName, mounted } |
Sí |
afterUnmount |
{ appName } |
No |
beforeDestroy |
{} |
No |
afterDestroy |
{} |
No |
“Cancelable” significa que el core comprueba el resultado y aborta la operación.
Cancelar una fase after* o cualquiera de las dos de destroy no tiene ningún
efecto en el core: el resultado no se inspecciona.
En el camino de keep-alive (hide() / show()), las fases de montaje y
desmontaje se disparan con un keepAlive: true extra en el contexto.
Compruébalo si tu hook asume que esas fases significan un desmontaje real.
Cancelar, mutar, cortocircuitar
Sección titulada «Cancelar, mutar, cortocircuitar»No llamar a next() cancela la operación:
wu.hooks.use('beforeMount', async (ctx, next) => { if (!(await checkAuth(ctx.appName))) return; // sin next() → cancelado await next();});Mutar funciona de dos formas: cambiando el objeto de contexto directamente, o
pasando un parche a next(), que se fusiona de forma superficial:
wu.hooks.use('beforeUpdate', async (ctx, next) => { await next({ props: { ...ctx.props, traceId: newId() } });});A los hooks se les hace await. next() es asíncrono y devuelve el resultado
aguas abajo, así que puedes envolver todo el resto de la cadena en un
temporizador o en un try/finally.
Fail-closed desde la v2.7
Sección titulada «Fail-closed desde la v2.7»Un hook que lanza antes de llamar a next() cancela la operación. Antes de
la v2.7 el error se registraba y la cadena continuaba, lo que significaba que una
guarda de autorización que fallaba —pongamos, un checkAuth() que se topa con un
error de red— dejaba montar la app igualmente. Fallar abierto en una
comprobación de seguridad es el valor por defecto equivocado. (Fail-closed
significa que, ante un fallo, la operación se deniega.)
// Esto ahora bloquea el montaje si checkAuth se rechaza.wu.hooks.use('beforeMount', async (ctx, next) => { await checkAuth(ctx.appName); await next();});
// Vuelve al comportamiento antiguo donde sea realmente apropiado:wu.hooks.use('afterMount', async (ctx, next) => { await sendTelemetry(ctx); // no rompas el montaje por la analítica await next();}, { failOpen: true });Caso límite que conviene saber. El fail-closed aplica solo si el hook
lanzó antes de que se llamara a next(). Un lanzamiento después de que
next() haya devuelto se registra y el resultado de la cadena se mantiene: la
operación no se cancela. El resultado de “cancelado por lanzamiento” lleva el
error original; una cancelación silenciosa (simplemente no llamar a next()) no.
Otros métodos de hooks
Sección titulada «Otros métodos de hooks»remove(phase: string, name: string): voiduseMultiple(phases: string[], middleware, options?): () => voidgetHooks(phase?: string): Array | Record<string, Array>getStats(): objectcleanup(phase?: string): voidCasos límite. remove() es un no-op silencioso para una fase o un nombre
desconocidos, y elimina solo la primera coincidencia. getHooks(phase) devuelve
el array interno vivo, incluidas las funciones de middleware — trátalo como
de solo lectura. cleanup() sin argumento reinicia las 12 fases y el registro de
ejecución.
useMultiple() tiene una trampa real: nombra cada registro
`${options.name}_${phase}`, así que llamarlo sin un name produce las
cadenas literales "undefined_beforeMount", "undefined_afterMount", etc. Dos
llamadas anónimas a useMultiple() colisionan. Pasa siempre un name.
Helpers de hooks
Sección titulada «Helpers de hooks»createSimpleHook(fn)createConditionalHook(condition, fn)createGuardHook(shouldContinue)createTransformHook(transformer)createTimedHook(fn, timeout = 5000)import { createGuardHook, createTransformHook } from 'wu-framework';
wu.hooks.use('beforeMount', createGuardHook(async (ctx) => { return await userCanSee(ctx.appName); // falsy → cancela el montaje}));
wu.hooks.use('beforeUpdate', createTransformHook((ctx) => ({ props: { ...ctx.props, locale: currentLocale() },})));| Helper | Comportamiento |
|---|---|
createSimpleHook |
Ejecuta fn y continúa. Un lanzamiento en fn cancela (fail-closed). |
createConditionalHook |
Ejecuta fn solo si condition es truthy. Continúa siempre en cualquier caso. |
createGuardHook |
Continúa solo si shouldContinue devuelve algo truthy. Falsy cancela. |
createTransformHook |
Fusiona el valor devuelto por el transformador en el contexto. |
createTimedHook |
Compite fn contra un timeout. Siempre continúa, incluso con error o timeout — es fail-open por construcción, independientemente de la opción failOpen. |
Plugins
Sección titulada «Plugins»import { usePlugin, createPlugin } from 'wu-framework';createPlugin() y use()
Sección titulada «createPlugin() y use()»createPlugin(config: { name: string; permissions?: string[]; install?: (api, options) => void; uninstall?: (api) => void; // más cualquiera de los 8 hooks}): Plugin
use(plugin: Plugin | ((options) => Plugin), options?: object): voidimport { usePlugin, createPlugin } from 'wu-framework';
const analytics = createPlugin({ name: 'analytics', permissions: ['events'],
install(api, options) { this.endpoint = options.endpoint; api.on('app:mounted', (e) => this.send('mount', e.data)); },
afterMount({ appName, mountTime }) { this.send('timing', { appName, mountTime }); },
onError({ phase, error, appName }) { this.send('error', { phase, appName, message: error.message }); },
uninstall(api) { this.flush(); },});
usePlugin(analytics, { endpoint: '/telemetry' });También se acepta una función factoría: se la llama con options y debe
devolver el objeto plugin. Las factorías no obtienen acceso al core.
Casos límite. use() devuelve undefined: no hay handle ni función de baja;
usa wu.pluginSystem.uninstall(name). Instalar un nombre que ya existe registra
Plugin "<name>" already installed y devuelve sin fusionar las opciones nuevas.
Si install() lanza, el error se propaga fuera de use() y el plugin no queda
registrado: sus hooks nunca se ejecutan.
La validación lanza ante: un plugin que no sea un objeto, un name ausente o que
no sea cadena, un nombre de más de 50 caracteres, un hook declarado que no sea
una función, unos permissions que no sean un array, y cualquier cadena de
permiso desconocida.
createPlugin() en sí no valida nada: un nombre ausente solo aflora después,
en use().
Los 8 hooks de plugin
Sección titulada «Los 8 hooks de plugin»| Hook | Contexto |
|---|---|
beforeInit |
{ config } |
afterInit |
{ config } |
beforeMount |
{ appName, containerSelector } |
afterMount |
{ appName, containerSelector, mountTime } |
beforeUnmount |
{ appName } |
afterUnmount |
{ appName } |
onError |
{ phase, error, appName? }, donde phase es 'init', 'mount' o 'unmount' |
onDestroy |
{} |
install y uninstall no son hooks: no están protegidos por timeout ni
aislados de errores.
Devolver exactamente false desde un hook aborta el bucle de despacho. El
core respeta eso para beforeMount y beforeUnmount, cancelando la operación.
Cualquier otro valor de retorno continúa.
Fíjate en la interacción con el manejo de errores: un hook que lanza se
captura y se trata como undefined, es decir, “continuar”. Si quieres vetar,
return false; no lances.
Los 6 permisos
Sección titulada «Los 6 permisos»El valor por defecto es ['events']. El objeto de API que se entrega a
install() contiene solo lo que desbloquean los permisos declarados, más una
pequeña base siempre presente: version, info, getAppInfo(name),
getMountedApps(), getStats().
| Permiso | Desbloquea |
|---|---|
events |
api.emit, api.on, api.off |
store |
api.getState, api.setState |
mount |
api.mount, api.unmount |
config |
api.configure — solo se aplican las claves debug y logLevel; todo lo demás se descarta silenciosamente |
apps |
Nada. Se acepta como válido pero no hay ninguna API detrás. El listado de apps ya está en la API base. |
unsafe |
Todo lo anterior, más api._unsafeCore — el objeto core en crudo. Registra un aviso al instalar. |
Declarar permissions: [] da cero permisos, no el valor por defecto: el
valor por defecto solo aplica cuando el campo está ausente.
El objeto de API está congelado en profundidad, de forma recursiva, antes de
llegar al plugin. Los objetos planos y los arrays se congelan; las funciones y
las instancias de clase se dejan invocables. Esa última excepción implica que
api._unsafeCore no está congelado — que es justamente el sentido de
unsafe, y la razón para tratarlo como último recurso.
El timeout de 5 segundos de los hooks
Sección titulada «El timeout de 5 segundos de los hooks»Cada invocación de un hook de plugin compite contra un timeout de 5000 ms
(configurable con new WuPluginSystem(core, { hookTimeout })).
Al agotarse el timeout, el resultado del hook pasa a ser undefined y el
despacho continúa. No lanza, no cancela la operación, y no da de baja ni
deshabilita el plugin. La promesa subyacente no se cancela: sigue
ejecutándose en segundo plano. Lo mismo aplica a un hook que lanza: registrado,
tragado, tratado como “continuar”.
La consecuencia es que los plugins son honestamente de mejor esfuerzo. Un plugin
no puede romper tu página, y tampoco puede bloquear nada de forma fiable salvo
devolviendo false con prontitud.
Otros métodos de plugin
Sección titulada «Otros métodos de plugin»uninstall(pluginName: string): voidgetPlugin(pluginName: string): Plugin | undefinedgetStats(): { totalPlugins, plugins, hooks }cleanup(): voidCasos límite. uninstall() sobre un nombre desconocido es un aviso, no un
lanzamiento. Llama al uninstall() del plugin (con los errores capturados y
registrados) y elimina exactamente las callbacks de hook que registró.
getPlugin() devuelve el objeto plugin original, no el registro con sandbox. No
hay list() ni getPlugins(): usa getStats().plugins.
Limitaciones honestas
Sección titulada «Limitaciones honestas»- Los plugins no pueden bloquear de forma fiable. Los lanzamientos y los
timeouts se tragan y se tratan como “continuar”; solo un
return falsea tiempo veta, y solo parabeforeMountybeforeUnmount. - El sandbox es una comodidad, no una frontera de seguridad. Un plugin es
JavaScript ejecutándose en tu página. Puede alcanzar
window, el DOM y elwuglobal. El modelo de permisos documenta la intención y atrapa accidentes; no contiene a un plugin hostil. - El permiso
appsno hace nada. Valida y no concede ninguna API. - Los hooks son la mitad poderosa y no tienen sandbox alguno — por diseño, pero conviene recordarlo antes de aceptar uno de una dependencia.
useMultiple()sin unnameproduce identificadores que colisionan.createTimedHookes fail-open independientemente de la opciónfailOpen, lo que lo convierte en la herramienta equivocada para una guarda.
