wu-framework 1.2.1 → 2.1.1

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