Ir al contenido

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
],
});

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 completo

Esa 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.

┌─ 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.

┌─ 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.

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.

┌─ 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 window vivo que se podían esquivar a través de self, top y parent.
  • 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.

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'

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.

  • Tus propias apps, o desarrollomodule.
  • 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 compiladoeval.
  • 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.