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 theserver 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.jsonexiste pero no supera la validación →init()lanza un error. El framework no degrada un manifiesto rechazado a un valor por defecto permisivo; hacerlo permitiría que una app con unentrypeligroso se registrase igualmente.wu.jsonexiste pero no se puede parsear (un servidor de desarrollo que devuelveindex.htmlpara 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 nadaawait 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 sí se respetan: url, sandbox, strictFallback,
keepAlive, strategy, roles, container.
3. Hace falta CORS en ambos recursos
Sección titulada «3. Hace falta CORS en ambos recursos»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-appexport 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
javascriptomodule 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.
5. strict ya no degrada en silencio (v2.7)
Sección titulada «5. strict ya no degrada en silencio (v2.7)»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.
Cosas menores que muerden
Sección titulada «Cosas menores que muerden»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.
Diagnóstico
Sección titulada «Diagnóstico»wu.verbose(); // registro de depuración completoconsole.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”.
