Sandboxing
@tuvix.js/sandbox में दो tools हैं। CssSandbox ऐप को shadow root के अंदर रखता है, ताकि ऐप की CSS और पेज की CSS अलग रहें। JsSandbox code को proxy window पर चलाता है, ताकि उसकी global writes proxy पर जाएँ। आप जिन ऐप्स को sandbox: true के साथ register करते हैं, उन पर orchestrator CssSandbox लगाता है; JsSandbox वह कभी नहीं लगाता।
हर ऐप के लिए CSS isolation
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 }
});इस ऐप के लिए orchestrator:
#widgetपर एक open shadow root attach करता है (या पिछले mount वाला shadow root दोबारा इस्तेमाल करता है);- उसके अंदर
<div data-tuvix-sandbox-root>बनाता है और वही elementmountकोcontainerके रूप में देता है; - entry के
stylesकोdocument.headमें नहीं, shadow root के अंदर<link>tags के रूप में जोड़ता है, औरmountcall करने से पहले उनके load होने काloader.timeoutतक इंतज़ार करता है।
अब पेज की CSS ऐप तक नहीं पहुँचती, और ऐप की CSS पेज तक नहीं पहुँचती। color और font-family जैसी inherited properties host element से फिर भी अंदर आती हैं; इन्हें रोकने के लिए ऐप की CSS में इन्हें :host पर set करें।
इसे तब इस्तेमाल करें जब कोई ऐप global selectors (button { ... }, CSS reset) ship करता हो, या पेज की CSS ऐप को बिगाड़ती हो।
Edge cases
- फ्रेमवर्क runtime पर जो styles inject करता है, वे shadow root के बाहर
document.headमें जाते हैं। Vite का CSS injection, CSS-in-JS libraries और Angular के component styles, तीनों ऐसा करते हैं। एक CSS file ship करें और उसेentry.stylesमें लिखें, या CSS को खुद shadow root में जोड़ें। document.bodyमें रेंडर होने वाले portals, modals और toasts shadow root से बाहर चले जाते हैं और ऐप के styles खो देते हैं।- ऐप के अंदर से
document.querySelectorshadow root के elements नहीं खोज पाता। इसकी जगहcontainerसे query करें। - Loader-function entries में कोई
stylesनहीं होते, इसलिए आपका bundler उनकी CSSdocument.headमें inject करता है, जहाँ से shadow root उसे अंदर नहीं आने देता। Sandboxed ऐप्स को{ scripts, styles, type }entry के साथ register करें।
CssSandbox को हाथ से इस्तेमाल करें
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);| Method | असर |
|---|---|
wrap(container) | एक open shadow root attach करता है, या पहले से मौजूद open shadow root दोबारा इस्तेमाल करता है, और container के children को उसमें ले जाता है। |
adoptStyles(shadow, { urls, cssText }) | Shadow root की शुरुआत में <link> और <style> elements जोड़ता है। जो URL वहाँ पहले से adopt हो चुका है, उसे छोड़ देता है। { elements, ready, remove } लौटाता है। |
addStyle(shadow, css) / removeStyle(shadow, el) | एक <style> जोड़ता या हटाता है। |
unwrap(container) | Style के अलावा बाकी nodes को light DOM में वापस ले जाता है और एक <slot> छोड़ता है, ताकि वे रेंडर हों। <style> और stylesheet <link> nodes (addStyle और adoptStyles वाले भी) DOM से निकाल लेता है; उस container का अगला wrap उन्हें क्रम से वापस लगा देता है। |
जब element shadow root होस्ट नहीं कर सकता (जैसे <input>) या उस पर पहले से closed shadow root है, तो wrap throw करता है। ready कभी reject नहीं होता। jsdom links पर कभी load fire नहीं करता, इसलिए jsdom tests में ready को await न करें। Container unwrapped हो, तब removeStyle या adopted.remove() call करने पर वह stylesheet अगले 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- Proxy के ज़रिए होने वाली writes और
Object.definePropertysandbox के एक store में जाते हैं। Reads पहले उस store में देखते हैं, फिर असलीwindowमें। strict: trueके साथ, built-in allowlist औरallowedGlobalsके बाहर के किसी global को assign करना, top-levelvarसे declare करना या उस परObject.definePropertyचलानाTypeErrorthrow करता है और कुछ भी store नहीं करता।execScript(code)codeकोwith (scope)के अंदर चलाता है, जहाँwindow,self,globalThisऔरthisproxy से bound होते हैं। Bare assignments (x = 1), top-levelvar xऔरwindow.x = 1, तीनों proxy पर जाते हैं;letऔरconstscript के भीतर local रहते हैं।execScriptके अंदर undeclared identifier पढ़ने परReferenceErrorthrow होने के बजायundefinedमिलता है, और"use strict"directive का कोई असर नहीं होता, क्योंकिwithको sloppy mode चाहिए। यहnew Functionइस्तेमाल करता है, इसलिए आपकी Content Security Policy मेंunsafe-evalहोना चाहिए।activate()औरdeactivate()activeflag set करते हैं। Proxy दोनों states में writes store करता है।
JsSandbox सिर्फ़ उस code को कवर करता है जो proxyWindow के ज़रिए globals तक पहुँचता है। <script> tag से load हुई script, bundled module, timers और event listeners, ये सब असली window इस्तेमाल करते हैं। इसे अपने चलाए snippets में गलती से होने वाली global writes के खिलाफ़ एक guard मानें, security boundary नहीं।
Combined Sandbox
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 storecss के false होने पर activate null लौटाता है; तब वह कुछ भी attach नहीं करता।
माइक्रो ऐप के अंदर इस्तेमाल करें
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);
},
});जब orchestrator ऐप को mount करता है, तो register पर sandbox: true को प्राथमिकता दें; यह यही काम करता है और remounts के बीच shadow root दोबारा इस्तेमाल करता है।
Sandbox कब न करें
- ऐप
document.bodyमें portals इस्तेमाल करता है और उसे वहाँ अपने styles चाहिए। - ऐप को पेज के design system से style होना चाहिए।
- आपको असली JavaScript isolation चाहिए।
<iframe>इस्तेमाल करें।