Modos de sandbox
El modo de sandbox elige dónde se ejecuta el JavaScript de la app. No afecta al DOM: todas las apps reciben un shadow root en todos los modos.
await wu.init({ sandbox: 'strict', // valor global por defecto apps: [ { name: 'cart', url: '...' }, // strict { name: 'legacy', url: '...', sandbox: 'eval' }, // override por app ],});Lee esto primero
Sección titulada «Lee esto primero»Ningún modo de sandbox es una barrera de seguridad frente a código hostil.
module ejecuta la app en tu ventana sin ningún aislamiento. strict y eval
la ejecutan en un iframe del mismo origen cuyo document.createElement se
redirige al documento anfitrión, lo que significa que cualquier nodo que cree la
app lleva directamente de vuelta hacia fuera:
document.createElement('div').ownerDocument.defaultView.wu // el wu completoEsa redirección no es un descuido; es necesaria para que React y Vue no tropiecen
con desajustes de ownerDocument al insertar nodos en el shadow root. Wu
enmascara window.parent, window.top y window.frameElement, pero esta vía
está abierta por diseño.
Lo que los modos te dan de verdad es contención de tu propio código: globales aislados y limpieza garantizada de temporizadores, intervalos, frames de animación y listeners al desmontar. Para código que no escribiste y no puedes auditar necesitas una barrera de origen real: un iframe de origen cruzado que tú controles, o un worker. Mira el modelo de amenazas.
module — el modo por defecto
Sección titulada «module — el modo por defecto»┌─ Ventana principal ─────────────────────────────────────┐│ import('https://cart.example.com/src/main.js') ││ ↓ ││ El código corre en el scope GLOBAL. window se comparte.││ ││ WuProxySandbox.patchWindow() durante la carga: ││ IDs de setTimeout / setInterval → rastreados ││ IDs de requestAnimationFrame → rastreados ││ tuplas de addEventListener → rastreadas ││ Al desmontar: se deshace todo lo rastreado. ││ ││ ┌─ Shadow DOM ─────────────────────────────────────┐ ││ │ El DOM renderizado de la app (CSS aislado) │ ││ └──────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────┘El modo module es un rastreador de limpieza, no aislamiento. El código de la
app se ejecuta en la ventana principal. window.foo = 1 contamina el objeto
global. Dos apps que parcheen Array.prototype se pelearán. Un error síncrono
en el scope del módulo puede tumbar el montaje.
Lo que sí te da es que montar y desmontar la misma app cincuenta veces no filtre cincuenta intervalos: el proxy registra los efectos secundarios producidos durante la carga y los deshace al desmontar.
Funciona todo lo que funciona en un módulo ES normal: HMR de Vite, tree shaking, source maps, import dinámico, top-level await.
Úsalo para tus propias apps, equipos internos y todo el desarrollo.
strict — realm separado
Sección titulada «strict — realm separado»┌─ Ventana principal ─────────────────────────────────────┐│ ┌─ Shadow DOM ─────────────────────────────────────┐ ││ │ La app renderiza aquí │ ││ └────────────▲─────────────────────────────────────┘ ││ │ document.querySelector/body redirigidos││ ┌─ Iframe oculto (display:none, 0×0) ──────────────┐ ││ │ contentWindow = su propia window (globales │ ││ │ separados) │ ││ │ <base href="https://cart.example.com/"> │ ││ │ import('…/main.js') ← un import dinámico REAL │ ││ │ window.wu = fachada congelada y restringida │ ││ └──────────────────────────────────────────────────┘ │└─────────────────────────────────────────────────────────┘Cada app strict recibe su propio iframe oculto. Los globales están separados:
dos apps pueden tener cada una su window.React sin colisionar. El desmontaje es
radical: destruir el iframe mata su realm, y además Wu limpia cada temporizador
rastreado usando el clearTimeout/clearInterval del propio iframe (usar
los del anfitrión cancelaría temporizadores ajenos que casualmente compartan ID).
Como el import es un import() genuino, el HMR, los source maps y el tree
shaking siguen funcionando — esto es lo que separa a strict de los runtimes
clásicos de micro-frontends basados en iframes.
El wu que hay dentro del iframe es una fachada congelada, no la instancia
real. Expone define, mount, unmount, app, hide, show, isHidden,
emit/on/off/once, vistas restringidas de store y eventBus,
getState/setState/onStateChange, ai, version, info, getStats,
getSandboxInfo, silence, verbose. Internos como wu.core, wu.cache y
wu.pluginSystem no son accesibles a través de ella.
Costes: un iframe por app, y el origen de la app debe enviar cabeceras CORS para la carga del módulo.
strictFallback vale false desde la v2.7
Sección titulada «strictFallback vale false desde la v2.7»Si el import() del iframe falla — CORS, red, contenido mixto bloqueado —
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'.Antes de la v2.7 degradaba a eval con un aviso, lo que significaba que
cualquiera capaz de romper CORS podía degradar una app que habías aislado
deliberadamente. Además, en la práctica la degradación suele estar rota: eval
no puede ejecutar módulos ES, así que una app ESM degradada lanza un segundo
error, más confuso, que esconde el fallo de CORS original.
Vuelve a activarlo solo donde la degradación tenga sentido de verdad: un bundle UMD de primera parte y de confianza, en desarrollo:
await wu.init({ sandbox: 'strict', strictFallback: true, // global apps: [{ name: 'legacy', url: '…', strictFallback: true }], // o por app});El valor por app gana al global.
eval — scripts clásicos, sin evaluación
Sección titulada «eval — scripts clásicos, sin evaluación»┌─ Ventana principal ─────────────────────────────────────┐│ 1. obtiene la página HTML de la app ││ 2. parsea: extrae <script> y <style>, limpia el DOM ││ (se eliminan atributos on* y URLs javascript:/ ││ vbscript:/data:, y se informa del recuento) ││ 3. DOM + estilos → Shadow DOM ││ 4. ┌─ Iframe oculto (realm separado) ──────────────┐ ││ │ <script src=…> y <script>…</script> │ ││ │ ← los ejecuta el NAVEGADOR, de forma nativa │ ││ └───────────────────────────────────────────────┘ ││ 5. espera a wu.define() │└─────────────────────────────────────────────────────────┘Pese al nombre, el modo eval de la v2.7 no evalúa nada. Antes compilaba
cada script de la app con new Function('proxy', code) y lo ejecutaba bajo
with(proxy){}. Ese módulo ya no existe. Ahora los scripts se entregan al
navegador como etiquetas <script> corrientes dentro del sandbox de iframe,
en el orden del documento, y el navegador los ejecuta por la misma vía que
cualquier otro script de la página.
Dos cosas mejoraron a la vez:
- Aislamiento. Un realm separado de verdad, en lugar de trampas de Proxy
sobre el
windowvivo que se podían esquivar a través deself,topyparent. - CSP. Wu ya no necesita
script-src 'unsafe-eval'en ninguna parte. Un script externo de la app no necesita excepción alguna; uno en línea necesita'unsafe-inline', un requisito mucho más débil.
En consecuencia, getSandboxInfo() ahora informa isolationLevel: 'iframe' para
las apps en modo eval. El antiguo valor 'proxy-trap' sobrevive en la unión de
TypeScript por compatibilidad, pero nunca se emite.
El modo conserva su nombre, así que sandbox: 'eval' lo sigue seleccionando.
Lo que sigue sin poder hacer: módulos ES. Para eso usa module o strict.
Sin HMR, sin source maps, sin tree shaking.
Úsalo para bundles heredados ya publicados como UMD/IIFE que no puedes recompilar como módulos.
Comparación
Sección titulada «Comparación»module |
strict |
eval |
|
|---|---|---|---|
| Globales aislados | ❌ | ✅ | ✅ |
| Realm separado | ❌ | ✅ iframe | ✅ iframe |
| Contiene código hostil | ❌ | ❌ | ❌ |
| Desmontaje garantizado | efectos rastreados | ✅ radical | ✅ radical |
| Módulos ES | ✅ | ✅ | ❌ |
| Tree shaking | ✅ | ✅ | ❌ |
| Source maps | ✅ | ✅ | ❌ |
| HMR de Vite | ✅ | ✅ | ❌ |
| Formato del bundle | ESM | ESM | UMD/IIFE |
| Necesita CORS | para la descarga | sí, obligatorio | para la descarga inicial |
Necesita unsafe-eval |
no | no | no (desde la v2.7) |
| Coste extra por app | ninguno | un iframe | un iframe |
isolationLevel |
'none' |
'iframe' |
'iframe' |
Verificar lo que has obtenido
Sección titulada «Verificar lo que has obtenido»getSandboxInfo() es la única respuesta honesta a “¿está esta app realmente
aislada?”:
await wu.mount('untrusted', '#slot');
const info = wu.getSandboxInfo('untrusted');// { requestedMode, actualMode, isolationLevel, mounted }
if (info?.actualMode !== 'strict') { throw new Error(`Se esperaba aislamiento strict, se obtuvo ${info?.actualMode}`);}actualMode solo puede diferir de requestedMode si activaste
strictFallback: true. Con el valor por defecto de la v2.7, un montaje strict
fallido lanza un error en lugar de convertirse calladamente en otra cosa.
Cómo elegir
Sección titulada «Cómo elegir»- Tus propias apps, o desarrollo →
module. - Apps con estado global en conflicto (dos versiones de React, polyfills que
compiten, una librería que parchea prototipos) →
strict. - Un bundle UMD/IIFE ya compilado →
eval. - Código en el que no confías → ninguno de los anteriores. Ponlo en un iframe
de origen cruzado que tú controles y háblale por
postMessage. Mira Apps de terceros.
