wu-framework 1.1.15 → 1.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +52 -20
  2. package/dist/wu-framework.cjs.js +1 -1
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15511 -15146
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js +1 -1
  7. package/dist/wu-framework.esm.js.map +1 -1
  8. package/dist/wu-framework.umd.js +1 -1
  9. package/dist/wu-framework.umd.js.map +1 -1
  10. package/package.json +166 -161
  11. package/src/adapters/angular/ai.js +30 -30
  12. package/src/adapters/angular/index.d.ts +154 -154
  13. package/src/adapters/angular/index.js +932 -932
  14. package/src/adapters/angular.d.ts +3 -3
  15. package/src/adapters/angular.js +3 -3
  16. package/src/adapters/index.js +168 -168
  17. package/src/adapters/lit/ai.js +20 -20
  18. package/src/adapters/lit/index.d.ts +120 -120
  19. package/src/adapters/lit/index.js +721 -721
  20. package/src/adapters/lit.d.ts +3 -3
  21. package/src/adapters/lit.js +3 -3
  22. package/src/adapters/preact/ai.js +33 -33
  23. package/src/adapters/preact/index.d.ts +108 -108
  24. package/src/adapters/preact/index.js +661 -661
  25. package/src/adapters/preact.d.ts +3 -3
  26. package/src/adapters/preact.js +3 -3
  27. package/src/adapters/react/index.js +48 -54
  28. package/src/adapters/react.d.ts +3 -3
  29. package/src/adapters/react.js +3 -3
  30. package/src/adapters/shared.js +64 -64
  31. package/src/adapters/solid/ai.js +32 -32
  32. package/src/adapters/solid/index.d.ts +101 -101
  33. package/src/adapters/solid/index.js +586 -586
  34. package/src/adapters/solid.d.ts +3 -3
  35. package/src/adapters/solid.js +3 -3
  36. package/src/adapters/svelte/ai.js +31 -31
  37. package/src/adapters/svelte/index.d.ts +166 -166
  38. package/src/adapters/svelte/index.js +798 -798
  39. package/src/adapters/svelte.d.ts +3 -3
  40. package/src/adapters/svelte.js +3 -3
  41. package/src/adapters/vanilla/ai.js +30 -30
  42. package/src/adapters/vanilla/index.d.ts +179 -179
  43. package/src/adapters/vanilla/index.js +785 -785
  44. package/src/adapters/vanilla.d.ts +3 -3
  45. package/src/adapters/vanilla.js +3 -3
  46. package/src/adapters/vue/ai.js +52 -52
  47. package/src/adapters/vue/index.d.ts +299 -299
  48. package/src/adapters/vue/index.js +610 -610
  49. package/src/adapters/vue.d.ts +3 -3
  50. package/src/adapters/vue.js +3 -3
  51. package/src/ai/wu-ai-actions.js +261 -261
  52. package/src/ai/wu-ai-agent.js +546 -546
  53. package/src/ai/wu-ai-browser-primitives.js +354 -354
  54. package/src/ai/wu-ai-browser.js +380 -380
  55. package/src/ai/wu-ai-context.js +332 -332
  56. package/src/ai/wu-ai-conversation.js +613 -613
  57. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  58. package/src/ai/wu-ai-permissions.js +381 -381
  59. package/src/ai/wu-ai-provider.js +700 -700
  60. package/src/ai/wu-ai-schema.js +225 -225
  61. package/src/ai/wu-ai-triggers.js +396 -396
  62. package/src/ai/wu-ai.js +804 -804
  63. package/src/core/wu-app.js +236 -236
  64. package/src/core/wu-cache.js +498 -477
  65. package/src/core/wu-core.js +1412 -1398
  66. package/src/core/wu-error-boundary.js +396 -382
  67. package/src/core/wu-event-bus.js +390 -348
  68. package/src/core/wu-hooks.js +350 -350
  69. package/src/core/wu-html-parser.js +199 -190
  70. package/src/core/wu-iframe-sandbox.js +328 -328
  71. package/src/core/wu-loader.js +385 -273
  72. package/src/core/wu-logger.js +142 -134
  73. package/src/core/wu-manifest.js +532 -509
  74. package/src/core/wu-mcp-bridge.js +432 -432
  75. package/src/core/wu-overrides.js +510 -510
  76. package/src/core/wu-performance.js +228 -228
  77. package/src/core/wu-plugin.js +401 -348
  78. package/src/core/wu-prefetch.js +414 -414
  79. package/src/core/wu-proxy-sandbox.js +477 -476
  80. package/src/core/wu-sandbox.js +779 -779
  81. package/src/core/wu-script-executor.js +161 -113
  82. package/src/core/wu-snapshot-sandbox.js +227 -227
  83. package/src/core/wu-store.js +13 -3
  84. package/src/core/wu-strategies.js +256 -256
  85. package/src/core/wu-style-bridge.js +477 -477
  86. package/src/index.d.ts +317 -0
  87. package/src/index.js +234 -224
  88. package/src/utils/dependency-resolver.js +327 -327
