Ir al contenido

Errores comunes

Casi todos los problemas con Wu son una de seis cosas. Las seis son invisibles en desarrollo y evidentes en producción, que es la peor combinación posible. Lee esto antes de desplegar.

1. Un wu.json ausente es el bug nº 1 exclusivo de producción

Sección titulada «1. Un wu.json ausente es el bug nº 1 exclusivo de producción»

wu.json debe servirse en <appUrl>/wu.json.

En desarrollo, su ausencia queda oculta. Un 404 hace que Wu sintetice un manifiesto por defecto ({ name: <último segmento de la URL>, entry: 'index.js' }) y luego ejecute un sondeo HEAD de 8 rutas — src/, la raíz, dist/, public/, build/, assets/, lib/, es/ — hasta que algo responda con JavaScript. Un servidor de desarrollo de Vite suele responder, así que todo parece funcionar.

En producción el sondeo falla y el error es críptico:

Failed to load module script: Expected a JavaScript module script but the
server responded with a MIME type of "text/html".

Ocurre porque los hosts estáticos (S3, Netlify, GitHub Pages, el fallback SPA de CloudFront) devuelven index.html para rutas desconocidas, con content type text/html, y el navegador se niega a importarlo como módulo.

Solución: publica wu.json junto a tu bundle, siempre, con un entry que use un prefijo de carpeta conocido.

{ "name": "cart", "entry": "assets/index-a1b2c3.js" }

Si construís con wu-cli, esto ya está resuelto: wu build emite el wu.json de cada app con el entry exacto del bundle hasheado. El manifiesto manual solo hace falta si empaquetás por tu cuenta.

Fíjate en la asimetría, porque importa al depurar:

  • 404 en wu.json → manifiesto por defecto, sin error.
  • wu.json existe pero no supera la validacióninit() lanza un error. El framework no degrada un manifiesto rechazado a un valor por defecto permisivo; hacerlo permitiría que una app con un entry peligroso se registrase igualmente.
  • wu.json existe pero no se puede parsear (un servidor de desarrollo que devuelve index.html para esa ruta) → se trata como un 404, manifiesto por defecto.

2. entry y styleMode en wu.init() se ignoran en silencio

Sección titulada «2. entry y styleMode en wu.init() se ignoran en silencio»
// ❌ Ninguno de los dos hace nada
await wu.init({
apps: [{ name: 'cart', url: '...', entry: 'main.js', styleMode: 'isolated' }],
});

Wu siempre obtiene <url>/wu.json y lee entry y styleMode de ahí. Los campos en línea se descartan sin ningún aviso.

// ✅ public/wu.json — el único sitio de donde se leen
{ "name": "cart", "entry": "src/main.jsx", "styleMode": "fully-isolated" }

Campos en línea que se respetan: url, sandbox, strictFallback, keepAlive, strategy, roles, container.

El shell obtiene dos cosas de otro origen: wu.json y el bundle de entrada. Ambos necesitan Access-Control-Allow-Origin cubriendo el origen del shell.

// vite.config.js en la micro-app
export default { server: { cors: true } };

En producción, configura CORS en el bucket o la CDN que hay delante de cada app. El origen del shell debe estar en la lista de permitidos de la CDN de cada app. Una cabecera ausente en wu.json bloquea toda la carga antes de que se llegue a pedir el bundle.

sandbox: 'strict' es aún más sensible: el import() del iframe es una carga de módulo entre orígenes y, desde la v2.7, un fallo de CORS ahí lanza un error en lugar de degradar (mira el punto 5).

4. El JavaScript debe servirse con un content type de JavaScript

Sección titulada «4. El JavaScript debe servirse con un content type de JavaScript»

El sondeo de rutas de Wu rechaza cualquier candidato cuyo Content-Type empiece por text/html: así es como detecta un fallback HTML de SPA haciéndose pasar por tu bundle. Acepta:

  • cualquier cosa que contenga javascript o module
  • text/plain
  • un content type vacío (entonces confía en la extensión .js / .mjs)

Si tu host sirve .js como text/html, ningún candidato del sondeo encajará nunca y la ruta de respaldo fallará al importar.

