इंस्टॉलेशन
npm install @tuvix.js/sandbox
# or pnpm add @tuvix.js/sandboxइसे कब इस्तेमाल करें
CssSandbox का उपयोग तब करें जब माइक्रो ऐप की CSS और पेज की CSS एक-दूसरे को प्रभावित न करें। यह ऐप के DOM को container पर बने एक open shadow root में ले जाता है, ताकि ऐप के selectors अंदर रहें और पेज के selectors बाहर। जब आप sandbox: true के साथ कोई ऐप register करते हैं, तो @tuvix.js/core का orchestrator यह काम आपके लिए करता है। जब आप orchestrator के बिना ऐप्स mount करते हैं, तब इस पैकेज को खुद कॉल करें।
JsSandbox का उपयोग तब करें जब आप third-party script text चलाते हैं और चाहते हैं कि उसके global writes असली window से दूर रहें। आप कोड को execScript के ज़रिए चलाते हैं, जो window, this और free identifiers को एक Proxy के ज़रिए resolve करता है। orchestrator इसे लागू नहीं करता।
जब आपको untrusted कोड के लिए पक्का isolation चाहिए, तो कुछ और चुनें। sandbox attribute वाला <iframe> कोड को उसका अपना global object और origin देता है; यह पैकेज ऐसा नहीं करता।
क्विक स्टार्ट
orchestrator के ज़रिए (सिर्फ़ CSS):
import { createOrchestrator } from '@tuvix.js/core';
const orchestrator = createOrchestrator();
orchestrator.register({
name: 'cart',
entry: { scripts: ['/apps/cart/main.js'], styles: ['/apps/cart/style.css'] },
container: '#cart',
activeWhen: '/cart/*',
sandbox: true,
});
await orchestrator.start();orchestrator #cart को एक shadow root में लपेटता है, ऐप को उसके अंदर एक <div data-tuvix-sandbox-root="cart"> में रेंडर करता है, और style.css को <head> के बजाय shadow root में लोड करता है। Mount इन stylesheets का इंतज़ार loader timeout (default रूप से 10000 ms) तक करता है। ऐप दोबारा mount होने पर orchestrator उसी shadow root का फिर से उपयोग करता है।
हाथ से:
import { createSandbox } from '@tuvix.js/sandbox';
const container = document.getElementById('cart')!;
const sandbox = createSandbox({ css: true, js: true, strict: true, allowedGlobals: ['cartState'] });
const shadowRoot = sandbox.activate(container);
const target = shadowRoot ?? container;
const adopted = sandbox.css.adoptStyles(shadowRoot!, { urls: ['/apps/cart/style.css'] });
await adopted.ready;
target.append(Object.assign(document.createElement('p'), { textContent: 'Cart' }));
sandbox.js.execScript('window.cartState = { items: 0 }');
console.log('cartState' in window); // false
sandbox.deactivate(container); // children back in the light DOM, styles set aside
sandbox.activate(container); // same shadow root, styles restored
sandbox.destroy(container);API
createSandbox
import type { ISandbox, SandboxOptions } from '@tuvix.js/sandbox';
declare function createSandbox(options?: SandboxOptions): ISandbox;new Sandbox(options) लौटाता है।
| विकल्प | Type | Default | अर्थ |
|---|---|---|---|
css | boolean | true | activate container को shadow root में लपेटता है। false होने पर activate null लौटाता है और कुछ attach नहीं करता। |
js | boolean | true | activate/deactivate sandbox.js.active को बदलते हैं। sandbox.js.proxyWindow वाली proxy दोनों स्थितियों में writes को असली window से दूर रखती है। |
allowedGlobals | string[] | [] | अतिरिक्त global नाम जिन्हें strict mode sandboxed कोड को assign करने देता है। इन पर writes फिर भी proxy पर ही जाते हैं। |
strict | boolean | false | जब sandboxed कोड built-in allowlist और allowedGlobals के बाहर कोई global assign या define करता है, तो TypeError throw करता है। |
Sandbox
import { Sandbox } from '@tuvix.js/sandbox';
const sandbox = new Sandbox({ css: false, js: true });
console.log(sandbox.activate(document.body)); // nullcreateSandbox के पीछे की class। यह ISandbox implement करती है:
| Member | Signature | व्यवहार |
|---|---|---|
css | readonly ICssSandbox | वह CssSandbox instance जिसका यह उपयोग करती है। |
js | readonly IJsSandbox | वह JsSandbox instance जिसका यह उपयोग करती है। |
activate | (container: HTMLElement) => ShadowRoot | null | css चालू होने पर css.wrap(container), फिर js चालू होने पर js.activate()। shadow root या null लौटाता है। |
deactivate | (container: HTMLElement) => void | js.deactivate(), फिर css.unwrap(container)। |
destroy | (container: HTMLElement) => void | deactivate(container), फिर js.reset(), जो हर stored global साफ़ करता है। |
CssSandbox
import { CssSandbox } from '@tuvix.js/sandbox';
const cssSandbox = new CssSandbox();
const container = document.querySelector<HTMLElement>('#widget')!;
const shadowRoot = cssSandbox.wrap(container);
const style = cssSandbox.addStyle(shadowRoot, ':host { display: block; } p { color: teal; }');
cssSandbox.unwrap(container); // the <style> leaves the DOM and waits for the next wrap
cssSandbox.removeStyle(shadowRoot, style); // removed for good: the next wrap does not restore it| Method | Signature | व्यवहार |
|---|---|---|
wrap | (container: HTMLElement) => ShadowRoot | container के children को एक open shadow root में ले जाता है और उसे लौटाता है। |
unwrap | (container: HTMLElement) => void | children को वापस light DOM में ले जाता है, shadow root की stylesheets को अलग रख देता है और shadow root में एक <slot> छोड़ता है। |
addStyle | (shadowRoot: ShadowRoot, css: string) => HTMLStyleElement | css वाला एक <style> shadow root के अंत में जोड़ता है। |
removeStyle | (shadowRoot: ShadowRoot, styleEl: HTMLStyleElement) => void | styleEl को हटाता है। जब host unwrapped होता है, तो यह styleEl को अलग रखी गई सूची से भी हटाता है, ताकि अगला wrap उसे वापस न लाए। |
adoptStyles | (shadowRoot: ShadowRoot, styles: AdoptStylesOptions) => AdoptedStyles | <link> और <style> elements को shadow root की शुरुआत में डालता है। |
wrap और remounts
एक बार attach होने के बाद DOM element अपना shadow root नहीं खो सकता। wrap इसे ऐसे संभालता है:
- अगर इस
CssSandboxinstance ने container को पहले ही wrap किया है और unwrap नहीं किया, तोwrapवही shadow root लौटाता है और कुछ नहीं हिलाता। - अगर container के पास एक open
shadowRootहै (किसी पिछलेwrap, किसीunwrap, या किसी दूसरेCssSandboxinstance से), तोwrapवह<slot>हटाता है जोunwrapने छोड़ा था,unwrapकी अलग रखी stylesheets को वापस लगाता है (शुरुआत में, उनके मूल क्रम में) और उसी root का फिर से उपयोग करता है। - वरना यह
container.attachShadow({ mode: 'open' })कॉल करता है।
इसके बाद यह हर light DOM child को shadow root में ले जाता है। wrap सिर्फ़ तब throw करता है जब attachShadow असफल हो, उदाहरण के लिए ऐसे element पर जो shadow root host नहीं कर सकता (<img>, <input>) या जिसके पास पहले से closed root है: [Tuvix Sandbox] Cannot attach Shadow DOM to <img>: ...।
unwrap और slot
unwrap shadow root के children को वापस container में ले जाता है। यह <style> और <link rel="stylesheet"> nodes को DOM से निकाल लेता है, चाहे वे addStyle, adoptStyles या ऐप से आए हों, ताकि ऐप की CSS लागू होना बंद हो जाए और पेज तक कभी न पहुँचे। यह उन्हें shadow root के साथ रखता है। shadow root बना रहता है और उसमें एक <slot data-tuvix-sandbox-slot> रहता है, ताकि ब्राउज़र वापस लाए गए children को उसके ज़रिए रेंडर करे। जिस container को इस instance ने wrap नहीं किया, उसके लिए unwrap कुछ नहीं करता।
उस host का अगला wrap, चाहे इस instance से हो या किसी दूसरे CssSandbox instance से, slot हटाता है, अलग रखी stylesheets को क्रम से वापस लगाता है और children को फिर अंदर ले जाता है। remount के बाद आपको styles फिर से adopt नहीं करनी पड़तीं। host के unwrapped रहते removeStyle(shadowRoot, el) या adopted.remove() कॉल करने से वे sheets हमेशा के लिए हट जाती हैं।
adoptStyles
import { CssSandbox } from '@tuvix.js/sandbox';
const cssSandbox = new CssSandbox();
const shadowRoot = cssSandbox.wrap(document.querySelector<HTMLElement>('#cart')!);
const adopted = cssSandbox.adoptStyles(shadowRoot, {
urls: ['/apps/cart/style.css', '/shared/tokens.css'],
cssText: ':host { display: block; }',
});
await adopted.ready; // every <link> fired load or error
console.log(adopted.elements.length); // 3
adopted.remove();| विकल्प | Type | Default | अर्थ |
|---|---|---|---|
urls | readonly string[] | [] | हर URL क्रम से एक <link rel="stylesheet" data-tuvix-style> बनता है। |
cssText | string | readonly string[] | कोई नहीं | हर string एक <style data-tuvix-style> बनती है, जो links के बाद रखी जाती है। |
व्यवहार:
- elements shadow root की शुरुआत में, पहले adopt की गई styles के बाद जाते हैं, ताकि ऐप का content और
addStylestyles cascade में बाद में आएँ। - जो URL इस shadow root में पहले से adopt हो चुका है, उसे छोड़ दिया जाता है और वह
elementsमें नहीं दिखता। इसमेंunwrapके अलग रखे links भी शामिल हैं। अगर पहले की किसी कॉल ने वह link बनाया था, तोreadyफिर भी उस मौजूदा link का इंतज़ार करता है। readyतब resolve होता है जब हर linkloadयाerrorfire कर दे। यह कभी reject नहीं होता, इसलिए 404 वाली stylesheet आपको नहीं रोकती। jsdom इनमें से कोई event fire नहीं करता, इसलिए jsdom tests मेंreadyको await न करें।remove()सिर्फ़ वे elements हटाता है जिन्हें इस कॉल ने डाला था, अलग रखे गए elements सहित, ताकि अगलाwrapउन्हें वापस न लाए।
JsSandbox
import { JsSandbox } from '@tuvix.js/sandbox';
const jsSandbox = new JsSandbox(['analyticsQueue'], true);
jsSandbox.activate();
jsSandbox.execScript('window.analyticsQueue = []'); // allowed, stored on the proxy
console.log('analyticsQueue' in window); // false
console.log(Reflect.get(jsSandbox.proxyWindow, 'analyticsQueue')); // []
try {
jsSandbox.execScript('window.tracker = {}');
} catch (error) {
console.log(error instanceof TypeError); // true
}
jsSandbox.reset();Constructor: new JsSandbox(allowedGlobals: string[] = [], strict = false)।
| Member | Signature | व्यवहार |
|---|---|---|
proxyWindow | readonly WindowProxy | असली window के ऊपर बनी Proxy। |
active | readonly boolean | Flag जिसे activate सेट करता है और deactivate/reset साफ़ करते हैं। |
activate | () => void | active को true करता है। |
deactivate | () => void | active को false करता है। stored globals बने रहते हैं। |
execScript | (code: string) => unknown | code को new Function के ज़रिए with (scope) के अंदर sloppy-mode script के रूप में चलाता है। window, self, globalThis और top-level this proxy होते हैं, और free identifiers इसी के ज़रिए resolve होते हैं। eval("...") उसी scope के अंदर direct eval बना रहता है। कोड जो लौटाता है, वही लौटाता है। |
reset | () => void | हर stored global साफ़ करता है और active को false करता है। |
Proxy:
- Reads पहले sandbox का अपना store जाँचते हैं, फिर असली
windowतक जाते हैं। असली window के जिन functions मेंprototypeनहीं होता (fetch,setTimeout), वे असलीwindowसे bound होकर लौटते हैं, हर function के लिए एक बार, इसलिएproxyWindow.fetch === proxyWindow.fetch। proxyWindow.window,.self,.globalThis, और जब पेज किसी iframe में नहीं होता तब.top/.parent/.frames, खुद proxy लौटाते हैं। iframe मेंtopऔरparentअसली ही रहते हैं।- Assignments और
Object.definePropertyvalue को proxy पर store करते हैं। असलीwindowकभी नहीं बदलता।valueके बिना descriptor ({ enumerable: false }) stored value को बनाए रखता है। Accessor descriptors (get/set) औरconfigurable: falseहोने परObject.definePropertyTypeErrorthrow करता है। delete proxyWindow.xstored value हटाता है। असली window की property के लिए यहfalseलौटाता है, जो strict-mode कोड मेंTypeErrorthrow करता है।instore और असलीwindowदोनों जाँचता है।
Strict mode (strict: true): built-in allowlist और allowedGlobals के बाहर के किसी नाम के लिए assignment (window.x = 1 या सीधा x = 1), top-level var x = 1 या Object.defineProperty कॉल TypeError: [Tuvix Sandbox] Strict mode blocked write to global "tracker". Add it to allowedGlobals to permit it. throw करता है और कुछ store नहीं करता। यह जाँच sandbox के active होने या न होने, दोनों स्थितियों में चलती है। Symbol keys हमेशा blocked मानी जाती हैं। built-in allowlist में console, timers, Promise, Map, JSON, fetch, URL, navigator, document, location और history जैसे standard globals शामिल हैं। अनुमति वाले writes, इन नामों पर writes सहित, proxy पर ही रहते हैं।
Types
| Type | अर्थ |
|---|---|
SandboxOptions | { css?, js?, allowedGlobals?, strict? }, createSandbox के विकल्प। |
ISandbox | वह interface जिसे Sandbox implement करती है: css, js, activate, deactivate, destroy। |
ICssSandbox | वह interface जिसे CssSandbox implement करता है। |
IJsSandbox | वह interface जिसे JsSandbox implement करता है। |
AdoptStylesOptions | adoptStyles के लिए { urls?, cssText? }। |
AdoptedStyles | adoptStyles से लौटाया गया { elements, ready, remove }। |
Edge cases
execScriptके अंदरx = 1, top-levelvar x = 1,window.x = 1,window.window.x = 1,top.x = 1औरeval("...")के अंदर यही statements, सभीxको proxy पर store करते हैं, असलीwindowपर कभी नहीं।letऔरconstscript के लिए local रहते हैं।execScriptके अंदर undeclared identifier पढ़ने परReferenceErrorthrow होने के बजायundefinedमिलता है, औरcodeमें"use strict"directive का कोई असर नहीं होता, क्योंकिwithको sloppy mode चाहिए।f()जैसी सामान्य function call के अंदरthisअसली global object होता है, जैसा sloppy-mode JavaScript तय करता है। ऐसे function मेंthis.x = 1असलीwindowपर लिखता है।execScriptसे चलाए जाने वाले कोड मेंwindow.x = 1लिखें।execScriptnew Functionका उपयोग करता है, इसलिए'unsafe-eval'के बिना Content Security Policy इसे block करती है।JsSandbox,SandboxयाcreateSandbox()बनाते समयwindowपढ़ा जाता है, इसलिए सर्वर पर येReferenceErrorthrow करते हैं।CssSandboxकोdocumentसिर्फ़ तब चाहिए जब आप इसके methods कॉल करते हैं।- दो
CssSandboxinstances एक host साझा कर सकते हैं। दूसराwrapपहले instance के shadow root का फिर से उपयोग करता है। - container के wrapped रहते आप जो light DOM children जोड़ते हैं, वे shadow root के बाहर रहते हैं और किसी slot के ज़रिए रेंडर नहीं होते, इसलिए दिखते नहीं। इसके बजाय shadow root में append करें।
Sandbox({ css: false }).activate()nullलौटाता है। container में रेंडर करें।
यह क्या नहीं करता
- यह timers, event listeners,
documentया<script>tags को patch नहीं करता। जो कोडproxyWindowसे नहीं गुज़रता, वह असली पेज को प्रभावित करता है। - यह किसी ऐप के पीछे छोड़े गए intervals या listeners साफ़ नहीं करता। इन्हें ऐप के
unmountमें साफ़ करें। - यह कोड को अलग origin या global object नहीं देता। untrusted कोड के लिए
<iframe sandbox>इस्तेमाल करें। - यह shadow root को detach नहीं करता। DOM में इसके लिए कोई API नहीं है;
unwrapइसके बजाय एक slot छोड़ता है। - यह inheritance नहीं रोकता। host या उसके ancestors पर सेट किए गए
color,font-familyऔर CSS custom properties फिर भी shadow root में पहुँचते हैं।
0.4 से माइग्रेशन
Sandbox({ css: false }).activate(container)nullलौटाता है और कोई shadow root attach नहीं करता। 0.4 में यह एक shadow root attach करता था और container के children को छिपा देता था।ISandbox.activateअबShadowRoot | nullलौटाता है:tsimport { createSandbox } from '@tuvix.js/sandbox'; const container = document.querySelector<HTMLElement>('#app')!; const sandbox = createSandbox({ css: false }); // 0.4: const root: ShadowRoot = sandbox.activate(container); const root = sandbox.activate(container) ?? container;strict: trueblocked write परTypeErrorthrow करता है। 0.4 में यह एक warning log करता था और value store कर देता था। आपकी scripts जो नाम सेट करती हैं, उन्हेंallowedGlobalsमें जोड़ें।execScriptकोड कोwithblock के अंदर चलाता है। कोड strict-mode कोड नहीं हो सकता, undeclared identifier पढ़ने पर throw होने के बजायundefinedमिलता है, और सीधाx = 1अब असलीwindowके बजाय proxy पर जाता है।unwrapshadow root की stylesheets को रखता है और अगलाwrapउन्हें वापस लाता है। 0.4 मेंunwrapउन्हें delete कर देता था। remount के बादcssTextयाaddStylestyles दोबारा जोड़ने वाला कोड हटाएँ, नहीं तो duplicates बनेंगे।0.4 के docs में लिखा था कि
js: trueaddEventListener, timers औरdocument.createElementको intercept करता है। यह कभी ऐसा नहीं करता था, और 0.5 भी नहीं करता।