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,1398 +1,1412 @@
1
- /**
2
- * WU-FRAMEWORK: UNIVERSAL MICROFRONTENDS
3
- * Motor principal agnostico - Funciona con cualquier framework
4
- */
5
-
6
- import { WuLoader } from './wu-loader.js';
7
- import { WuSandbox } from './wu-sandbox.js';
8
- import { WuManifest } from './wu-manifest.js';
9
- import { logger } from './wu-logger.js';
10
- import { default as store } from './wu-store.js';
11
- import { WuApp } from './wu-app.js';
12
- import { WuCache } from './wu-cache.js';
13
- import { WuEventBus } from './wu-event-bus.js';
14
- import { WuPerformance } from './wu-performance.js';
15
- import { WuPluginSystem } from './wu-plugin.js';
16
- import { WuLoadingStrategy } from './wu-strategies.js';
17
- import { WuErrorBoundary } from './wu-error-boundary.js';
18
- import { WuLifecycleHooks } from './wu-hooks.js';
19
- import { WuHtmlParser } from './wu-html-parser.js';
20
- import { WuScriptExecutor } from './wu-script-executor.js';
21
- import { WuIframeSandbox } from './wu-iframe-sandbox.js';
22
- import { WuPrefetch } from './wu-prefetch.js';
23
- import { WuOverrides } from './wu-overrides.js';
24
-
25
- export class WuCore {
26
- constructor(options = {}) {
27
- // Registros principales
28
- this.apps = new Map(); // Apps registradas
29
- this.definitions = new Map(); // Definiciones de lifecycle
30
- this.manifests = new Map(); // Manifiestos cargados
31
- this.mounted = new Map(); // Apps montadas
32
- this.hidden = new Map(); // Keep-alive hidden apps
33
- this._pendingUnmounts = new Map(); // Deferred unmount timers (StrictMode compat)
34
- this._mountingPromises = new Map(); // In-flight mount dedup
35
-
36
- // Componentes core
37
- this.loader = new WuLoader();
38
- this.sandbox = new WuSandbox();
39
- this.manifest = new WuManifest();
40
- this.store = store;
41
-
42
- // Strict sandbox support: HTML entry + script execution in proxy
43
- this.htmlParser = new WuHtmlParser();
44
- this.scriptExecutor = new WuScriptExecutor();
45
-
46
- // Sistemas esenciales
47
- this.cache = new WuCache({ storage: 'localStorage', maxSize: 100 }); // 100MB cache
48
- this.eventBus = new WuEventBus();
49
- this.performance = new WuPerformance();
50
-
51
- // Advanced systems
52
- this.pluginSystem = new WuPluginSystem(this);
53
- this.strategies = new WuLoadingStrategy(this);
54
- this.errorBoundary = new WuErrorBoundary(this);
55
- this.hooks = new WuLifecycleHooks(this);
56
- this.prefetcher = new WuPrefetch(this);
57
- this.overrides = new WuOverrides();
58
-
59
- // Estado
60
- this.isInitialized = false;
61
-
62
- logger.wuInfo('Wu Framework initialized - Universal Microfrontends');
63
- }
64
-
65
- /**
66
- * Inicializar wu-framework con configuracion de apps
67
- * @param {Object} config - Configuracion { apps: [{name, url}, ...] }
68
- */
69
- async init(config) {
70
- if (this.isInitialized) {
71
- logger.wuWarn('Framework already initialized');
72
- return;
73
- }
74
-
75
- // Global sandbox mode: 'module' (default) or 'strict'
76
- this._sandboxMode = config.sandbox || 'module';
77
-
78
- logger.wuDebug(`Initializing (sandbox: ${this._sandboxMode}) with apps:`, config.apps?.map(app => app.name));
79
-
80
- try {
81
- // Execute beforeInit hooks
82
- const beforeInitResult = await this.hooks.execute('beforeInit', { config });
83
- if (beforeInitResult.cancelled) {
84
- logger.wuWarn('Initialization cancelled by beforeInit hook');
85
- return;
86
- }
87
-
88
- // Call plugin beforeInit hooks
89
- await this.pluginSystem.callHook('beforeInit', { config });
90
-
91
- // Configure and apply cookie overrides (QA/testing: wu-override:<app>=<url>)
92
- if (config.overrides) {
93
- this.overrides.configure(config.overrides);
94
- }
95
- const apps = config.apps || [];
96
- this.overrides.refresh();
97
- this.overrides.applyToApps(apps);
98
-
99
- // Registrar todas las apps
100
- for (const appConfig of apps) {
101
- await this.registerApp(appConfig);
102
- }
103
-
104
- // Preload apps with eager/preload strategies
105
- await this.strategies.preload(config.apps || []);
106
-
107
- this.isInitialized = true;
108
-
109
- // Execute afterInit hooks
110
- await this.hooks.execute('afterInit', { config });
111
-
112
- // Call plugin afterInit hooks
113
- await this.pluginSystem.callHook('afterInit', { config });
114
-
115
- logger.wuInfo('Framework initialized successfully');
116
- } catch (error) {
117
- logger.wuError('Initialization failed:', error);
118
-
119
- // Call plugin error hooks
120
- await this.pluginSystem.callHook('onError', { phase: 'init', error });
121
-
122
- throw error;
123
- }
124
- }
125
-
126
- /**
127
- * Registrar una aplicacion
128
- * @param {Object} appConfig - { name, url, keepAlive, sandbox, container, ... }
129
- */
130
- async registerApp(appConfig) {
131
- const { name, url } = appConfig;
132
-
133
- try {
134
- logger.wuDebug(`Registering app: ${name} from ${url}`);
135
-
136
- // Cargar manifest
137
- const manifestData = await this.manifest.load(url);
138
- this.manifests.set(name, manifestData);
139
-
140
- // Registrar la app — preserve all config fields (keepAlive, sandbox, container, etc.)
141
- this.apps.set(name, {
142
- ...appConfig,
143
- manifest: manifestData,
144
- status: 'registered'
145
- });
146
-
147
- logger.wuDebug(`App ${name} registered successfully`);
148
- } catch (error) {
149
- logger.wuError(`Failed to register app ${name}:`, error);
150
- throw error;
151
- }
152
- }
153
-
154
- /**
155
- * Definir lifecycle de una micro-app
156
- * @param {string} appName - Nombre de la app
157
- * @param {Object} lifecycle - { mount, unmount }
158
- */
159
- define(appName, lifecycle) {
160
- if (!lifecycle.mount) {
161
- throw new Error(`[Wu] Mount function required for app: ${appName}`);
162
- }
163
-
164
- this.definitions.set(appName, lifecycle);
165
-
166
- // Dispatch custom event for external listeners
167
- const event = new CustomEvent('wu:app:ready', {
168
- detail: { appName, timestamp: Date.now() }
169
- });
170
- window.dispatchEvent(event);
171
-
172
- logger.wuDebug(`Lifecycle defined for: ${appName}`);
173
- }
174
-
175
- /**
176
- * Mount app with multi-retry mounting and recovery.
177
- * If the app is in keep-alive (hidden) state, shows it instantly.
178
- *
179
- * @param {string} appName - Nombre de la app
180
- * @param {string} containerSelector - Selector del contenedor
181
- */
182
- async mount(appName, containerSelector) {
183
- // ── StrictMode guard: cancel pending deferred unmount ──
184
- // React StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
185
- // The cleanup fires between two mounts. By deferring the actual unmount,
186
- // the second mount cancels it and the app stays alive — zero flicker.
187
- if (this._pendingUnmounts.has(appName)) {
188
- clearTimeout(this._pendingUnmounts.get(appName));
189
- this._pendingUnmounts.delete(appName);
190
- logger.wuDebug(`${appName} deferred unmount cancelled by remount`);
191
- }
192
-
193
- // Already mounted in same container → no-op
194
- if (this.mounted.has(appName)) {
195
- const existing = this.mounted.get(appName);
196
- if (existing.containerSelector === containerSelector) {
197
- logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
198
- return;
199
- }
200
- }
201
-
202
- // Deduplicate concurrent mounts (StrictMode fires effect twice)
203
- if (this._mountingPromises.has(appName)) {
204
- logger.wuDebug(`${appName} mount already in progress, deduplicating`);
205
- return await this._mountingPromises.get(appName);
206
- }
207
-
208
- // Check if app is in keep-alive (hidden) state
209
- const hiddenEntry = this.hidden.get(appName);
210
- if (hiddenEntry) {
211
- if (hiddenEntry.containerSelector === containerSelector) {
212
- // Same container → instant show (no reload)
213
- return await this.show(appName);
214
- }
215
- // Different container → destroy hidden state, remount normally
216
- await this._destroyHidden(appName);
217
- }
218
-
219
- // Track mount promise for deduplication
220
- const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
221
- this._mountingPromises.set(appName, mountPromise);
222
-
223
- try {
224
- return await mountPromise;
225
- } finally {
226
- this._mountingPromises.delete(appName);
227
- }
228
- }
229
-
230
- /**
231
- * Mount with recovery: self-healing app mounting
232
- */
233
- async mountWithRecovery(appName, containerSelector, attempt = 0) {
234
- const maxAttempts = 3;
235
-
236
- try {
237
- // Start performance measurement
238
- this.performance.startMeasure('mount', appName);
239
-
240
- logger.wuDebug(`Mounting ${appName} in ${containerSelector} (attempt ${attempt + 1})`);
241
-
242
- // Execute beforeLoad hooks
243
- const beforeLoadResult = await this.hooks.execute('beforeLoad', { appName, containerSelector, attempt });
244
- if (beforeLoadResult.cancelled) {
245
- logger.wuWarn('Mount cancelled by beforeLoad hook');
246
- return;
247
- }
248
-
249
- // Call plugin beforeMount hooks
250
- const pluginBeforeMount = await this.pluginSystem.callHook('beforeMount', { appName, containerSelector });
251
- if (pluginBeforeMount === false) {
252
- logger.wuWarn('Mount cancelled by plugin beforeMount hook');
253
- return;
254
- }
255
-
256
- // Verify app is registered
257
- const app = this.apps.get(appName);
258
- if (!app) {
259
- throw new Error(`App ${appName} not registered. Call wu.init() first.`);
260
- }
261
-
262
- // Container reality check
263
- const container = document.querySelector(containerSelector);
264
- if (!container) {
265
- throw new Error(`Container not found: ${containerSelector}`);
266
- }
267
-
268
- // Create sandbox - pasar manifest con styleMode y URL de la app
269
- const sandbox = this.sandbox.create(appName, container, {
270
- manifest: app.manifest,
271
- styleMode: app.manifest?.styleMode,
272
- appUrl: app.url // Pasar URL de la app para filtrar estilos de apps fully-isolated
273
- });
274
-
275
- // Execute afterLoad hooks
276
- await this.hooks.execute('afterLoad', { appName, containerSelector, sandbox });
277
-
278
- // Resolve lifecycle definition
279
- let lifecycle = this.definitions.get(appName);
280
- if (!lifecycle) {
281
- // Load remote app
282
- await this.loadAndMountRemoteApp(app, sandbox);
283
- lifecycle = this.definitions.get(appName);
284
-
285
- if (!lifecycle) {
286
- throw new Error(`App ${appName} did not register with wu.define()`);
287
- }
288
- }
289
-
290
- // Execute beforeMount hooks
291
- const beforeMountResult = await this.hooks.execute('beforeMount', { appName, containerSelector, sandbox, lifecycle });
292
- if (beforeMountResult.cancelled) {
293
- logger.wuWarn('Mount cancelled by beforeMount hook');
294
- return;
295
- }
296
-
297
- // Wait for styles to be ready before mounting
298
- if (sandbox.stylesReady) {
299
- logger.wuDebug(`Waiting for styles to be ready for ${appName}...`);
300
- await sandbox.stylesReady;
301
- logger.wuDebug(`Styles ready for ${appName}`);
302
- }
303
-
304
- // Execute mount lifecycle
305
- await lifecycle.mount(sandbox.container);
306
-
307
- // Register mounted app
308
- this.mounted.set(appName, {
309
- app,
310
- sandbox,
311
- lifecycle,
312
- container: sandbox.container,
313
- hostContainer: container,
314
- containerSelector,
315
- timestamp: Date.now(),
316
- state: 'stable'
317
- });
318
-
319
- // End performance measurement
320
- const mountTime = this.performance.endMeasure('mount', appName);
321
-
322
- // Execute afterMount hooks
323
- await this.hooks.execute('afterMount', { appName, containerSelector, sandbox, mountTime });
324
-
325
- // Call plugin afterMount hooks
326
- await this.pluginSystem.callHook('afterMount', { appName, containerSelector, mountTime });
327
-
328
- // Emit mount event
329
- this.eventBus.emit('app:mounted', { appName, mountTime, attempt }, { appName });
330
-
331
- logger.wuInfo(`${appName} mounted successfully in ${mountTime.toFixed(2)}ms`);
332
-
333
- } catch (error) {
334
- logger.wuError(`Mount attempt ${attempt + 1} failed for ${appName}:`, error);
335
-
336
- // Use error boundary for intelligent error handling
337
- const errorResult = await this.errorBoundary.handle(error, {
338
- appName,
339
- containerSelector,
340
- retryCount: attempt,
341
- container: containerSelector
342
- });
343
-
344
- // Si el error boundary recupero el error, no necesitamos reintentar
345
- if (errorResult.recovered) {
346
- logger.wuDebug('Error recovered by error boundary');
347
- return;
348
- }
349
-
350
- // Recovery protocol
351
- if (attempt < maxAttempts - 1 && errorResult.action === 'retry') {
352
- logger.wuDebug('Initiating recovery protocol...');
353
-
354
- // Clean app state
355
- await this.appStateCleanup(appName, containerSelector);
356
-
357
- // Temporal stabilization
358
- await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
359
-
360
- // Recursive mounting with recovery
361
- return await this.mountWithRecovery(appName, containerSelector, attempt + 1);
362
- }
363
-
364
- // Call plugin error hooks
365
- await this.pluginSystem.callHook('onError', { phase: 'mount', error, appName });
366
-
367
- // Final mount failure - error boundary already handled fallback UI
368
- throw error;
369
- }
370
- }
371
-
372
- /**
373
- * App state cleanup: Enhanced container cleanup with framework protection
374
- */
375
- async appStateCleanup(appName, containerSelector) {
376
- try {
377
- logger.wuDebug(`Starting app state cleanup for ${appName}...`);
378
-
379
- // Clear hidden (keep-alive) state if present
380
- if (this.hidden.has(appName)) {
381
- try {
382
- await this._destroyHidden(appName);
383
- } catch (hiddenError) {
384
- logger.wuWarn('Hidden app cleanup failed:', hiddenError);
385
- }
386
- }
387
-
388
- // Clear any existing mounted state safely
389
- if (this.mounted.has(appName)) {
390
- try {
391
- await this.unmount(appName, { force: true });
392
- } catch (unmountError) {
393
- logger.wuWarn('Unmount failed during cleanup:', unmountError);
394
- }
395
- }
396
-
397
- // Enhanced container cleanup with Vue safety measures
398
- const container = document.querySelector(containerSelector);
399
- if (container) {
400
- // Protect Vue's reactivity system
401
- if (container.shadowRoot) {
402
- try {
403
- // Clear shadow root content safely
404
- const shadowChildren = Array.from(container.shadowRoot.children);
405
- shadowChildren.forEach(child => {
406
- try {
407
- child.remove();
408
- } catch (removeError) {
409
- logger.wuWarn('Failed to remove shadow child:', removeError);
410
- }
411
- });
412
- } catch (shadowError) {
413
- logger.wuWarn('Shadow root cleanup failed:', shadowError);
414
- }
415
- }
416
-
417
- // Clear any direct children if no shadow root
418
- if (!container.shadowRoot && container.children.length > 0) {
419
- try {
420
- container.innerHTML = '';
421
- } catch (htmlError) {
422
- logger.wuWarn('Container innerHTML cleanup failed:', htmlError);
423
- }
424
- }
425
-
426
- // Reset container attributes
427
- container.removeAttribute('data-wu-app');
428
- container.removeAttribute('data-quantum-state');
429
- container.removeAttribute('wu-debug');
430
- }
431
-
432
- // Reset definition state
433
- this.definitions.delete(appName);
434
-
435
- // Clear sandbox registry
436
- if (this.sandbox && this.sandbox.sandboxes) {
437
- this.sandbox.sandboxes.delete(appName);
438
- }
439
-
440
- logger.wuDebug(`App state cleaned successfully for ${appName}`);
441
-
442
- } catch (cleanupError) {
443
- logger.wuWarn(`App cleanup partial failure for ${appName}:`, cleanupError);
444
-
445
- // Emergency cleanup - force clear everything
446
- try {
447
- const container = document.querySelector(containerSelector);
448
- if (container) {
449
- container.style.display = 'none';
450
- setTimeout(() => {
451
- if (container) {
452
- container.style.display = '';
453
- }
454
- }, 100);
455
- }
456
- } catch (emergencyError) {
457
- logger.wuError('Emergency cleanup failed:', emergencyError);
458
- }
459
- }
460
- }
461
-
462
- /**
463
- * Remote app loader: Load app in the configured sandbox mode.
464
- *
465
- * Three modes:
466
- *
467
- * - module (default): ES6 import() + patchWindow for side-effect tracking.
468
- * Works with Vite, HMR, ES modules. App code runs in global scope.
469
- * Proxy is a cleanup tracker, not an isolation boundary.
470
- *
471
- * - strict: Hidden iframe + real import(). True JS isolation.
472
- * App code runs in iframe's window (separate global context).
473
- * Document operations proxied to Shadow DOM.
474
- * Preserves: tree shaking, source maps, HMR.
475
- * Falls back to eval mode if import() fails (CORS, etc.)
476
- *
477
- * - eval: Fetch HTML parse scripts execute with(proxy).
478
- * Maximum JS isolation via with(proxy) statement.
479
- * Requires bundled apps (UMD/IIFE), not ES modules.
480
- * No tree shaking, no source maps, no HMR.
481
- *
482
- * Set per-app: { name: 'app', url: '...', sandbox: 'strict' }
483
- * Or globally: wu.init({ sandbox: 'strict', apps: [...] })
484
- */
485
- async loadAndMountRemoteApp(app, sandbox) {
486
- const mode = app.sandbox || this._sandboxMode || 'module';
487
-
488
- if (mode === 'strict') {
489
- await this._loadStrict(app, sandbox);
490
- } else if (mode === 'eval') {
491
- await this._loadEval(app, sandbox);
492
- } else {
493
- await this._loadModule(app, sandbox);
494
- }
495
- }
496
-
497
- /**
498
- * MODULE MODE: import() + patchWindow (default).
499
- * Side effects tracked during load, cleaned on unmount.
500
- * App code runs in global scope.
501
- */
502
- async _loadModule(app, sandbox) {
503
- const moduleUrl = await this.resolveModulePath(app);
504
- logger.wuDebug(`[module] Loading ES module: ${moduleUrl}`);
505
-
506
- const jsSandbox = sandbox.jsSandbox;
507
- if (jsSandbox?.patchWindow) {
508
- jsSandbox.patchWindow();
509
- }
510
-
511
- try {
512
- await this.moduleLoader(moduleUrl, app.name);
513
- logger.wuDebug(`[module] ES module loaded: ${app.name}`);
514
- } catch (error) {
515
- logger.wuError(`[module] Failed to load ${moduleUrl}:`, error);
516
- throw error;
517
- } finally {
518
- if (jsSandbox?.unpatchWindow) {
519
- jsSandbox.unpatchWindow();
520
- }
521
- }
522
- }
523
-
524
- /**
525
- * STRICT MODE: Hidden iframe + real import().
526
- *
527
- * The iframe provides a separate window context. import() inside the iframe
528
- * is a real ES module import — tree shaking, source maps, and HMR all work.
529
- *
530
- * Pipeline:
531
- * 1. Create hidden iframe with <base href="appUrl">
532
- * 2. Patch iframe's document → DOM operations go to Shadow DOM
533
- * 3. import() the app module inside iframe
534
- * 4. Wait for wu.define() registration
535
- *
536
- * If import() fails (CORS, network, etc.), falls back to eval mode
537
- * with a console warning explaining why.
538
- */
539
- async _loadStrict(app, sandbox) {
540
- logger.wuDebug(`[strict] Loading ${app.name} via iframe sandbox`);
541
-
542
- // Create and activate iframe sandbox
543
- const iframeSandbox = new WuIframeSandbox(app.name);
544
- iframeSandbox.activate(app.url, sandbox.container, sandbox.shadowRoot);
545
- sandbox.iframeSandbox = iframeSandbox;
546
-
547
- try {
548
- // Resolve module path (same logic as module mode)
549
- const moduleUrl = await this.resolveModulePath(app);
550
- logger.wuDebug(`[strict] Importing module in iframe: ${moduleUrl}`);
551
-
552
- // Import module inside iframe — real import()!
553
- await iframeSandbox.importModule(moduleUrl);
554
- logger.wuDebug(`[strict] Module imported for ${app.name}`);
555
-
556
- } catch (importError) {
557
- // import() failed likely CORS or module error.
558
- // Fall back to eval mode (fetch + parse + with(proxy)).
559
- logger.wuWarn(
560
- `[strict] iframe import failed for ${app.name}: ${importError.message}\n` +
561
- `Falling back to eval mode (fetch + parse + execute with proxy).\n` +
562
- `To fix: ensure the app's dev server sets Access-Control-Allow-Origin: * headers,\n` +
563
- `or use sandbox: 'eval' explicitly for UMD/IIFE bundles.`
564
- );
565
-
566
- // Destroy failed iframe
567
- iframeSandbox.destroy();
568
- sandbox.iframeSandbox = null;
569
-
570
- // Fallback to eval mode
571
- await this._loadEval(app, sandbox);
572
- return;
573
- }
574
-
575
- // Wait for wu.define()
576
- await this._waitForDefine(app.name, 'strict');
577
-
578
- logger.wuDebug(`[strict] ${app.name} loaded and registered via iframe`);
579
- }
580
-
581
- /**
582
- * EVAL MODE: Fetch HTML → parse → execute scripts inside proxy.
583
- *
584
- * Maximum JS isolation via with(proxy) statement — all unqualified
585
- * identifiers (setTimeout, document, fetch) go through proxy traps.
586
- *
587
- * Requires bundled apps (UMD/IIFE). ES modules cannot be eval'd.
588
- * No tree shaking, no source maps, no HMR.
589
- *
590
- * Pipeline:
591
- * 1. Fetch HTML from app URL
592
- * 2. Parse: extract scripts (inline + external), styles, clean DOM
593
- * 3. Inject DOM + styles into Shadow DOM
594
- * 4. Execute all scripts inside the proxy via WuScriptExecutor
595
- * 5. Wait for wu.define()
596
- */
597
- async _loadEval(app, sandbox) {
598
- logger.wuDebug(`[eval] Loading ${app.name} from ${app.url}`);
599
-
600
- const jsSandbox = sandbox.jsSandbox;
601
- const proxy = jsSandbox.getProxy();
602
-
603
- if (!proxy) {
604
- throw new Error(`[eval] No active proxy for ${app.name}. Sandbox must be activated first.`);
605
- }
606
-
607
- // 1. Fetch and parse HTML
608
- const parsed = await this.htmlParser.fetchAndParse(app.url, app.name);
609
-
610
- // 2. Inject clean DOM into container
611
- if (parsed.dom) {
612
- sandbox.container.innerHTML = parsed.dom;
613
- }
614
-
615
- // 3. Inject styles into shadow root
616
- const styleTarget = sandbox.shadowRoot || sandbox.container;
617
-
618
- for (const cssText of parsed.styles.inline) {
619
- const style = document.createElement('style');
620
- style.textContent = cssText;
621
- styleTarget.appendChild(style);
622
- }
623
-
624
- for (const href of parsed.styles.external) {
625
- const link = document.createElement('link');
626
- link.rel = 'stylesheet';
627
- link.href = href;
628
- styleTarget.appendChild(link);
629
- }
630
-
631
- // 4. Build and execute scripts inside the proxy
632
- const scripts = [];
633
- for (const content of parsed.scripts.inline) {
634
- scripts.push({ content });
635
- }
636
- for (const src of parsed.scripts.external) {
637
- scripts.push({ src });
638
- }
639
-
640
- await this.scriptExecutor.executeAll(scripts, app.name, proxy);
641
- logger.wuDebug(`[eval] Scripts executed for ${app.name}`);
642
-
643
- // 5. Wait for wu.define()
644
- await this._waitForDefine(app.name, 'eval');
645
-
646
- logger.wuDebug(`[eval] ${app.name} loaded and registered`);
647
- }
648
-
649
- /**
650
- * Wait for an app to call wu.define() with a timeout.
651
- * Shared by strict and eval modes.
652
- */
653
- async _waitForDefine(appName, mode) {
654
- const maxWaitTime = 10000;
655
- const checkInterval = 50;
656
- const startTime = Date.now();
657
-
658
- while (!this.definitions.has(appName)) {
659
- if (Date.now() - startTime >= maxWaitTime) {
660
- throw new Error(
661
- `[${mode}] App '${appName}' loaded but wu.define() was not called within ${maxWaitTime}ms.\n` +
662
- `Make sure your app calls: window.wu.define('${appName}', { mount, unmount })`
663
- );
664
- }
665
- await new Promise(resolve => setTimeout(resolve, checkInterval));
666
- }
667
- }
668
-
669
- /**
670
- * Module path resolver: Intelligent URL construction with fallback
671
- * Intelligently resolves module paths with real-time validation
672
- */
673
- async resolveModulePath(app) {
674
- let entryFile = app.manifest?.entry || 'main.js';
675
- const baseUrl = app.url.replace(/\/$/, ''); // Remove trailing slash
676
-
677
- // Normalize path: Remove duplicated directories
678
- // If entry already starts with 'src/', 'dist/', etc., use it as-is
679
- const hasFolderPrefix = /^(src|dist|public|build|assets|lib|es)\//.test(entryFile);
680
-
681
- if (hasFolderPrefix) {
682
- logger.wuDebug(`Entry already has folder prefix: ${entryFile}`);
683
- // Entry already has folder, just use baseUrl + entryFile
684
- const directPath = `${baseUrl}/${entryFile}`;
685
- logger.wuDebug(`Using direct path: ${directPath}`);
686
- return directPath;
687
- }
688
-
689
- // Multi-path candidates (in order of preference)
690
- const pathCandidates = [
691
- `${baseUrl}/src/${entryFile}`, // Standard structure
692
- `${baseUrl}/${entryFile}`, // Root level
693
- `${baseUrl}/dist/${entryFile}`, // Built version
694
- `${baseUrl}/public/${entryFile}`, // Public folder
695
- `${baseUrl}/build/${entryFile}`, // Build folder
696
- `${baseUrl}/assets/${entryFile}`, // Assets folder
697
- `${baseUrl}/lib/${entryFile}`, // Library folder
698
- `${baseUrl}/es/${entryFile}` // ES modules folder
699
- ];
700
-
701
- logger.wuDebug(`Attempting path resolution for ${app.name}...`);
702
-
703
- // Smart path discovery: Try each candidate with validation
704
- for (let i = 0; i < pathCandidates.length; i++) {
705
- const candidate = pathCandidates[i];
706
-
707
- try {
708
- logger.wuDebug(`Testing path candidate ${i + 1}/${pathCandidates.length}: ${candidate}`);
709
-
710
- // Path validation with enhanced verification
711
- const isValid = await this.validatePath(candidate);
712
-
713
- if (isValid) {
714
- logger.wuDebug(`Path resolved successfully: ${candidate}`);
715
- return candidate;
716
- } else {
717
- logger.wuDebug(`Path candidate ${i + 1} failed validation: ${candidate}`);
718
- }
719
-
720
- } catch (error) {
721
- logger.wuDebug(`Path candidate ${i + 1} threw error: ${candidate} - ${error.message}`);
722
- continue;
723
- }
724
- }
725
-
726
- // Fallback: If all candidates fail, use the first one and let the error bubble up
727
- const fallbackPath = pathCandidates[0];
728
- logger.wuWarn(`All path candidates failed, using fallback: ${fallbackPath}`);
729
- return fallbackPath;
730
- }
731
-
732
- /**
733
- * Path validator: Smart existence verification with module testing
734
- * Validates if a path exists and can be loaded as an ES module
735
- */
736
- async validatePath(url) {
737
- try {
738
- // Enhanced validation: Try actual module import for reliable verification
739
- logger.wuDebug(`Testing path: ${url}`);
740
-
741
- // First, try a GET request to check if file exists and is accessible
742
- const response = await fetch(url, {
743
- method: 'GET',
744
- cache: 'no-cache',
745
- signal: AbortSignal.timeout(2000) // 2 second timeout
746
- });
747
-
748
- if (!response.ok) {
749
- logger.wuDebug(`Path validation failed - HTTP ${response.status}: ${url}`);
750
- return false;
751
- }
752
-
753
- // Check content type and file extension
754
- const contentType = response.headers.get('content-type') || '';
755
- const isJavaScript =
756
- contentType.includes('javascript') ||
757
- contentType.includes('module') ||
758
- contentType.includes('text/plain') || // Some servers serve JS as plain text
759
- url.endsWith('.js') ||
760
- url.endsWith('.mjs');
761
-
762
- if (!isJavaScript) {
763
- logger.wuDebug(`Path validation failed - Invalid content type '${contentType}': ${url}`);
764
- return false;
765
- }
766
-
767
- // Final verification: Check if content looks like a valid module
768
- const content = await response.text();
769
-
770
- // Detect HTML fallback: Check if server returned HTML instead of JS
771
- // Only check if content STARTS with HTML markers (trimmed), not if it contains them anywhere
772
- // This avoids false positives for Angular/React bundles that contain template strings
773
- const trimmedContent = content.trim().toLowerCase();
774
- const isHtmlFallback =
775
- trimmedContent.startsWith('<!doctype') ||
776
- trimmedContent.startsWith('<html') ||
777
- trimmedContent.startsWith('<head') ||
778
- trimmedContent.startsWith('<body') ||
779
- trimmedContent.startsWith('<!-');
780
-
781
- if (isHtmlFallback) {
782
- logger.wuDebug(`Path validation failed - Server returned HTML fallback page: ${url}`);
783
- return false;
784
- }
785
-
786
- // Check for valid JavaScript module content
787
- const hasModuleContent =
788
- content.includes('export') ||
789
- content.includes('import') ||
790
- content.includes('wu.define') ||
791
- content.includes('module.exports') ||
792
- content.includes('console.log') ||
793
- (content.includes('function') && content.length > 10);
794
-
795
- if (!hasModuleContent) {
796
- logger.wuDebug(`Path validation failed - No valid module content: ${url}`);
797
- logger.wuDebug(`Content preview: ${content.substring(0, 100)}...`);
798
- return false;
799
- }
800
-
801
- logger.wuDebug(`Path validation successful: ${url} (${content.length} chars)`);
802
- return true;
803
-
804
- } catch (error) {
805
- // Network, timeout, or parsing error means path is invalid
806
- logger.wuDebug(`Path validation failed for ${url}: ${error.message}`);
807
- return false;
808
- }
809
- }
810
-
811
- /**
812
- * Module loader: Advanced registration patterns
813
- * Handles asynchronous registration with timing synchronization
814
- * Verifica que definitions tenga el lifecycle despues de cargar
815
- */
816
- async moduleLoader(moduleUrl, appName) {
817
- // Check if already registered
818
- if (this.definitions.has(appName)) {
819
- logger.wuDebug(`App ${appName} already registered`);
820
- return;
821
- }
822
-
823
- logger.wuDebug(`Using event-based registration for ${appName}`);
824
-
825
- // Load module first
826
- try {
827
- await import(/* @vite-ignore */ moduleUrl);
828
- } catch (loadError) {
829
- logger.wuError(`Failed to import module ${moduleUrl}:`, loadError);
830
- throw loadError;
831
- }
832
-
833
- // Wait for wu.define() to be called with real verification
834
- const maxWaitTime = 10000; // 10 segundos
835
- const checkInterval = 50; // Verificar cada 50ms
836
- const startTime = Date.now();
837
-
838
- while (!this.definitions.has(appName)) {
839
- const elapsed = Date.now() - startTime;
840
-
841
- if (elapsed >= maxWaitTime) {
842
- throw new Error(
843
- `App '${appName}' module loaded but wu.define() was not called within ${maxWaitTime}ms.\n\n` +
844
- `Make sure your module calls:\n` +
845
- ` wu.define('${appName}', { mount, unmount })\n\n` +
846
- `Or using window.wu:\n` +
847
- ` window.wu.define('${appName}', { mount, unmount })`
848
- );
849
- }
850
-
851
- // Esperar un poco antes de verificar de nuevo
852
- await new Promise(resolve => setTimeout(resolve, checkInterval));
853
- }
854
-
855
- logger.wuDebug(`App ${appName} loaded and registered (verified in definitions)`);
856
- }
857
-
858
- /**
859
- * Desmontar una aplicacion.
860
- *
861
- * With keepAlive, the app is hidden instead of destroyed.
862
- * All DOM, JS state, timers, and iframe are preserved.
863
- * Re-mounting shows the app instantly.
864
- *
865
- * keepAlive is resolved from (in priority order):
866
- * 1. options.keepAlive (per-call override)
867
- * 2. app config keepAlive (set via wu.app() or registerApp)
868
- * 3. false (default: destroy)
869
- *
870
- * Use options.force = true to destroy even if keepAlive is set.
871
- *
872
- * @param {string} appName - Nombre de la app
873
- * @param {Object} [options] - Unmount options
874
- * @param {boolean} [options.keepAlive] - Preserve state for instant re-mount
875
- * @param {boolean} [options.force] - Force destroy even if keepAlive
876
- */
877
- async unmount(appName, options = {}) {
878
- logger.wuDebug(`Unmounting ${appName}`);
879
-
880
- const mounted = this.mounted.get(appName);
881
- if (!mounted) {
882
- // Check if it's hidden (keep-alive) — force destroy if requested
883
- if (options.force && this.hidden.has(appName)) {
884
- return await this._destroyHidden(appName);
885
- }
886
- logger.wuWarn(`App ${appName} not mounted`);
887
- return;
888
- }
889
-
890
- // Resolve keepAlive: per-call > per-app config > default false
891
- const keepAlive = options.force
892
- ? false
893
- : (options.keepAlive ?? mounted.app?.keepAlive ?? false);
894
-
895
- if (keepAlive) {
896
- return await this.hide(appName);
897
- }
898
-
899
- // Force → immediate unmount (no deferral)
900
- if (options.force) {
901
- return await this._executeUnmount(appName, mounted);
902
- }
903
-
904
- // ── Deferred unmount: 60ms window for React StrictMode ──
905
- // StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
906
- // The cleanup fires between two mounts. By deferring the actual unmount,
907
- // the second mount() call cancels the timer and the app stays alive.
908
- if (this._pendingUnmounts.has(appName)) {
909
- clearTimeout(this._pendingUnmounts.get(appName));
910
- }
911
-
912
- this._pendingUnmounts.set(appName, setTimeout(async () => {
913
- this._pendingUnmounts.delete(appName);
914
- // Re-verify: only unmount if the same mount entry is still current
915
- if (this.mounted.has(appName) && this.mounted.get(appName) === mounted) {
916
- try {
917
- await this._executeUnmount(appName, mounted);
918
- } catch (error) {
919
- logger.wuError(`Deferred unmount failed for ${appName}:`, error);
920
- }
921
- }
922
- }, 60));
923
- }
924
-
925
- /**
926
- * Execute the actual unmount immediately (no deferral).
927
- * Called by the deferred timer, force unmount, or destroy.
928
- * @private
929
- */
930
- async _executeUnmount(appName, mounted) {
931
- try {
932
- // Execute beforeUnmount hooks
933
- const beforeUnmountResult = await this.hooks.execute('beforeUnmount', { appName, mounted });
934
- if (beforeUnmountResult.cancelled) {
935
- logger.wuWarn('Unmount cancelled by beforeUnmount hook');
936
- return;
937
- }
938
-
939
- // Call plugin beforeUnmount hooks
940
- const pluginBeforeUnmount = await this.pluginSystem.callHook('beforeUnmount', { appName });
941
- if (pluginBeforeUnmount === false) {
942
- logger.wuWarn('Unmount cancelled by plugin beforeUnmount hook');
943
- return;
944
- }
945
-
946
- // Ejecutar unmount del lifecycle si existe
947
- if (mounted.lifecycle?.unmount) {
948
- await mounted.lifecycle.unmount(mounted.container);
949
- }
950
-
951
- // Destroy iframe sandbox if present (strict mode)
952
- if (mounted.sandbox.iframeSandbox) {
953
- mounted.sandbox.iframeSandbox.destroy();
954
- mounted.sandbox.iframeSandbox = null;
955
- }
956
-
957
- // Limpiar sandbox
958
- this.sandbox.cleanup(mounted.sandbox);
959
-
960
- // Remover del registro de montadas
961
- this.mounted.delete(appName);
962
-
963
- // Execute afterUnmount hooks
964
- await this.hooks.execute('afterUnmount', { appName });
965
-
966
- // Call plugin afterUnmount hooks
967
- await this.pluginSystem.callHook('afterUnmount', { appName });
968
-
969
- // Emit unmount event
970
- this.eventBus.emit('app:unmounted', { appName }, { appName });
971
-
972
- logger.wuDebug(`${appName} unmounted successfully`);
973
- } catch (error) {
974
- logger.wuError(`Failed to unmount ${appName}:`, error);
975
-
976
- // Call plugin error hooks
977
- await this.pluginSystem.callHook('onError', { phase: 'unmount', error, appName });
978
-
979
- // Emit error event
980
- this.eventBus.emit('app:error', { appName, error: error.message }, { appName });
981
- throw error;
982
- }
983
- }
984
-
985
- /**
986
- * Hide a mounted app (keep-alive).
987
- *
988
- * Preserves all state: DOM in Shadow DOM, JS in iframe, timers, listeners.
989
- * The app's optional `deactivate()` lifecycle hook is called.
990
- * Re-show with `show()` or `mount()` with the same container.
991
- *
992
- * @param {string} appName - App to hide
993
- */
994
- async hide(appName) {
995
- const mounted = this.mounted.get(appName);
996
- if (!mounted) {
997
- logger.wuWarn(`Cannot hide ${appName}: not mounted`);
998
- return;
999
- }
1000
-
1001
- logger.wuDebug(`Hiding ${appName} (keep-alive)`);
1002
-
1003
- // Call optional deactivate lifecycle hook
1004
- if (mounted.lifecycle?.deactivate) {
1005
- try {
1006
- await mounted.lifecycle.deactivate(mounted.container);
1007
- } catch (err) {
1008
- logger.wuWarn(`deactivate() failed for ${appName}:`, err);
1009
- }
1010
- }
1011
-
1012
- // Execute beforeUnmount hooks (so plugins know)
1013
- await this.hooks.execute('beforeUnmount', { appName, mounted, keepAlive: true });
1014
- await this.pluginSystem.callHook('beforeUnmount', { appName, keepAlive: true });
1015
-
1016
- // Hide the host container — all Shadow DOM content stays intact
1017
- mounted.hostContainer.style.display = 'none';
1018
- mounted.state = 'hidden';
1019
- mounted.hiddenAt = Date.now();
1020
-
1021
- // Move from mounted → hidden
1022
- this.hidden.set(appName, mounted);
1023
- this.mounted.delete(appName);
1024
-
1025
- // Execute afterUnmount hooks
1026
- await this.hooks.execute('afterUnmount', { appName, keepAlive: true });
1027
- await this.pluginSystem.callHook('afterUnmount', { appName, keepAlive: true });
1028
-
1029
- // Emit event
1030
- this.eventBus.emit('app:hidden', { appName }, { appName });
1031
-
1032
- logger.wuInfo(`${appName} hidden (keep-alive) — state preserved`);
1033
- }
1034
-
1035
- /**
1036
- * Show a hidden (keep-alive) app.
1037
- *
1038
- * Restores visibility instantly — no reload, no remount.
1039
- * The app's optional `activate()` lifecycle hook is called.
1040
- *
1041
- * @param {string} appName - App to show
1042
- */
1043
- async show(appName) {
1044
- const hidden = this.hidden.get(appName);
1045
- if (!hidden) {
1046
- logger.wuWarn(`Cannot show ${appName}: not in keep-alive state`);
1047
- return;
1048
- }
1049
-
1050
- this.performance.startMeasure('show', appName);
1051
- logger.wuDebug(`Showing ${appName} from keep-alive`);
1052
-
1053
- // Execute beforeMount hooks
1054
- await this.hooks.execute('beforeMount', {
1055
- appName,
1056
- containerSelector: hidden.containerSelector,
1057
- sandbox: hidden.sandbox,
1058
- lifecycle: hidden.lifecycle,
1059
- keepAlive: true
1060
- });
1061
- await this.pluginSystem.callHook('beforeMount', {
1062
- appName,
1063
- containerSelector: hidden.containerSelector,
1064
- keepAlive: true
1065
- });
1066
-
1067
- // Show the host container
1068
- hidden.hostContainer.style.display = '';
1069
- hidden.state = 'stable';
1070
- delete hidden.hiddenAt;
1071
-
1072
- // Move from hidden → mounted
1073
- this.mounted.set(appName, hidden);
1074
- this.hidden.delete(appName);
1075
-
1076
- // Call optional activate lifecycle hook
1077
- if (hidden.lifecycle?.activate) {
1078
- try {
1079
- await hidden.lifecycle.activate(hidden.container);
1080
- } catch (err) {
1081
- logger.wuWarn(`activate() failed for ${appName}:`, err);
1082
- }
1083
- }
1084
-
1085
- const showTime = this.performance.endMeasure('show', appName);
1086
-
1087
- // Execute afterMount hooks
1088
- await this.hooks.execute('afterMount', {
1089
- appName,
1090
- containerSelector: hidden.containerSelector,
1091
- sandbox: hidden.sandbox,
1092
- mountTime: showTime,
1093
- keepAlive: true
1094
- });
1095
- await this.pluginSystem.callHook('afterMount', {
1096
- appName,
1097
- containerSelector: hidden.containerSelector,
1098
- mountTime: showTime,
1099
- keepAlive: true
1100
- });
1101
-
1102
- // Emit event
1103
- this.eventBus.emit('app:shown', { appName, showTime }, { appName });
1104
-
1105
- logger.wuInfo(`${appName} shown from keep-alive in ${showTime.toFixed(2)}ms`);
1106
- }
1107
-
1108
- /**
1109
- * Force-destroy a hidden (keep-alive) app.
1110
- * Runs full cleanup: lifecycle unmount, iframe destroy, sandbox cleanup.
1111
- *
1112
- * @param {string} appName
1113
- * @private
1114
- */
1115
- async _destroyHidden(appName) {
1116
- const hidden = this.hidden.get(appName);
1117
- if (!hidden) return;
1118
-
1119
- logger.wuDebug(`Force-destroying hidden app: ${appName}`);
1120
-
1121
- // Show first (so unmount sees the container)
1122
- hidden.hostContainer.style.display = '';
1123
- hidden.state = 'stable';
1124
-
1125
- // Move back to mounted temporarily
1126
- this.mounted.set(appName, hidden);
1127
- this.hidden.delete(appName);
1128
-
1129
- // Now do a full unmount
1130
- await this.unmount(appName, { force: true });
1131
- }
1132
-
1133
- /**
1134
- * Check if an app is in keep-alive (hidden) state.
1135
- * @param {string} appName
1136
- * @returns {boolean}
1137
- */
1138
- isHidden(appName) {
1139
- return this.hidden.has(appName);
1140
- }
1141
-
1142
- /**
1143
- * Cargar componente compartido (para imports/exports)
1144
- * @param {string} componentPath - Ruta del componente (ej: "shared.Button")
1145
- */
1146
- async use(componentPath) {
1147
- const [appName, componentName] = componentPath.split('.');
1148
-
1149
- if (!appName || !componentName) {
1150
- throw new Error(`Invalid component path: ${componentPath}. Use format "app.component"`);
1151
- }
1152
-
1153
- const app = this.apps.get(appName);
1154
- if (!app) {
1155
- throw new Error(`App ${appName} not registered`);
1156
- }
1157
-
1158
- const manifest = this.manifests.get(appName);
1159
- const exportPath = manifest?.wu?.exports?.[componentName];
1160
-
1161
- if (!exportPath) {
1162
- throw new Error(`Component ${componentName} not exported by ${appName}`);
1163
- }
1164
-
1165
- // Cargar componente
1166
- return await this.loader.loadComponent(app.url, exportPath);
1167
- }
1168
-
1169
- /**
1170
- * Obtener informacion de una app
1171
- * @param {string} appName - Nombre de la app
1172
- */
1173
- getAppInfo(appName) {
1174
- return {
1175
- registered: this.apps.get(appName),
1176
- manifest: this.manifests.get(appName),
1177
- mounted: this.mounted.get(appName),
1178
- definition: this.definitions.get(appName)
1179
- };
1180
- }
1181
-
1182
- /**
1183
- * Obtener estadisticas del framework
1184
- */
1185
- getStats() {
1186
- return {
1187
- registered: this.apps.size,
1188
- defined: this.definitions.size,
1189
- mounted: this.mounted.size,
1190
- hidden: this.hidden.size,
1191
- apps: Array.from(this.apps.keys())
1192
- };
1193
- }
1194
-
1195
- /**
1196
- * Store methods: Convenience methods for state management
1197
- */
1198
-
1199
- /**
1200
- * Get value from global store
1201
- * @param {string} path - Dot notation path
1202
- * @returns {*} Value at path
1203
- */
1204
- getState(path) {
1205
- return this.store.get(path);
1206
- }
1207
-
1208
- /**
1209
- * Set value in global store
1210
- * @param {string} path - Dot notation path
1211
- * @param {*} value - Value to set
1212
- * @returns {number} Sequence number
1213
- */
1214
- setState(path, value) {
1215
- return this.store.set(path, value);
1216
- }
1217
-
1218
- /**
1219
- * Subscribe to state changes
1220
- * @param {string} pattern - Path or pattern
1221
- * @param {Function} callback - Callback function
1222
- * @returns {Function} Unsubscribe function
1223
- */
1224
- onStateChange(pattern, callback) {
1225
- return this.store.on(pattern, callback);
1226
- }
1227
-
1228
- /**
1229
- * Batch set multiple state values
1230
- * @param {Object} updates - Object with path:value pairs
1231
- * @returns {Array} Sequence numbers
1232
- */
1233
- batchState(updates) {
1234
- return this.store.batch(updates);
1235
- }
1236
-
1237
- /**
1238
- * Get store metrics
1239
- * @returns {Object} Performance metrics
1240
- */
1241
- getStoreMetrics() {
1242
- return this.store.getMetrics();
1243
- }
1244
-
1245
- /**
1246
- * Clear all state
1247
- */
1248
- clearState() {
1249
- this.store.clear();
1250
- }
1251
-
1252
- /**
1253
- * Set a URL override for an app (QA/testing).
1254
- * Sets a cookie so the override persists across page reloads.
1255
- * Only affects the current browser — no one else sees it.
1256
- *
1257
- * @param {string} appName - App to override
1258
- * @param {string} url - Override URL (e.g., 'http://localhost:5173')
1259
- * @param {Object} [options]
1260
- * @param {number} [options.maxAge=86400] - Cookie lifetime in seconds (default: 24h)
1261
- *
1262
- * @example
1263
- * wu.override('cart', 'http://localhost:5173');
1264
- * wu.override('header', 'https://preview-abc123.vercel.app');
1265
- */
1266
- override(appName, url, options) {
1267
- this.overrides.set(appName, url, options);
1268
- }
1269
-
1270
- /**
1271
- * Remove URL override for an app.
1272
- * @param {string} appName
1273
- */
1274
- removeOverride(appName) {
1275
- this.overrides.remove(appName);
1276
- }
1277
-
1278
- /**
1279
- * Get all active overrides.
1280
- * @returns {Object} { appName: url, ... }
1281
- */
1282
- getOverrides() {
1283
- return this.overrides.getAll();
1284
- }
1285
-
1286
- /**
1287
- * Remove all overrides.
1288
- */
1289
- clearOverrides() {
1290
- this.overrides.clearAll();
1291
- }
1292
-
1293
- /**
1294
- * Prefetch one or more apps before they're needed.
1295
- *
1296
- * Uses Speculation Rules API (Chrome 121+), falls back to
1297
- * <link rel="modulepreload"> or <link rel="prefetch">.
1298
- *
1299
- * @param {string|string[]} appNames - App name(s) to prefetch
1300
- * @param {Object} [options]
1301
- * @param {'immediate'|'hover'|'visible'|'idle'} [options.on='immediate'] - When to trigger
1302
- * @param {string|Element} [options.target] - Element for hover/visible triggers
1303
- * @param {'conservative'|'moderate'|'eager'} [options.eagerness='moderate'] - Speculation eagerness
1304
- * @returns {Promise<void>|Function} Promise or cleanup function
1305
- *
1306
- * @example
1307
- * wu.prefetch('cart');
1308
- * wu.prefetch('cart', { on: 'hover', target: '#cart-link' });
1309
- * wu.prefetch('cart', { on: 'visible', target: '#cart-section' });
1310
- * wu.prefetch(['profile', 'settings'], { on: 'idle' });
1311
- */
1312
- prefetch(appNames, options) {
1313
- return this.prefetcher.prefetch(appNames, options);
1314
- }
1315
-
1316
- /**
1317
- * Prefetch all registered but not-yet-mounted apps.
1318
- * @param {Object} [options] - Same options as prefetch()
1319
- */
1320
- prefetchAll(options) {
1321
- return this.prefetcher.prefetchAll(options);
1322
- }
1323
-
1324
- /**
1325
- * Create WuApp instance for declarative usage
1326
- * @param {string} name - App name
1327
- * @param {Object} config - Configuration { url, container, autoInit }
1328
- * @returns {WuApp} WuApp instance
1329
- */
1330
- app(name, config) {
1331
- return new WuApp(name, config, this);
1332
- }
1333
-
1334
- /**
1335
- * Limpiar todo el framework
1336
- */
1337
- async destroy() {
1338
- logger.wuDebug('Destroying framework...');
1339
-
1340
- try {
1341
- // Execute beforeDestroy hooks
1342
- await this.hooks.execute('beforeDestroy', {});
1343
-
1344
- // Call plugin onDestroy hooks
1345
- await this.pluginSystem.callHook('onDestroy', {});
1346
-
1347
- // Cancel all pending deferred unmounts
1348
- for (const timer of this._pendingUnmounts.values()) {
1349
- clearTimeout(timer);
1350
- }
1351
- this._pendingUnmounts.clear();
1352
- this._mountingPromises.clear();
1353
-
1354
- // Force-destroy all hidden (keep-alive) apps first
1355
- for (const appName of [...this.hidden.keys()]) {
1356
- await this._destroyHidden(appName);
1357
- }
1358
-
1359
- // Desmontar todas las apps
1360
- for (const appName of [...this.mounted.keys()]) {
1361
- await this.unmount(appName, { force: true });
1362
- }
1363
-
1364
- // Limpiar sistemas esenciales
1365
- this.cache.clear();
1366
- this.eventBus.removeAll();
1367
- this.eventBus.clearHistory();
1368
- this.performance.clearMetrics();
1369
-
1370
- // Limpiar advanced systems
1371
- this.pluginSystem.cleanup();
1372
- this.strategies.cleanup();
1373
- this.errorBoundary.cleanup();
1374
- this.hooks.cleanup();
1375
- this.prefetcher.cleanup();
1376
-
1377
- // Limpiar registros
1378
- this.apps.clear();
1379
- this.definitions.clear();
1380
- this.manifests.clear();
1381
- this.mounted.clear();
1382
- this.hidden.clear();
1383
-
1384
- // Limpiar store
1385
- this.store.clear();
1386
-
1387
- this.isInitialized = false;
1388
-
1389
- // Execute afterDestroy hooks
1390
- await this.hooks.execute('afterDestroy', {});
1391
-
1392
- logger.wuDebug('Framework destroyed');
1393
- } catch (error) {
1394
- logger.wuError('Error during destroy:', error);
1395
- throw error;
1396
- }
1397
- }
1398
- }
1
+ /**
2
+ * WU-FRAMEWORK: UNIVERSAL MICROFRONTENDS
3
+ * Motor principal agnostico - Funciona con cualquier framework
4
+ */
5
+
6
+ import { WuLoader } from './wu-loader.js';
7
+ import { WuSandbox } from './wu-sandbox.js';
8
+ import { WuManifest } from './wu-manifest.js';
9
+ import { logger } from './wu-logger.js';
10
+ import { default as store } from './wu-store.js';
11
+ import { WuApp } from './wu-app.js';
12
+ import { WuCache } from './wu-cache.js';
13
+ import { WuEventBus } from './wu-event-bus.js';
14
+ import { WuPerformance } from './wu-performance.js';
15
+ import { WuPluginSystem } from './wu-plugin.js';
16
+ import { WuLoadingStrategy } from './wu-strategies.js';
17
+ import { WuErrorBoundary } from './wu-error-boundary.js';
18
+ import { WuLifecycleHooks } from './wu-hooks.js';
19
+ import { WuHtmlParser } from './wu-html-parser.js';
20
+ import { WuScriptExecutor } from './wu-script-executor.js';
21
+ import { WuIframeSandbox } from './wu-iframe-sandbox.js';
22
+ import { WuPrefetch } from './wu-prefetch.js';
23
+ import { WuOverrides } from './wu-overrides.js';
24
+
25
+ export class WuCore {
26
+ constructor(options = {}) {
27
+ // Registros principales
28
+ this.apps = new Map(); // Apps registradas
29
+ this.definitions = new Map(); // Definiciones de lifecycle
30
+ this.manifests = new Map(); // Manifiestos cargados
31
+ this.mounted = new Map(); // Apps montadas
32
+ this.hidden = new Map(); // Keep-alive hidden apps
33
+ this._pendingUnmounts = new Map(); // Deferred unmount timers (StrictMode compat)
34
+ this._mountingPromises = new Map(); // In-flight mount dedup
35
+
36
+ // Componentes core
37
+ this.loader = new WuLoader();
38
+ this.sandbox = new WuSandbox();
39
+ this.manifest = new WuManifest();
40
+ this.store = store;
41
+
42
+ // Strict sandbox support: HTML entry + script execution in proxy
43
+ this.htmlParser = new WuHtmlParser();
44
+ this.scriptExecutor = new WuScriptExecutor();
45
+
46
+ // Sistemas esenciales
47
+ this.cache = new WuCache({ storage: 'localStorage', maxSize: 100 }); // 100MB cache
48
+ this.eventBus = new WuEventBus();
49
+ this.performance = new WuPerformance();
50
+
51
+ // Advanced systems
52
+ this.pluginSystem = new WuPluginSystem(this);
53
+ this.strategies = new WuLoadingStrategy(this);
54
+ this.errorBoundary = new WuErrorBoundary(this);
55
+ this.hooks = new WuLifecycleHooks(this);
56
+ this.prefetcher = new WuPrefetch(this);
57
+ this.overrides = new WuOverrides();
58
+
59
+ // Estado
60
+ this.isInitialized = false;
61
+
62
+ logger.wuInfo('Wu Framework initialized - Universal Microfrontends');
63
+ }
64
+
65
+ /**
66
+ * Inicializar wu-framework con configuracion de apps
67
+ * @param {Object} config - Configuracion { apps: [{name, url}, ...] }
68
+ */
69
+ async init(config) {
70
+ if (this.isInitialized) {
71
+ logger.wuWarn('Framework already initialized');
72
+ return;
73
+ }
74
+
75
+ // Global sandbox mode: 'module' (default) or 'strict'
76
+ this._sandboxMode = config.sandbox || 'module';
77
+
78
+ logger.wuDebug(`Initializing (sandbox: ${this._sandboxMode}) with apps:`, config.apps?.map(app => app.name));
79
+
80
+ try {
81
+ // Execute beforeInit hooks
82
+ const beforeInitResult = await this.hooks.execute('beforeInit', { config });
83
+ if (beforeInitResult.cancelled) {
84
+ logger.wuWarn('Initialization cancelled by beforeInit hook');
85
+ return;
86
+ }
87
+
88
+ // Call plugin beforeInit hooks
89
+ await this.pluginSystem.callHook('beforeInit', { config });
90
+
91
+ // Configure and apply cookie overrides (QA/testing: wu-override:<app>=<url>)
92
+ if (config.overrides) {
93
+ this.overrides.configure(config.overrides);
94
+ }
95
+ const apps = config.apps || [];
96
+ this.overrides.refresh();
97
+ this.overrides.applyToApps(apps);
98
+
99
+ // Registrar todas las apps
100
+ for (const appConfig of apps) {
101
+ await this.registerApp(appConfig);
102
+ }
103
+
104
+ // Preload apps with eager/preload strategies
105
+ await this.strategies.preload(config.apps || []);
106
+
107
+ this.isInitialized = true;
108
+
109
+ // Execute afterInit hooks
110
+ await this.hooks.execute('afterInit', { config });
111
+
112
+ // Call plugin afterInit hooks
113
+ await this.pluginSystem.callHook('afterInit', { config });
114
+
115
+ logger.wuInfo('Framework initialized successfully');
116
+ } catch (error) {
117
+ logger.wuError('Initialization failed:', error);
118
+
119
+ // Call plugin error hooks
120
+ await this.pluginSystem.callHook('onError', { phase: 'init', error });
121
+
122
+ throw error;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Registrar una aplicacion
128
+ * @param {Object} appConfig - { name, url, keepAlive, sandbox, container, ... }
129
+ */
130
+ async registerApp(appConfig) {
131
+ const { name, url } = appConfig;
132
+
133
+ try {
134
+ logger.wuDebug(`Registering app: ${name} from ${url}`);
135
+
136
+ // Cargar manifest
137
+ const manifestData = await this.manifest.load(url);
138
+ this.manifests.set(name, manifestData);
139
+
140
+ // Registrar la app — preserve all config fields (keepAlive, sandbox, container, etc.)
141
+ this.apps.set(name, {
142
+ ...appConfig,
143
+ manifest: manifestData,
144
+ status: 'registered'
145
+ });
146
+
147
+ logger.wuDebug(`App ${name} registered successfully`);
148
+ } catch (error) {
149
+ logger.wuError(`Failed to register app ${name}:`, error);
150
+ throw error;
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Definir lifecycle de una micro-app
156
+ * @param {string} appName - Nombre de la app
157
+ * @param {Object} lifecycle - { mount, unmount }
158
+ */
159
+ define(appName, lifecycle) {
160
+ if (!lifecycle.mount) {
161
+ throw new Error(`[Wu] Mount function required for app: ${appName}`);
162
+ }
163
+
164
+ this.definitions.set(appName, lifecycle);
165
+
166
+ // Dispatch custom event for external listeners
167
+ const event = new CustomEvent('wu:app:ready', {
168
+ detail: { appName, timestamp: Date.now() }
169
+ });
170
+ window.dispatchEvent(event);
171
+
172
+ logger.wuDebug(`Lifecycle defined for: ${appName}`);
173
+ }
174
+
175
+ /**
176
+ * Mount app with multi-retry mounting and recovery.
177
+ * If the app is in keep-alive (hidden) state, shows it instantly.
178
+ *
179
+ * @param {string} appName - Nombre de la app
180
+ * @param {string} containerSelector - Selector del contenedor
181
+ */
182
+ async mount(appName, containerSelector) {
183
+ // ── StrictMode guard: cancel pending deferred unmount ──
184
+ // React StrictMode cycle: effect(mount) → cleanup(unmount) → effect(mount)
185
+ // The cleanup fires between two mounts. By deferring the actual unmount,
186
+ // the second mount cancels it and the app stays alive — zero flicker.
187
+ if (this._pendingUnmounts.has(appName)) {
188
+ clearTimeout(this._pendingUnmounts.get(appName));
189
+ this._pendingUnmounts.delete(appName);
190
+ logger.wuDebug(`${appName} deferred unmount cancelled by remount`);
191
+ }
192
+
193
+ // Already mounted in same container → no-op
194
+ if (this.mounted.has(appName)) {
195
+ const existing = this.mounted.get(appName);
196
+ if (existing.containerSelector === containerSelector) {
197
+ logger.wuDebug(`${appName} already mounted in ${containerSelector}`);
198
+ return;
199
+ }
200
+ }
201
+
202
+ // Deduplicate concurrent mounts (StrictMode fires effect twice)
203
+ if (this._mountingPromises.has(appName)) {
204
+ logger.wuDebug(`${appName} mount already in progress, deduplicating`);
205
+ return await this._mountingPromises.get(appName);
206
+ }
207
+
208
+ // Check if app is in keep-alive (hidden) state
209
+ const hiddenEntry = this.hidden.get(appName);
210
+ if (hiddenEntry) {
211
+ if (hiddenEntry.containerSelector === containerSelector) {
212
+ // Same container → instant show (no reload)
213
+ return await this.show(appName);
214
+ }
215
+ // Different container → destroy hidden state, remount normally
216
+ await this._destroyHidden(appName);
217
+ }
218
+
219
+ // Track mount promise for deduplication
220
+ const mountPromise = this.mountWithRecovery(appName, containerSelector, 0);
221
+ this._mountingPromises.set(appName, mountPromise);
222
+
223
+ try {
224
+ return await mountPromise;
225
+ } finally {
226
+ this._mountingPromises.delete(appName);
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Mount with recovery: self-healing app mounting
232
+ */
233
+ async mountWithRecovery(appName, containerSelector, attempt = 0) {
234
+ const maxAttempts = 3;
235
+
236
+ try {
237
+ // Start performance measurement
238
+ this.performance.startMeasure('mount', appName);
239
+
240
+ logger.wuDebug(`Mounting ${appName} in ${containerSelector} (attempt ${attempt + 1})`);
241
+
242
+ // Execute beforeLoad hooks
243
+ const beforeLoadResult = await this.hooks.execute('beforeLoad', { appName, containerSelector, attempt });
244
+ if (beforeLoadResult.cancelled) {
245
+ logger.wuWarn('Mount cancelled by beforeLoad hook');
246
+ return;
247
+ }
248
+
249
+ // Call plugin beforeMount hooks
250
+ const pluginBeforeMount = await this.pluginSystem.callHook('beforeMount', { appName, containerSelector });
251
+ if (pluginBeforeMount === false) {
252
+ logger.wuWarn('Mount cancelled by plugin beforeMount hook');
253
+ return;
254
+ }
255
+
256
+ // Verify app is registered
257
+ const app = this.apps.get(appName);
258
+ if (!app) {
259
+ throw new Error(`App ${appName} not registered. Call wu.init() first.`);
260
+ }
261
+
262
+ // Container reality check
263
+ const container = document.querySelector(containerSelector);
264
+ if (!container) {
265
+ throw new Error(`Container not found: ${containerSelector}`);
266
+ }
267
+
268
+ // Create sandbox - pasar manifest con styleMode y URL de la app
269
+ const sandbox = this.sandbox.create(appName, container, {
270
+ manifest: app.manifest,
271
+ styleMode: app.manifest?.styleMode,
272
+ appUrl: app.url // Pasar URL de la app para filtrar estilos de apps fully-isolated
273
+ });
274
+
275
+ // Execute afterLoad hooks
276
+ await this.hooks.execute('afterLoad', { appName, containerSelector, sandbox });
277
+
278
+ // Resolve lifecycle definition
279
+ let lifecycle = this.definitions.get(appName);
280
+ if (!lifecycle) {
281
+ // Load remote app
282
+ await this.loadAndMountRemoteApp(app, sandbox);
283
+ lifecycle = this.definitions.get(appName);
284
+
285
+ if (!lifecycle) {
286
+ throw new Error(`App ${appName} did not register with wu.define()`);
287
+ }
288
+ }
289
+
290
+ // Execute beforeMount hooks
291
+ const beforeMountResult = await this.hooks.execute('beforeMount', { appName, containerSelector, sandbox, lifecycle });
292
+ if (beforeMountResult.cancelled) {
293
+ logger.wuWarn('Mount cancelled by beforeMount hook');
294
+ return;
295
+ }
296
+
297
+ // Wait for styles to be ready before mounting
298
+ if (sandbox.stylesReady) {
299
+ logger.wuDebug(`Waiting for styles to be ready for ${appName}...`);
300
+ await sandbox.stylesReady;
301
+ logger.wuDebug(`Styles ready for ${appName}`);
302
+ }
303
+
304
+ // Execute mount lifecycle
305
+ await lifecycle.mount(sandbox.container);
306
+
307
+ // Register mounted app
308
+ this.mounted.set(appName, {
309
+ app,
310
+ sandbox,
311
+ lifecycle,
312
+ container: sandbox.container,
313
+ hostContainer: container,
314
+ containerSelector,
315
+ timestamp: Date.now(),
316
+ state: 'stable'
317
+ });
318
+
319
+ // End performance measurement
320
+ const mountTime = this.performance.endMeasure('mount', appName);
321
+
322
+ // Execute afterMount hooks
323
+ await this.hooks.execute('afterMount', { appName, containerSelector, sandbox, mountTime });
324
+
325
+ // Call plugin afterMount hooks
326
+ await this.pluginSystem.callHook('afterMount', { appName, containerSelector, mountTime });
327
+
328
+ // Emit mount event
329
+ this.eventBus.emit('app:mounted', { appName, mountTime, attempt }, { appName });
330
+
331
+ logger.wuInfo(`${appName} mounted successfully in ${mountTime.toFixed(2)}ms`);
332
+
333
+ } catch (error) {
334
+ logger.wuError(`Mount attempt ${attempt + 1} failed for ${appName}:`, error);
335
+
336
+ // Cleanup sandbox to prevent orphaned shadow DOMs
337
+ try {
338
+ if (this.sandbox && this.sandbox.sandboxes && this.sandbox.sandboxes.has(appName)) {
339
+ const sb = this.sandbox.sandboxes.get(appName);
340
+ if (sb && sb.proxySandbox) {
341
+ sb.proxySandbox.deactivate();
342
+ }
343
+ this.sandbox.sandboxes.delete(appName);
344
+ logger.wuDebug(`Sandbox cleaned up after mount failure for ${appName}`);
345
+ }
346
+ } catch (cleanupError) {
347
+ logger.wuWarn(`Sandbox cleanup failed for ${appName}:`, cleanupError);
348
+ }
349
+
350
+ // Use error boundary for intelligent error handling
351
+ const errorResult = await this.errorBoundary.handle(error, {
352
+ appName,
353
+ containerSelector,
354
+ retryCount: attempt,
355
+ container: containerSelector
356
+ });
357
+
358
+ // Si el error boundary recupero el error, no necesitamos reintentar
359
+ if (errorResult.recovered) {
360
+ logger.wuDebug('Error recovered by error boundary');
361
+ return;
362
+ }
363
+
364
+ // Recovery protocol
365
+ if (attempt < maxAttempts - 1 && errorResult.action === 'retry') {
366
+ logger.wuDebug('Initiating recovery protocol...');
367
+
368
+ // Clean app state
369
+ await this.appStateCleanup(appName, containerSelector);
370
+
371
+ // Temporal stabilization
372
+ await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
373
+
374
+ // Recursive mounting with recovery
375
+ return await this.mountWithRecovery(appName, containerSelector, attempt + 1);
376
+ }
377
+
378
+ // Call plugin error hooks
379
+ await this.pluginSystem.callHook('onError', { phase: 'mount', error, appName });
380
+
381
+ // Final mount failure - error boundary already handled fallback UI
382
+ throw error;
383
+ }
384
+ }
385
+
386
+ /**
387
+ * App state cleanup: Enhanced container cleanup with framework protection
388
+ */
389
+ async appStateCleanup(appName, containerSelector) {
390
+ try {
391
+ logger.wuDebug(`Starting app state cleanup for ${appName}...`);
392
+
393
+ // Clear hidden (keep-alive) state if present
394
+ if (this.hidden.has(appName)) {
395
+ try {
396
+ await this._destroyHidden(appName);
397
+ } catch (hiddenError) {
398
+ logger.wuWarn('Hidden app cleanup failed:', hiddenError);
399
+ }
400
+ }
401
+
402
+ // Clear any existing mounted state safely
403
+ if (this.mounted.has(appName)) {
404
+ try {
405
+ await this.unmount(appName, { force: true });
406
+ } catch (unmountError) {
407
+ logger.wuWarn('Unmount failed during cleanup:', unmountError);
408
+ }
409
+ }
410
+
411
+ // Enhanced container cleanup with Vue safety measures
412
+ const container = document.querySelector(containerSelector);
413
+ if (container) {
414
+ // Protect Vue's reactivity system
415
+ if (container.shadowRoot) {
416
+ try {
417
+ // Clear shadow root content safely
418
+ const shadowChildren = Array.from(container.shadowRoot.children);
419
+ shadowChildren.forEach(child => {
420
+ try {
421
+ child.remove();
422
+ } catch (removeError) {
423
+ logger.wuWarn('Failed to remove shadow child:', removeError);
424
+ }
425
+ });
426
+ } catch (shadowError) {
427
+ logger.wuWarn('Shadow root cleanup failed:', shadowError);
428
+ }
429
+ }
430
+
431
+ // Clear any direct children if no shadow root
432
+ if (!container.shadowRoot && container.children.length > 0) {
433
+ try {
434
+ container.innerHTML = '';
435
+ } catch (htmlError) {
436
+ logger.wuWarn('Container innerHTML cleanup failed:', htmlError);
437
+ }
438
+ }
439
+
440
+ // Reset container attributes
441
+ container.removeAttribute('data-wu-app');
442
+ container.removeAttribute('data-quantum-state');
443
+ container.removeAttribute('wu-debug');
444
+ }
445
+
446
+ // Reset definition state
447
+ this.definitions.delete(appName);
448
+
449
+ // Clear sandbox registry
450
+ if (this.sandbox && this.sandbox.sandboxes) {
451
+ this.sandbox.sandboxes.delete(appName);
452
+ }
453
+
454
+ logger.wuDebug(`App state cleaned successfully for ${appName}`);
455
+
456
+ } catch (cleanupError) {
457
+ logger.wuWarn(`App cleanup partial failure for ${appName}:`, cleanupError);
458
+
459
+ // Emergency cleanup - force clear everything
460
+ try {
461
+ const container = document.querySelector(containerSelector);
462
+ if (container) {
463
+ container.style.display = 'none';
464
+ setTimeout(() => {
465
+ if (container) {
466
+ container.style.display = '';
467
+ }
468
+ }, 100);
469
+ }
470
+ } catch (emergencyError) {
471
+ logger.wuError('Emergency cleanup failed:', emergencyError);
472
+ }
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Remote app loader: Load app in the configured sandbox mode.
478
+ *
479
+ * Three modes:
480
+ *
481
+ * - module (default): ES6 import() + patchWindow for side-effect tracking.
482
+ * Works with Vite, HMR, ES modules. App code runs in global scope.
483
+ * Proxy is a cleanup tracker, not an isolation boundary.
484
+ *
485
+ * - strict: Hidden iframe + real import(). True JS isolation.
486
+ * App code runs in iframe's window (separate global context).
487
+ * Document operations proxied to Shadow DOM.
488
+ * Preserves: tree shaking, source maps, HMR.
489
+ * Falls back to eval mode if import() fails (CORS, etc.)
490
+ *
491
+ * - eval: Fetch HTML → parse scripts → execute with(proxy).
492
+ * Maximum JS isolation via with(proxy) statement.
493
+ * Requires bundled apps (UMD/IIFE), not ES modules.
494
+ * No tree shaking, no source maps, no HMR.
495
+ *
496
+ * Set per-app: { name: 'app', url: '...', sandbox: 'strict' }
497
+ * Or globally: wu.init({ sandbox: 'strict', apps: [...] })
498
+ */
499
+ async loadAndMountRemoteApp(app, sandbox) {
500
+ const mode = app.sandbox || this._sandboxMode || 'module';
501
+
502
+ if (mode === 'strict') {
503
+ await this._loadStrict(app, sandbox);
504
+ } else if (mode === 'eval') {
505
+ await this._loadEval(app, sandbox);
506
+ } else {
507
+ await this._loadModule(app, sandbox);
508
+ }
509
+ }
510
+
511
+ /**
512
+ * MODULE MODE: import() + patchWindow (default).
513
+ * Side effects tracked during load, cleaned on unmount.
514
+ * App code runs in global scope.
515
+ */
516
+ async _loadModule(app, sandbox) {
517
+ const moduleUrl = await this.resolveModulePath(app);
518
+ logger.wuDebug(`[module] Loading ES module: ${moduleUrl}`);
519
+
520
+ const jsSandbox = sandbox.jsSandbox;
521
+ if (jsSandbox?.patchWindow) {
522
+ jsSandbox.patchWindow();
523
+ }
524
+
525
+ try {
526
+ await this.moduleLoader(moduleUrl, app.name);
527
+ logger.wuDebug(`[module] ES module loaded: ${app.name}`);
528
+ } catch (error) {
529
+ logger.wuError(`[module] Failed to load ${moduleUrl}:`, error);
530
+ throw error;
531
+ } finally {
532
+ if (jsSandbox?.unpatchWindow) {
533
+ jsSandbox.unpatchWindow();
534
+ }
535
+ }
536
+ }
537
+
538
+ /**
539
+ * STRICT MODE: Hidden iframe + real import().
540
+ *
541
+ * The iframe provides a separate window context. import() inside the iframe
542
+ * is a real ES module import — tree shaking, source maps, and HMR all work.
543
+ *
544
+ * Pipeline:
545
+ * 1. Create hidden iframe with <base href="appUrl">
546
+ * 2. Patch iframe's document → DOM operations go to Shadow DOM
547
+ * 3. import() the app module inside iframe
548
+ * 4. Wait for wu.define() registration
549
+ *
550
+ * If import() fails (CORS, network, etc.), falls back to eval mode
551
+ * with a console warning explaining why.
552
+ */
553
+ async _loadStrict(app, sandbox) {
554
+ logger.wuDebug(`[strict] Loading ${app.name} via iframe sandbox`);
555
+
556
+ // Create and activate iframe sandbox
557
+ const iframeSandbox = new WuIframeSandbox(app.name);
558
+ iframeSandbox.activate(app.url, sandbox.container, sandbox.shadowRoot);
559
+ sandbox.iframeSandbox = iframeSandbox;
560
+
561
+ try {
562
+ // Resolve module path (same logic as module mode)
563
+ const moduleUrl = await this.resolveModulePath(app);
564
+ logger.wuDebug(`[strict] Importing module in iframe: ${moduleUrl}`);
565
+
566
+ // Import module inside iframe — real import()!
567
+ await iframeSandbox.importModule(moduleUrl);
568
+ logger.wuDebug(`[strict] Module imported for ${app.name}`);
569
+
570
+ } catch (importError) {
571
+ // import() failed — likely CORS or module error.
572
+ // Fall back to eval mode (fetch + parse + with(proxy)).
573
+ logger.wuWarn(
574
+ `[strict] iframe import failed for ${app.name}: ${importError.message}\n` +
575
+ `Falling back to eval mode (fetch + parse + execute with proxy).\n` +
576
+ `To fix: ensure the app's dev server sets Access-Control-Allow-Origin: * headers,\n` +
577
+ `or use sandbox: 'eval' explicitly for UMD/IIFE bundles.`
578
+ );
579
+
580
+ // Destroy failed iframe
581
+ iframeSandbox.destroy();
582
+ sandbox.iframeSandbox = null;
583
+
584
+ // Fallback to eval mode
585
+ await this._loadEval(app, sandbox);
586
+ return;
587
+ }
588
+
589
+ // Wait for wu.define()
590
+ await this._waitForDefine(app.name, 'strict');
591
+
592
+ logger.wuDebug(`[strict] ${app.name} loaded and registered via iframe`);
593
+ }
594
+
595
+ /**
596
+ * EVAL MODE: Fetch HTML → parse → execute scripts inside proxy.
597
+ *
598
+ * Maximum JS isolation via with(proxy) statement all unqualified
599
+ * identifiers (setTimeout, document, fetch) go through proxy traps.
600
+ *
601
+ * Requires bundled apps (UMD/IIFE). ES modules cannot be eval'd.
602
+ * No tree shaking, no source maps, no HMR.
603
+ *
604
+ * Pipeline:
605
+ * 1. Fetch HTML from app URL
606
+ * 2. Parse: extract scripts (inline + external), styles, clean DOM
607
+ * 3. Inject DOM + styles into Shadow DOM
608
+ * 4. Execute all scripts inside the proxy via WuScriptExecutor
609
+ * 5. Wait for wu.define()
610
+ */
611
+ async _loadEval(app, sandbox) {
612
+ logger.wuDebug(`[eval] Loading ${app.name} from ${app.url}`);
613
+
614
+ const jsSandbox = sandbox.jsSandbox;
615
+ const proxy = jsSandbox.getProxy();
616
+
617
+ if (!proxy) {
618
+ throw new Error(`[eval] No active proxy for ${app.name}. Sandbox must be activated first.`);
619
+ }
620
+
621
+ // 1. Fetch and parse HTML
622
+ const parsed = await this.htmlParser.fetchAndParse(app.url, app.name);
623
+
624
+ // 2. Inject clean DOM into container
625
+ if (parsed.dom) {
626
+ sandbox.container.innerHTML = parsed.dom;
627
+ }
628
+
629
+ // 3. Inject styles into shadow root
630
+ const styleTarget = sandbox.shadowRoot || sandbox.container;
631
+
632
+ for (const cssText of parsed.styles.inline) {
633
+ const style = document.createElement('style');
634
+ style.textContent = cssText;
635
+ styleTarget.appendChild(style);
636
+ }
637
+
638
+ for (const href of parsed.styles.external) {
639
+ const link = document.createElement('link');
640
+ link.rel = 'stylesheet';
641
+ link.href = href;
642
+ styleTarget.appendChild(link);
643
+ }
644
+
645
+ // 4. Build and execute scripts inside the proxy
646
+ const scripts = [];
647
+ for (const content of parsed.scripts.inline) {
648
+ scripts.push({ content });
649
+ }
650
+ for (const src of parsed.scripts.external) {
651
+ scripts.push({ src });
652
+ }
653
+
654
+ await this.scriptExecutor.executeAll(scripts, app.name, proxy);
655
+ logger.wuDebug(`[eval] Scripts executed for ${app.name}`);
656
+
657
+ // 5. Wait for wu.define()
658
+ await this._waitForDefine(app.name, 'eval');
659
+
660
+ logger.wuDebug(`[eval] ${app.name} loaded and registered`);
661
+ }
662
+
663
+ /**
664
+ * Wait for an app to call wu.define() with a timeout.
665
+ * Shared by strict and eval modes.
666
+ */
667
+ async _waitForDefine(appName, mode) {
668
+ const maxWaitTime = 10000;
669
+ const checkInterval = 50;
670
+ const startTime = Date.now();
671
+
672
+ while (!this.definitions.has(appName)) {
673
+ if (Date.now() - startTime >= maxWaitTime) {
674
+ throw new Error(
675
+ `[${mode}] App '${appName}' loaded but wu.define() was not called within ${maxWaitTime}ms.\n` +
676
+ `Make sure your app calls: window.wu.define('${appName}', { mount, unmount })`
677
+ );
678
+ }
679
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
680
+ }
681
+ }
682
+
683
+ /**
684
+ * Module path resolver: Intelligent URL construction with fallback
685
+ * Intelligently resolves module paths with real-time validation
686
+ */
687
+ async resolveModulePath(app) {
688
+ const entryFile = app.manifest?.entry || 'main.js';
689
+ const baseUrl = app.url.replace(/\/$/, ''); // Remove trailing slash
690
+
691
+ // Normalize path: Remove duplicated directories
692
+ // If entry already starts with 'src/', 'dist/', etc., use it as-is
693
+ const hasFolderPrefix = /^(src|dist|public|build|assets|lib|es)\//.test(entryFile);
694
+
695
+ if (hasFolderPrefix) {
696
+ logger.wuDebug(`Entry already has folder prefix: ${entryFile}`);
697
+ // Entry already has folder, just use baseUrl + entryFile
698
+ const directPath = `${baseUrl}/${entryFile}`;
699
+ logger.wuDebug(`Using direct path: ${directPath}`);
700
+ return directPath;
701
+ }
702
+
703
+ // Multi-path candidates (in order of preference)
704
+ const pathCandidates = [
705
+ `${baseUrl}/src/${entryFile}`, // Standard structure
706
+ `${baseUrl}/${entryFile}`, // Root level
707
+ `${baseUrl}/dist/${entryFile}`, // Built version
708
+ `${baseUrl}/public/${entryFile}`, // Public folder
709
+ `${baseUrl}/build/${entryFile}`, // Build folder
710
+ `${baseUrl}/assets/${entryFile}`, // Assets folder
711
+ `${baseUrl}/lib/${entryFile}`, // Library folder
712
+ `${baseUrl}/es/${entryFile}` // ES modules folder
713
+ ];
714
+
715
+ logger.wuDebug(`Attempting path resolution for ${app.name}...`);
716
+
717
+ // Smart path discovery: Try each candidate with validation
718
+ for (let i = 0; i < pathCandidates.length; i++) {
719
+ const candidate = pathCandidates[i];
720
+
721
+ try {
722
+ logger.wuDebug(`Testing path candidate ${i + 1}/${pathCandidates.length}: ${candidate}`);
723
+
724
+ // Path validation with enhanced verification
725
+ const isValid = await this.validatePath(candidate);
726
+
727
+ if (isValid) {
728
+ logger.wuDebug(`Path resolved successfully: ${candidate}`);
729
+ return candidate;
730
+ } else {
731
+ logger.wuDebug(`Path candidate ${i + 1} failed validation: ${candidate}`);
732
+ }
733
+
734
+ } catch (error) {
735
+ logger.wuDebug(`Path candidate ${i + 1} threw error: ${candidate} - ${error.message}`);
736
+ continue;
737
+ }
738
+ }
739
+
740
+ // Fallback: If all candidates fail, use the first one and let the error bubble up
741
+ const fallbackPath = pathCandidates[0];
742
+ logger.wuWarn(`All path candidates failed, using fallback: ${fallbackPath}`);
743
+ return fallbackPath;
744
+ }
745
+
746
+ /**
747
+ * Path validator: Smart existence verification with module testing
748
+ * Validates if a path exists and can be loaded as an ES module
749
+ */
750
+ async validatePath(url) {
751
+ try {
752
+ // Enhanced validation: Try actual module import for reliable verification
753
+ logger.wuDebug(`Testing path: ${url}`);
754
+
755
+ // First, try a GET request to check if file exists and is accessible
756
+ const response = await fetch(url, {
757
+ method: 'GET',
758
+ cache: 'no-cache',
759
+ signal: AbortSignal.timeout(2000) // 2 second timeout
760
+ });
761
+
762
+ if (!response.ok) {
763
+ logger.wuDebug(`Path validation failed - HTTP ${response.status}: ${url}`);
764
+ return false;
765
+ }
766
+
767
+ // Check content type and file extension
768
+ const contentType = response.headers.get('content-type') || '';
769
+ const isJavaScript =
770
+ contentType.includes('javascript') ||
771
+ contentType.includes('module') ||
772
+ contentType.includes('text/plain') || // Some servers serve JS as plain text
773
+ url.endsWith('.js') ||
774
+ url.endsWith('.mjs');
775
+
776
+ if (!isJavaScript) {
777
+ logger.wuDebug(`Path validation failed - Invalid content type '${contentType}': ${url}`);
778
+ return false;
779
+ }
780
+
781
+ // Final verification: Check if content looks like a valid module
782
+ const content = await response.text();
783
+
784
+ // Detect HTML fallback: Check if server returned HTML instead of JS
785
+ // Only check if content STARTS with HTML markers (trimmed), not if it contains them anywhere
786
+ // This avoids false positives for Angular/React bundles that contain template strings
787
+ const trimmedContent = content.trim().toLowerCase();
788
+ const isHtmlFallback =
789
+ trimmedContent.startsWith('<!doctype') ||
790
+ trimmedContent.startsWith('<html') ||
791
+ trimmedContent.startsWith('<head') ||
792
+ trimmedContent.startsWith('<body') ||
793
+ trimmedContent.startsWith('<!-');
794
+
795
+ if (isHtmlFallback) {
796
+ logger.wuDebug(`Path validation failed - Server returned HTML fallback page: ${url}`);
797
+ return false;
798
+ }
799
+
800
+ // Check for valid JavaScript module content
801
+ const hasModuleContent =
802
+ content.includes('export') ||
803
+ content.includes('import') ||
804
+ content.includes('wu.define') ||
805
+ content.includes('module.exports') ||
806
+ content.includes('console.log') ||
807
+ (content.includes('function') && content.length > 10);
808
+
809
+ if (!hasModuleContent) {
810
+ logger.wuDebug(`Path validation failed - No valid module content: ${url}`);
811
+ logger.wuDebug(`Content preview: ${content.substring(0, 100)}...`);
812
+ return false;
813
+ }
814
+
815
+ logger.wuDebug(`Path validation successful: ${url} (${content.length} chars)`);
816
+ return true;
817
+
818
+ } catch (error) {
819
+ // Network, timeout, or parsing error means path is invalid
820
+ logger.wuDebug(`Path validation failed for ${url}: ${error.message}`);
821
+ return false;
822
+ }
823
+ }
824
+
825
+ /**
826
+ * Module loader: Advanced registration patterns
827
+ * Handles asynchronous registration with timing synchronization
828
+ * Verifica que definitions tenga el lifecycle despues de cargar
829
+ */
830
+ async moduleLoader(moduleUrl, appName) {
831
+ // Check if already registered
832
+ if (this.definitions.has(appName)) {
833
+ logger.wuDebug(`App ${appName} already registered`);
834
+ return;
835
+ }
836
+
837
+ logger.wuDebug(`Using event-based registration for ${appName}`);
838
+
839
+ // Load module first
840
+ try {
841
+ await import(/* @vite-ignore */ moduleUrl);
842
+ } catch (loadError) {
843
+ logger.wuError(`Failed to import module ${moduleUrl}:`, loadError);
844
+ throw loadError;
845
+ }
846
+
847
+ // Wait for wu.define() to be called with real verification
848
+ const maxWaitTime = 10000; // 10 segundos
849
+ const checkInterval = 50; // Verificar cada 50ms
850
+ const startTime = Date.now();
851
+
852
+ while (!this.definitions.has(appName)) {
853
+ const elapsed = Date.now() - startTime;
854
+
855
+ if (elapsed >= maxWaitTime) {
856
+ throw new Error(
857
+ `App '${appName}' module loaded but wu.define() was not called within ${maxWaitTime}ms.\n\n` +
858
+ `Make sure your module calls:\n` +
859
+ ` wu.define('${appName}', { mount, unmount })\n\n` +
860
+ `Or using window.wu:\n` +
861
+ ` window.wu.define('${appName}', { mount, unmount })`
862
+ );
863
+ }
864
+
865
+ // Esperar un poco antes de verificar de nuevo
866
+ await new Promise(resolve => setTimeout(resolve, checkInterval));
867
+ }
868
+
869
+ logger.wuDebug(`App ${appName} loaded and registered (verified in definitions)`);
870
+ }
871
+
872
+ /**
873
+ * Desmontar una aplicacion.
874
+ *
875
+ * With keepAlive, the app is hidden instead of destroyed.
876
+ * All DOM, JS state, timers, and iframe are preserved.
877
+ * Re-mounting shows the app instantly.
878
+ *
879
+ * keepAlive is resolved from (in priority order):
880
+ * 1. options.keepAlive (per-call override)
881
+ * 2. app config keepAlive (set via wu.app() or registerApp)
882
+ * 3. false (default: destroy)
883
+ *
884
+ * Use options.force = true to destroy even if keepAlive is set.
885
+ *
886
+ * @param {string} appName - Nombre de la app
887
+ * @param {Object} [options] - Unmount options
888
+ * @param {boolean} [options.keepAlive] - Preserve state for instant re-mount
889
+ * @param {boolean} [options.force] - Force destroy even if keepAlive
890
+ */
891
+ async unmount(appName, options = {}) {
892
+ logger.wuDebug(`Unmounting ${appName}`);
893
+
894
+ const mounted = this.mounted.get(appName);
895
+ if (!mounted) {
896
+ // Check if it's hidden (keep-alive) — force destroy if requested
897
+ if (options.force && this.hidden.has(appName)) {
898
+ return await this._destroyHidden(appName);
899
+ }
900
+ logger.wuWarn(`App ${appName} not mounted`);
901
+ return;
902
+ }
903
+
904
+ // Resolve keepAlive: per-call > per-app config > default false
905
+ const keepAlive = options.force
906
+ ? false
907
+ : (options.keepAlive ?? mounted.app?.keepAlive ?? false);
908
+
909
+ if (keepAlive) {
910
+ return await this.hide(appName);
911
+ }
912
+
913
+ // Force → immediate unmount (no deferral)
914
+ if (options.force) {
915
+ return await this._executeUnmount(appName, mounted);
916
+ }
917
+
918
+ // ── Deferred unmount: 60ms window for React StrictMode ──
919
+ // StrictMode cycle: effect(mount) cleanup(unmount) → effect(mount)
920
+ // The cleanup fires between two mounts. By deferring the actual unmount,
921
+ // the second mount() call cancels the timer and the app stays alive.
922
+ if (this._pendingUnmounts.has(appName)) {
923
+ clearTimeout(this._pendingUnmounts.get(appName));
924
+ }
925
+
926
+ this._pendingUnmounts.set(appName, setTimeout(async () => {
927
+ this._pendingUnmounts.delete(appName);
928
+ // Re-verify: only unmount if the same mount entry is still current
929
+ if (this.mounted.has(appName) && this.mounted.get(appName) === mounted) {
930
+ try {
931
+ await this._executeUnmount(appName, mounted);
932
+ } catch (error) {
933
+ logger.wuError(`Deferred unmount failed for ${appName}:`, error);
934
+ }
935
+ }
936
+ }, 60));
937
+ }
938
+
939
+ /**
940
+ * Execute the actual unmount immediately (no deferral).
941
+ * Called by the deferred timer, force unmount, or destroy.
942
+ * @private
943
+ */
944
+ async _executeUnmount(appName, mounted) {
945
+ try {
946
+ // Execute beforeUnmount hooks
947
+ const beforeUnmountResult = await this.hooks.execute('beforeUnmount', { appName, mounted });
948
+ if (beforeUnmountResult.cancelled) {
949
+ logger.wuWarn('Unmount cancelled by beforeUnmount hook');
950
+ return;
951
+ }
952
+
953
+ // Call plugin beforeUnmount hooks
954
+ const pluginBeforeUnmount = await this.pluginSystem.callHook('beforeUnmount', { appName });
955
+ if (pluginBeforeUnmount === false) {
956
+ logger.wuWarn('Unmount cancelled by plugin beforeUnmount hook');
957
+ return;
958
+ }
959
+
960
+ // Ejecutar unmount del lifecycle si existe
961
+ if (mounted.lifecycle?.unmount) {
962
+ await mounted.lifecycle.unmount(mounted.container);
963
+ }
964
+
965
+ // Destroy iframe sandbox if present (strict mode)
966
+ if (mounted.sandbox.iframeSandbox) {
967
+ mounted.sandbox.iframeSandbox.destroy();
968
+ mounted.sandbox.iframeSandbox = null;
969
+ }
970
+
971
+ // Limpiar sandbox
972
+ this.sandbox.cleanup(mounted.sandbox);
973
+
974
+ // Remover del registro de montadas
975
+ this.mounted.delete(appName);
976
+
977
+ // Execute afterUnmount hooks
978
+ await this.hooks.execute('afterUnmount', { appName });
979
+
980
+ // Call plugin afterUnmount hooks
981
+ await this.pluginSystem.callHook('afterUnmount', { appName });
982
+
983
+ // Emit unmount event
984
+ this.eventBus.emit('app:unmounted', { appName }, { appName });
985
+
986
+ logger.wuDebug(`${appName} unmounted successfully`);
987
+ } catch (error) {
988
+ logger.wuError(`Failed to unmount ${appName}:`, error);
989
+
990
+ // Call plugin error hooks
991
+ await this.pluginSystem.callHook('onError', { phase: 'unmount', error, appName });
992
+
993
+ // Emit error event
994
+ this.eventBus.emit('app:error', { appName, error: error.message }, { appName });
995
+ throw error;
996
+ }
997
+ }
998
+
999
+ /**
1000
+ * Hide a mounted app (keep-alive).
1001
+ *
1002
+ * Preserves all state: DOM in Shadow DOM, JS in iframe, timers, listeners.
1003
+ * The app's optional `deactivate()` lifecycle hook is called.
1004
+ * Re-show with `show()` or `mount()` with the same container.
1005
+ *
1006
+ * @param {string} appName - App to hide
1007
+ */
1008
+ async hide(appName) {
1009
+ const mounted = this.mounted.get(appName);
1010
+ if (!mounted) {
1011
+ logger.wuWarn(`Cannot hide ${appName}: not mounted`);
1012
+ return;
1013
+ }
1014
+
1015
+ logger.wuDebug(`Hiding ${appName} (keep-alive)`);
1016
+
1017
+ // Call optional deactivate lifecycle hook
1018
+ if (mounted.lifecycle?.deactivate) {
1019
+ try {
1020
+ await mounted.lifecycle.deactivate(mounted.container);
1021
+ } catch (err) {
1022
+ logger.wuWarn(`deactivate() failed for ${appName}:`, err);
1023
+ }
1024
+ }
1025
+
1026
+ // Execute beforeUnmount hooks (so plugins know)
1027
+ await this.hooks.execute('beforeUnmount', { appName, mounted, keepAlive: true });
1028
+ await this.pluginSystem.callHook('beforeUnmount', { appName, keepAlive: true });
1029
+
1030
+ // Hide the host container all Shadow DOM content stays intact
1031
+ mounted.hostContainer.style.display = 'none';
1032
+ mounted.state = 'hidden';
1033
+ mounted.hiddenAt = Date.now();
1034
+
1035
+ // Move from mounted → hidden
1036
+ this.hidden.set(appName, mounted);
1037
+ this.mounted.delete(appName);
1038
+
1039
+ // Execute afterUnmount hooks
1040
+ await this.hooks.execute('afterUnmount', { appName, keepAlive: true });
1041
+ await this.pluginSystem.callHook('afterUnmount', { appName, keepAlive: true });
1042
+
1043
+ // Emit event
1044
+ this.eventBus.emit('app:hidden', { appName }, { appName });
1045
+
1046
+ logger.wuInfo(`${appName} hidden (keep-alive) state preserved`);
1047
+ }
1048
+
1049
+ /**
1050
+ * Show a hidden (keep-alive) app.
1051
+ *
1052
+ * Restores visibility instantly — no reload, no remount.
1053
+ * The app's optional `activate()` lifecycle hook is called.
1054
+ *
1055
+ * @param {string} appName - App to show
1056
+ */
1057
+ async show(appName) {
1058
+ const hidden = this.hidden.get(appName);
1059
+ if (!hidden) {
1060
+ logger.wuWarn(`Cannot show ${appName}: not in keep-alive state`);
1061
+ return;
1062
+ }
1063
+
1064
+ this.performance.startMeasure('show', appName);
1065
+ logger.wuDebug(`Showing ${appName} from keep-alive`);
1066
+
1067
+ // Execute beforeMount hooks
1068
+ await this.hooks.execute('beforeMount', {
1069
+ appName,
1070
+ containerSelector: hidden.containerSelector,
1071
+ sandbox: hidden.sandbox,
1072
+ lifecycle: hidden.lifecycle,
1073
+ keepAlive: true
1074
+ });
1075
+ await this.pluginSystem.callHook('beforeMount', {
1076
+ appName,
1077
+ containerSelector: hidden.containerSelector,
1078
+ keepAlive: true
1079
+ });
1080
+
1081
+ // Show the host container
1082
+ hidden.hostContainer.style.display = '';
1083
+ hidden.state = 'stable';
1084
+ delete hidden.hiddenAt;
1085
+
1086
+ // Move from hidden → mounted
1087
+ this.mounted.set(appName, hidden);
1088
+ this.hidden.delete(appName);
1089
+
1090
+ // Call optional activate lifecycle hook
1091
+ if (hidden.lifecycle?.activate) {
1092
+ try {
1093
+ await hidden.lifecycle.activate(hidden.container);
1094
+ } catch (err) {
1095
+ logger.wuWarn(`activate() failed for ${appName}:`, err);
1096
+ }
1097
+ }
1098
+
1099
+ const showTime = this.performance.endMeasure('show', appName);
1100
+
1101
+ // Execute afterMount hooks
1102
+ await this.hooks.execute('afterMount', {
1103
+ appName,
1104
+ containerSelector: hidden.containerSelector,
1105
+ sandbox: hidden.sandbox,
1106
+ mountTime: showTime,
1107
+ keepAlive: true
1108
+ });
1109
+ await this.pluginSystem.callHook('afterMount', {
1110
+ appName,
1111
+ containerSelector: hidden.containerSelector,
1112
+ mountTime: showTime,
1113
+ keepAlive: true
1114
+ });
1115
+
1116
+ // Emit event
1117
+ this.eventBus.emit('app:shown', { appName, showTime }, { appName });
1118
+
1119
+ logger.wuInfo(`${appName} shown from keep-alive in ${showTime.toFixed(2)}ms`);
1120
+ }
1121
+
1122
+ /**
1123
+ * Force-destroy a hidden (keep-alive) app.
1124
+ * Runs full cleanup: lifecycle unmount, iframe destroy, sandbox cleanup.
1125
+ *
1126
+ * @param {string} appName
1127
+ * @private
1128
+ */
1129
+ async _destroyHidden(appName) {
1130
+ const hidden = this.hidden.get(appName);
1131
+ if (!hidden) return;
1132
+
1133
+ logger.wuDebug(`Force-destroying hidden app: ${appName}`);
1134
+
1135
+ // Show first (so unmount sees the container)
1136
+ hidden.hostContainer.style.display = '';
1137
+ hidden.state = 'stable';
1138
+
1139
+ // Move back to mounted temporarily
1140
+ this.mounted.set(appName, hidden);
1141
+ this.hidden.delete(appName);
1142
+
1143
+ // Now do a full unmount
1144
+ await this.unmount(appName, { force: true });
1145
+ }
1146
+
1147
+ /**
1148
+ * Check if an app is in keep-alive (hidden) state.
1149
+ * @param {string} appName
1150
+ * @returns {boolean}
1151
+ */
1152
+ isHidden(appName) {
1153
+ return this.hidden.has(appName);
1154
+ }
1155
+
1156
+ /**
1157
+ * Cargar componente compartido (para imports/exports)
1158
+ * @param {string} componentPath - Ruta del componente (ej: "shared.Button")
1159
+ */
1160
+ async use(componentPath) {
1161
+ const [appName, componentName] = componentPath.split('.');
1162
+
1163
+ if (!appName || !componentName) {
1164
+ throw new Error(`Invalid component path: ${componentPath}. Use format "app.component"`);
1165
+ }
1166
+
1167
+ const app = this.apps.get(appName);
1168
+ if (!app) {
1169
+ throw new Error(`App ${appName} not registered`);
1170
+ }
1171
+
1172
+ const manifest = this.manifests.get(appName);
1173
+ const exportPath = manifest?.wu?.exports?.[componentName];
1174
+
1175
+ if (!exportPath) {
1176
+ throw new Error(`Component ${componentName} not exported by ${appName}`);
1177
+ }
1178
+
1179
+ // Cargar componente
1180
+ return await this.loader.loadComponent(app.url, exportPath);
1181
+ }
1182
+
1183
+ /**
1184
+ * Obtener informacion de una app
1185
+ * @param {string} appName - Nombre de la app
1186
+ */
1187
+ getAppInfo(appName) {
1188
+ return {
1189
+ registered: this.apps.get(appName),
1190
+ manifest: this.manifests.get(appName),
1191
+ mounted: this.mounted.get(appName),
1192
+ definition: this.definitions.get(appName)
1193
+ };
1194
+ }
1195
+
1196
+ /**
1197
+ * Obtener estadisticas del framework
1198
+ */
1199
+ getStats() {
1200
+ return {
1201
+ registered: this.apps.size,
1202
+ defined: this.definitions.size,
1203
+ mounted: this.mounted.size,
1204
+ hidden: this.hidden.size,
1205
+ apps: Array.from(this.apps.keys())
1206
+ };
1207
+ }
1208
+
1209
+ /**
1210
+ * Store methods: Convenience methods for state management
1211
+ */
1212
+
1213
+ /**
1214
+ * Get value from global store
1215
+ * @param {string} path - Dot notation path
1216
+ * @returns {*} Value at path
1217
+ */
1218
+ getState(path) {
1219
+ return this.store.get(path);
1220
+ }
1221
+
1222
+ /**
1223
+ * Set value in global store
1224
+ * @param {string} path - Dot notation path
1225
+ * @param {*} value - Value to set
1226
+ * @returns {number} Sequence number
1227
+ */
1228
+ setState(path, value) {
1229
+ return this.store.set(path, value);
1230
+ }
1231
+
1232
+ /**
1233
+ * Subscribe to state changes
1234
+ * @param {string} pattern - Path or pattern
1235
+ * @param {Function} callback - Callback function
1236
+ * @returns {Function} Unsubscribe function
1237
+ */
1238
+ onStateChange(pattern, callback) {
1239
+ return this.store.on(pattern, callback);
1240
+ }
1241
+
1242
+ /**
1243
+ * Batch set multiple state values
1244
+ * @param {Object} updates - Object with path:value pairs
1245
+ * @returns {Array} Sequence numbers
1246
+ */
1247
+ batchState(updates) {
1248
+ return this.store.batch(updates);
1249
+ }
1250
+
1251
+ /**
1252
+ * Get store metrics
1253
+ * @returns {Object} Performance metrics
1254
+ */
1255
+ getStoreMetrics() {
1256
+ return this.store.getMetrics();
1257
+ }
1258
+
1259
+ /**
1260
+ * Clear all state
1261
+ */
1262
+ clearState() {
1263
+ this.store.clear();
1264
+ }
1265
+
1266
+ /**
1267
+ * Set a URL override for an app (QA/testing).
1268
+ * Sets a cookie so the override persists across page reloads.
1269
+ * Only affects the current browser — no one else sees it.
1270
+ *
1271
+ * @param {string} appName - App to override
1272
+ * @param {string} url - Override URL (e.g., 'http://localhost:5173')
1273
+ * @param {Object} [options]
1274
+ * @param {number} [options.maxAge=86400] - Cookie lifetime in seconds (default: 24h)
1275
+ *
1276
+ * @example
1277
+ * wu.override('cart', 'http://localhost:5173');
1278
+ * wu.override('header', 'https://preview-abc123.vercel.app');
1279
+ */
1280
+ override(appName, url, options) {
1281
+ this.overrides.set(appName, url, options);
1282
+ }
1283
+
1284
+ /**
1285
+ * Remove URL override for an app.
1286
+ * @param {string} appName
1287
+ */
1288
+ removeOverride(appName) {
1289
+ this.overrides.remove(appName);
1290
+ }
1291
+
1292
+ /**
1293
+ * Get all active overrides.
1294
+ * @returns {Object} { appName: url, ... }
1295
+ */
1296
+ getOverrides() {
1297
+ return this.overrides.getAll();
1298
+ }
1299
+
1300
+ /**
1301
+ * Remove all overrides.
1302
+ */
1303
+ clearOverrides() {
1304
+ this.overrides.clearAll();
1305
+ }
1306
+
1307
+ /**
1308
+ * Prefetch one or more apps before they're needed.
1309
+ *
1310
+ * Uses Speculation Rules API (Chrome 121+), falls back to
1311
+ * <link rel="modulepreload"> or <link rel="prefetch">.
1312
+ *
1313
+ * @param {string|string[]} appNames - App name(s) to prefetch
1314
+ * @param {Object} [options]
1315
+ * @param {'immediate'|'hover'|'visible'|'idle'} [options.on='immediate'] - When to trigger
1316
+ * @param {string|Element} [options.target] - Element for hover/visible triggers
1317
+ * @param {'conservative'|'moderate'|'eager'} [options.eagerness='moderate'] - Speculation eagerness
1318
+ * @returns {Promise<void>|Function} Promise or cleanup function
1319
+ *
1320
+ * @example
1321
+ * wu.prefetch('cart');
1322
+ * wu.prefetch('cart', { on: 'hover', target: '#cart-link' });
1323
+ * wu.prefetch('cart', { on: 'visible', target: '#cart-section' });
1324
+ * wu.prefetch(['profile', 'settings'], { on: 'idle' });
1325
+ */
1326
+ prefetch(appNames, options) {
1327
+ return this.prefetcher.prefetch(appNames, options);
1328
+ }
1329
+
1330
+ /**
1331
+ * Prefetch all registered but not-yet-mounted apps.
1332
+ * @param {Object} [options] - Same options as prefetch()
1333
+ */
1334
+ prefetchAll(options) {
1335
+ return this.prefetcher.prefetchAll(options);
1336
+ }
1337
+
1338
+ /**
1339
+ * Create WuApp instance for declarative usage
1340
+ * @param {string} name - App name
1341
+ * @param {Object} config - Configuration { url, container, autoInit }
1342
+ * @returns {WuApp} WuApp instance
1343
+ */
1344
+ app(name, config) {
1345
+ return new WuApp(name, config, this);
1346
+ }
1347
+
1348
+ /**
1349
+ * Limpiar todo el framework
1350
+ */
1351
+ async destroy() {
1352
+ logger.wuDebug('Destroying framework...');
1353
+
1354
+ try {
1355
+ // Execute beforeDestroy hooks
1356
+ await this.hooks.execute('beforeDestroy', {});
1357
+
1358
+ // Call plugin onDestroy hooks
1359
+ await this.pluginSystem.callHook('onDestroy', {});
1360
+
1361
+ // Cancel all pending deferred unmounts
1362
+ for (const timer of this._pendingUnmounts.values()) {
1363
+ clearTimeout(timer);
1364
+ }
1365
+ this._pendingUnmounts.clear();
1366
+ this._mountingPromises.clear();
1367
+
1368
+ // Force-destroy all hidden (keep-alive) apps first
1369
+ for (const appName of [...this.hidden.keys()]) {
1370
+ await this._destroyHidden(appName);
1371
+ }
1372
+
1373
+ // Desmontar todas las apps
1374
+ for (const appName of [...this.mounted.keys()]) {
1375
+ await this.unmount(appName, { force: true });
1376
+ }
1377
+
1378
+ // Limpiar sistemas esenciales
1379
+ this.cache.clear();
1380
+ this.eventBus.removeAll();
1381
+ this.eventBus.clearHistory();
1382
+ this.performance.clearMetrics();
1383
+
1384
+ // Limpiar advanced systems
1385
+ this.pluginSystem.cleanup();
1386
+ this.strategies.cleanup();
1387
+ this.errorBoundary.cleanup();
1388
+ this.hooks.cleanup();
1389
+ this.prefetcher.cleanup();
1390
+
1391
+ // Limpiar registros
1392
+ this.apps.clear();
1393
+ this.definitions.clear();
1394
+ this.manifests.clear();
1395
+ this.mounted.clear();
1396
+ this.hidden.clear();
1397
+
1398
+ // Limpiar store
1399
+ this.store.clear();
1400
+
1401
+ this.isInitialized = false;
1402
+
1403
+ // Execute afterDestroy hooks
1404
+ await this.hooks.execute('afterDestroy', {});
1405
+
1406
+ logger.wuDebug('Framework destroyed');
1407
+ } catch (error) {
1408
+ logger.wuError('Error during destroy:', error);
1409
+ throw error;
1410
+ }
1411
+ }
1412
+ }