Isolation
@tuvix.js/sandbox propose deux outils. CssSandbox place une app dans un shadow root pour séparer son CSS de celui de la page. JsSandbox exécute du code avec un proxy de window, pour que ses écritures globales arrivent sur le proxy. L'orchestrateur utilise CssSandbox pour les apps que vous enregistrez avec sandbox: true ; il n'applique jamais JsSandbox.
Isolation CSS par 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 }
});Pour cette app, l'orchestrateur :
- attache un shadow root ouvert à
#widget(ou réutilise celui d'un montage précédent) ; - crée un
<div data-tuvix-sandbox-root>à l'intérieur et passe cet élément àmountcommecontainer; - ajoute les
stylesde l'entrée sous forme de balises<link>dans le shadow root, pas dansdocument.head, et attend leur chargement pendant au plusloader.timeoutavant d'appelermount.
Le CSS de la page n'atteint plus l'app, et le CSS de l'app n'atteint plus la page. Les propriétés héritées comme color et font-family arrivent toujours depuis l'élément hôte ; définissez-les sur :host dans le CSS de l'app pour l'empêcher.
Utilisez-la quand une app livre des sélecteurs globaux (button { ... }, un reset CSS) ou quand le CSS de la page casse l'app.
Cas limites
- Les styles qu'un framework injecte à l'exécution vont dans
document.head, hors du shadow root. L'injection CSS de Vite, les bibliothèques CSS-in-JS et les styles de composants d'Angular font tous cela. Livrez un fichier CSS et listez-le dansentry.styles, ou ajoutez vous-même le CSS au shadow root. - Les portals, modales et toasts rendus dans
document.bodysortent du shadow root et perdent les styles de l'app. document.querySelectordepuis l'app ne trouve pas les éléments du shadow root. Faites vos requêtes depuiscontainer.- Les entrées de type fonction de chargement ne listent aucun
styles: votre bundler injecte donc leur CSS dansdocument.head, où le shadow root le tient à l'écart. Enregistrez les apps isolées avec une entrée{ scripts, styles, type }.
CssSandbox à la main
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éthode | Effet |
|---|---|
wrap(container) | Attache un shadow root ouvert, ou réutilise un shadow root ouvert existant, et y déplace les enfants du conteneur. |
adoptStyles(shadow, { urls, cssText }) | Insère des éléments <link> et <style> au début du shadow root. Ignore une URL déjà adoptée à cet endroit. Renvoie { elements, ready, remove }. |
addStyle(shadow, css) / removeStyle(shadow, el) | Ajoute ou retire un <style>. |
unwrap(container) | Replace les nœuds autres que les styles dans le light DOM et laisse un <slot> pour qu'ils s'affichent. Retire du DOM les nœuds <style> et les <link> de feuille de style (y compris ceux de addStyle et adoptStyles) ; le wrap suivant de ce conteneur les remet dans l'ordre. |
wrap lève une exception quand l'élément ne peut pas héberger de shadow root (par exemple <input>) ou en a déjà un fermé. ready ne rejette jamais. jsdom ne déclenche jamais load sur les liens : n'attendez donc pas ready dans les tests jsdom. Quand vous appelez removeStyle ou adopted.remove() pendant que le conteneur est désenveloppé, cette feuille de style ne revient pas au wrap suivant.
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- Les écritures et les
Object.definePropertyfaits via le proxy vont dans un stockage propre à la sandbox. Les lectures consultent ce stockage, puis le vraiwindow. - Avec
strict: true, affecter, déclarer avec unvarde premier niveau ou définir avecObject.definePropertyun global absent de la liste autorisée intégrée et deallowedGlobalslève uneTypeErroret ne stocke rien. execScript(code)exécutecodedanswith (scope), avecwindow,self,globalThisetthisliés au proxy. Les affectations nues (x = 1), lesvar xde premier niveau etwindow.x = 1arrivent tous sur le proxy ;letetconstrestent locaux au script.- Dans
execScript, lire un identifiant non déclaré renvoieundefinedau lieu de lever uneReferenceError, et une directive"use strict"n'a aucun effet, carwithexige le mode non strict.execScriptutilisenew Function: votre Content Security Policy doit donc autoriserunsafe-eval. activate()etdeactivate()positionnent l'indicateuractive. Le proxy stocke les écritures dans les deux états.
JsSandbox ne couvre que le code qui accède aux globaux via proxyWindow. Un script chargé par une balise <script>, un module bundlé, les timers et les event listeners utilisent tous le vrai window. Considérez-la comme une protection contre les écritures globales accidentelles dans les extraits que vous exécutez, pas comme une frontière de sécurité.
Sandbox combinée
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 renvoie null quand css vaut false ; dans ce cas, il n'attache rien.
L'utiliser dans une 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);
},
});Préférez sandbox: true sur register quand l'orchestrateur monte l'app : il fait le même travail et réutilise le shadow root d'un montage à l'autre.
Quand ne pas isoler
- L'app utilise des portals vers
document.bodyet a besoin de ses styles à cet endroit. - Le design system de la page doit styliser l'app.
- Vous avez besoin d'une vraie isolation JavaScript. Utilisez une
<iframe>.