Un 405/501 en HEAD está contemplado: Wu reintenta con un GET por rangos de 256 bytes.

Antes de la v2.7, una app strict cuyo import() en el iframe fallaba caía a eval con solo un aviso. Eso significaba que cualquiera capaz de romper CORS podía degradar una app que habías aislado deliberadamente.

Desde la v2.7, strictFallback vale false por defecto y mount() lanza un error:

[strict] iframe import failed for 'cart' and strictFallback is disabled.
Original error: ...
Fix: ensure the app's dev server sets Access-Control-Allow-Origin headers,
or explicitly allow degrade with { strictFallback: true } or sandbox: 'eval'.

Si una app strict que antes funcionaba ahora lanza un error tras actualizar, tenías un problema de CORS desde el principio y estabas ejecutando en modo eval sin saberlo. Arregla las cabeceras en lugar de reactivar el respaldo.

Además, la degradación suele ser inútil: el modo eval no puede ejecutar módulos ES, así que una app ESM que degrada produce un segundo error, más confuso, que enmascara el fallo de CORS original.

Verifica lo que realmente obtuviste:

const info = wu.getSandboxInfo('cart');
if (info?.actualMode !== 'strict') throw new Error('No está aislada');

6. El sondeo de 8 peticiones cuesta de verdad

Sección titulada «6. El sondeo de 8 peticiones cuesta de verdad»

Sin un prefijo de carpeta reconocido en entry, cada primer montaje lanza ocho peticiones HEAD en paralelo. El resultado se cachea por baseUrl + entry, así que los remontajes no lo repiten, pero el primer pintado lo paga, y ocho 404 en el panel de red parecen un bug.

Prefijos reconocidos: src/, dist/, public/, build/, assets/, lib/, es/. Usa uno.


React StrictMode no es problema. mount()/unmount() llevan conteo de referencias con un temporizador de gracia de 60 ms, así que el efecto de doble invocación no provoca parpadeo ni un doble montaje. No necesitas desactivar StrictMode.

wu.update() puede no hacer nada, y lo dice. Si el adaptador de la app no anunció un slot update, wu.update() registra un aviso y devuelve false en lugar de fingir. Comprueba el valor de retorno si te importa.

Solo el último unmount() desmonta. Con conteo de referencias, tres montajes necesitan tres desmontajes. { force: true } se salta esto cuando necesitas un desmontaje inmediato.

keepAlive oculta, no pausa. Los temporizadores siguen corriendo, los intervalos siguen disparándose, los WebSockets siguen abiertos. Usa el slot de ciclo de vida deactivate() para silenciar las cosas tú mismo.

El Shadow DOM rompe algunas librerías. Cualquier cosa que consulte document directamente buscando elementos que ella misma renderizó — algunos date pickers, librerías de tooltips antiguas, portales que apuntan a document.body — no los encontrará. En modo strict/eval, Wu parchea el document.querySelector y el document.body del iframe para que apunten al shadow root, lo que arregla muchos de estos casos. En modo module no lo hace.

Las fuentes y @font-face no se heredan dentro de un shadow root. Se resuelven contra el documento, así que en general funcionan, pero un @font-face declarado dentro de un shadow root lo ignoran casi todos los navegadores. Declara las fuentes en el documento anfitrión.

Una app que nunca llama a wu.define() agota el tiempo de espera. Tras 10 s obtienes:

App 'cart' loaded but wu.define() was not called within 10000ms.

Normalmente el bundle lanzó un error antes de llegar al registro, o el name de register() no coincide con el name de init().

wu.json se cachea en memoria. Las llamadas repetidas a init() lo reutilizan. Usa wu.manifest.clearCache(pattern) si estás intercambiando manifiestos en caliente en un entorno de pruebas.

wu.verbose(); // registro de depuración completo
console.log(wu.inspect());
console.log(wu.getSandboxInfo('cart'));

inspect() te da las apps registradas frente a las definidas frente a las montadas, el modo de sandbox real por app, las capacidades vivas, los eventos recientes y la instantánea del store — suficiente para distinguir “nunca se cargó” de “se cargó pero nunca se registró” de “se montó en el contenedor equivocado”.