Sandboxing
@tuvix.js/sandbox tiene dos herramientas. CssSandbox coloca una app dentro de un shadow root para que su CSS y el CSS de la página no se mezclen. JsSandbox ejecuta código contra un window proxy para que sus escrituras globales caigan en el proxy. El orquestador usa CssSandbox para las apps que registras con sandbox: true; nunca aplica JsSandbox.
Aislamiento de CSS por app
import { createOrchestrator } from 'tuvix.js';
const orchestrator = createOrchestrator();
orchestrator.register({
name: 'widget',
entry: {
scripts: ['https://cdn.example.com/widget/main.js'],
styles: ['https://cdn.example.com/widget/main.css'],
type: 'module',
},
container: '#widget',
activeWhen: '/',
sandbox: true, // same as { css: true }
});Para esta app, el orquestador:
- adjunta un shadow root abierto a
#widget(o reutiliza el de un montaje anterior); - crea un
<div data-tuvix-sandbox-root>dentro y pasa ese elemento amountcomocontainer; - añade los
stylesde la entry como etiquetas<link>dentro del shadow root, no endocument.head, y espera hastaloader.timeouta que carguen antes de llamar amount.
El CSS de la página ya no llega a la app, y el CSS de la app ya no llega a la página. Las propiedades heredadas como color y font-family siguen llegando desde el elemento host; defínelas en :host en el CSS de la app para evitarlo.
Úsalo cuando una app incluye selectores globales (button { ... }, un reset de CSS) o cuando el CSS de la página rompe la app.
Casos límite
- Los estilos que un framework inyecta en runtime van a
document.head, fuera del shadow root. La inyección de CSS de Vite, las librerías CSS-in-JS y los estilos de componentes de Angular lo hacen. Distribuye un archivo CSS y lístalo enentry.styles, o añade tú mismo el CSS al shadow root. - Los portales, modales y toasts que se renderizan en
document.bodysalen del shadow root y pierden los estilos de la app. document.querySelectordesde dentro de la app no encuentra elementos en el shadow root. Consulta desdecontainer.- Las entries de tipo función loader no listan
styles, así que tu bundler inyecta su CSS endocument.head, donde el shadow root lo deja fuera. Registra las apps con sandbox con una entry{ scripts, styles, type }.
CssSandbox a mano
import { CssSandbox } from '@tuvix.js/sandbox';
declare const container: HTMLElement;
const css = new CssSandbox();
const shadow = css.wrap(container); // moves existing children into the shadow root
const adopted = css.adoptStyles(shadow, {
urls: ['/widget/main.css'],
cssText: ':host { display: block; font-family: system-ui; }',
});
await adopted.ready; // resolves once each <link> fired load or error
const button = document.createElement('button');
button.textContent = 'Buy';
shadow.append(button);
// Later
adopted.remove();
css.unwrap(container);| Método | Efecto |
|---|---|
wrap(container) | Adjunta un shadow root abierto, o reutiliza uno abierto existente, y mueve dentro los hijos del contenedor. |
adoptStyles(shadow, { urls, cssText }) | Inserta elementos <link> y <style> al principio del shadow root. Omite una URL ya adoptada allí. Devuelve { elements, ready, remove }. |
addStyle(shadow, css) / removeStyle(shadow, el) | Añade o elimina un <style>. |
unwrap(container) | Devuelve los nodos que no son de estilo al light DOM y deja un <slot> para que se rendericen. Saca del DOM los nodos <style> y <link> de hojas de estilo (incluidos los de addStyle y adoptStyles); el siguiente wrap de ese contenedor los vuelve a colocar en orden. |
wrap lanza una excepción cuando el elemento no puede alojar un shadow root (por ejemplo <input>) o ya tiene uno cerrado. ready nunca se rechaza. jsdom nunca dispara load en los links, así que no esperes ready en tests con jsdom. Cuando llamas a removeStyle o a adopted.remove() mientras el contenedor no está envuelto, esa hoja de estilos no vuelve en el siguiente wrap.
JsSandbox
import { JsSandbox } from '@tuvix.js/sandbox';
const js = new JsSandbox(['dataLayer'], /* strict */ true);
js.execScript('window.dataLayer = []'); // allowed, stored on the proxy
console.log('dataLayer' in window); // false, unless the page defined it
try {
js.execScript('counter = 1'); // bare assignment to a global that is not allowed
} catch (error) {
console.log(error instanceof TypeError); // true: strict mode blocked the write
}
const loose = new JsSandbox(); // not strict
loose.execScript('var version = 2; build = 7;');
console.log(Reflect.get(loose.proxyWindow, 'version'), 'version' in window); // 2 false
js.reset(); // forget every stored global- Las escrituras y
Object.definePropertya través del proxy van a un almacén del sandbox. Las lecturas consultan ese almacén y después elwindowreal. - Con
strict: true, asignar, declarar convaren el nivel superior o aplicarObject.definePropertya un global fuera de la allowlist integrada y deallowedGlobalslanza unTypeErrory no guarda nada. execScript(code)ejecutacodedentro dewith (scope), conwindow,self,globalThisythisenlazados al proxy. Las asignaciones directas (x = 1),var xen el nivel superior ywindow.x = 1caen en el proxy;letyconstse quedan locales al script.- Dentro de
execScript, leer un identificador no declarado devuelveundefineden lugar de lanzar unReferenceError, y una directiva"use strict"no tiene efecto, porquewithnecesita sloppy mode. Usanew Function, así que tu Content Security Policy necesitaunsafe-eval. activate()ydeactivate()establecen el flagactive. El proxy guarda las escrituras en ambos estados.
JsSandbox solo cubre el código que llega a los globales a través de proxyWindow. Un script cargado con una etiqueta <script>, un módulo empaquetado, los timers y los event listeners usan el window real. Trátalo como una protección contra escrituras globales accidentales en los fragmentos que ejecutas, no como una frontera de seguridad.
Sandbox combinado
import { createSandbox } from '@tuvix.js/sandbox';
declare const container: HTMLElement;
const sandbox = createSandbox({ css: true, js: false });
const shadow = sandbox.activate(container); // ShadowRoot | null
if (shadow) sandbox.css.addStyle(shadow, '.box { padding: 8px; }');
sandbox.deactivate(container); // unwrap
sandbox.destroy(container); // deactivate + reset the JS storeactivate devuelve null cuando css es false; en ese caso no adjunta nada.
Usarlo dentro de una micro app
import { defineMicroApp } from '@tuvix.js/core';
import { CssSandbox } from '@tuvix.js/sandbox';
const css = new CssSandbox();
export default defineMicroApp({
name: 'widget',
mount({ container }) {
const shadow = css.wrap(container);
css.addStyle(shadow, 'p { color: rebeccapurple; }');
const text = document.createElement('p');
text.textContent = 'Isolated';
shadow.append(text);
},
unmount({ container }) {
container.shadowRoot?.replaceChildren();
css.unwrap(container);
},
});Prefiere sandbox: true en register cuando el orquestador monta la app; hace el mismo trabajo y reutiliza el shadow root entre montajes.
Cuándo no usar sandbox
- La app usa portales hacia
document.bodyy necesita sus estilos allí. - El sistema de diseño de la página debe dar estilo a la app.
- Necesitas aislamiento real de JavaScript. Usa un
<iframe>.