wu-framework 1.1.14 → 1.1.16

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 (90) hide show
  1. package/LICENSE +39 -39
  2. package/README.md +408 -408
  3. package/dist/wu-framework.cjs.js.map +1 -1
  4. package/dist/wu-framework.dev.js +15151 -15151
  5. package/dist/wu-framework.dev.js.map +1 -1
  6. package/dist/wu-framework.esm.js.map +1 -1
  7. package/dist/wu-framework.umd.js.map +1 -1
  8. package/integrations/astro/README.md +127 -127
  9. package/integrations/astro/WuApp.astro +63 -63
  10. package/integrations/astro/WuShell.astro +39 -39
  11. package/integrations/astro/index.js +68 -68
  12. package/integrations/astro/package.json +38 -38
  13. package/integrations/astro/types.d.ts +53 -53
  14. package/package.json +161 -161
  15. package/src/adapters/angular/ai.js +30 -30
  16. package/src/adapters/angular/index.d.ts +154 -154
  17. package/src/adapters/angular/index.js +932 -932
  18. package/src/adapters/angular.d.ts +3 -3
  19. package/src/adapters/angular.js +3 -3
  20. package/src/adapters/index.js +168 -168
  21. package/src/adapters/lit/ai.js +20 -20
  22. package/src/adapters/lit/index.d.ts +120 -120
  23. package/src/adapters/lit/index.js +721 -721
  24. package/src/adapters/lit.d.ts +3 -3
  25. package/src/adapters/lit.js +3 -3
  26. package/src/adapters/preact/ai.js +33 -33
  27. package/src/adapters/preact/index.d.ts +108 -108
  28. package/src/adapters/preact/index.js +661 -661
  29. package/src/adapters/preact.d.ts +3 -3
  30. package/src/adapters/preact.js +3 -3
  31. package/src/adapters/react/index.js +48 -54
  32. package/src/adapters/react.d.ts +3 -3
  33. package/src/adapters/react.js +3 -3
  34. package/src/adapters/shared.js +64 -64
  35. package/src/adapters/solid/ai.js +32 -32
  36. package/src/adapters/solid/index.d.ts +101 -101
  37. package/src/adapters/solid/index.js +586 -586
  38. package/src/adapters/solid.d.ts +3 -3
  39. package/src/adapters/solid.js +3 -3
  40. package/src/adapters/svelte/ai.js +31 -31
  41. package/src/adapters/svelte/index.d.ts +166 -166
  42. package/src/adapters/svelte/index.js +798 -798
  43. package/src/adapters/svelte.d.ts +3 -3
  44. package/src/adapters/svelte.js +3 -3
  45. package/src/adapters/vanilla/ai.js +30 -30
  46. package/src/adapters/vanilla/index.d.ts +179 -179
  47. package/src/adapters/vanilla/index.js +785 -785
  48. package/src/adapters/vanilla.d.ts +3 -3
  49. package/src/adapters/vanilla.js +3 -3
  50. package/src/adapters/vue/ai.js +52 -52
  51. package/src/adapters/vue/index.d.ts +299 -299
  52. package/src/adapters/vue/index.js +610 -610
  53. package/src/adapters/vue.d.ts +3 -3
  54. package/src/adapters/vue.js +3 -3
  55. package/src/ai/wu-ai-actions.js +261 -261
  56. package/src/ai/wu-ai-agent.js +546 -546
  57. package/src/ai/wu-ai-browser-primitives.js +354 -354
  58. package/src/ai/wu-ai-browser.js +380 -380
  59. package/src/ai/wu-ai-context.js +332 -332
  60. package/src/ai/wu-ai-conversation.js +613 -613
  61. package/src/ai/wu-ai-orchestrate.js +1021 -1021
  62. package/src/ai/wu-ai-permissions.js +381 -381
  63. package/src/ai/wu-ai-provider.js +700 -700
  64. package/src/ai/wu-ai-schema.js +225 -225
  65. package/src/ai/wu-ai-triggers.js +396 -396
  66. package/src/ai/wu-ai.js +804 -804
  67. package/src/core/wu-app.js +236 -236
  68. package/src/core/wu-cache.js +477 -477
  69. package/src/core/wu-core.js +1398 -1398
  70. package/src/core/wu-error-boundary.js +382 -382
  71. package/src/core/wu-event-bus.js +348 -348
  72. package/src/core/wu-hooks.js +350 -350
  73. package/src/core/wu-html-parser.js +190 -190
  74. package/src/core/wu-iframe-sandbox.js +328 -328
  75. package/src/core/wu-loader.js +272 -272
  76. package/src/core/wu-logger.js +134 -134
  77. package/src/core/wu-manifest.js +509 -509
  78. package/src/core/wu-mcp-bridge.js +432 -432
  79. package/src/core/wu-overrides.js +510 -510
  80. package/src/core/wu-performance.js +228 -228
  81. package/src/core/wu-plugin.js +348 -348
  82. package/src/core/wu-prefetch.js +414 -414
  83. package/src/core/wu-proxy-sandbox.js +476 -476
  84. package/src/core/wu-sandbox.js +779 -779
  85. package/src/core/wu-script-executor.js +113 -113
  86. package/src/core/wu-snapshot-sandbox.js +227 -227
  87. package/src/core/wu-strategies.js +256 -256
  88. package/src/core/wu-style-bridge.js +477 -477
  89. package/src/index.js +224 -224
  90. package/src/utils/dependency-resolver.js +327 -327
@@ -1,1398 +1,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
- // 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
+ // 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
+ }