@@ -1,328 +1,328 @@
1
- /**
2
- * WU-IFRAME-SANDBOX: Real JS isolation using hidden iframes.
3
- *
4
- * Architecture:
5
- * ┌── Main Window ────────────────────────────────┐
6
- * │ ┌── Shadow DOM Container ──────────────────┐ │
7
- * │ │ App renders here (CSS isolated) │ │
8
- * │ └──────────────────────────────────────────┘ │
9
- * │ ┌── Hidden iframe ────────────────────────┐ │
10
- * │ │ import() runs here (REAL modules) │ │
11
- * │ │ window = iframe.contentWindow (ISOLATED)│ │
12
- * │ │ document patched → Shadow DOM │ │
13
- * │ └────────────────────────────────────────-─┘ │
14
- * └───────────────────────────────────────────────┘
15
- *
16
- * Why iframe?
17
- * - import() is REAL → tree shaking, source maps, HMR all work
18
- * - iframe has its own window → globals are isolated
19
- * - Destroying iframe kills all timers/listeners at once
20
- *
21
- * How it works:
22
- * 1. Create hidden iframe with <base href="appUrl"> for URL resolution
23
- * 2. Patch iframe's document: createElement → main document (no ownerDocument issues),
24
- * querySelector/body → Shadow DOM container
25
- * 3. Track timers for guaranteed cleanup (some browsers don't kill iframe timers)
26
- * 4. import() the app module inside iframe → runs in isolated context
27
- * 5. App calls wu.define() → lifecycle registered on parent's WuCore
28
- * 6. On unmount: destroy iframe = nuclear cleanup
29
- *
30
- * Fallback:
31
- * If import() fails (CORS, module errors), wu-core falls back to eval mode
32
- * (fetch HTML + parse + execute with(proxy)).
33
- */
34
-
35
- import { logger } from './wu-logger.js';
36
-
37
- export class WuIframeSandbox {
38
- constructor(appName) {
39
- this.appName = appName;
40
- this.iframe = null;
41
- this._active = false;
42
-
43
- // Side-effect tracking for guaranteed cleanup
44
- this._timers = new Set();
45
- this._intervals = new Set();
46
- this._rafs = new Set();
47
- this._listeners = [];
48
- }
49
-
50
- /**
51
- * Create and activate the iframe sandbox.
52
- *
53
- * @param {string} appUrl - App's base URL (for <base href> and relative imports)
54
- * @param {HTMLElement} shadowContainer - Shadow DOM container for DOM redirection
55
- * @param {ShadowRoot|null} shadowRoot - Shadow root for query scoping
56
- * @returns {Window} The iframe's contentWindow (isolated execution context)
57
- */
58
- activate(appUrl, shadowContainer, shadowRoot) {
59
- if (this._active) return this.iframe.contentWindow;
60
-
61
- // 1. Create hidden iframe
62
- const iframe = document.createElement('iframe');
63
- iframe.setAttribute('data-wu-sandbox', this.appName);
64
- iframe.style.cssText = 'display:none !important;position:absolute;width:0;height:0;border:0;';
65
-
66
- // Must be in DOM before accessing contentWindow
67
- document.body.appendChild(iframe);
68
- this.iframe = iframe;
69
-
70
- // 2. Write base HTML with <base href> pointing to app URL.
71
- // This makes relative URL resolution work for fetch(), CSS url(), etc.
72
- // import() of full URLs works regardless of base.
73
- const baseUrl = appUrl.replace(/\/$/, '');
74
- const iframeWin = iframe.contentWindow;
75
- const iframeDoc = iframeWin.document;
76
-
77
- iframeDoc.open();
78
- iframeDoc.write(
79
- `<!DOCTYPE html><html><head><base href="${baseUrl}/"></head><body></body></html>`
80
- );
81
- iframeDoc.close();
82
-
83
- // 3. Make wu available inside iframe for wu.define()
84
- iframeWin.wu = window.wu;
85
-
86
- // 4. Patch document: redirect DOM operations to Shadow DOM
87
- this._patchDocument(iframeWin, shadowContainer, shadowRoot);
88
-
89
- // 5. Track timers for guaranteed cleanup
90
- this._patchTimers(iframeWin);
91
-
92
- this._active = true;
93
- logger.wuDebug(`[IframeSandbox] Activated for ${this.appName} (base: ${baseUrl})`);
94
- return iframeWin;
95
- }
96
-
97
- /**
98
- * Import an ES module inside the iframe via real import().
99
- * Preserves tree shaking, source maps, and Vite HMR.
100
- *
101
- * @param {string} url - Full module URL to import
102
- * @param {number} [timeout=30000] - Max wait time in ms
103
- * @returns {Promise<void>}
104
- */
105
- importModule(url, timeout = 30000) {
106
- if (!this._active) {
107
- throw new Error(`[IframeSandbox] Not active for ${this.appName}`);
108
- }
109
-
110
- return new Promise((resolve, reject) => {
111
- const channelId = `wu_${this.appName}_${Date.now()}`;
112
-
113
- // Listen for import completion via postMessage
114
- const onMessage = (event) => {
115
- if (event.data?.channelId !== channelId) return;
116
- cleanup();
117
- if (event.data.error) {
118
- reject(new Error(event.data.error));
119
- } else {
120
- resolve();
121
- }
122
- };
123
-
124
- const timer = setTimeout(() => {
125
- cleanup();
126
- reject(new Error(
127
- `[IframeSandbox] import() timed out for ${this.appName}: ${url}`
128
- ));
129
- }, timeout);
130
-
131
- const cleanup = () => {
132
- window.removeEventListener('message', onMessage);
133
- clearTimeout(timer);
134
- };
135
-
136
- window.addEventListener('message', onMessage);
137
-
138
- // Inject module script into iframe
139
- const iframeDoc = this.iframe.contentWindow.document;
140
- const script = iframeDoc.createElement('script');
141
- script.type = 'module';
142
- script.textContent =
143
- `import("${url.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}")` +
144
- `.then(() => parent.postMessage({ channelId: "${channelId}", success: true }, '*'))` +
145
- `.catch(e => parent.postMessage({ channelId: "${channelId}", error: e.message || String(e) }, '*'));`;
146
-
147
- iframeDoc.head.appendChild(script);
148
- logger.wuDebug(`[IframeSandbox] Importing module: ${url}`);
149
- });
150
- }
151
-
152
- /**
153
- * Patch the iframe's document to redirect DOM operations.
154
- *
155
- * Critical patches:
156
- * - createElement/createTextNode → main document (avoids ownerDocument mismatch)
157
- * React/Vue create nodes and append to Shadow DOM container.
158
- * Nodes must belong to the main document to avoid cross-document adoption issues.
159
- *
160
- * - querySelector/body → Shadow DOM container
161
- * Libraries that query the document will find app elements in the Shadow DOM.
162
- *
163
- * - addEventListener → tracked for cleanup
164
- */
165
- _patchDocument(iframeWin, shadowContainer, shadowRoot) {
166
- const iframeDoc = iframeWin.document;
167
- const queryTarget = shadowRoot || shadowContainer;
168
- const mainDoc = document; // parent document
169
-
170
- // --- Node creation: use main document to avoid ownerDocument mismatch ---
171
- // React uses container.ownerDocument.createElement() internally,
172
- // but other code might use document.createElement() directly.
173
- // By redirecting to main document, all nodes belong to the same document tree.
174
- iframeDoc.createElement = (tag, options) => mainDoc.createElement(tag, options);
175
- iframeDoc.createElementNS = (ns, tag, options) => mainDoc.createElementNS(ns, tag, options);
176
- iframeDoc.createTextNode = (text) => mainDoc.createTextNode(text);
177
- iframeDoc.createComment = (text) => mainDoc.createComment(text);
178
- iframeDoc.createDocumentFragment = () => mainDoc.createDocumentFragment();
179
-
180
- // --- DOM queries: redirect to Shadow DOM ---
181
- iframeDoc.querySelector = (sel) => queryTarget.querySelector(sel);
182
- iframeDoc.querySelectorAll = (sel) => queryTarget.querySelectorAll(sel);
183
- iframeDoc.getElementById = (id) => queryTarget.querySelector(`#${id}`);
184
- iframeDoc.getElementsByClassName = (cls) => queryTarget.querySelectorAll(`.${cls}`);
185
- iframeDoc.getElementsByTagName = (tag) => queryTarget.querySelectorAll(tag);
186
-
187
- // --- document.body → shadow container ---
188
- // Frameworks that append to document.body (portals, modals) will target the Shadow DOM.
189
- try {
190
- Object.defineProperty(iframeDoc, 'body', {
191
- get: () => shadowContainer,
192
- configurable: true
193
- });
194
- } catch {
195
- // Some environments don't allow redefining body — not critical
196
- logger.wuDebug('[IframeSandbox] Could not redefine document.body');
197
- }
198
-
199
- // --- document.addEventListener: track for cleanup ---
200
- const origDocAdd = iframeDoc.addEventListener.bind(iframeDoc);
201
- const origDocRemove = iframeDoc.removeEventListener.bind(iframeDoc);
202
-
203
- iframeDoc.addEventListener = (event, handler, options) => {
204
- this._listeners.push({ target: iframeDoc, event, handler, options });
205
- origDocAdd(event, handler, options);
206
- };
207
-
208
- iframeDoc.removeEventListener = (event, handler, options) => {
209
- this._listeners = this._listeners.filter(
210
- l => !(l.target === iframeDoc && l.event === event && l.handler === handler)
211
- );
212
- origDocRemove(event, handler, options);
213
- };
214
-
215
- logger.wuDebug(`[IframeSandbox] Document patched for ${this.appName}`);
216
- }
217
-
218
- /**
219
- * Patch timers in the iframe for guaranteed cleanup.
220
- * Some browsers don't fully kill timers when an iframe is removed.
221
- * We track all IDs and clear them explicitly on destroy.
222
- */
223
- _patchTimers(iframeWin) {
224
- const origSetTimeout = iframeWin.setTimeout.bind(iframeWin);
225
- const origClearTimeout = iframeWin.clearTimeout.bind(iframeWin);
226
- const origSetInterval = iframeWin.setInterval.bind(iframeWin);
227
- const origClearInterval = iframeWin.clearInterval.bind(iframeWin);
228
-
229
- iframeWin.setTimeout = (fn, ms, ...args) => {
230
- const id = origSetTimeout((...a) => {
231
- this._timers.delete(id);
232
- if (typeof fn === 'function') fn(...a);
233
- }, ms, ...args);
234
- this._timers.add(id);
235
- return id;
236
- };
237
-
238
- iframeWin.clearTimeout = (id) => {
239
- this._timers.delete(id);
240
- origClearTimeout(id);
241
- };
242
-
243
- iframeWin.setInterval = (fn, ms, ...args) => {
244
- const id = origSetInterval(fn, ms, ...args);
245
- this._intervals.add(id);
246
- return id;
247
- };
248
-
249
- iframeWin.clearInterval = (id) => {
250
- this._intervals.delete(id);
251
- origClearInterval(id);
252
- };
253
-
254
- // requestAnimationFrame may not exist in all iframe contexts
255
- if (iframeWin.requestAnimationFrame) {
256
- const origRAF = iframeWin.requestAnimationFrame.bind(iframeWin);
257
- const origCancelRAF = iframeWin.cancelAnimationFrame.bind(iframeWin);
258
-
259
- iframeWin.requestAnimationFrame = (fn) => {
260
- const id = origRAF((...a) => {
261
- this._rafs.delete(id);
262
- fn(...a);
263
- });
264
- this._rafs.add(id);
265
- return id;
266
- };
267
-
268
- iframeWin.cancelAnimationFrame = (id) => {
269
- this._rafs.delete(id);
270
- origCancelRAF(id);
271
- };
272
- }
273
-
274
- logger.wuDebug(`[IframeSandbox] Timer tracking active for ${this.appName}`);
275
- }
276
-
277
- /**
278
- * Destroy the iframe and all side effects.
279
- * Nuclear cleanup: kills everything at once.
280
- */
281
- destroy() {
282
- if (!this._active) return;
283
- this._active = false;
284
-
285
- // 1. Clear all tracked timers
286
- for (const id of this._timers) { try { clearTimeout(id); } catch {} }
287
- for (const id of this._intervals) { try { clearInterval(id); } catch {} }
288
- for (const id of this._rafs) { try { cancelAnimationFrame(id); } catch {} }
289
- this._timers.clear();
290
- this._intervals.clear();
291
- this._rafs.clear();
292
-
293
- // 2. Remove all tracked event listeners
294
- for (const { target, event, handler, options } of this._listeners) {
295
- try { target.removeEventListener(event, handler, options); } catch {}
296
- }
297
- this._listeners = [];
298
-
299
- // 3. Wipe and remove iframe
300
- if (this.iframe) {
301
- try {
302
- const doc = this.iframe.contentDocument;
303
- if (doc) {
304
- doc.open();
305
- doc.write('');
306
- doc.close();
307
- }
308
- } catch {
309
- // Cross-origin or already detached — ignore
310
- }
311
-
312
- if (this.iframe.parentNode) {
313
- this.iframe.parentNode.removeChild(this.iframe);
314
- }
315
- this.iframe = null;
316
- }
317
-
318
- logger.wuDebug(`[IframeSandbox] Destroyed for ${this.appName}`);
319
- }
320
-
321
- /**
322
- * Check if this sandbox is active.
323
- * @returns {boolean}
324
- */
325
- isActive() {
326
- return this._active;
327
- }
328
- }
1
+ /**
2
+ * WU-IFRAME-SANDBOX: Real JS isolation using hidden iframes.
3
+ *
4
+ * Architecture:
5
+ * ┌── Main Window ────────────────────────────────┐
6
+ * │ ┌── Shadow DOM Container ──────────────────┐ │
7
+ * │ │ App renders here (CSS isolated) │ │
8
+ * │ └──────────────────────────────────────────┘ │
9
+ * │ ┌── Hidden iframe ────────────────────────┐ │
10
+ * │ │ import() runs here (REAL modules) │ │
11
+ * │ │ window = iframe.contentWindow (ISOLATED)│ │
12
+ * │ │ document patched → Shadow DOM │ │
13
+ * │ └────────────────────────────────────────-─┘ │
14
+ * └───────────────────────────────────────────────┘
15
+ *
16
+ * Why iframe?
17
+ * - import() is REAL → tree shaking, source maps, HMR all work
18
+ * - iframe has its own window → globals are isolated
19
+ * - Destroying iframe kills all timers/listeners at once
20
+ *
21
+ * How it works:
22
+ * 1. Create hidden iframe with <base href="appUrl"> for URL resolution
23
+ * 2. Patch iframe's document: createElement → main document (no ownerDocument issues),
24
+ * querySelector/body → Shadow DOM container
25
+ * 3. Track timers for guaranteed cleanup (some browsers don't kill iframe timers)
26
+ * 4. import() the app module inside iframe → runs in isolated context
27
+ * 5. App calls wu.define() → lifecycle registered on parent's WuCore
28
+ * 6. On unmount: destroy iframe = nuclear cleanup
29
+ *
30
+ * Fallback:
31
+ * If import() fails (CORS, module errors), wu-core falls back to eval mode
32
+ * (fetch HTML + parse + execute with(proxy)).
33
+ */
34
+
35
+ import { logger } from './wu-logger.js';
36
+
37
+ export class WuIframeSandbox {
38
+ constructor(appName) {
39
+ this.appName = appName;
40
+ this.iframe = null;
41
+ this._active = false;
42
+
43
+ // Side-effect tracking for guaranteed cleanup
44
+ this._timers = new Set();
45
+ this._intervals = new Set();
46
+ this._rafs = new Set();
47
+ this._listeners = [];
48
+ }
49
+
50
+ /**
51
+ * Create and activate the iframe sandbox.
52
+ *
53
+ * @param {string} appUrl - App's base URL (for <base href> and relative imports)
54
+ * @param {HTMLElement} shadowContainer - Shadow DOM container for DOM redirection
55
+ * @param {ShadowRoot|null} shadowRoot - Shadow root for query scoping
56
+ * @returns {Window} The iframe's contentWindow (isolated execution context)
57
+ */
58
+ activate(appUrl, shadowContainer, shadowRoot) {
59
+ if (this._active) return this.iframe.contentWindow;
60
+
61
+ // 1. Create hidden iframe
62
+ const iframe = document.createElement('iframe');
63
+ iframe.setAttribute('data-wu-sandbox', this.appName);
64
+ iframe.style.cssText = 'display:none !important;position:absolute;width:0;height:0;border:0;';
65
+
66
+ // Must be in DOM before accessing contentWindow
67
+ document.body.appendChild(iframe);
68
+ this.iframe = iframe;
69
+
70
+ // 2. Write base HTML with <base href> pointing to app URL.
71
+ // This makes relative URL resolution work for fetch(), CSS url(), etc.
72
+ // import() of full URLs works regardless of base.
73
+ const baseUrl = appUrl.replace(/\/$/, '');
74
+ const iframeWin = iframe.contentWindow;
75
+ const iframeDoc = iframeWin.document;
76
+
77
+ iframeDoc.open();
78
+ iframeDoc.write(
79
+ `<!DOCTYPE html><html><head><base href="${baseUrl}/"></head><body></body></html>`
80
+ );
81
+ iframeDoc.close();
82
+
83
+ // 3. Make wu available inside iframe for wu.define()
84
+ iframeWin.wu = window.wu;
85
+
86
+ // 4. Patch document: redirect DOM operations to Shadow DOM
87
+ this._patchDocument(iframeWin, shadowContainer, shadowRoot);
88
+
89
+ // 5. Track timers for guaranteed cleanup
90
+ this._patchTimers(iframeWin);
91
+
92
+ this._active = true;
93
+ logger.wuDebug(`[IframeSandbox] Activated for ${this.appName} (base: ${baseUrl})`);
94
+ return iframeWin;
95
+ }
96
+
97
+ /**
98
+ * Import an ES module inside the iframe via real import().
99
+ * Preserves tree shaking, source maps, and Vite HMR.
100
+ *
101
+ * @param {string} url - Full module URL to import
102
+ * @param {number} [timeout=30000] - Max wait time in ms
103
+ * @returns {Promise<void>}
104
+ */
105
+ importModule(url, timeout = 30000) {
106
+ if (!this._active) {
107
+ throw new Error(`[IframeSandbox] Not active for ${this.appName}`);
108
+ }
109
+
110
+ return new Promise((resolve, reject) => {
111
+ const channelId = `wu_${this.appName}_${Date.now()}`;
112
+
113
+ // Listen for import completion via postMessage
114
+ const onMessage = (event) => {
115
+ if (event.data?.channelId !== channelId) return;
116
+ cleanup();
117
+ if (event.data.error) {
118
+ reject(new Error(event.data.error));
119
+ } else {
120
+ resolve();
121
+ }
122
+ };
123
+
124
+ const timer = setTimeout(() => {
125
+ cleanup();
126
+ reject(new Error(
127
+ `[IframeSandbox] import() timed out for ${this.appName}: ${url}`
128
+ ));
129
+ }, timeout);
130
+
131
+ const cleanup = () => {
132
+ window.removeEventListener('message', onMessage);
133
+ clearTimeout(timer);
134
+ };
135
+
136
+ window.addEventListener('message', onMessage);
137
+
138
+ // Inject module script into iframe
139
+ const iframeDoc = this.iframe.contentWindow.document;
140
+ const script = iframeDoc.createElement('script');
141
+ script.type = 'module';
142
+ script.textContent =
143
+ `import("${url.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}")` +
144
+ `.then(() => parent.postMessage({ channelId: "${channelId}", success: true }, '*'))` +
145
+ `.catch(e => parent.postMessage({ channelId: "${channelId}", error: e.message || String(e) }, '*'));`;
146
+
147
+ iframeDoc.head.appendChild(script);
148
+ logger.wuDebug(`[IframeSandbox] Importing module: ${url}`);
149
+ });
150
+ }
151
+
152
+ /**
153
+ * Patch the iframe's document to redirect DOM operations.
154
+ *
155
+ * Critical patches:
156
+ * - createElement/createTextNode → main document (avoids ownerDocument mismatch)
157
+ * React/Vue create nodes and append to Shadow DOM container.
158
+ * Nodes must belong to the main document to avoid cross-document adoption issues.
159
+ *
160
+ * - querySelector/body → Shadow DOM container
161
+ * Libraries that query the document will find app elements in the Shadow DOM.
162
+ *
163
+ * - addEventListener → tracked for cleanup
164
+ */
165
+ _patchDocument(iframeWin, shadowContainer, shadowRoot) {
166
+ const iframeDoc = iframeWin.document;
167
+ const queryTarget = shadowRoot || shadowContainer;
168
+ const mainDoc = document; // parent document
169
+
170
+ // --- Node creation: use main document to avoid ownerDocument mismatch ---
171
+ // React uses container.ownerDocument.createElement() internally,
172
+ // but other code might use document.createElement() directly.
173
+ // By redirecting to main document, all nodes belong to the same document tree.
174
+ iframeDoc.createElement = (tag, options) => mainDoc.createElement(tag, options);
175
+ iframeDoc.createElementNS = (ns, tag, options) => mainDoc.createElementNS(ns, tag, options);
176
+ iframeDoc.createTextNode = (text) => mainDoc.createTextNode(text);
177
+ iframeDoc.createComment = (text) => mainDoc.createComment(text);
178
+ iframeDoc.createDocumentFragment = () => mainDoc.createDocumentFragment();
179
+
180
+ // --- DOM queries: redirect to Shadow DOM ---
181
+ iframeDoc.querySelector = (sel) => queryTarget.querySelector(sel);
182
+ iframeDoc.querySelectorAll = (sel) => queryTarget.querySelectorAll(sel);
183
+ iframeDoc.getElementById = (id) => queryTarget.querySelector(`#${id}`);
184
+ iframeDoc.getElementsByClassName = (cls) => queryTarget.querySelectorAll(`.${cls}`);
185
+ iframeDoc.getElementsByTagName = (tag) => queryTarget.querySelectorAll(tag);
186
+
187
+ // --- document.body → shadow container ---
188
+ // Frameworks that append to document.body (portals, modals) will target the Shadow DOM.
189
+ try {
190
+ Object.defineProperty(iframeDoc, 'body', {
191
+ get: () => shadowContainer,
192
+ configurable: true
193
+ });
194
+ } catch {
195
+ // Some environments don't allow redefining body — not critical
196
+ logger.wuDebug('[IframeSandbox] Could not redefine document.body');
197
+ }
198
+
199
+ // --- document.addEventListener: track for cleanup ---
200
+ const origDocAdd = iframeDoc.addEventListener.bind(iframeDoc);
201
+ const origDocRemove = iframeDoc.removeEventListener.bind(iframeDoc);
202
+
203
+ iframeDoc.addEventListener = (event, handler, options) => {
204
+ this._listeners.push({ target: iframeDoc, event, handler, options });
205
+ origDocAdd(event, handler, options);
206
+ };
207
+
208
+ iframeDoc.removeEventListener = (event, handler, options) => {
209
+ this._listeners = this._listeners.filter(
210
+ l => !(l.target === iframeDoc && l.event === event && l.handler === handler)
211
+ );
212
+ origDocRemove(event, handler, options);
213
+ };
214
+
215
+ logger.wuDebug(`[IframeSandbox] Document patched for ${this.appName}`);
216
+ }
217
+
218
+ /**
219
+ * Patch timers in the iframe for guaranteed cleanup.
220
+ * Some browsers don't fully kill timers when an iframe is removed.
221
+ * We track all IDs and clear them explicitly on destroy.
222
+ */
223
+ _patchTimers(iframeWin) {
224
+ const origSetTimeout = iframeWin.setTimeout.bind(iframeWin);
225
+ const origClearTimeout = iframeWin.clearTimeout.bind(iframeWin);
226
+ const origSetInterval = iframeWin.setInterval.bind(iframeWin);
227
+ const origClearInterval = iframeWin.clearInterval.bind(iframeWin);
228
+
229
+ iframeWin.setTimeout = (fn, ms, ...args) => {
230
+ const id = origSetTimeout((...a) => {
231
+ this._timers.delete(id);
232
+ if (typeof fn === 'function') fn(...a);
233
+ }, ms, ...args);
234
+ this._timers.add(id);
235
+ return id;
236
+ };
237
+
238
+ iframeWin.clearTimeout = (id) => {
239
+ this._timers.delete(id);
240
+ origClearTimeout(id);
241
+ };
242
+
243
+ iframeWin.setInterval = (fn, ms, ...args) => {
244
+ const id = origSetInterval(fn, ms, ...args);
245
+ this._intervals.add(id);
246
+ return id;
247
+ };
248
+
249
+ iframeWin.clearInterval = (id) => {
250
+ this._intervals.delete(id);
251
+ origClearInterval(id);
252
+ };
253
+
254
+ // requestAnimationFrame may not exist in all iframe contexts
255
+ if (iframeWin.requestAnimationFrame) {
256
+ const origRAF = iframeWin.requestAnimationFrame.bind(iframeWin);
257
+ const origCancelRAF = iframeWin.cancelAnimationFrame.bind(iframeWin);
258
+
259
+ iframeWin.requestAnimationFrame = (fn) => {
260
+ const id = origRAF((...a) => {
261
+ this._rafs.delete(id);
262
+ fn(...a);
263
+ });
264
+ this._rafs.add(id);
265
+ return id;
266
+ };
267
+
268
+ iframeWin.cancelAnimationFrame = (id) => {
269
+ this._rafs.delete(id);
270
+ origCancelRAF(id);
271
+ };
272
+ }
273
+
274
+ logger.wuDebug(`[IframeSandbox] Timer tracking active for ${this.appName}`);
275
+ }
276
+
277
+ /**
278
+ * Destroy the iframe and all side effects.
279
+ * Nuclear cleanup: kills everything at once.
280
+ */
281
+ destroy() {
282
+ if (!this._active) return;
283
+ this._active = false;
284
+
285
+ // 1. Clear all tracked timers
286
+ for (const id of this._timers) { try { clearTimeout(id); } catch {} }
287
+ for (const id of this._intervals) { try { clearInterval(id); } catch {} }
288
+ for (const id of this._rafs) { try { cancelAnimationFrame(id); } catch {} }
289
+ this._timers.clear();
290
+ this._intervals.clear();
291
+ this._rafs.clear();
292
+
293
+ // 2. Remove all tracked event listeners
294
+ for (const { target, event, handler, options } of this._listeners) {
295
+ try { target.removeEventListener(event, handler, options); } catch {}
296
+ }
297
+ this._listeners = [];
298
+
299
+ // 3. Wipe and remove iframe
300
+ if (this.iframe) {
301
+ try {
302
+ const doc = this.iframe.contentDocument;
303
+ if (doc) {
304
+ doc.open();
305
+ doc.write('');
306
+ doc.close();
307
+ }
308
+ } catch {
309
+ // Cross-origin or already detached — ignore
310
+ }
311
+
312
+ if (this.iframe.parentNode) {
313
+ this.iframe.parentNode.removeChild(this.iframe);
314
+ }
315
+ this.iframe = null;
316
+ }
317
+
318
+ logger.wuDebug(`[IframeSandbox] Destroyed for ${this.appName}`);
319
+ }
320
+
321
+ /**
322
+ * Check if this sandbox is active.
323
+ * @returns {boolean}
324
+ */
325
+ isActive() {
326
+ return this._active;
327
+ }
328
+ }